1 import * as vscode from "vscode";
2 import { Catalog, RegistryAgent, parseCatalog } from "./catalog";
3
4 /**
5 * The remote ACP agent registry: fetches a curated catalog of ACP-compatible
6 * agents, caches the last good copy for offline use, and installs a chosen
7 * agent by writing its definition into the user's `sigit.agents` configuration.
8 *
9 * "Install" here means *register* — siGit never downloads or runs an agent
10 * binary on the user's behalf. Each catalog entry may carry a human-readable
11 * `install` hint that we surface but never execute.
12 */
13
14 const DEFAULT_REGISTRY_URL =
15 "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
16 const CACHE_KEY = "sigit.registry.cache";
17 const FETCH_TIMEOUT_MS = 10_000;
18
19 interface CacheEntry {
20 url: string;
21 fetchedAt: number;
22 agents: RegistryAgent[];
23 }
24
25 export interface FetchResult {
26 agents: RegistryAgent[];
27 /** True when the network fetch failed and cached data was served instead. */
28 fromCache: boolean;
29 /** The network error, present only when `fromCache` is true. */
30 error?: string;
31 }
32
33 function config(): vscode.WorkspaceConfiguration {
34 return vscode.workspace.getConfiguration("sigit");
35 }
36
37 /** The configured registry URL, or the built-in siGit catalog. */
38 export function registryUrl(): string {
39 const configured = config().get<string>("registry.url");
40 return configured && configured.trim() !== "" ? configured.trim() : DEFAULT_REGISTRY_URL;
41 }
42
43 /** Keys of agents already present in the `sigit.agents` configuration. */
44 export function installedKeys(): Set<string> {
45 const registry = config().get<Record<string, unknown>>("agents") ?? {};
46 return new Set(Object.keys(registry));
47 }
48
49 async function fetchFromNetwork(url: string): Promise<Catalog> {
50 const controller = new AbortController();
51 const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
52 try {
53 const response = await fetch(url, {
54 signal: controller.signal,
55 headers: { accept: "application/json" }
56 });
57 if (!response.ok) {
58 throw new Error(`HTTP ${response.status} ${response.statusText}`);
59 }
60 return parseCatalog(await response.text());
61 } finally {
62 clearTimeout(timer);
63 }
64 }
65
66 /**
67 * Fetch the agent catalog. On success the result is cached in global state. On
68 * failure the last cached catalog (for the same URL) is served when available,
69 * with `fromCache: true`; otherwise the error is re-thrown.
70 */
71 export async function fetchCatalog(context: vscode.ExtensionContext): Promise<FetchResult> {
72 const url = registryUrl();
73 try {
74 const catalog = await fetchFromNetwork(url);
75 const entry: CacheEntry = { url, fetchedAt: Date.now(), agents: catalog.agents };
76 await context.globalState.update(CACHE_KEY, entry);
77 return { agents: catalog.agents, fromCache: false };
78 } catch (err) {
79 const cached = context.globalState.get<CacheEntry>(CACHE_KEY);
80 if (cached && cached.url === url) {
81 return {
82 agents: cached.agents,
83 fromCache: true,
84 error: (err as Error).message
85 };
86 }
87 throw err;
88 }
89 }
90
91 /**
92 * Register a catalog agent into the user's `sigit.agents` configuration. Returns
93 * `false` without writing when an agent with the same key is already installed.
94 */
95 export async function installAgent(agent: RegistryAgent): Promise<boolean> {
96 const cfg = config();
97 const registry = { ...(cfg.get<Record<string, unknown>>("agents") ?? {}) };
98 if (Object.prototype.hasOwnProperty.call(registry, agent.key)) {
99 return false;
100 }
101 registry[agent.key] = {
102 name: agent.name,
103 command: agent.command,
104 args: agent.args,
105 env: agent.env
106 };
107 await cfg.update("agents", registry, vscode.ConfigurationTarget.Global);
108 return true;
109 }
110
111 /** Make `key` the default agent (`sigit.agent.default`), user-wide. */
112 export async function setDefaultAgent(key: string): Promise<void> {
113 await config().update("agent.default", key, vscode.ConfigurationTarget.Global);
114 }