@falkenslab/agent-kit 0.24.0 → 0.26.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.
- package/README.md +2 -0
- package/dist/chat/chatController.d.ts +10 -0
- package/dist/chat/chatController.js +39 -1
- package/dist/chat/extensions.d.ts +9 -0
- package/dist/chat/extensions.js +18 -1
- package/dist/chat/runs.d.ts +3 -1
- package/dist/chat/runs.js +1 -1
- package/dist/core/agentSpec.d.ts +16 -0
- package/dist/core/claudeAiConnectors.d.ts +35 -0
- package/dist/core/claudeAiConnectors.js +76 -0
- package/dist/core/extensionCatalog.d.ts +6 -1
- package/dist/core/extensionCatalog.js +10 -8
- package/dist/core/extensionOptions.d.ts +5 -0
- package/dist/core/extensionOptions.js +10 -0
- package/dist/core/extensions.d.ts +6 -0
- package/dist/core/externalExtensions.d.ts +24 -1
- package/dist/core/externalExtensions.js +78 -12
- package/dist/core/hooks/remoteGate.d.ts +22 -0
- package/dist/core/hooks/remoteGate.js +35 -0
- package/dist/core/liveExtensions.d.ts +38 -0
- package/dist/core/liveExtensions.js +69 -0
- package/dist/core/marketplaces.d.ts +1 -0
- package/dist/core/messages/de.js +13 -1
- package/dist/core/messages/en.d.ts +20 -0
- package/dist/core/messages/en.js +13 -1
- package/dist/core/messages/es.js +13 -1
- package/dist/core/messages/fr.js +13 -1
- package/dist/core/npxPackages.d.ts +2 -2
- package/dist/core/runner.js +41 -0
- package/dist/core/session.d.ts +11 -2
- package/dist/core/session.js +47 -3
- package/dist/core/sessionFacts.d.ts +20 -0
- package/dist/core/sessionFacts.js +1 -0
- package/dist/extensions/awareness/catalog.d.ts +8 -0
- package/dist/extensions/awareness/catalog.js +92 -0
- package/dist/extensions/awareness/index.d.ts +3 -1
- package/dist/extensions/awareness/index.js +15 -6
- package/dist/extensions/awareness/labels.js +8 -5
- package/dist/extensions/awareness/tools.d.ts +2 -2
- package/dist/extensions/awareness/tools.js +10 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/tui/extensionCommand.js +11 -6
- package/extensions/awareness/skills/extensions/SKILL.md +56 -0
- package/package.json +1 -1
- package/schemas/marketplace-catalog.schema.json +6 -0
|
@@ -10,10 +10,37 @@ 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
|
|
@@ -155,9 +182,20 @@ async function pluginServers(dir, raw) {
|
|
|
155
182
|
throw new Error("its MCP servers aren't a JSON object.");
|
|
156
183
|
const servers = {};
|
|
157
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
|
+
}
|
|
158
192
|
if (!server || typeof server.command !== "string")
|
|
159
193
|
throw new Error(`its MCP server "${name}" has no command.`);
|
|
160
|
-
servers[name] = {
|
|
194
|
+
servers[name] = {
|
|
195
|
+
command: server.command,
|
|
196
|
+
...(Array.isArray(server.args) ? { args: server.args.map(String) } : {}),
|
|
197
|
+
...(server.env ? { env: server.env } : {}),
|
|
198
|
+
};
|
|
161
199
|
}
|
|
162
200
|
return servers;
|
|
163
201
|
}
|
|
@@ -201,6 +239,12 @@ function serverLaunch(name, server, folders, packages = {}) {
|
|
|
201
239
|
*/
|
|
202
240
|
async function serversProblem(manifest, root, packages) {
|
|
203
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
|
+
}
|
|
204
248
|
const npx = npxServer(server);
|
|
205
249
|
if (typeof npx === "string")
|
|
206
250
|
return `${npx} (its server "${name}")`;
|
|
@@ -222,6 +266,8 @@ async function serversProblem(manifest, root, packages) {
|
|
|
222
266
|
async function fetchServerPackages(manifest, cache) {
|
|
223
267
|
const packages = {};
|
|
224
268
|
for (const server of Object.values(manifest.servers)) {
|
|
269
|
+
if (isRemoteServer(server))
|
|
270
|
+
continue;
|
|
225
271
|
const npx = npxServer(server);
|
|
226
272
|
if (!npx || typeof npx === "string")
|
|
227
273
|
continue;
|
|
@@ -312,8 +358,8 @@ export function extensionLauncherPath() {
|
|
|
312
358
|
// Node runs it as another process: outside the archive when packaged (#42).
|
|
313
359
|
return outsideArchive(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "assets", "extension-launcher.mjs"));
|
|
314
360
|
}
|
|
315
|
-
/** Its tools' chat labels in the kit's language, from its manifest: `{field}` takes the input; for each of its servers. */
|
|
316
|
-
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) {
|
|
317
363
|
const language = getLanguage();
|
|
318
364
|
const fill = (template, input) => template.replace(/\{(\w+)\}/g, (_, field) => (input[field] === undefined ? "" : truncate(String(input[field]), 60)));
|
|
319
365
|
return Object.fromEntries(Object.entries(manifest.labels).flatMap(([tool, byLanguage]) => {
|
|
@@ -321,7 +367,7 @@ function manifestToolLabels(manifest) {
|
|
|
321
367
|
if (!texts)
|
|
322
368
|
return [];
|
|
323
369
|
const label = { label: (input) => fill(texts.label, input), ...(texts.phrase ? { phrase: texts.phrase } : {}) };
|
|
324
|
-
return
|
|
370
|
+
return sessionServers.map((server) => [`mcp__${server}__${tool}`, label]);
|
|
325
371
|
}));
|
|
326
372
|
}
|
|
327
373
|
/** The extension an installed one makes: its servers started through the launcher, its rules from its manifest. */
|
|
@@ -334,10 +380,27 @@ function externalExtension(installed, manifest, cache, unavailable = []) {
|
|
|
334
380
|
...(unavailable.length ? { unavailable } : {}),
|
|
335
381
|
async contribute() {
|
|
336
382
|
const servers = {};
|
|
383
|
+
const remote = [];
|
|
337
384
|
if (Object.keys(manifest.servers).length)
|
|
338
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) : {};
|
|
339
390
|
for (const [name, server] of Object.entries(manifest.servers)) {
|
|
340
|
-
|
|
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 });
|
|
341
404
|
if (typeof launch === "string")
|
|
342
405
|
continue; // checked when it loaded
|
|
343
406
|
// Its `env` reaches it (the SDK merges it with the agent's); the launcher removes the rest.
|
|
@@ -359,9 +422,10 @@ function externalExtension(installed, manifest, cache, unavailable = []) {
|
|
|
359
422
|
// Its manifest's list, or its servers' own word: the server is the code the kit trusts
|
|
360
423
|
// least, so its hints only fill the list's absence (ADR-025).
|
|
361
424
|
...(manifest.readOnlyTools
|
|
362
|
-
? { readOnlyTools: Object.keys(
|
|
425
|
+
? { readOnlyTools: Object.keys(servers).flatMap((server) => manifest.readOnlyTools.map((tool) => `mcp__${server}__${tool}`)) }
|
|
363
426
|
: { readOnlyTools: [], readOnlyHintServers: Object.keys(servers) }),
|
|
364
|
-
toolLabels: manifestToolLabels(manifest),
|
|
427
|
+
toolLabels: manifestToolLabels(manifest, Object.keys(servers)),
|
|
428
|
+
...(remote.length ? { remoteServers: remote } : {}),
|
|
365
429
|
...(manifest.help ? { helpLines: [manifest.help] } : {}),
|
|
366
430
|
};
|
|
367
431
|
},
|
|
@@ -373,7 +437,7 @@ function optionDefaults(declared) {
|
|
|
373
437
|
}
|
|
374
438
|
/** Why its options keep it from running (a server naming one it doesn't declare, a required one without a value), or nothing. */
|
|
375
439
|
function optionsProblem(manifest, values) {
|
|
376
|
-
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)]));
|
|
377
441
|
const undeclared = named.filter((key) => !manifest.userConfig[key]);
|
|
378
442
|
if (undeclared.length)
|
|
379
443
|
return `names ${undeclared.map((key) => "${user_config." + key + "}").join(", ")}, which its userConfig doesn't declare`;
|
|
@@ -490,6 +554,8 @@ export async function addExtension(source, scopeDir, options = {}) {
|
|
|
490
554
|
throw new Error(`${manifest.name} can't be installed: it ${servers}.`);
|
|
491
555
|
// What it needs and this computer lacks: said, never installed, and it installs anyway (#63).
|
|
492
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);
|
|
493
559
|
const target = path.join(scopeDir, manifest.name);
|
|
494
560
|
// Replacing what's installed there would delete the source itself.
|
|
495
561
|
if (path.resolve(from) === path.resolve(target))
|
|
@@ -516,12 +582,12 @@ export async function addExtension(source, scopeDir, options = {}) {
|
|
|
516
582
|
}
|
|
517
583
|
lock[manifest.name] = { source: origin, ...provenance, sha256, stored: true, ...locked, enabled: true, installed: new Date().toISOString() };
|
|
518
584
|
await writeLock(scopeDir, lock);
|
|
519
|
-
return { name: manifest.name, replaced, missing, ...(commit ? { commit } : {}) };
|
|
585
|
+
return { name: manifest.name, replaced, missing, remote, ...(commit ? { commit } : {}) };
|
|
520
586
|
}
|
|
521
587
|
if (options.link) {
|
|
522
588
|
lock[manifest.name] = { source: from, linked: true, sha256: "", ...locked, enabled: true, installed: new Date().toISOString() };
|
|
523
589
|
await writeLock(scopeDir, lock);
|
|
524
|
-
return { name: manifest.name, replaced, missing };
|
|
590
|
+
return { name: manifest.name, replaced, missing, remote };
|
|
525
591
|
}
|
|
526
592
|
await cp(from, target, { recursive: true, filter: (file) => path.basename(file) !== ".git" });
|
|
527
593
|
lock[manifest.name] = {
|
|
@@ -533,7 +599,7 @@ export async function addExtension(source, scopeDir, options = {}) {
|
|
|
533
599
|
installed: new Date().toISOString(),
|
|
534
600
|
};
|
|
535
601
|
await writeLock(scopeDir, lock);
|
|
536
|
-
return { name: manifest.name, replaced, missing, ...(commit ? { commit } : {}) };
|
|
602
|
+
return { name: manifest.name, replaced, missing, remote, ...(commit ? { commit } : {}) };
|
|
537
603
|
}
|
|
538
604
|
finally {
|
|
539
605
|
if (clone)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
/** What a call to a remote MCP server does (#70): goes on, asks the person first, or is blocked. */
|
|
3
|
+
export type RemoteServersPolicy = "allow" | "ask" | "deny";
|
|
4
|
+
/**
|
|
5
|
+
* PreToolUse hook for remote MCP servers (#70): what the model sends one leaves the computer.
|
|
6
|
+
* - A claude.ai connector's tool the agent didn't choose is always blocked: a connector the
|
|
7
|
+
* account gained after the session opened, which the session's denylist doesn't name.
|
|
8
|
+
* - A remote server's tool (an installed extension's, the agent's own, a chosen connector) goes
|
|
9
|
+
* on with `allow`; with `ask`, the person approves each call, told where it goes (blocked when
|
|
10
|
+
* no one can be asked); with `deny`, it's blocked.
|
|
11
|
+
* Approving gives no decision of its own, so the other gates (plan mode, the file scope) still judge.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createRemoteGate(options: {
|
|
14
|
+
runDir: string;
|
|
15
|
+
policy: RemoteServersPolicy;
|
|
16
|
+
/** Where a tool's call goes (a host, a connector), or nothing when it isn't remote. */
|
|
17
|
+
destination: (tool: string) => string | undefined;
|
|
18
|
+
/** The chosen connectors' tool prefixes (`serverToolPrefix()`). */
|
|
19
|
+
allowedConnectors: readonly string[];
|
|
20
|
+
/** Whether a person can be asked now (not autonomous). */
|
|
21
|
+
canAsk: () => boolean;
|
|
22
|
+
}): HookCallback;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { askForDecision } from "./humanInput.js";
|
|
2
|
+
import { isConnectorTool } from "../claudeAiConnectors.js";
|
|
3
|
+
import { t } from "../messages/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* PreToolUse hook for remote MCP servers (#70): what the model sends one leaves the computer.
|
|
6
|
+
* - A claude.ai connector's tool the agent didn't choose is always blocked: a connector the
|
|
7
|
+
* account gained after the session opened, which the session's denylist doesn't name.
|
|
8
|
+
* - A remote server's tool (an installed extension's, the agent's own, a chosen connector) goes
|
|
9
|
+
* on with `allow`; with `ask`, the person approves each call, told where it goes (blocked when
|
|
10
|
+
* no one can be asked); with `deny`, it's blocked.
|
|
11
|
+
* Approving gives no decision of its own, so the other gates (plan mode, the file scope) still judge.
|
|
12
|
+
*/
|
|
13
|
+
export function createRemoteGate(options) {
|
|
14
|
+
const deny = (input, reason) => ({ hookSpecificOutput: { hookEventName: input.hook_event_name, permissionDecision: "deny", permissionDecisionReason: reason } });
|
|
15
|
+
return async (input) => {
|
|
16
|
+
const pre = input;
|
|
17
|
+
const tool = pre.tool_name;
|
|
18
|
+
if (isConnectorTool(tool) && !options.allowedConnectors.some((prefix) => tool.startsWith(prefix))) {
|
|
19
|
+
return deny(pre, "This agent doesn't use that claude.ai connector: it isn't one of the connectors it lets in. Don't call it again.");
|
|
20
|
+
}
|
|
21
|
+
const destination = options.destination(tool);
|
|
22
|
+
if (!destination || options.policy === "allow")
|
|
23
|
+
return {};
|
|
24
|
+
if (options.policy === "deny" || !options.canAsk()) {
|
|
25
|
+
return deny(pre, `This agent sends nothing to remote servers here: calling ${destination} isn't allowed. Do the task without it, or tell the person it needs that service.`);
|
|
26
|
+
}
|
|
27
|
+
const answer = await askForDecision(options.runDir, {
|
|
28
|
+
title: t().remoteCallTitle(destination),
|
|
29
|
+
lines: [t().toolLine(tool), t().parametersLine(JSON.stringify(pre.tool_input, null, 2))],
|
|
30
|
+
});
|
|
31
|
+
if (answer === "" || answer === "y" || answer === "yes")
|
|
32
|
+
return {};
|
|
33
|
+
return deny(pre, `The person didn't let this call go to ${destination}.`);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -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) => {
|
package/dist/core/messages/de.js
CHANGED
|
@@ -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,18 @@ 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)`,
|
|
63
|
+
remoteCallTitle: (destination) => `Das an ${destination} senden? Es verlässt diesen Computer`,
|
|
64
|
+
connectorsAllowed: (names) => `claude.ai-Connectoren: ${names}`,
|
|
65
|
+
connectorsMissing: (names) => `Nicht in deinem claude.ai-Konto: ${names} (verbinde ihn auf claude.ai und öffne die Sitzung neu)`,
|
|
54
66
|
extensionInstalled: (name) => `(${name} installiert: in einem neuen Gespräch da, oder sofort mit /extensions reload, ohne dieses zu verlieren)`,
|
|
55
67
|
extensionUpdated: (name, from, to) => `(${name} aktualisiert, ${from} → ${to}: in einem neuen Gespräch da, oder sofort mit /extensions reload)`,
|
|
56
68
|
extensionsUpToDate: "(alle Erweiterungen aus einem Marketplace sind aktuell)",
|
|
@@ -68,6 +68,26 @@ 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;
|
|
85
|
+
/** Before a call to a remote MCP server, when the agent asks first (#70): where it goes. */
|
|
86
|
+
remoteCallTitle(destination: string): string;
|
|
87
|
+
/** In /extensions: the claude.ai connectors let in (#70). */
|
|
88
|
+
connectorsAllowed(names: string): string;
|
|
89
|
+
/** In /extensions: connectors the agent asked for that the account hasn't. */
|
|
90
|
+
connectorsMissing(names: string): string;
|
|
71
91
|
extensionUpdated(name: string, from: string, to: string): string;
|
|
72
92
|
extensionsUpToDate: string;
|
|
73
93
|
extensionRemoved(name: string, withData: boolean): string;
|
package/dist/core/messages/en.js
CHANGED
|
@@ -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,18 @@ 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)`,
|
|
63
|
+
remoteCallTitle: (destination) => `Send this to ${destination}? It leaves this computer`,
|
|
64
|
+
connectorsAllowed: (names) => `claude.ai connectors: ${names}`,
|
|
65
|
+
connectorsMissing: (names) => `Not in your claude.ai account: ${names} (connect it on claude.ai, then reopen the session)`,
|
|
54
66
|
extensionUpdated: (name, from, to) => `(${name} updated, ${from} → ${to}: it's there in a new conversation, or now with /extensions reload)`,
|
|
55
67
|
extensionsUpToDate: "(every extension from a marketplace is up to date)",
|
|
56
68
|
extensionRemoved: (name, withData) => `(${name} removed${withData ? ", with its data" : "; its data is kept for a reinstall"})`,
|
package/dist/core/messages/es.js
CHANGED
|
@@ -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,18 @@ 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)`,
|
|
63
|
+
remoteCallTitle: (destination) => `¿Enviar esto a ${destination}? Sale de este ordenador`,
|
|
64
|
+
connectorsAllowed: (names) => `Conectores de claude.ai: ${names}`,
|
|
65
|
+
connectorsMissing: (names) => `No están en tu cuenta de claude.ai: ${names} (conéctalo en claude.ai y vuelve a abrir la sesión)`,
|
|
54
66
|
extensionInstalled: (name) => `(${name} instalada: está en una conversación nueva, o ya con /extensions reload, sin perder esta)`,
|
|
55
67
|
extensionUpdated: (name, from, to) => `(${name} actualizada, ${from} → ${to}: está en una conversación nueva, o ya con /extensions reload)`,
|
|
56
68
|
extensionsUpToDate: "(todas las extensiones de un marketplace están al día)",
|
package/dist/core/messages/fr.js
CHANGED
|
@@ -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,18 @@ 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)`,
|
|
63
|
+
remoteCallTitle: (destination) => `Envoyer ceci à ${destination} ? Cela quitte cet ordinateur`,
|
|
64
|
+
connectorsAllowed: (names) => `Connecteurs claude.ai : ${names}`,
|
|
65
|
+
connectorsMissing: (names) => `Absents de votre compte claude.ai : ${names} (connectez-le sur claude.ai, puis rouvrez la session)`,
|
|
54
66
|
extensionInstalled: (name) => `(${name} installée : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload, en gardant celle-ci)`,
|
|
55
67
|
extensionUpdated: (name, from, to) => `(${name} mise à jour, ${from} → ${to} : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload)`,
|
|
56
68
|
extensionsUpToDate: "(toutes les extensions d'un marketplace sont à jour)",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionDirs,
|
|
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:
|
|
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. */
|
package/dist/core/runner.js
CHANGED
|
@@ -19,6 +19,45 @@ 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
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Turns back on the servers the CLI saved as switched off in an earlier session (#75):
|
|
40
|
+
* `toggleMcpServer(name, false)` is saved in the Claude Code config of the session's folder
|
|
41
|
+
* (`~/.claude.json`, `disabledMcpServers`), not only in the session (confirmed empirically, CLI
|
|
42
|
+
* 2.1.300), so a server the kit passes again would start disabled. The kit passes only what it
|
|
43
|
+
* wants running, so one of them that starts disabled is turned on, which also removes the saved
|
|
44
|
+
* entry. Only while the session's servers start: switching one off later in the session is the
|
|
45
|
+
* live switch's (liveExtensions.ts).
|
|
46
|
+
*/
|
|
47
|
+
async function restoreSavedSwitches(result, options) {
|
|
48
|
+
const passed = new Set(Object.keys(options.mcpServers ?? {}));
|
|
49
|
+
if (!passed.size)
|
|
50
|
+
return;
|
|
51
|
+
for (let attempt = 0; attempt < 30; attempt++) {
|
|
52
|
+
const status = (await result.mcpServerStatus().catch(() => [])).filter((server) => passed.has(server.name));
|
|
53
|
+
for (const server of status)
|
|
54
|
+
if (server.status === "disabled")
|
|
55
|
+
await result.toggleMcpServer(server.name, true).catch(() => undefined);
|
|
56
|
+
if (status.length && !status.some((server) => server.status === "pending" || server.status === "disabled"))
|
|
57
|
+
return;
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
22
61
|
/**
|
|
23
62
|
* Thin wrapper around the SDK's own `query()`: same two-argument shape (a plain string
|
|
24
63
|
* for a one-shot run, or an `AsyncIterable<SDKUserMessage>` — see `createInputQueue()` —
|
|
@@ -38,7 +77,9 @@ export function runQuery(prompt, options) {
|
|
|
38
77
|
reloadPlugins: () => result.reloadPlugins(),
|
|
39
78
|
reconnectMcpServer: (server) => result.reconnectMcpServer(server),
|
|
40
79
|
mcpServerStatus: () => result.mcpServerStatus(),
|
|
80
|
+
...signInControls(result),
|
|
41
81
|
});
|
|
82
|
+
void restoreSavedSwitches(result, options);
|
|
42
83
|
// The main agent's tool calls by id, to name the results that come back for them.
|
|
43
84
|
const toolNames = new Map();
|
|
44
85
|
async function* generateEvents() {
|