@falkenslab/agent-kit 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +2 -0
  2. package/dist/chat/chatController.d.ts +10 -0
  3. package/dist/chat/chatController.js +39 -1
  4. package/dist/chat/extensions.d.ts +9 -0
  5. package/dist/chat/extensions.js +14 -2
  6. package/dist/chat/runs.d.ts +3 -1
  7. package/dist/chat/runs.js +1 -1
  8. package/dist/core/extensionCatalog.d.ts +8 -1
  9. package/dist/core/extensionCatalog.js +13 -9
  10. package/dist/core/extensionOptions.d.ts +5 -0
  11. package/dist/core/extensionOptions.js +10 -0
  12. package/dist/core/extensions.d.ts +13 -0
  13. package/dist/core/extensions.js +13 -0
  14. package/dist/core/externalExtensions.d.ts +27 -1
  15. package/dist/core/externalExtensions.js +80 -13
  16. package/dist/core/liveExtensions.d.ts +38 -0
  17. package/dist/core/liveExtensions.js +69 -0
  18. package/dist/core/marketplaces.d.ts +1 -0
  19. package/dist/core/messages/de.js +10 -1
  20. package/dist/core/messages/en.d.ts +14 -0
  21. package/dist/core/messages/en.js +10 -1
  22. package/dist/core/messages/es.js +10 -1
  23. package/dist/core/messages/fr.js +10 -1
  24. package/dist/core/npxPackages.d.ts +2 -2
  25. package/dist/core/runner.js +17 -0
  26. package/dist/core/session.d.ts +6 -2
  27. package/dist/core/session.js +5 -2
  28. package/dist/core/sessionFacts.d.ts +14 -0
  29. package/dist/extensions/awareness/catalog.d.ts +8 -0
  30. package/dist/extensions/awareness/catalog.js +92 -0
  31. package/dist/extensions/awareness/index.d.ts +3 -1
  32. package/dist/extensions/awareness/index.js +15 -6
  33. package/dist/extensions/awareness/labels.js +8 -5
  34. package/dist/extensions/awareness/tools.d.ts +2 -2
  35. package/dist/extensions/awareness/tools.js +4 -3
  36. package/dist/index.d.ts +3 -3
  37. package/dist/index.js +1 -1
  38. package/dist/tui/extensionCommand.js +17 -9
  39. package/extensions/awareness/skills/extensions/SKILL.md +56 -0
  40. package/package.json +1 -1
  41. package/schemas/agent-kit-metadata.schema.json +6 -0
  42. package/schemas/marketplace-catalog.schema.json +21 -1
@@ -5,15 +5,42 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { promisify } from "node:util";
8
- import { capabilitiesProblem, parseCapabilities, providedBy } from "./extensions.js";
8
+ import { capabilitiesProblem, parseCapabilities, parseDescriptions, providedBy } from "./extensions.js";
9
9
  import { getLanguage } from "./messages/index.js";
10
10
  import { truncate } from "./toolLabels.js";
11
11
  import { agentKitVersion } from "./version.js";
12
12
  import { outsideArchive } from "./packaged.js";
13
- import { extensionSecretsPath, missingOptions, optionsNamed, parseOptionValue, parseUserConfig, readOptionValues, removeSecrets, storeOptionValues } from "./extensionOptions.js";
13
+ import { extensionSecretsPath, fillOptions, missingOptions, optionsNamed, parseOptionValue, parseUserConfig, readOptionValues, removeSecrets, storeOptionValues } from "./extensionOptions.js";
14
14
  import { isTrusted } from "./workspaceTrust.js";
15
15
  import { checkRequirements, describeMissing, findOnPath, parseRequirements, requirementsProblem } from "./extensionRequirements.js";
16
16
  import { cachedPackageDir, cachedPackageProblem, cachePackage, npxServer, packageCacheDir, packageSpec } from "./npxPackages.js";
17
+ /** Whether a server is remote (#61). */
18
+ export function isRemoteServer(server) {
19
+ return "url" in server;
20
+ }
21
+ /**
22
+ * The name a session gives an extension's remote server: its own, prefixed with the extension's.
23
+ * The SDK's CLI keeps OAuth tokens by server name (confirmed empirically, CLI 2.1.300): a server
24
+ * named like one in the runner's own Claude Code would sign in with the runner's token.
25
+ */
26
+ export function remoteServerName(extension, server) {
27
+ return `${extension}-${server}`;
28
+ }
29
+ /** The hosts an extension's remote servers talk to, for the person to read before installing it. */
30
+ export function remoteHosts(manifest) {
31
+ return [
32
+ ...new Set(Object.values(manifest.servers).flatMap((server) => {
33
+ if (!isRemoteServer(server))
34
+ return [];
35
+ try {
36
+ return [new URL(server.url.replace(/\$\{[^}]*\}/g, "x")).host];
37
+ }
38
+ catch {
39
+ return [server.url];
40
+ }
41
+ })),
42
+ ];
43
+ }
17
44
  export const LOCK_FILE = "extensions.lock.json";
18
45
  /**
19
46
  * An installed extension's data folder: `${CLAUDE_PLUGIN_DATA}` in its servers' arguments and
@@ -97,6 +124,7 @@ export async function readExternalManifest(dir) {
97
124
  return {
98
125
  name,
99
126
  description: typeof raw.description === "string" ? raw.description : "",
127
+ descriptions: parseDescriptions(kit.description),
100
128
  ...text("version"),
101
129
  ...(author && typeof author === "object" && typeof author.name === "string"
102
130
  ? { author: { name: author.name, ...(author.email ? { email: author.email } : {}), ...(author.url ? { url: author.url } : {}) } }
@@ -154,9 +182,20 @@ async function pluginServers(dir, raw) {
154
182
  throw new Error("its MCP servers aren't a JSON object.");
155
183
  const servers = {};
156
184
  for (const [name, server] of Object.entries(declared)) {
185
+ if (server && (server.type === "http" || server.type === "sse")) {
186
+ if (typeof server.url !== "string")
187
+ throw new Error(`its MCP server "${name}" has no url.`);
188
+ const headers = server.headers && typeof server.headers === "object" ? Object.fromEntries(Object.entries(server.headers).map(([key, value]) => [key, String(value)])) : undefined;
189
+ servers[name] = { type: server.type, url: server.url, ...(headers ? { headers } : {}) };
190
+ continue;
191
+ }
157
192
  if (!server || typeof server.command !== "string")
158
193
  throw new Error(`its MCP server "${name}" has no command.`);
159
- servers[name] = { command: server.command, ...(Array.isArray(server.args) ? { args: server.args.map(String) } : {}), ...(server.env ? { env: server.env } : {}) };
194
+ servers[name] = {
195
+ command: server.command,
196
+ ...(Array.isArray(server.args) ? { args: server.args.map(String) } : {}),
197
+ ...(server.env ? { env: server.env } : {}),
198
+ };
160
199
  }
161
200
  return servers;
162
201
  }
@@ -200,6 +239,12 @@ function serverLaunch(name, server, folders, packages = {}) {
200
239
  */
201
240
  async function serversProblem(manifest, root, packages) {
202
241
  for (const [name, server] of Object.entries(manifest.servers)) {
242
+ if (isRemoteServer(server)) {
243
+ // Never in clear over the network: plain HTTP only to this computer (a server under development).
244
+ if (!/^https:\/\//i.test(server.url) && !/^http:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/i.test(server.url))
245
+ return `talks to its server "${name}" at ${server.url}: a remote server must be https (http only on localhost)`;
246
+ continue;
247
+ }
203
248
  const npx = npxServer(server);
204
249
  if (typeof npx === "string")
205
250
  return `${npx} (its server "${name}")`;
@@ -221,6 +266,8 @@ async function serversProblem(manifest, root, packages) {
221
266
  async function fetchServerPackages(manifest, cache) {
222
267
  const packages = {};
223
268
  for (const server of Object.values(manifest.servers)) {
269
+ if (isRemoteServer(server))
270
+ continue;
224
271
  const npx = npxServer(server);
225
272
  if (!npx || typeof npx === "string")
226
273
  continue;
@@ -311,8 +358,8 @@ export function extensionLauncherPath() {
311
358
  // Node runs it as another process: outside the archive when packaged (#42).
312
359
  return outsideArchive(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "assets", "extension-launcher.mjs"));
313
360
  }
314
- /** Its tools' chat labels in the kit's language, from its manifest: `{field}` takes the input; for each of its servers. */
315
- function manifestToolLabels(manifest) {
361
+ /** Its tools' chat labels in the kit's language, from its manifest: `{field}` takes the input; for each of its servers, by their session names. */
362
+ function manifestToolLabels(manifest, sessionServers) {
316
363
  const language = getLanguage();
317
364
  const fill = (template, input) => template.replace(/\{(\w+)\}/g, (_, field) => (input[field] === undefined ? "" : truncate(String(input[field]), 60)));
318
365
  return Object.fromEntries(Object.entries(manifest.labels).flatMap(([tool, byLanguage]) => {
@@ -320,7 +367,7 @@ function manifestToolLabels(manifest) {
320
367
  if (!texts)
321
368
  return [];
322
369
  const label = { label: (input) => fill(texts.label, input), ...(texts.phrase ? { phrase: texts.phrase } : {}) };
323
- return Object.keys(manifest.servers).map((server) => [`mcp__${server}__${tool}`, label]);
370
+ return sessionServers.map((server) => [`mcp__${server}__${tool}`, label]);
324
371
  }));
325
372
  }
326
373
  /** The extension an installed one makes: its servers started through the launcher, its rules from its manifest. */
@@ -333,10 +380,27 @@ function externalExtension(installed, manifest, cache, unavailable = []) {
333
380
  ...(unavailable.length ? { unavailable } : {}),
334
381
  async contribute() {
335
382
  const servers = {};
383
+ const remote = [];
336
384
  if (Object.keys(manifest.servers).length)
337
385
  await mkdir(installed.dataDir, { recursive: true });
386
+ const folders = { root: installed.dir, data: installed.dataDir };
387
+ // A remote server's options go into its URL and headers here, when the session opens: the
388
+ // SDK fixes them then, so a changed value takes opening the session again (#61).
389
+ const values = Object.values(manifest.servers).some(isRemoteServer) ? await readOptionValues(installed.privateDir, installed.name, manifest.userConfig, installed.lock.options) : {};
338
390
  for (const [name, server] of Object.entries(manifest.servers)) {
339
- const launch = serverLaunch(name, server, { root: installed.dir, data: installed.dataDir }, { cache, locked: installed.lock.packages });
391
+ if (isRemoteServer(server)) {
392
+ const fill = (text) => fillOptions(expand(text, folders), values);
393
+ // A header naming an option without a value isn't sent: `Authorization: Bearer ` would
394
+ // turn the server's OAuth off ("OAuth fallback is disabled when headers.Authorization is
395
+ // set", confirmed empirically, CLI 2.1.300), so a token stays optional beside signing in.
396
+ const unset = (text) => optionsNamed([text]).some((key) => values[key] === undefined || values[key] === "");
397
+ const headers = Object.entries(server.headers ?? {}).filter(([, value]) => !unset(value));
398
+ const sessionName = remoteServerName(manifest.name, name);
399
+ servers[sessionName] = { type: server.type, url: fill(server.url), ...(headers.length ? { headers: Object.fromEntries(headers.map(([key, value]) => [key, fill(value)])) } : {}) };
400
+ remote.push(sessionName);
401
+ continue;
402
+ }
403
+ const launch = serverLaunch(name, server, folders, { cache, locked: installed.lock.packages });
340
404
  if (typeof launch === "string")
341
405
  continue; // checked when it loaded
342
406
  // Its `env` reaches it (the SDK merges it with the agent's); the launcher removes the rest.
@@ -358,9 +422,10 @@ function externalExtension(installed, manifest, cache, unavailable = []) {
358
422
  // Its manifest's list, or its servers' own word: the server is the code the kit trusts
359
423
  // least, so its hints only fill the list's absence (ADR-025).
360
424
  ...(manifest.readOnlyTools
361
- ? { readOnlyTools: Object.keys(manifest.servers).flatMap((server) => manifest.readOnlyTools.map((tool) => `mcp__${server}__${tool}`)) }
425
+ ? { readOnlyTools: Object.keys(servers).flatMap((server) => manifest.readOnlyTools.map((tool) => `mcp__${server}__${tool}`)) }
362
426
  : { readOnlyTools: [], readOnlyHintServers: Object.keys(servers) }),
363
- toolLabels: manifestToolLabels(manifest),
427
+ toolLabels: manifestToolLabels(manifest, Object.keys(servers)),
428
+ ...(remote.length ? { remoteServers: remote } : {}),
364
429
  ...(manifest.help ? { helpLines: [manifest.help] } : {}),
365
430
  };
366
431
  },
@@ -372,7 +437,7 @@ function optionDefaults(declared) {
372
437
  }
373
438
  /** Why its options keep it from running (a server naming one it doesn't declare, a required one without a value), or nothing. */
374
439
  function optionsProblem(manifest, values) {
375
- const named = optionsNamed(Object.values(manifest.servers).flatMap((server) => [server.command, ...(server.args ?? []), ...Object.values(server.env ?? {}).map(String)]));
440
+ const named = optionsNamed(Object.values(manifest.servers).flatMap((server) => isRemoteServer(server) ? [server.url, ...Object.values(server.headers ?? {})] : [server.command, ...(server.args ?? []), ...Object.values(server.env ?? {}).map(String)]));
376
441
  const undeclared = named.filter((key) => !manifest.userConfig[key]);
377
442
  if (undeclared.length)
378
443
  return `names ${undeclared.map((key) => "${user_config." + key + "}").join(", ")}, which its userConfig doesn't declare`;
@@ -489,6 +554,8 @@ export async function addExtension(source, scopeDir, options = {}) {
489
554
  throw new Error(`${manifest.name} can't be installed: it ${servers}.`);
490
555
  // What it needs and this computer lacks: said, never installed, and it installs anyway (#63).
491
556
  const missing = (await checkRequirements(manifest.requirements)).filter((check) => !check.ok).map((check) => missingRequirement(check));
557
+ // Where what the agent sends goes, for the person to know (#61).
558
+ const remote = remoteHosts(manifest);
492
559
  const target = path.join(scopeDir, manifest.name);
493
560
  // Replacing what's installed there would delete the source itself.
494
561
  if (path.resolve(from) === path.resolve(target))
@@ -515,12 +582,12 @@ export async function addExtension(source, scopeDir, options = {}) {
515
582
  }
516
583
  lock[manifest.name] = { source: origin, ...provenance, sha256, stored: true, ...locked, enabled: true, installed: new Date().toISOString() };
517
584
  await writeLock(scopeDir, lock);
518
- return { name: manifest.name, replaced, missing, ...(commit ? { commit } : {}) };
585
+ return { name: manifest.name, replaced, missing, remote, ...(commit ? { commit } : {}) };
519
586
  }
520
587
  if (options.link) {
521
588
  lock[manifest.name] = { source: from, linked: true, sha256: "", ...locked, enabled: true, installed: new Date().toISOString() };
522
589
  await writeLock(scopeDir, lock);
523
- return { name: manifest.name, replaced, missing };
590
+ return { name: manifest.name, replaced, missing, remote };
524
591
  }
525
592
  await cp(from, target, { recursive: true, filter: (file) => path.basename(file) !== ".git" });
526
593
  lock[manifest.name] = {
@@ -532,7 +599,7 @@ export async function addExtension(source, scopeDir, options = {}) {
532
599
  installed: new Date().toISOString(),
533
600
  };
534
601
  await writeLock(scopeDir, lock);
535
- return { name: manifest.name, replaced, missing, ...(commit ? { commit } : {}) };
602
+ return { name: manifest.name, replaced, missing, remote, ...(commit ? { commit } : {}) };
536
603
  }
537
604
  finally {
538
605
  if (clone)
@@ -47,6 +47,8 @@ export interface LiveExtension {
47
47
  capabilities: ExtensionCapability[];
48
48
  /** What it can't do on this computer (#63). */
49
49
  unavailable: UnavailableCapability[];
50
+ /** Its remote servers, by session name (#61): their options apply when the session opens, and they may need signing in. */
51
+ remoteServers: string[];
50
52
  help: string[];
51
53
  }
52
54
  /** The session's own lists the switch keeps in step: the same objects the gates, the chat and the view read. */
@@ -78,3 +80,39 @@ export declare function createExtensionSwitch(extensions: readonly LiveExtension
78
80
  export type ExtensionRestart = (name: string) => Promise<boolean>;
79
81
  /** The restart for a session's installed extensions. */
80
82
  export declare function createExtensionRestart(extensions: readonly LiveExtension[], targets: LiveTargets): ExtensionRestart;
83
+ /** Where signing in to an extension's remote servers stands (#61). */
84
+ export type SignIn =
85
+ /** Its servers are connected: nothing to sign in to. */
86
+ {
87
+ status: "signed-in";
88
+ }
89
+ /** The links the person opens to sign in, one per server that needs it. */
90
+ | {
91
+ status: "open";
92
+ links: {
93
+ server: string;
94
+ url: string;
95
+ }[];
96
+ }
97
+ /** It isn't running in this session, has no remote server, or the SDK can't sign in. */
98
+ | {
99
+ status: "unavailable";
100
+ reason: "not-running" | "not-remote" | "unsupported";
101
+ };
102
+ /** Signing in to installed extensions' remote servers, and out (#61). */
103
+ export interface ExtensionAuth {
104
+ /** The running extensions whose remote servers need the person to sign in. */
105
+ needed(): Promise<string[]>;
106
+ /**
107
+ * Starts signing in to `name`'s servers that need it, giving the links to open: the SDK's CLI
108
+ * waits for the browser at its localhost callback. With `callbackUrl` (the address the browser
109
+ * ended on, pasted from another computer), finishes it instead, and reconnects.
110
+ */
111
+ signIn(name: string, callbackUrl?: string): Promise<SignIn>;
112
+ /** Whether all of `name`'s remote servers are connected, waiting up to `timeoutMs` for it. */
113
+ connected(name: string, timeoutMs?: number): Promise<boolean>;
114
+ /** Signs out of `name`'s remote servers; false when it can't be done here. */
115
+ signOut(name: string): Promise<boolean>;
116
+ }
117
+ /** Signing in and out for a session's installed extensions. */
118
+ export declare function createExtensionAuth(extensions: readonly LiveExtension[], targets: Pick<LiveTargets, "controls">): ExtensionAuth;
@@ -97,6 +97,10 @@ export function createExtensionRestart(extensions, targets) {
97
97
  const reconnect = targets.controls().reconnectMcpServer;
98
98
  if (!extension || !reconnect || !extension.servers.length)
99
99
  return false;
100
+ // A remote server's URL and headers are fixed when the session opens: restarting it would keep
101
+ // the old values (#61), so its options take opening the session again.
102
+ if (extension.remoteServers.length)
103
+ return false;
100
104
  try {
101
105
  for (const server of extension.servers)
102
106
  await reconnect(server);
@@ -108,6 +112,71 @@ export function createExtensionRestart(extensions, targets) {
108
112
  }
109
113
  };
110
114
  }
115
+ /** Signing in and out for a session's installed extensions. */
116
+ export function createExtensionAuth(extensions, targets) {
117
+ const remote = (name) => extensions.find((candidate) => candidate.name === name)?.remoteServers ?? [];
118
+ const statuses = async () => {
119
+ const status = targets.controls().mcpServerStatus;
120
+ return new Map(status ? (await status().catch(() => [])).map((server) => [server.name, server.status]) : []);
121
+ };
122
+ return {
123
+ async needed() {
124
+ const now = await statuses();
125
+ return extensions.filter((extension) => extension.remoteServers.some((server) => now.get(server) === "needs-auth")).map((extension) => extension.name);
126
+ },
127
+ async signIn(name, callbackUrl) {
128
+ const extension = extensions.find((candidate) => candidate.name === name);
129
+ if (!extension)
130
+ return { status: "unavailable", reason: "not-running" };
131
+ if (!extension.remoteServers.length)
132
+ return { status: "unavailable", reason: "not-remote" };
133
+ const controls = targets.controls();
134
+ const now = await statuses();
135
+ const pending = extension.remoteServers.filter((server) => now.get(server) === "needs-auth");
136
+ if (!pending.length)
137
+ return { status: "signed-in" };
138
+ if (callbackUrl) {
139
+ if (!controls.mcpSubmitOAuthCallbackUrl || !controls.reconnectMcpServer)
140
+ return { status: "unavailable", reason: "unsupported" };
141
+ // A pasted callback stores the token, but the session stays needs-auth until the server
142
+ // reconnects (confirmed empirically, CLI 2.1.300).
143
+ for (const server of pending) {
144
+ await controls.mcpSubmitOAuthCallbackUrl(server, callbackUrl);
145
+ await controls.reconnectMcpServer(server);
146
+ }
147
+ return (await this.connected(name, 5000)) ? { status: "signed-in" } : { status: "open", links: [] };
148
+ }
149
+ if (!controls.mcpAuthenticate)
150
+ return { status: "unavailable", reason: "unsupported" };
151
+ const links = [];
152
+ for (const server of pending) {
153
+ const started = await controls.mcpAuthenticate(server);
154
+ if (started.authUrl)
155
+ links.push({ server, url: started.authUrl });
156
+ }
157
+ return { status: "open", links };
158
+ },
159
+ async connected(name, timeoutMs = 0) {
160
+ const servers = remote(name);
161
+ for (const start = Date.now();; await new Promise((resolve) => setTimeout(resolve, 500))) {
162
+ const now = await statuses();
163
+ if (servers.length && servers.every((server) => now.get(server) === "connected"))
164
+ return true;
165
+ if (Date.now() - start >= timeoutMs)
166
+ return false;
167
+ }
168
+ },
169
+ async signOut(name) {
170
+ const servers = remote(name);
171
+ const clear = targets.controls().mcpClearAuth;
172
+ if (!servers.length || !clear)
173
+ return false;
174
+ for (const server of servers)
175
+ await clear(server);
176
+ return true;
177
+ },
178
+ };
179
+ }
111
180
  /** The kit's lists, as the extension is now. */
112
181
  function follow(extension, enabled, targets) {
113
182
  const without = (list, items) => {
@@ -120,4 +120,5 @@ export declare function installFromMarketplace(dir: string, spec: string, scopeD
120
120
  commit?: string;
121
121
  marketplace: string;
122
122
  missing: string[];
123
+ remote: string[];
123
124
  }>;
@@ -35,7 +35,7 @@ export const de = {
35
35
  extensionSwitched: (name, enabled) => `(${name} ${enabled ? "aktiviert" : "deaktiviert"}, in derselben Sitzung)`,
36
36
  extensionToggled: (name, enabled) => `(${name} ${enabled ? "aktiviert" : "deaktiviert"}: die Sitzung wird mit derselben Unterhaltung neu geöffnet)`,
37
37
  extensionNotInstalled: (name) => `(${name} ist nicht installiert: /extensions listet sie auf)`,
38
- extensionsUsage: "(Verwendung: /extensions, enable|disable <Name>, config <Name> [Schlüssel=Wert…], search [Wörter], install <Name>, update [Name], remove <Name> [--delete-data], reload, trust)",
38
+ extensionsUsage: "(Verwendung: /extensions, enable|disable <Name>, config <Name> [Schlüssel=Wert…], search [Wörter], install <Name>, update [Name], remove <Name> [--delete-data], reload, trust, sign-in <Name> [Rückruf-URL], sign-out <Name>)",
39
39
  extensionsCantReopen: "(dieser Chat kann seine Sitzung nicht neu öffnen: im Terminal aktivieren oder deaktivieren und neu starten)",
40
40
  extensionOptions: (name) => `Optionen von ${name}:`,
41
41
  extensionNoOptions: (name) => `(${name} hat keine Optionen)`,
@@ -51,6 +51,15 @@ export const de = {
51
51
  extensionIsInstalled: "(installiert)",
52
52
  extensionUpdateAvailable: (installed) => `(installiert: ${installed}, Update verfügbar)`,
53
53
  extensionLacks: (name, what) => `(${name}: ${what})`,
54
+ extensionTalksTo: (name, hosts) => `(${name} spricht mit ${hosts}: was dein Agent dorthin sendet, verlässt diesen Computer)`,
55
+ extensionNeedsSignIn: (name) => `${name} braucht deine Anmeldung: /extensions sign-in ${name}`,
56
+ extensionSignInOpen: (name, url) => `(zum Anmelden bei ${name} öffne ${url} : sobald du dort fertig bist, ist sie hier verbunden)`,
57
+ extensionSignInPaste: (name) => `(auf einem anderen Computer? Füge die Adresse ein, bei der der Browser landet: /extensions sign-in ${name} <diese Adresse>)`,
58
+ extensionSignedIn: (name) => `(bei ${name} angemeldet: ihre Werkzeuge sind bereit)`,
59
+ extensionSignInWaiting: (name) => `(warte noch auf die Anmeldung bei ${name}; /extensions sign-in ${name} gibt den Link erneut)`,
60
+ extensionAlreadySignedIn: (name) => `(${name} ist bereits angemeldet)`,
61
+ extensionSignInUnavailable: (name) => `(bei ${name} kann man sich hier nicht anmelden: sie läuft nicht in dieser Sitzung, hat keinen entfernten Server, oder das SDK dieses Agenten kann es nicht)`,
62
+ extensionSignedOut: (name) => `(bei ${name} abgemeldet: sie bittet dich, dich erneut anzumelden)`,
54
63
  extensionInstalled: (name) => `(${name} installiert: in einem neuen Gespräch da, oder sofort mit /extensions reload, ohne dieses zu verlieren)`,
55
64
  extensionUpdated: (name, from, to) => `(${name} aktualisiert, ${from} → ${to}: in einem neuen Gespräch da, oder sofort mit /extensions reload)`,
56
65
  extensionsUpToDate: "(alle Erweiterungen aus einem Marketplace sind aktuell)",
@@ -68,6 +68,20 @@ export interface Messages {
68
68
  extensionInstalled(name: string): string;
69
69
  /** After installing: a program it needs that this computer lacks (#63); `what` is the reason, in English. */
70
70
  extensionLacks(name: string, what: string): string;
71
+ /** After installing one with remote servers (#61): the hosts it talks to. */
72
+ extensionTalksTo(name: string, hosts: string): string;
73
+ /** In /extensions: a running extension whose remote server needs the person to sign in. */
74
+ extensionNeedsSignIn(name: string): string;
75
+ /** The link to sign in to an extension's remote server. */
76
+ extensionSignInOpen(name: string, url: string): string;
77
+ /** How to finish signing in from another computer. */
78
+ extensionSignInPaste(name: string): string;
79
+ extensionSignedIn(name: string): string;
80
+ /** Signing in hasn't finished after a while. */
81
+ extensionSignInWaiting(name: string): string;
82
+ extensionAlreadySignedIn(name: string): string;
83
+ extensionSignInUnavailable(name: string): string;
84
+ extensionSignedOut(name: string): string;
71
85
  extensionUpdated(name: string, from: string, to: string): string;
72
86
  extensionsUpToDate: string;
73
87
  extensionRemoved(name: string, withData: boolean): string;
@@ -34,7 +34,7 @@ export const en = {
34
34
  extensionSwitched: (name, enabled) => `(${name} ${enabled ? "enabled" : "disabled"}, in this same session)`,
35
35
  extensionToggled: (name, enabled) => `(${name} ${enabled ? "enabled" : "disabled"}: the session reopens, with the same conversation)`,
36
36
  extensionNotInstalled: (name) => `(${name} isn't installed: /extensions lists them)`,
37
- extensionsUsage: "(usage: /extensions, enable|disable <name>, config <name> [key=value…], search [words], install <name>, update [name], remove <name> [--delete-data], reload, trust)",
37
+ extensionsUsage: "(usage: /extensions, enable|disable <name>, config <name> [key=value…], search [words], install <name>, update [name], remove <name> [--delete-data], reload, trust, sign-in <name> [callback URL], sign-out <name>)",
38
38
  extensionsCantReopen: "(this chat can't reopen its session: enable or disable it from the terminal, and start again)",
39
39
  extensionOptions: (name) => `${name}'s options:`,
40
40
  extensionNoOptions: (name) => `(${name} has no options)`,
@@ -51,6 +51,15 @@ export const en = {
51
51
  extensionUpdateAvailable: (installed) => `(installed: ${installed}, update available)`,
52
52
  extensionInstalled: (name) => `(${name} installed: it's there in a new conversation, or now with /extensions reload, keeping this one)`,
53
53
  extensionLacks: (name, what) => `(${name} ${what})`,
54
+ extensionTalksTo: (name, hosts) => `(${name} talks to ${hosts}: what your agent sends there leaves this computer)`,
55
+ extensionNeedsSignIn: (name) => `${name} needs you to sign in: /extensions sign-in ${name}`,
56
+ extensionSignInOpen: (name, url) => `(to sign in to ${name}, open ${url} : when you're done there, it's connected here)`,
57
+ extensionSignInPaste: (name) => `(on another computer? Paste the address the browser ends on: /extensions sign-in ${name} <that address>)`,
58
+ extensionSignedIn: (name) => `(signed in to ${name}: its tools are ready)`,
59
+ extensionSignInWaiting: (name) => `(still waiting for the sign-in to ${name}; /extensions sign-in ${name} gives the link again)`,
60
+ extensionAlreadySignedIn: (name) => `(${name} is already signed in)`,
61
+ extensionSignInUnavailable: (name) => `(${name} can't be signed in to here: it isn't running in this session, has no remote server, or this agent's SDK can't sign in)`,
62
+ extensionSignedOut: (name) => `(signed out of ${name}: it will ask you to sign in again)`,
54
63
  extensionUpdated: (name, from, to) => `(${name} updated, ${from} → ${to}: it's there in a new conversation, or now with /extensions reload)`,
55
64
  extensionsUpToDate: "(every extension from a marketplace is up to date)",
56
65
  extensionRemoved: (name, withData) => `(${name} removed${withData ? ", with its data" : "; its data is kept for a reinstall"})`,
@@ -35,7 +35,7 @@ export const es = {
35
35
  extensionSwitched: (name, enabled) => `(${name} ${enabled ? "activada" : "desactivada"}, en esta misma sesión)`,
36
36
  extensionToggled: (name, enabled) => `(${name} ${enabled ? "activada" : "desactivada"}: la sesión se reabre, con la misma conversación)`,
37
37
  extensionNotInstalled: (name) => `(${name} no está instalada: /extensions las lista)`,
38
- extensionsUsage: "(uso: /extensions, enable|disable <nombre>, config <nombre> [clave=valor…], search [palabras], install <nombre>, update [nombre], remove <nombre> [--delete-data], reload, trust)",
38
+ extensionsUsage: "(uso: /extensions, enable|disable <nombre>, config <nombre> [clave=valor…], search [palabras], install <nombre>, update [nombre], remove <nombre> [--delete-data], reload, trust, sign-in <nombre> [URL de vuelta], sign-out <nombre>)",
39
39
  extensionsCantReopen: "(este chat no puede reabrir su sesión: actívala o desactívala desde la terminal y vuelve a empezar)",
40
40
  extensionOptions: (name) => `Opciones de ${name}:`,
41
41
  extensionNoOptions: (name) => `(${name} no tiene opciones)`,
@@ -51,6 +51,15 @@ export const es = {
51
51
  extensionIsInstalled: "(instalada)",
52
52
  extensionUpdateAvailable: (installed) => `(instalada: ${installed}, hay actualización)`,
53
53
  extensionLacks: (name, what) => `(${name}: ${what})`,
54
+ extensionTalksTo: (name, hosts) => `(${name} habla con ${hosts}: lo que tu agente le envíe sale de este ordenador)`,
55
+ extensionNeedsSignIn: (name) => `${name} necesita que inicies sesión: /extensions sign-in ${name}`,
56
+ extensionSignInOpen: (name, url) => `(para iniciar sesión en ${name}, abre ${url} : cuando termines allí, queda conectada aquí)`,
57
+ extensionSignInPaste: (name) => `(¿en otro ordenador? Pega la dirección en la que acaba el navegador: /extensions sign-in ${name} <esa dirección>)`,
58
+ extensionSignedIn: (name) => `(sesión iniciada en ${name}: sus herramientas están listas)`,
59
+ extensionSignInWaiting: (name) => `(sigo esperando el inicio de sesión en ${name}; /extensions sign-in ${name} vuelve a dar el enlace)`,
60
+ extensionAlreadySignedIn: (name) => `(${name} ya tiene la sesión iniciada)`,
61
+ extensionSignInUnavailable: (name) => `(no se puede iniciar sesión en ${name} aquí: no se ejecuta en esta sesión, no tiene servidor remoto, o el SDK de este agente no sabe hacerlo)`,
62
+ extensionSignedOut: (name) => `(sesión cerrada en ${name}: te pedirá que vuelvas a iniciarla)`,
54
63
  extensionInstalled: (name) => `(${name} instalada: está en una conversación nueva, o ya con /extensions reload, sin perder esta)`,
55
64
  extensionUpdated: (name, from, to) => `(${name} actualizada, ${from} → ${to}: está en una conversación nueva, o ya con /extensions reload)`,
56
65
  extensionsUpToDate: "(todas las extensiones de un marketplace están al día)",
@@ -35,7 +35,7 @@ export const fr = {
35
35
  extensionSwitched: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"}, dans cette même session)`,
36
36
  extensionToggled: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"} : la session se rouvre, avec la même conversation)`,
37
37
  extensionNotInstalled: (name) => `(${name} n'est pas installée : /extensions les liste)`,
38
- extensionsUsage: "(usage : /extensions, enable|disable <nom>, config <nom> [clé=valeur…], search [mots], install <nom>, update [nom], remove <nom> [--delete-data], reload, trust)",
38
+ extensionsUsage: "(usage : /extensions, enable|disable <nom>, config <nom> [clé=valeur…], search [mots], install <nom>, update [nom], remove <nom> [--delete-data], reload, trust, sign-in <nom> [URL de retour], sign-out <nom>)",
39
39
  extensionsCantReopen: "(ce chat ne peut pas rouvrir sa session : activez-la ou désactivez-la depuis le terminal, puis recommencez)",
40
40
  extensionOptions: (name) => `Options de ${name} :`,
41
41
  extensionNoOptions: (name) => `(${name} n'a pas d'options)`,
@@ -51,6 +51,15 @@ export const fr = {
51
51
  extensionIsInstalled: "(installée)",
52
52
  extensionUpdateAvailable: (installed) => `(installée : ${installed}, mise à jour disponible)`,
53
53
  extensionLacks: (name, what) => `(${name} : ${what})`,
54
+ extensionTalksTo: (name, hosts) => `(${name} échange avec ${hosts} : ce que votre agent y envoie quitte cet ordinateur)`,
55
+ extensionNeedsSignIn: (name) => `${name} a besoin que vous vous connectiez : /extensions sign-in ${name}`,
56
+ extensionSignInOpen: (name, url) => `(pour vous connecter à ${name}, ouvrez ${url} : une fois terminé là-bas, elle est connectée ici)`,
57
+ extensionSignInPaste: (name) => `(sur un autre ordinateur ? Collez l'adresse où le navigateur arrive : /extensions sign-in ${name} <cette adresse>)`,
58
+ extensionSignedIn: (name) => `(connecté à ${name} : ses outils sont prêts)`,
59
+ extensionSignInWaiting: (name) => `(toujours en attente de la connexion à ${name} ; /extensions sign-in ${name} redonne le lien)`,
60
+ extensionAlreadySignedIn: (name) => `(${name} est déjà connectée)`,
61
+ extensionSignInUnavailable: (name) => `(impossible de se connecter à ${name} ici : elle ne tourne pas dans cette session, n'a pas de serveur distant, ou le SDK de cet agent ne sait pas le faire)`,
62
+ extensionSignedOut: (name) => `(déconnecté de ${name} : elle vous demandera de vous reconnecter)`,
54
63
  extensionInstalled: (name) => `(${name} installée : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload, en gardant celle-ci)`,
55
64
  extensionUpdated: (name, from, to) => `(${name} mise à jour, ${from} → ${to} : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload)`,
56
65
  extensionsUpToDate: "(toutes les extensions d'un marketplace sont à jour)",
@@ -1,4 +1,4 @@
1
- import type { ExtensionDirs, PluginServer } from "./externalExtensions.js";
1
+ import type { ExtensionDirs, LocalServer } from "./externalExtensions.js";
2
2
  /**
3
3
  * Installed extensions' servers run with `npx` (#54): a `.mcp.json` that says
4
4
  * `npx -y <package>@<x.y.z>` runs as it is in Claude Code, and in the kit too, which doesn't run
@@ -32,7 +32,7 @@ export declare const packageSpec: (server: NpxServer) => string;
32
32
  * does but not as the kit can, or its package. Its command is `npx` (`npx.cmd`), its options
33
33
  * only `-y`/`--yes`, then `<package>@<x.y.z>` and the server's own arguments.
34
34
  */
35
- export declare function npxServer(server: PluginServer): NpxServer | string | undefined;
35
+ export declare function npxServer(server: LocalServer): NpxServer | string | undefined;
36
36
  /** Where an agent keeps the packages its extensions' npx servers run: in its scope (the project's when it has none), shared by both. */
37
37
  export declare function packageCacheDir(dirs: ExtensionDirs): string | undefined;
38
38
  /** A package's folder in the cache. */
@@ -19,6 +19,22 @@ export function toolResultText(content) {
19
19
  .map((part) => part.text)
20
20
  .join("\n");
21
21
  }
22
+ /**
23
+ * The SDK's sign-in methods for a remote MCP server behind OAuth, when its `Query` has them: they
24
+ * aren't in its typings (confirmed empirically, SDK 0.3.300; ADR-028), so each is checked first.
25
+ */
26
+ function signInControls(result) {
27
+ const methods = result;
28
+ const call = (name) => (typeof methods[name] === "function" ? (...args) => methods[name].apply(result, args) : undefined);
29
+ const authenticate = call("mcpAuthenticate");
30
+ const submit = call("mcpSubmitOAuthCallbackUrl");
31
+ const clear = call("mcpClearAuth");
32
+ return {
33
+ ...(authenticate ? { mcpAuthenticate: (server) => authenticate(server) } : {}),
34
+ ...(submit ? { mcpSubmitOAuthCallbackUrl: (server, callbackUrl) => submit(server, callbackUrl) } : {}),
35
+ ...(clear ? { mcpClearAuth: (server) => clear(server) } : {}),
36
+ };
37
+ }
22
38
  /**
23
39
  * Thin wrapper around the SDK's own `query()`: same two-argument shape (a plain string
24
40
  * for a one-shot run, or an `AsyncIterable<SDKUserMessage>` — see `createInputQueue()` —
@@ -38,6 +54,7 @@ export function runQuery(prompt, options) {
38
54
  reloadPlugins: () => result.reloadPlugins(),
39
55
  reconnectMcpServer: (server) => result.reconnectMcpServer(server),
40
56
  mcpServerStatus: () => result.mcpServerStatus(),
57
+ ...signInControls(result),
41
58
  });
42
59
  // The main agent's tool calls by id, to name the results that come back for them.
43
60
  const toolNames = new Map();
@@ -1,4 +1,4 @@
1
- import type { Options, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
1
+ import type { McpServerStatus, Options, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { type TranscriptLogger } from "./hooks/transcriptLogger.js";
3
3
  import { type ModeControl } from "./modeControl.js";
4
4
  import { type AboutExtension } from "./extensions.js";
@@ -7,7 +7,7 @@ import type { ToolLabels } from "./toolLabels.js";
7
7
  import type { AgentSpec, BaseSessionConfig } from "./agentSpec.js";
8
8
  import { type Language } from "./language.js";
9
9
  import { type RunFolder } from "./runs.js";
10
- import { type ExtensionRestart, type ExtensionSwitch } from "./liveExtensions.js";
10
+ import { type ExtensionAuth, type ExtensionRestart, type ExtensionSwitch } from "./liveExtensions.js";
11
11
  /**
12
12
  * Builds the `options` object passed to the Agent SDK's `query()` — everything about
13
13
  * *how* the agent runs a session (system prompt, tools, MCP servers, hooks), independent
@@ -47,6 +47,8 @@ export declare function buildSessionOptions<TConfig extends BaseSessionConfig>(c
47
47
  switchExtension: ExtensionSwitch;
48
48
  /** Restarts an installed extension's servers in the running session, with its options as they are now (#55). */
49
49
  restartExtension: ExtensionRestart;
50
+ /** Signing in to installed extensions' remote servers, and out (#61). */
51
+ extensionAuth: ExtensionAuth;
50
52
  }>;
51
53
  export { createModeControl, togglePlanMode, type ModeControl } from "./modeControl.js";
52
54
  /** The extensions of a session: where they're installed, what's installed, what runs and what can't (with why). */
@@ -73,6 +75,8 @@ export interface ExtensionsStatus {
73
75
  }[];
74
76
  /** The running session's tools, as the model names them (empty until it has started): what a capability's tools are checked against. */
75
77
  sessionTools?: () => readonly string[];
78
+ /** Its MCP servers as they are now (empty until it has started): which remote ones need signing in (#61). */
79
+ serverStatus?: () => Promise<McpServerStatus[]>;
76
80
  }
77
81
  /**
78
82
  * The CLI's folder for a project, where it keeps each session's transcript and large tool
@@ -22,7 +22,7 @@ import { replyLanguageInstruction } from "./language.js";
22
22
  import { chooseLanguage } from "./messages/index.js";
23
23
  import { createRunStore } from "./runs.js";
24
24
  import { createSessionView, registerSessionView } from "./sessionFacts.js";
25
- import { createExtensionRestart, createExtensionSwitch, mountPlugin } from "./liveExtensions.js";
25
+ import { createExtensionAuth, createExtensionRestart, createExtensionSwitch, mountPlugin } from "./liveExtensions.js";
26
26
  import { secretsDir } from "./extensionOptions.js";
27
27
  import { createElicitationHandler } from "./elicitation.js";
28
28
  import { createReadOnlyHints } from "./readOnlyHints.js";
@@ -358,6 +358,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
358
358
  active: resolved.active.map(({ extension }) => extension.name),
359
359
  about: Object.fromEntries(resolved.active.map(({ extension, manifest, unavailable }, index) => [extension.name, aboutExtension(manifest, Object.keys(added[index]?.mcpServers ?? {}), language, unavailable)])),
360
360
  sessionTools: () => late.view?.controls().tools ?? [],
361
+ serverStatus: async () => (await late.view?.controls().mcpServerStatus?.().catch(() => [])) ?? [],
361
362
  inactive: resolved.inactive,
362
363
  };
363
364
  // What the session is, for the extensions that ask (sessionFacts.ts, #43).
@@ -405,6 +406,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
405
406
  provides: manifest.provides,
406
407
  capabilities: manifest.capabilities,
407
408
  unavailable,
409
+ remoteServers: contribution?.remoteServers ?? [],
408
410
  help: contribution?.helpLines ?? [],
409
411
  },
410
412
  ];
@@ -412,8 +414,9 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
412
414
  const liveTargets = { readOnlyTools, allowedSubagentTypes, toolLabels, status: extensions, facts: viewBase, controls: () => late.view?.controls() ?? {} };
413
415
  const switchExtension = createExtensionSwitch(liveExtensions, liveTargets);
414
416
  const restartExtension = createExtensionRestart(liveExtensions, liveTargets);
417
+ const extensionAuth = createExtensionAuth(liveExtensions, liveTargets);
415
418
  const apis = Object.fromEntries(resolved.active.flatMap(({ extension }, index) => (added[index]?.api ? [[extension.name, added[index].api]] : [])));
416
- return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language, toolLabels, extensions, apis, switchExtension, restartExtension };
419
+ return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language, toolLabels, extensions, apis, switchExtension, restartExtension, extensionAuth };
417
420
  }
418
421
  export { createModeControl, togglePlanMode } from "./modeControl.js";
419
422
  /** The Claude Code configuration folder the SDK's CLI uses: `CLAUDE_CONFIG_DIR`, or `~/.claude`. */
@@ -71,6 +71,20 @@ export interface SessionLive {
71
71
  reconnectMcpServer?: (server: string) => Promise<void>;
72
72
  /** Its MCP servers as they are now: their tools and what each says of itself (plan mode's read-only hints). */
73
73
  mcpServerStatus?: () => Promise<McpServerStatus[]>;
74
+ /**
75
+ * Signing in to a remote server behind OAuth (#61): the SDK's `Query` has these, undocumented
76
+ * (in `sdk.mjs`, not in `sdk.d.ts`; confirmed empirically, SDK 0.3.300). Attached only when they
77
+ * exist, so a session whose SDK lacks them just can't sign in.
78
+ */
79
+ mcpAuthenticate?: (server: string) => Promise<McpAuthentication>;
80
+ mcpSubmitOAuthCallbackUrl?: (server: string, callbackUrl: string) => Promise<unknown>;
81
+ mcpClearAuth?: (server: string) => Promise<unknown>;
82
+ }
83
+ /** What starting a remote server's sign-in gives (the SDK's `mcpAuthenticate()`): the link for the person, and whether the CLI waits for its localhost callback. */
84
+ export interface McpAuthentication {
85
+ authUrl?: string;
86
+ requiresUserAction?: boolean;
87
+ callbackExpected?: boolean;
74
88
  }
75
89
  /** The facts known when the options are built: everything but the mode and what only the running session knows. */
76
90
  export type StaticSessionFacts = Omit<SessionFacts, "mode" | "switchableModes" | "tools" | "commands" | "context" | "extensions"> & {
@@ -0,0 +1,8 @@
1
+ import { z } from "zod";
2
+ import type { BaseSessionConfig } from "../../core/agentSpec.js";
3
+ import type { SessionFacts } from "../../core/sessionFacts.js";
4
+ /** The awareness's catalog tool, over the agent's extension folders and its own marketplace and topics. */
5
+ export declare function createCatalogTool(config: BaseSessionConfig, session: () => Promise<SessionFacts>): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<{
6
+ words: z.ZodOptional<z.ZodString>;
7
+ refresh: z.ZodOptional<z.ZodBoolean>;
8
+ }>;