@falkenslab/agent-kit 0.25.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/dist/chat/extensions.js +5 -0
- 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/hooks/remoteGate.d.ts +22 -0
- package/dist/core/hooks/remoteGate.js +35 -0
- package/dist/core/messages/de.js +3 -0
- package/dist/core/messages/en.d.ts +6 -0
- package/dist/core/messages/en.js +3 -0
- package/dist/core/messages/es.js +3 -0
- package/dist/core/messages/fr.js +3 -0
- package/dist/core/runner.js +24 -0
- package/dist/core/session.d.ts +5 -0
- package/dist/core/session.js +42 -1
- package/dist/core/sessionFacts.d.ts +6 -0
- package/dist/core/sessionFacts.js +1 -0
- package/dist/extensions/awareness/tools.js +6 -0
- package/package.json +1 -1
package/dist/chat/extensions.js
CHANGED
|
@@ -120,6 +120,11 @@ export async function chatExtensionsCommand(line, status, canReopen) {
|
|
|
120
120
|
const lines = [status?.active.length ? t().extensionsRunning(status.active.join(", ")) : t().extensionsNone];
|
|
121
121
|
if (status)
|
|
122
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(", ")));
|
|
123
128
|
for (const { name: off, reason } of status?.inactive ?? [])
|
|
124
129
|
lines.push(t().extensionOff(off, reason));
|
|
125
130
|
if (status?.installed.length)
|
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
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/core/messages/de.js
CHANGED
|
@@ -60,6 +60,9 @@ export const de = {
|
|
|
60
60
|
extensionAlreadySignedIn: (name) => `(${name} ist bereits angemeldet)`,
|
|
61
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
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)`,
|
|
63
66
|
extensionInstalled: (name) => `(${name} installiert: in einem neuen Gespräch da, oder sofort mit /extensions reload, ohne dieses zu verlieren)`,
|
|
64
67
|
extensionUpdated: (name, from, to) => `(${name} aktualisiert, ${from} → ${to}: in einem neuen Gespräch da, oder sofort mit /extensions reload)`,
|
|
65
68
|
extensionsUpToDate: "(alle Erweiterungen aus einem Marketplace sind aktuell)",
|
|
@@ -82,6 +82,12 @@ export interface Messages {
|
|
|
82
82
|
extensionAlreadySignedIn(name: string): string;
|
|
83
83
|
extensionSignInUnavailable(name: string): string;
|
|
84
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;
|
|
85
91
|
extensionUpdated(name: string, from: string, to: string): string;
|
|
86
92
|
extensionsUpToDate: string;
|
|
87
93
|
extensionRemoved(name: string, withData: boolean): string;
|
package/dist/core/messages/en.js
CHANGED
|
@@ -60,6 +60,9 @@ export const en = {
|
|
|
60
60
|
extensionAlreadySignedIn: (name) => `(${name} is already signed in)`,
|
|
61
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
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)`,
|
|
63
66
|
extensionUpdated: (name, from, to) => `(${name} updated, ${from} → ${to}: it's there in a new conversation, or now with /extensions reload)`,
|
|
64
67
|
extensionsUpToDate: "(every extension from a marketplace is up to date)",
|
|
65
68
|
extensionRemoved: (name, withData) => `(${name} removed${withData ? ", with its data" : "; its data is kept for a reinstall"})`,
|
package/dist/core/messages/es.js
CHANGED
|
@@ -60,6 +60,9 @@ export const es = {
|
|
|
60
60
|
extensionAlreadySignedIn: (name) => `(${name} ya tiene la sesión iniciada)`,
|
|
61
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
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)`,
|
|
63
66
|
extensionInstalled: (name) => `(${name} instalada: está en una conversación nueva, o ya con /extensions reload, sin perder esta)`,
|
|
64
67
|
extensionUpdated: (name, from, to) => `(${name} actualizada, ${from} → ${to}: está en una conversación nueva, o ya con /extensions reload)`,
|
|
65
68
|
extensionsUpToDate: "(todas las extensiones de un marketplace están al día)",
|
package/dist/core/messages/fr.js
CHANGED
|
@@ -60,6 +60,9 @@ export const fr = {
|
|
|
60
60
|
extensionAlreadySignedIn: (name) => `(${name} est déjà connectée)`,
|
|
61
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
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)`,
|
|
63
66
|
extensionInstalled: (name) => `(${name} installée : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload, en gardant celle-ci)`,
|
|
64
67
|
extensionUpdated: (name, from, to) => `(${name} mise à jour, ${from} → ${to} : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload)`,
|
|
65
68
|
extensionsUpToDate: "(toutes les extensions d'un marketplace sont à jour)",
|
package/dist/core/runner.js
CHANGED
|
@@ -35,6 +35,29 @@ function signInControls(result) {
|
|
|
35
35
|
...(clear ? { mcpClearAuth: (server) => clear(server) } : {}),
|
|
36
36
|
};
|
|
37
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
|
+
}
|
|
38
61
|
/**
|
|
39
62
|
* Thin wrapper around the SDK's own `query()`: same two-argument shape (a plain string
|
|
40
63
|
* for a one-shot run, or an `AsyncIterable<SDKUserMessage>` — see `createInputQueue()` —
|
|
@@ -56,6 +79,7 @@ export function runQuery(prompt, options) {
|
|
|
56
79
|
mcpServerStatus: () => result.mcpServerStatus(),
|
|
57
80
|
...signInControls(result),
|
|
58
81
|
});
|
|
82
|
+
void restoreSavedSwitches(result, options);
|
|
59
83
|
// The main agent's tool calls by id, to name the results that come back for them.
|
|
60
84
|
const toolNames = new Map();
|
|
61
85
|
async function* generateEvents() {
|
package/dist/core/session.d.ts
CHANGED
|
@@ -77,6 +77,11 @@ export interface ExtensionsStatus {
|
|
|
77
77
|
sessionTools?: () => readonly string[];
|
|
78
78
|
/** Its MCP servers as they are now (empty until it has started): which remote ones need signing in (#61). */
|
|
79
79
|
serverStatus?: () => Promise<McpServerStatus[]>;
|
|
80
|
+
/** The claude.ai connectors let in (#70), and the ones asked for that the account hasn't; none when the agent asks for none. */
|
|
81
|
+
connectors?: {
|
|
82
|
+
allowed: string[];
|
|
83
|
+
missing: string[];
|
|
84
|
+
};
|
|
80
85
|
}
|
|
81
86
|
/**
|
|
82
87
|
* The CLI's folder for a project, where it keeps each session's transcript and large tool
|
package/dist/core/session.js
CHANGED
|
@@ -8,6 +8,8 @@ import { createSubagentTypeGate } from "./hooks/subagentTypeGate.js";
|
|
|
8
8
|
import { createSubagentForegroundGate } from "./hooks/subagentForegroundGate.js";
|
|
9
9
|
import { createFileScopeGate } from "./hooks/fileScopeGate.js";
|
|
10
10
|
import { createPlanGate } from "./hooks/planGate.js";
|
|
11
|
+
import { createRemoteGate } from "./hooks/remoteGate.js";
|
|
12
|
+
import { chooseConnectors, discoverConnectors, serverToolPrefix } from "./claudeAiConnectors.js";
|
|
11
13
|
import { createHumanApprovalServer } from "./tools/humanApproval.js";
|
|
12
14
|
import { createManualLoginServer } from "./tools/manualLogin.js";
|
|
13
15
|
import { createTimeServer } from "./tools/time.js";
|
|
@@ -200,6 +202,12 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
200
202
|
];
|
|
201
203
|
// Packaged, the SDK would look for its CLI binary inside the archive, and hang (#42).
|
|
202
204
|
const claudeExecutable = unpackedClaudeExecutable();
|
|
205
|
+
// The claude.ai connectors the agent lets in (#70): the account asked which it has, the rest
|
|
206
|
+
// denied by name (a session setting, never the person's config). None asked, none looked for.
|
|
207
|
+
const connectors = config.claudeAiConnectors?.length
|
|
208
|
+
? chooseConnectors(await discoverConnectors(config.projectDir, claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}), config.claudeAiConnectors)
|
|
209
|
+
: undefined;
|
|
210
|
+
const allowedConnectors = connectors?.allowed ?? [];
|
|
203
211
|
const sdkOptions = {
|
|
204
212
|
...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}),
|
|
205
213
|
systemPrompt: withReplyLine(promptSections.join("\n\n")),
|
|
@@ -220,7 +228,8 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
220
228
|
autoCompactEnabled: options.autoCompactEnabled ?? true,
|
|
221
229
|
autoMemoryEnabled: false,
|
|
222
230
|
includeGitInstructions: false,
|
|
223
|
-
disableClaudeAiConnectors:
|
|
231
|
+
disableClaudeAiConnectors: !allowedConnectors.length,
|
|
232
|
+
...(allowedConnectors.length && connectors.denied.length ? { deniedMcpServers: connectors.denied.map((serverName) => ({ serverName })) } : {}),
|
|
224
233
|
// An installed extension's plugin may carry hooks: none run (ADR-025). The kit's own,
|
|
225
234
|
// passed in `hooks`, still do (confirmed empirically).
|
|
226
235
|
...(externalPlugins.size ? { disableAllHooks: true } : {}),
|
|
@@ -292,6 +301,18 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
292
301
|
hooks: {
|
|
293
302
|
PreToolUse: [
|
|
294
303
|
{ hooks: [transcriptLogger.preToolUse] },
|
|
304
|
+
// Remote servers (#70): connectors not chosen are blocked; the rest as `remoteServers` says.
|
|
305
|
+
{
|
|
306
|
+
hooks: [
|
|
307
|
+
createRemoteGate({
|
|
308
|
+
runDir,
|
|
309
|
+
policy: config.remoteServers ?? "allow",
|
|
310
|
+
destination: (tool) => remoteDestinations.find(([prefix]) => tool.startsWith(prefix))?.[1],
|
|
311
|
+
allowedConnectors: allowedConnectors.map(serverToolPrefix),
|
|
312
|
+
canAsk: () => modeControl.mode !== "autonomous",
|
|
313
|
+
}),
|
|
314
|
+
],
|
|
315
|
+
},
|
|
295
316
|
...(fileTools.length || toolOnlyDirs.length
|
|
296
317
|
? [{ hooks: [createFileScopeGate({
|
|
297
318
|
projectDir: config.projectDir,
|
|
@@ -342,6 +363,24 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
342
363
|
PostToolUse: [{ hooks: [transcriptLogger.postToolUse] }],
|
|
343
364
|
},
|
|
344
365
|
};
|
|
366
|
+
// Where each remote server's calls go (#70), for the remote gate: an http/sse server's host
|
|
367
|
+
// (an installed extension's, the agent's own), or the claude.ai connector itself.
|
|
368
|
+
const remoteDestinations = [
|
|
369
|
+
...Object.entries(sdkOptions.mcpServers ?? {}).flatMap(([name, server]) => {
|
|
370
|
+
const remote = server;
|
|
371
|
+
if ((remote.type !== "http" && remote.type !== "sse") || !remote.url)
|
|
372
|
+
return [];
|
|
373
|
+
let host = remote.url;
|
|
374
|
+
try {
|
|
375
|
+
host = new URL(remote.url).host;
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// Not a URL the kit can read: the address as given.
|
|
379
|
+
}
|
|
380
|
+
return [[serverToolPrefix(name), host]];
|
|
381
|
+
}),
|
|
382
|
+
...allowedConnectors.map((name) => [serverToolPrefix(name), name]),
|
|
383
|
+
];
|
|
345
384
|
// The extensions' own hooks (the memory hears the person's messages), after the kit's.
|
|
346
385
|
for (const contribution of added) {
|
|
347
386
|
for (const [event, matchers] of Object.entries(contribution.hooks ?? {})) {
|
|
@@ -360,6 +399,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
360
399
|
sessionTools: () => late.view?.controls().tools ?? [],
|
|
361
400
|
serverStatus: async () => (await late.view?.controls().mcpServerStatus?.().catch(() => [])) ?? [],
|
|
362
401
|
inactive: resolved.inactive,
|
|
402
|
+
...(connectors ? { connectors: { allowed: connectors.allowed, missing: connectors.missing } } : {}),
|
|
363
403
|
};
|
|
364
404
|
// What the session is, for the extensions that ask (sessionFacts.ts, #43).
|
|
365
405
|
const viewBase = {
|
|
@@ -378,6 +418,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
378
418
|
})),
|
|
379
419
|
// Its own list: /extensions' status has another, and the live switch changes both.
|
|
380
420
|
inactive: [...resolved.inactive],
|
|
421
|
+
...(connectors ? { connectors: { allowed: connectors.allowed, missing: connectors.missing } } : {}),
|
|
381
422
|
},
|
|
382
423
|
subagents: Object.entries(allAgents).map(([name, agent]) => ({ name, description: agent.description })),
|
|
383
424
|
skills: sdkOptions.skills === "all" ? await pluginSkills(pluginRoots) : [...(sdkOptions.skills ?? [])],
|
|
@@ -37,6 +37,11 @@ export interface SessionFacts {
|
|
|
37
37
|
name: string;
|
|
38
38
|
reason: string;
|
|
39
39
|
}[];
|
|
40
|
+
/** The claude.ai connectors let in (#70), and the ones asked for that the account hasn't. */
|
|
41
|
+
connectors?: {
|
|
42
|
+
allowed: string[];
|
|
43
|
+
missing: string[];
|
|
44
|
+
};
|
|
40
45
|
};
|
|
41
46
|
/** The subagents it can delegate to. */
|
|
42
47
|
subagents: {
|
|
@@ -93,6 +98,7 @@ export type StaticSessionFacts = Omit<SessionFacts, "mode" | "switchableModes" |
|
|
|
93
98
|
servers: string[];
|
|
94
99
|
})[];
|
|
95
100
|
inactive: SessionFacts["extensions"]["inactive"];
|
|
101
|
+
connectors?: SessionFacts["extensions"]["connectors"];
|
|
96
102
|
};
|
|
97
103
|
};
|
|
98
104
|
export interface SessionView {
|
|
@@ -26,6 +26,7 @@ export function createSessionView(base, modeControl) {
|
|
|
26
26
|
tools: tools.length ? tools.filter((tool) => servers.some((server) => tool.startsWith(`mcp__${server}__`))) : servers.map((server) => `mcp__${server}__*`),
|
|
27
27
|
})),
|
|
28
28
|
inactive: base.extensions.inactive,
|
|
29
|
+
...(base.extensions.connectors ? { connectors: base.extensions.connectors } : {}),
|
|
29
30
|
},
|
|
30
31
|
tools,
|
|
31
32
|
skills: live.skills ?? base.skills,
|
|
@@ -27,6 +27,12 @@ export function describeSession(facts) {
|
|
|
27
27
|
});
|
|
28
28
|
const inactive = facts.extensions.inactive.map(({ name, reason }) => `- **${name}** is off: it ${reason}.`);
|
|
29
29
|
sections.push(`## Extensions\n\n${[...active, ...inactive].join("\n") || "None."}`);
|
|
30
|
+
// The claude.ai connectors let in (#70): the account's integrations, not installed extensions.
|
|
31
|
+
const connectors = facts.extensions.connectors;
|
|
32
|
+
if (connectors && (connectors.allowed.length || connectors.missing.length)) {
|
|
33
|
+
const missing = connectors.missing.length ? ` Asked for but not in the person's claude.ai account: ${connectors.missing.join(", ")} (they connect it on claude.ai, then the session opens again).` : "";
|
|
34
|
+
sections.push(`## claude.ai connectors\n\n${connectors.allowed.length ? `${connectors.allowed.join(", ")}: their tools are the person's claude.ai integrations; what you send them leaves the computer.` : "None of the ones asked for."}${missing}`);
|
|
35
|
+
}
|
|
30
36
|
if (facts.subagents.length)
|
|
31
37
|
sections.push(`## Subagents you can delegate to\n\n${facts.subagents.map(({ name, description }) => `- **${name}**: ${description}`).join("\n")}`);
|
|
32
38
|
const own = facts.tools.filter((tool) => !tool.startsWith("mcp__"));
|