@falkenslab/agent-kit 0.24.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.
- 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 +13 -1
- package/dist/chat/runs.d.ts +3 -1
- package/dist/chat/runs.js +1 -1
- 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/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 +10 -1
- package/dist/core/messages/en.d.ts +14 -0
- package/dist/core/messages/en.js +10 -1
- package/dist/core/messages/es.js +10 -1
- package/dist/core/messages/fr.js +10 -1
- package/dist/core/npxPackages.d.ts +2 -2
- package/dist/core/runner.js +17 -0
- package/dist/core/session.d.ts +6 -2
- package/dist/core/session.js +5 -2
- package/dist/core/sessionFacts.d.ts +14 -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 +4 -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
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](https://falkenslab.github.io/agent-kit/)
|
|
11
11
|
|
|
12
|
+
**[📖 Documentation](https://falkenslab.github.io/agent-kit/)** · [📖 Documentación en español](https://falkenslab.github.io/agent-kit/es/)
|
|
13
|
+
|
|
12
14
|
A foundation for building AI agents on top of the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). It isn't an agent itself: it solves once what every agent needs (human oversight, safety, memory and a good terminal interface), so each concrete agent only has to write what belongs to its own domain: what it knows how to do, with which tools, and how it talks.
|
|
13
15
|
|
|
14
16
|
## What an agent built with it can do
|
|
@@ -9,6 +9,7 @@ import { type ExtensionsStatus, type ModeControl } from "../core/session.js";
|
|
|
9
9
|
import { type Todo } from "../core/todos.js";
|
|
10
10
|
import { type ToolLabels } from "../core/toolLabels.js";
|
|
11
11
|
import type { ToolPhrase } from "../core/messages/index.js";
|
|
12
|
+
import type { SignIn } from "../core/liveExtensions.js";
|
|
12
13
|
import { type ExtensionOptionState, type ExtensionScope } from "../core/externalExtensions.js";
|
|
13
14
|
import { type CatalogEntry } from "../core/extensionCatalog.js";
|
|
14
15
|
import { type SessionOpener } from "./runs.js";
|
|
@@ -268,12 +269,21 @@ export declare function createChatController(options: Options | SessionOpener, s
|
|
|
268
269
|
* installed until `confirmed`: the result says which (`confirm`), for the view to ask the
|
|
269
270
|
* person. The running session is untouched: it's there in a new conversation, or with `reopen()`.
|
|
270
271
|
*/
|
|
272
|
+
/**
|
|
273
|
+
* Signs in to an installed extension's remote servers (#61): gives the person the links (never
|
|
274
|
+
* opens a browser), then says when the sign-in finishes. With `callbackUrl`, the address the
|
|
275
|
+
* browser ended on (from another computer), finishes it instead.
|
|
276
|
+
*/
|
|
277
|
+
signInExtension(name: string, callbackUrl?: string): Promise<SignIn>;
|
|
278
|
+
/** Signs out of an installed extension's remote servers (#61). */
|
|
279
|
+
signOutExtension(name: string): Promise<boolean>;
|
|
271
280
|
installExtension(spec: string, options?: {
|
|
272
281
|
scope?: ExtensionScope;
|
|
273
282
|
confirmed?: boolean;
|
|
274
283
|
}): Promise<{
|
|
275
284
|
name?: string;
|
|
276
285
|
missing?: string[];
|
|
286
|
+
remote?: string[];
|
|
277
287
|
confirm?: {
|
|
278
288
|
marketplace: string;
|
|
279
289
|
description?: string;
|
|
@@ -315,6 +315,10 @@ export async function createChatController(options, settings = {}) {
|
|
|
315
315
|
await controller.removeExtension(action.name, { deleteData: action.deleteData });
|
|
316
316
|
else if (action.type === "trust")
|
|
317
317
|
await controller.trustWorkspace();
|
|
318
|
+
else if (action.type === "sign-in")
|
|
319
|
+
await controller.signInExtension(action.name, action.callbackUrl);
|
|
320
|
+
else if (action.type === "sign-out")
|
|
321
|
+
await controller.signOutExtension(action.name);
|
|
318
322
|
else
|
|
319
323
|
await controller.reopen();
|
|
320
324
|
}
|
|
@@ -595,6 +599,37 @@ export async function createChatController(options, settings = {}) {
|
|
|
595
599
|
* installed until `confirmed`: the result says which (`confirm`), for the view to ask the
|
|
596
600
|
* person. The running session is untouched: it's there in a new conversation, or with `reopen()`.
|
|
597
601
|
*/
|
|
602
|
+
/**
|
|
603
|
+
* Signs in to an installed extension's remote servers (#61): gives the person the links (never
|
|
604
|
+
* opens a browser), then says when the sign-in finishes. With `callbackUrl`, the address the
|
|
605
|
+
* browser ended on (from another computer), finishes it instead.
|
|
606
|
+
*/
|
|
607
|
+
async signInExtension(name, callbackUrl) {
|
|
608
|
+
const auth = current.extensionAuth;
|
|
609
|
+
const result = auth ? await auth.signIn(name, callbackUrl) : { status: "unavailable", reason: "not-running" };
|
|
610
|
+
if (result.status === "unavailable")
|
|
611
|
+
notice(t().extensionSignInUnavailable(name));
|
|
612
|
+
else if (result.status === "signed-in")
|
|
613
|
+
notice(callbackUrl ? t().extensionSignedIn(name) : t().extensionAlreadySignedIn(name));
|
|
614
|
+
else {
|
|
615
|
+
for (const link of result.links)
|
|
616
|
+
notice(t().extensionSignInOpen(name, link.url));
|
|
617
|
+
notice(t().extensionSignInPaste(name));
|
|
618
|
+
// Waits in the background: the person signs in in their browser, whenever they do.
|
|
619
|
+
void auth.connected(name, 5 * 60_000).then((done) => {
|
|
620
|
+
notice(done ? t().extensionSignedIn(name) : t().extensionSignInWaiting(name));
|
|
621
|
+
if (done)
|
|
622
|
+
extensionNotices.push(`<system-reminder>The user signed in to the ${name} extension: its remote tools are available now.</system-reminder>`);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return result;
|
|
626
|
+
},
|
|
627
|
+
/** Signs out of an installed extension's remote servers (#61). */
|
|
628
|
+
async signOutExtension(name) {
|
|
629
|
+
const done = (await current.extensionAuth?.signOut(name).catch(() => false)) ?? false;
|
|
630
|
+
notice(done ? t().extensionSignedOut(name) : t().extensionSignInUnavailable(name));
|
|
631
|
+
return done;
|
|
632
|
+
},
|
|
598
633
|
async installExtension(spec, options = {}) {
|
|
599
634
|
if (!current.extensions)
|
|
600
635
|
throw new Error("This chat's session has no extensions folder.");
|
|
@@ -608,7 +643,10 @@ export async function createChatController(options, settings = {}) {
|
|
|
608
643
|
// What it needs and this computer lacks (#63): the person installs it, the kit never does.
|
|
609
644
|
for (const line of result.missing)
|
|
610
645
|
notice(t().extensionLacks(result.name, line));
|
|
611
|
-
|
|
646
|
+
// Where what the agent sends goes (#61).
|
|
647
|
+
if (result.remote.length)
|
|
648
|
+
notice(t().extensionTalksTo(result.name, result.remote.join(", ")));
|
|
649
|
+
return { name: result.name, missing: result.missing, remote: result.remote };
|
|
612
650
|
},
|
|
613
651
|
/** Updates one installed extension from its marketplace, or every one with a newer version there; the names updated. */
|
|
614
652
|
async updateExtension(name) {
|
|
@@ -25,6 +25,15 @@ export type ExtensionAction = {
|
|
|
25
25
|
type: "reload";
|
|
26
26
|
} | {
|
|
27
27
|
type: "trust";
|
|
28
|
+
}
|
|
29
|
+
/** Signing in to an extension's remote servers (#61), or finishing it with the callback URL pasted from another computer. */
|
|
30
|
+
| {
|
|
31
|
+
type: "sign-in";
|
|
32
|
+
name: string;
|
|
33
|
+
callbackUrl?: string;
|
|
34
|
+
} | {
|
|
35
|
+
type: "sign-out";
|
|
36
|
+
name: string;
|
|
28
37
|
};
|
|
29
38
|
/** One line for a catalog entry: what installs it, its version, whether it's installed or has an update, what it is. */
|
|
30
39
|
export declare function describeEntry(entry: CatalogEntry): string;
|
package/dist/chat/extensions.js
CHANGED
|
@@ -35,6 +35,13 @@ export function describeCapabilities(status) {
|
|
|
35
35
|
];
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
+
/** The running extensions whose remote servers need the person to sign in (#61). */
|
|
39
|
+
async function needingSignIn(status) {
|
|
40
|
+
const waiting = new Set((await status.serverStatus?.().catch(() => []))?.filter((server) => server.status === "needs-auth").map((server) => server.name));
|
|
41
|
+
if (!waiting.size)
|
|
42
|
+
return [];
|
|
43
|
+
return status.active.filter((name) => status.about[name]?.servers.some((server) => waiting.has(server)));
|
|
44
|
+
}
|
|
38
45
|
/** One line for a catalog entry: what installs it, its version, whether it's installed or has an update, what it is. */
|
|
39
46
|
export function describeEntry(entry) {
|
|
40
47
|
const state = entry.installed ? (entry.update ? ` ${t().extensionUpdateAvailable(entry.installed.manifest?.version ?? "?")}` : ` ${t().extensionIsInstalled}`) : "";
|
|
@@ -78,6 +85,11 @@ export async function chatExtensionsCommand(line, status, canReopen) {
|
|
|
78
85
|
return canReopen ? { lines: [], action: { type: "reload" } } : { lines: [t().extensionsCantReopen] };
|
|
79
86
|
if (verb === "trust" && !name)
|
|
80
87
|
return canReopen ? { lines: [], action: { type: "trust" } } : { lines: [t().extensionsCantReopen] };
|
|
88
|
+
// Signing in happens in the running session: no reopening (#61).
|
|
89
|
+
if (verb === "sign-in" && name)
|
|
90
|
+
return { lines: [], action: { type: "sign-in", name, ...(extra[0] ? { callbackUrl: extra[0] } : {}) } };
|
|
91
|
+
if (verb === "sign-out" && name)
|
|
92
|
+
return { lines: [], action: { type: "sign-out", name } };
|
|
81
93
|
if (action?.toLowerCase() === "config" && name) {
|
|
82
94
|
const installed = (status?.installed ?? []).find((extension) => extension.name === name && !extension.shadowed);
|
|
83
95
|
if (!installed)
|
|
@@ -107,7 +119,7 @@ export async function chatExtensionsCommand(line, status, canReopen) {
|
|
|
107
119
|
if (!action) {
|
|
108
120
|
const lines = [status?.active.length ? t().extensionsRunning(status.active.join(", ")) : t().extensionsNone];
|
|
109
121
|
if (status)
|
|
110
|
-
lines.push(...describeCapabilities(status));
|
|
122
|
+
lines.push(...describeCapabilities(status), ...(await needingSignIn(status)).map((extension) => t().extensionNeedsSignIn(extension)));
|
|
111
123
|
for (const { name: off, reason } of status?.inactive ?? [])
|
|
112
124
|
lines.push(t().extensionOff(off, reason));
|
|
113
125
|
if (status?.installed.length)
|
package/dist/chat/runs.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import type { ExtensionsStatus, ModeControl } from "../core/session.js";
|
|
3
3
|
import type { ToolLabels } from "../core/toolLabels.js";
|
|
4
|
-
import type { ExtensionRestart, ExtensionSwitch } from "../core/liveExtensions.js";
|
|
4
|
+
import type { ExtensionAuth, ExtensionRestart, ExtensionSwitch } from "../core/liveExtensions.js";
|
|
5
5
|
import { type RunFolder, type RunSummary } from "../core/runs.js";
|
|
6
6
|
/**
|
|
7
7
|
* Builds the session's options for a run folder, e.g.
|
|
@@ -17,6 +17,7 @@ export type SessionOpener = (run: RunFolder) => Promise<Options | {
|
|
|
17
17
|
apis?: Record<string, Record<string, unknown>>;
|
|
18
18
|
switchExtension?: ExtensionSwitch;
|
|
19
19
|
restartExtension?: ExtensionRestart;
|
|
20
|
+
extensionAuth?: ExtensionAuth;
|
|
20
21
|
}>;
|
|
21
22
|
export interface OpenedSession {
|
|
22
23
|
run: RunFolder;
|
|
@@ -27,6 +28,7 @@ export interface OpenedSession {
|
|
|
27
28
|
apis?: Record<string, Record<string, unknown>>;
|
|
28
29
|
switchExtension?: ExtensionSwitch;
|
|
29
30
|
restartExtension?: ExtensionRestart;
|
|
31
|
+
extensionAuth?: ExtensionAuth;
|
|
30
32
|
}
|
|
31
33
|
/** The run a chat starts with: the latest one with `--continue` (if any), a new one otherwise. */
|
|
32
34
|
export declare function firstRun(runsDir: string, argv?: readonly string[]): Promise<RunFolder>;
|
package/dist/chat/runs.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function firstRun(runsDir, argv = process.argv.slice(2)) {
|
|
|
13
13
|
export async function openSession(opener, run) {
|
|
14
14
|
const opened = await opener(run);
|
|
15
15
|
// The SDK's Options has no "options" field: this is buildSessionOptions()'s result.
|
|
16
|
-
return "options" in opened ? { run, options: opened.options, modeControl: opened.modeControl, toolLabels: opened.toolLabels, extensions: opened.extensions, apis: opened.apis, switchExtension: opened.switchExtension, restartExtension: opened.restartExtension } : { run, options: opened };
|
|
16
|
+
return "options" in opened ? { run, options: opened.options, modeControl: opened.modeControl, toolLabels: opened.toolLabels, extensions: opened.extensions, apis: opened.apis, switchExtension: opened.switchExtension, restartExtension: opened.restartExtension, extensionAuth: opened.extensionAuth } : { run, options: opened };
|
|
17
17
|
}
|
|
18
18
|
/** A run's date for a person, in the kit's language. */
|
|
19
19
|
export function runDate(date) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ExtensionDirs, type ExtensionScope, type InstalledExtension } from "./externalExtensions.js";
|
|
2
2
|
import { type DescribedCapability } from "./extensions.js";
|
|
3
|
+
import type { Language } from "./language.js";
|
|
3
4
|
import { type KnownMarketplace, type MarketplacePlugin } from "./marketplaces.js";
|
|
4
5
|
/**
|
|
5
6
|
* The agent's catalog of extensions (#56): what its known marketplaces offer, what's installed
|
|
@@ -42,14 +43,17 @@ export interface CatalogEntry {
|
|
|
42
43
|
name: string;
|
|
43
44
|
for: string[];
|
|
44
45
|
}[];
|
|
46
|
+
/** The hosts its remote servers talk to (#61): what the agent sends there leaves the computer. */
|
|
47
|
+
remote: string[];
|
|
45
48
|
}
|
|
46
49
|
/** Whether `offered` is newer than `installed` (x.y.z); a version that can't be compared isn't. */
|
|
47
50
|
export declare function isNewer(offered: string | undefined, installed: string | undefined): boolean;
|
|
48
|
-
/** Everything the known marketplaces offer, matching `words` (in its name, description, category, tags or keywords) when given. */
|
|
51
|
+
/** Everything the known marketplaces offer, matching `words` (in its name, description, category, tags or keywords) when given; its texts in `language` (the kit's by default). */
|
|
49
52
|
export declare function extensionCatalog(dirs: ExtensionDirs, options?: {
|
|
50
53
|
official?: string;
|
|
51
54
|
words?: readonly string[];
|
|
52
55
|
topics?: readonly string[];
|
|
56
|
+
language?: Language;
|
|
53
57
|
}): Promise<CatalogEntry[]>;
|
|
54
58
|
/** Whether installing `spec` needs the person's confirmation first: a plugin of a marketplace that isn't the agent's own. */
|
|
55
59
|
export declare function needsConfirmation(dirs: ExtensionDirs, spec: string, official?: string): Promise<{
|
|
@@ -72,6 +76,7 @@ export declare function installExtension(dirs: ExtensionDirs, spec: string, opti
|
|
|
72
76
|
marketplace: string;
|
|
73
77
|
commit?: string;
|
|
74
78
|
missing: string[];
|
|
79
|
+
remote: string[];
|
|
75
80
|
}>;
|
|
76
81
|
/**
|
|
77
82
|
* Updates an installed extension from the marketplace it came from: a fresh copy of the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdtemp, rm, stat } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { addExtension, codeStoreDir, hashExtension, inKitRange, installTarget, isGitSource, listInstalled, readExternalManifest, readLock, storedCodeDir } from "./externalExtensions.js";
|
|
4
|
+
import { addExtension, remoteHosts, codeStoreDir, hashExtension, inKitRange, installTarget, isGitSource, listInstalled, readExternalManifest, readLock, storedCodeDir } from "./externalExtensions.js";
|
|
5
5
|
import { cachePackage, cachedPackageDir } from "./npxPackages.js";
|
|
6
6
|
import { trustWorkspace } from "./workspaceTrust.js";
|
|
7
7
|
import { describeCapability, describeInLanguage } from "./extensions.js";
|
|
@@ -51,7 +51,7 @@ const IMAGE = /\.(svg|png|jpe?g|webp)$/i;
|
|
|
51
51
|
* the version (its `plugin.json` wins over the entry, as Claude Code computes it), its keywords,
|
|
52
52
|
* and its look, an image inside its folder only.
|
|
53
53
|
*/
|
|
54
|
-
async function offered(marketplace, plugin) {
|
|
54
|
+
async function offered(marketplace, plugin, language) {
|
|
55
55
|
try {
|
|
56
56
|
const resolved = resolvePluginSource(marketplace, plugin);
|
|
57
57
|
if ("source" in resolved && !isGitSource(resolved.source) && (await stat(resolved.source).catch(() => null))?.isDirectory()) {
|
|
@@ -66,16 +66,17 @@ async function offered(marketplace, plugin) {
|
|
|
66
66
|
};
|
|
67
67
|
const [logo, banner] = [await image(manifest?.catalog?.logo), await image(manifest?.catalog?.banner)];
|
|
68
68
|
const version = manifest?.version ?? plugin.version;
|
|
69
|
-
const capabilities = (manifest?.capabilities ?? []).map((capability) => describeCapability(capability,
|
|
69
|
+
const capabilities = (manifest?.capabilities ?? []).map((capability) => describeCapability(capability, language));
|
|
70
70
|
const requirements = (manifest?.requirements ?? []).map((requirement) => ({ name: requirement.name, for: requirement.for }));
|
|
71
|
-
const description = describeInLanguage(plugin.description ?? manifest?.description ?? "", manifest?.descriptions,
|
|
72
|
-
|
|
71
|
+
const description = describeInLanguage(plugin.description ?? manifest?.description ?? "", manifest?.descriptions, language);
|
|
72
|
+
const remote = manifest ? remoteHosts(manifest) : [];
|
|
73
|
+
return { ...(version ? { version } : {}), keywords: manifest?.keywords ?? [], ...(logo ? { logo } : {}), ...(banner ? { banner } : {}), capabilities, requirements, description, remote };
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
catch {
|
|
76
77
|
// A source the kit doesn't install: the entry's own fields.
|
|
77
78
|
}
|
|
78
|
-
return { ...(plugin.version ? { version: plugin.version } : {}), keywords: [], capabilities: [], requirements: [], description: plugin.description ?? "" };
|
|
79
|
+
return { ...(plugin.version ? { version: plugin.version } : {}), keywords: [], capabilities: [], requirements: [], description: plugin.description ?? "", remote: [] };
|
|
79
80
|
}
|
|
80
81
|
/** Whether `offered` is newer than `installed` (x.y.z); a version that can't be compared isn't. */
|
|
81
82
|
export function isNewer(offered, installed) {
|
|
@@ -83,7 +84,7 @@ export function isNewer(offered, installed) {
|
|
|
83
84
|
return false;
|
|
84
85
|
return inKitRange(offered, `>${installed.split("-")[0]}`);
|
|
85
86
|
}
|
|
86
|
-
/** Everything the known marketplaces offer, matching `words` (in its name, description, category, tags or keywords) when given. */
|
|
87
|
+
/** Everything the known marketplaces offer, matching `words` (in its name, description, category, tags or keywords) when given; its texts in `language` (the kit's by default). */
|
|
87
88
|
export async function extensionCatalog(dirs, options = {}) {
|
|
88
89
|
const installed = (await listInstalled(dirs)).filter((extension) => !extension.shadowed);
|
|
89
90
|
const words = (options.words ?? []).map((word) => word.toLowerCase());
|
|
@@ -92,7 +93,7 @@ export async function extensionCatalog(dirs, options = {}) {
|
|
|
92
93
|
const entries = [];
|
|
93
94
|
for (const marketplace of await knownMarketplaces(dirs, options.official)) {
|
|
94
95
|
for (const plugin of marketplace.manifest?.plugins ?? []) {
|
|
95
|
-
const look = await offered(marketplace, plugin);
|
|
96
|
+
const look = await offered(marketplace, plugin, options.language ?? getLanguage());
|
|
96
97
|
const topics = [...new Set([...look.keywords, ...(plugin.tags ?? []), ...(plugin.keywords ?? []), ...(plugin.category ? [plugin.category] : [])].map((topic) => topic.toLowerCase()))];
|
|
97
98
|
if (wanted && !topics.some((topic) => wanted.has(topic)))
|
|
98
99
|
continue;
|
|
@@ -117,6 +118,7 @@ export async function extensionCatalog(dirs, options = {}) {
|
|
|
117
118
|
...(look.banner ? { banner: look.banner } : {}),
|
|
118
119
|
capabilities: look.capabilities,
|
|
119
120
|
requirements: look.requirements,
|
|
121
|
+
remote: look.remote,
|
|
120
122
|
});
|
|
121
123
|
}
|
|
122
124
|
}
|
|
@@ -52,5 +52,10 @@ export declare function missingOptions(declared: Record<string, ExtensionOption>
|
|
|
52
52
|
export declare function storeOptionValues(scopeDir: string, name: string, declared: Record<string, ExtensionOption>, stored: Record<string, OptionValue>, changes: Record<string, OptionValue | undefined>): Promise<Record<string, OptionValue>>;
|
|
53
53
|
/** Removes an extension's sensitive values. */
|
|
54
54
|
export declare function removeSecrets(scopeDir: string, name: string): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* `${user_config.KEY}` in `text` replaced by its value: a list comma-separated, none as empty, as
|
|
57
|
+
* the launcher does for a local server. The kit's own, for a remote server's URL and headers (#61).
|
|
58
|
+
*/
|
|
59
|
+
export declare function fillOptions(text: string, values: Record<string, OptionValue>): string;
|
|
55
60
|
/** The `${user_config.KEY}` a server's values name. */
|
|
56
61
|
export declare function optionsNamed(values: readonly string[]): string[];
|
|
@@ -133,6 +133,16 @@ export async function storeOptionValues(scopeDir, name, declared, stored, change
|
|
|
133
133
|
export async function removeSecrets(scopeDir, name) {
|
|
134
134
|
await rm(extensionSecretsPath(scopeDir, name), { force: true });
|
|
135
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* `${user_config.KEY}` in `text` replaced by its value: a list comma-separated, none as empty, as
|
|
138
|
+
* the launcher does for a local server. The kit's own, for a remote server's URL and headers (#61).
|
|
139
|
+
*/
|
|
140
|
+
export function fillOptions(text, values) {
|
|
141
|
+
return text.replace(/\$\{user_config\.([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, key) => {
|
|
142
|
+
const value = values[key];
|
|
143
|
+
return value === undefined ? "" : Array.isArray(value) ? value.join(",") : String(value);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
136
146
|
/** The `${user_config.KEY}` a server's values name. */
|
|
137
147
|
export function optionsNamed(values) {
|
|
138
148
|
return [...new Set(values.flatMap((value) => [...value.matchAll(/\$\{user_config\.([A-Za-z_][A-Za-z0-9_]*)\}/g)].map((match) => match[1])))];
|
|
@@ -64,6 +64,12 @@ export interface ExtensionContribution {
|
|
|
64
64
|
* no `readOnlyTools`. A list in the manifest wins: its servers aren't here.
|
|
65
65
|
*/
|
|
66
66
|
readOnlyHintServers?: string[];
|
|
67
|
+
/**
|
|
68
|
+
* Its MCP servers that are remote (#61), by session name: the SDK fixes their URL and headers
|
|
69
|
+
* when the session opens, so a changed option takes opening it again, and they may need the
|
|
70
|
+
* person to sign in.
|
|
71
|
+
*/
|
|
72
|
+
remoteServers?: string[];
|
|
67
73
|
/** Its MCP tools that ask the person themselves: interactive mode's step gate doesn't ask first. */
|
|
68
74
|
selfAskingTools?: string[];
|
|
69
75
|
/** How the chat shows its tools, by full name, in the kit's language: its line and how it counts in a folded group. */
|
|
@@ -26,12 +26,34 @@ export interface ExtensionDirs {
|
|
|
26
26
|
/** One of the two scopes an extension can be installed in: the agent's or the project's. */
|
|
27
27
|
export type ExtensionScope = keyof ExtensionDirs;
|
|
28
28
|
/** One of a plugin's MCP servers, as its `.mcp.json` (or `plugin.json`'s `mcpServers`) declares it. */
|
|
29
|
-
|
|
29
|
+
/** A server the kit starts on this computer, through its launcher. */
|
|
30
|
+
export interface LocalServer {
|
|
30
31
|
command: string;
|
|
31
32
|
args?: string[];
|
|
32
33
|
/** Its variables: a value may take the agent's with `${VAR}`, and its folders with `${CLAUDE_PLUGIN_ROOT}` and `${CLAUDE_PLUGIN_DATA}`. */
|
|
33
34
|
env?: Record<string, string>;
|
|
34
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* A remote server (#61), over HTTP or SSE: nothing runs here. `${user_config.KEY}` and `${VAR}` in
|
|
38
|
+
* its URL and headers take their values when the session opens.
|
|
39
|
+
*/
|
|
40
|
+
export interface RemoteServer {
|
|
41
|
+
type: "http" | "sse";
|
|
42
|
+
url: string;
|
|
43
|
+
headers?: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
/** One of an extension's MCP servers, as its `.mcp.json` declares it. */
|
|
46
|
+
export type PluginServer = LocalServer | RemoteServer;
|
|
47
|
+
/** Whether a server is remote (#61). */
|
|
48
|
+
export declare function isRemoteServer(server: PluginServer): server is RemoteServer;
|
|
49
|
+
/**
|
|
50
|
+
* The name a session gives an extension's remote server: its own, prefixed with the extension's.
|
|
51
|
+
* The SDK's CLI keeps OAuth tokens by server name (confirmed empirically, CLI 2.1.300): a server
|
|
52
|
+
* named like one in the runner's own Claude Code would sign in with the runner's token.
|
|
53
|
+
*/
|
|
54
|
+
export declare function remoteServerName(extension: string, server: string): string;
|
|
55
|
+
/** The hosts an extension's remote servers talk to, for the person to read before installing it. */
|
|
56
|
+
export declare function remoteHosts(manifest: Pick<ExternalManifest, "servers">): string[];
|
|
35
57
|
/** How the chat shows one of its tools in one language: `{field}` takes the call's input. */
|
|
36
58
|
export interface ExternalToolLabel {
|
|
37
59
|
label: string;
|
|
@@ -230,6 +252,7 @@ export declare function addExtension(source: string, scopeDir: string, options?:
|
|
|
230
252
|
replaced: boolean;
|
|
231
253
|
commit?: string;
|
|
232
254
|
missing: string[];
|
|
255
|
+
remote: string[];
|
|
233
256
|
}>;
|
|
234
257
|
/**
|
|
235
258
|
* Removes an extension from a scope (its files and its lock entry; a linked one's folder is left
|
|
@@ -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)
|
|
@@ -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;
|