@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
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,12 @@ 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)));
|
|
123
|
+
// The claude.ai connectors the agent lets in, apart from installed extensions (#70).
|
|
124
|
+
if (status?.connectors?.allowed.length)
|
|
125
|
+
lines.push(t().connectorsAllowed(status.connectors.allowed.map((name) => name.replace(/^claude\.ai /, "")).join(", ")));
|
|
126
|
+
if (status?.connectors?.missing.length)
|
|
127
|
+
lines.push(t().connectorsMissing(status.connectors.missing.join(", ")));
|
|
111
128
|
for (const { name: off, reason } of status?.inactive ?? [])
|
|
112
129
|
lines.push(t().extensionOff(off, reason));
|
|
113
130
|
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) {
|
package/dist/core/agentSpec.d.ts
CHANGED
|
@@ -44,6 +44,22 @@ export interface BaseSessionConfig {
|
|
|
44
44
|
* catalog and `extension search` show only those with one of them. Without it, every one.
|
|
45
45
|
*/
|
|
46
46
|
catalogTopics?: string[];
|
|
47
|
+
/**
|
|
48
|
+
* The claude.ai connectors of the person's account this agent lets in (#70), by the name
|
|
49
|
+
* claude.ai shows them (`["Canva", "Notion"]`, any case): those already signed in on claude.ai,
|
|
50
|
+
* nothing to install. None by default: the kit keeps them out, since they're the account's
|
|
51
|
+
* integrations, not the agent's. When the session opens, the kit asks the account which it has
|
|
52
|
+
* (a few seconds) and lets in only these; one asked for that isn't there is said in `/extensions`.
|
|
53
|
+
*/
|
|
54
|
+
claudeAiConnectors?: string[];
|
|
55
|
+
/**
|
|
56
|
+
* What a call to a remote MCP server does (#70): an installed extension's `http`/`sse` server, one
|
|
57
|
+
* of the agent's own, or a claude.ai connector. What the model sends one leaves the computer, so
|
|
58
|
+
* an agent handling sensitive data (students') may keep it from them. `"allow"` (the default)
|
|
59
|
+
* lets it go on; `"ask"` asks the person before each call, saying where it goes (blocked when no
|
|
60
|
+
* one can be asked, as in autonomous mode); `"deny"` blocks it.
|
|
61
|
+
*/
|
|
62
|
+
remoteServers?: "allow" | "ask" | "deny";
|
|
47
63
|
/**
|
|
48
64
|
* Directories where the agent reads, searches and writes with the file tools (Write/Edit,
|
|
49
65
|
* scoped — see hooks/fileScopeGate.ts), e.g. a folder of its own notes. Setting one gives the
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Options } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* The claude.ai connectors an agent lets in (#70): the person's account's integrations (Canva,
|
|
4
|
+
* Notion…), which the kit keeps out by default (`disableClaudeAiConnectors`, ADR-018). The SDK
|
|
5
|
+
* lets them in all or none, and lets one out only by its name (`deniedMcpServers`); an allowlist
|
|
6
|
+
* would have to list every other server of the session exactly (confirmed empirically, CLI
|
|
7
|
+
* 2.1.300). So the kit asks the account which it has, when the session opens, and denies by name
|
|
8
|
+
* the ones the agent didn't choose; a gate blocks a call to any other (one added since).
|
|
9
|
+
*
|
|
10
|
+
* `toggleMcpServer()` is never used for them: it saves the switch in the person's own Claude Code
|
|
11
|
+
* config for the folder (`~/.claude.json`, `disabledMcpServers`), not only in the session
|
|
12
|
+
* (confirmed empirically, CLI 2.1.300).
|
|
13
|
+
*/
|
|
14
|
+
/** The name the SDK gives a claude.ai connector's server: `claude.ai <Name>`. */
|
|
15
|
+
export declare const CONNECTOR_PREFIX = "claude.ai ";
|
|
16
|
+
/** The prefix of a server's tools as the model names them: `claude.ai Claude Docs` → `mcp__claude_ai_Claude_Docs__`. */
|
|
17
|
+
export declare function serverToolPrefix(server: string): string;
|
|
18
|
+
/** Whether a tool is a claude.ai connector's. */
|
|
19
|
+
export declare function isConnectorTool(tool: string): boolean;
|
|
20
|
+
/** What the agent chose, set against what the account has: the servers to let in, to deny, and the names it asked for that aren't there. */
|
|
21
|
+
export interface ConnectorChoice {
|
|
22
|
+
allowed: string[];
|
|
23
|
+
denied: string[];
|
|
24
|
+
missing: string[];
|
|
25
|
+
}
|
|
26
|
+
/** Chooses among the account's connectors (`found`, as the SDK names them) the ones the agent wants, by the name claude.ai shows (any case). */
|
|
27
|
+
export declare function chooseConnectors(found: readonly string[], wanted: readonly string[]): ConnectorChoice;
|
|
28
|
+
/**
|
|
29
|
+
* The account's claude.ai connectors, by the names the SDK gives them: a session that sends no
|
|
30
|
+
* message, opened in `cwd` with connectors on, its servers' status read and closed. A few seconds;
|
|
31
|
+
* an empty list when it can't tell (no login, no network), so nothing is let in.
|
|
32
|
+
*/
|
|
33
|
+
export declare function discoverConnectors(cwd: string, options?: Pick<Options, "pathToClaudeCodeExecutable" | "env"> & {
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
}): Promise<string[]>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* The claude.ai connectors an agent lets in (#70): the person's account's integrations (Canva,
|
|
4
|
+
* Notion…), which the kit keeps out by default (`disableClaudeAiConnectors`, ADR-018). The SDK
|
|
5
|
+
* lets them in all or none, and lets one out only by its name (`deniedMcpServers`); an allowlist
|
|
6
|
+
* would have to list every other server of the session exactly (confirmed empirically, CLI
|
|
7
|
+
* 2.1.300). So the kit asks the account which it has, when the session opens, and denies by name
|
|
8
|
+
* the ones the agent didn't choose; a gate blocks a call to any other (one added since).
|
|
9
|
+
*
|
|
10
|
+
* `toggleMcpServer()` is never used for them: it saves the switch in the person's own Claude Code
|
|
11
|
+
* config for the folder (`~/.claude.json`, `disabledMcpServers`), not only in the session
|
|
12
|
+
* (confirmed empirically, CLI 2.1.300).
|
|
13
|
+
*/
|
|
14
|
+
/** The name the SDK gives a claude.ai connector's server: `claude.ai <Name>`. */
|
|
15
|
+
export const CONNECTOR_PREFIX = "claude.ai ";
|
|
16
|
+
/** The prefix of a server's tools as the model names them: `claude.ai Claude Docs` → `mcp__claude_ai_Claude_Docs__`. */
|
|
17
|
+
export function serverToolPrefix(server) {
|
|
18
|
+
return `mcp__${server.replace(/[^A-Za-z0-9_-]/g, "_")}__`;
|
|
19
|
+
}
|
|
20
|
+
/** Whether a tool is a claude.ai connector's. */
|
|
21
|
+
export function isConnectorTool(tool) {
|
|
22
|
+
// `claude.ai <Name>` becomes `mcp__claude_ai_<Name>__`: the prefix up to the name.
|
|
23
|
+
return tool.startsWith("mcp__claude_ai_");
|
|
24
|
+
}
|
|
25
|
+
/** Chooses among the account's connectors (`found`, as the SDK names them) the ones the agent wants, by the name claude.ai shows (any case). */
|
|
26
|
+
export function chooseConnectors(found, wanted) {
|
|
27
|
+
const label = (server) => (server.startsWith(CONNECTOR_PREFIX) ? server.slice(CONNECTOR_PREFIX.length) : server).toLowerCase();
|
|
28
|
+
const asked = new Set(wanted.map(label));
|
|
29
|
+
const allowed = found.filter((server) => asked.has(label(server)));
|
|
30
|
+
const present = new Set(allowed.map(label));
|
|
31
|
+
return { allowed, denied: found.filter((server) => !asked.has(label(server))), missing: wanted.filter((name) => !present.has(label(name))) };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The account's claude.ai connectors, by the names the SDK gives them: a session that sends no
|
|
35
|
+
* message, opened in `cwd` with connectors on, its servers' status read and closed. A few seconds;
|
|
36
|
+
* an empty list when it can't tell (no login, no network), so nothing is let in.
|
|
37
|
+
*/
|
|
38
|
+
export async function discoverConnectors(cwd, options = {}) {
|
|
39
|
+
let release = () => { };
|
|
40
|
+
const done = new Promise((resolve) => (release = resolve));
|
|
41
|
+
// A conversation that never sends a message, ended when the look is over: the session only starts its servers.
|
|
42
|
+
const prompt = { [Symbol.asyncIterator]: () => ({ next: () => done.then(() => ({ done: true, value: undefined })) }) };
|
|
43
|
+
const session = query({
|
|
44
|
+
prompt,
|
|
45
|
+
options: {
|
|
46
|
+
cwd,
|
|
47
|
+
settingSources: ["project"],
|
|
48
|
+
tools: [],
|
|
49
|
+
settings: { autoMemoryEnabled: false, disableClaudeAiConnectors: false },
|
|
50
|
+
...(options.pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable: options.pathToClaudeCodeExecutable } : {}),
|
|
51
|
+
...(options.env ? { env: options.env } : {}),
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
void (async () => {
|
|
55
|
+
for await (const message of session)
|
|
56
|
+
void message;
|
|
57
|
+
})().catch(() => undefined);
|
|
58
|
+
try {
|
|
59
|
+
const connectors = async () => (await session.mcpServerStatus()).filter((server) => server.scope === "claudeai" || server.name.startsWith(CONNECTOR_PREFIX));
|
|
60
|
+
// Connectors come from the account after the session starts: wait until none is pending.
|
|
61
|
+
const deadline = Date.now() + (options.timeoutMs ?? 10_000);
|
|
62
|
+
let found = await connectors().catch(() => []);
|
|
63
|
+
while (Date.now() < deadline && (!found.length || found.some((server) => server.status === "pending"))) {
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
65
|
+
found = await connectors().catch(() => []);
|
|
66
|
+
}
|
|
67
|
+
return found.map((server) => server.name);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
release();
|
|
74
|
+
session.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -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
|