chore: archive AgentDock v1 implementation
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const HOME = process.env.HOME || "/home/ai";
|
||||
const CC_SWITCH_DATABASE = path.join(HOME, ".cc-switch", "cc-switch.db");
|
||||
const ANSI = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
||||
const SAFE_ID = /^[a-zA-Z0-9._-]{1,128}$/;
|
||||
|
||||
export const CC_SWITCH_APPS = Object.freeze([
|
||||
{ id: "claude", toolId: "claude", name: "Claude Code" },
|
||||
{ id: "codex", toolId: "codex", name: "Codex" },
|
||||
{ id: "open-code", toolId: "opencode", name: "OpenCode" },
|
||||
]);
|
||||
|
||||
export const MULTICA_TOOL_IDS = Object.freeze(["claude", "codex", "codebuddy", "kimi", "opencode"]);
|
||||
|
||||
function clean(value) {
|
||||
return String(value || "").replace(ANSI, "").trim();
|
||||
}
|
||||
|
||||
function publicEndpoint(value) {
|
||||
const candidate = clean(value);
|
||||
if (!candidate || candidate === "N/A") return null;
|
||||
try {
|
||||
const url = new URL(candidate);
|
||||
return url.origin;
|
||||
} catch {
|
||||
return "configured";
|
||||
}
|
||||
}
|
||||
|
||||
export function parseCcProviderList(output) {
|
||||
const providers = [];
|
||||
let currentProviderId = null;
|
||||
for (const rawLine of clean(output).split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
const current = line.match(/^→\s*Current:\s*(\S+)/);
|
||||
if (current) currentProviderId = current[1];
|
||||
if (!line.startsWith("│") || !line.includes("┆")) continue;
|
||||
const columns = line.slice(1, line.endsWith("│") ? -1 : undefined).split("┆").map(clean);
|
||||
if (columns.length < 4 || columns[1] === "ID") continue;
|
||||
const [marker, id, name, endpoint] = columns;
|
||||
if (!SAFE_ID.test(id)) continue;
|
||||
providers.push({
|
||||
id,
|
||||
name: name || id,
|
||||
endpoint: publicEndpoint(endpoint),
|
||||
active: marker.includes("✓"),
|
||||
});
|
||||
}
|
||||
if (!currentProviderId) currentProviderId = providers.find((provider) => provider.active)?.id || null;
|
||||
return { currentProviderId, providers };
|
||||
}
|
||||
|
||||
export function parseMulticaConfig(output) {
|
||||
const values = {};
|
||||
for (const line of clean(output).split(/\r?\n/)) {
|
||||
const match = line.match(/^([a-z_]+):\s*(.*)$/);
|
||||
if (match) values[match[1]] = match[2] === "(not set)" ? null : match[2];
|
||||
}
|
||||
return {
|
||||
serverUrl: values.server_url || null,
|
||||
appUrl: values.app_url || null,
|
||||
workspaceId: values.workspace_id || null,
|
||||
configured: Boolean(values.server_url && values.workspace_id),
|
||||
};
|
||||
}
|
||||
|
||||
async function run(command, args, options = {}) {
|
||||
const { stdout, stderr } = await execFileAsync(command, args, {
|
||||
timeout: options.timeout || 10000,
|
||||
maxBuffer: options.maxBuffer || 1024 * 1024,
|
||||
env: { ...process.env, NO_COLOR: "1", TERM: "dumb" },
|
||||
});
|
||||
return clean(stdout || stderr);
|
||||
}
|
||||
|
||||
async function versionOf(command, args = ["--version"]) {
|
||||
try {
|
||||
const output = await run(command, args, { timeout: 8000 });
|
||||
return { installed: true, version: output.split(/\r?\n/)[0] || "installed" };
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") return { installed: false, version: null };
|
||||
return { installed: true, version: "unknown", error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function ccSwitchInitialized() {
|
||||
try {
|
||||
await fs.access(CC_SWITCH_DATABASE);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function ccSwitchSnapshot() {
|
||||
const availability = await versionOf("cc-switch");
|
||||
const initialized = availability.installed && await ccSwitchInitialized();
|
||||
const apps = await Promise.all(CC_SWITCH_APPS.map(async (app) => {
|
||||
if (!initialized) return { ...app, currentProviderId: null, providers: [] };
|
||||
try {
|
||||
return { ...app, ...parseCcProviderList(await run("cc-switch", ["--app", app.id, "provider", "list"])) };
|
||||
} catch (error) {
|
||||
return { ...app, currentProviderId: null, providers: [], error: error.message };
|
||||
}
|
||||
}));
|
||||
return {
|
||||
...availability,
|
||||
initialized,
|
||||
apps,
|
||||
unsupportedToolIds: ["codebuddy", "kimi", "qwen", "dsh"],
|
||||
};
|
||||
}
|
||||
|
||||
async function multicaSnapshot() {
|
||||
const availability = await versionOf("multica", ["version"]);
|
||||
if (!availability.installed) {
|
||||
return { ...availability, daemon: { status: "unavailable" }, config: { configured: false }, supportedToolIds: MULTICA_TOOL_IDS };
|
||||
}
|
||||
let daemon = { status: "unknown" };
|
||||
let config = { configured: false, serverUrl: null, appUrl: null, workspaceId: null };
|
||||
try {
|
||||
daemon = JSON.parse(await run("multica", ["daemon", "status", "--output", "json"]));
|
||||
} catch (error) {
|
||||
daemon = { status: "unknown", error: error.message };
|
||||
}
|
||||
try {
|
||||
config = parseMulticaConfig(await run("multica", ["config", "show"]));
|
||||
} catch (error) {
|
||||
config = { ...config, error: error.message };
|
||||
}
|
||||
return { ...availability, daemon, config, supportedToolIds: MULTICA_TOOL_IDS };
|
||||
}
|
||||
|
||||
let cachedSnapshot = null;
|
||||
let cachedAt = 0;
|
||||
|
||||
export async function integrationSnapshot({ force = false } = {}) {
|
||||
if (!force && cachedSnapshot && Date.now() - cachedAt < 5000) return cachedSnapshot;
|
||||
const [ccSwitch, multica] = await Promise.all([ccSwitchSnapshot(), multicaSnapshot()]);
|
||||
cachedSnapshot = { ccSwitch, multica };
|
||||
cachedAt = Date.now();
|
||||
return cachedSnapshot;
|
||||
}
|
||||
|
||||
function validateCcSwitchApp(appId) {
|
||||
if (!CC_SWITCH_APPS.some((app) => app.id === appId)) throw new Error("Unsupported CC Switch application");
|
||||
}
|
||||
|
||||
let mutationQueue = Promise.resolve();
|
||||
function serializeMutation(operation) {
|
||||
const result = mutationQueue.then(operation, operation);
|
||||
mutationQueue = result.catch(() => {});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function switchCcProvider(appId, providerId) {
|
||||
validateCcSwitchApp(appId);
|
||||
if (!SAFE_ID.test(providerId || "")) throw new Error("Invalid provider ID");
|
||||
return serializeMutation(async () => {
|
||||
const output = await run("cc-switch", ["--app", appId, "provider", "switch", providerId], { timeout: 30000 });
|
||||
cachedSnapshot = null;
|
||||
return { ok: true, output: output.split(/\r?\n/).slice(0, 4).join("\n") };
|
||||
});
|
||||
}
|
||||
|
||||
export async function importCcLiveConfig(appId) {
|
||||
validateCcSwitchApp(appId);
|
||||
return serializeMutation(async () => {
|
||||
const output = await run("cc-switch", ["--app", appId, "provider", "import-live"], { timeout: 30000 });
|
||||
cachedSnapshot = null;
|
||||
return { ok: true, output: output.split(/\r?\n/).slice(0, 6).join("\n") };
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user