@falkenslab/agent-kit 0.31.0 → 0.33.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 +175 -175
- package/dist/chat/chatController.d.ts +5 -0
- package/dist/chat/chatController.js +13 -0
- package/dist/core/claudeAuth.d.ts +53 -21
- package/dist/core/claudeAuth.js +135 -22
- package/dist/core/hooks/transcriptLogger.d.ts +2 -2
- package/dist/core/hooks/transcriptLogger.js +2 -2
- package/dist/core/mcpDepth.d.ts +9 -0
- package/dist/core/mcpDepth.js +12 -0
- package/dist/core/messages/de.js +9 -7
- package/dist/core/messages/en.d.ts +7 -5
- package/dist/core/messages/en.js +9 -7
- package/dist/core/messages/es.js +9 -7
- package/dist/core/messages/fr.js +9 -7
- package/dist/core/packaged.d.ts +6 -0
- package/dist/core/packaged.js +18 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/serveMcp.d.ts +28 -0
- package/dist/mcp/serveMcp.js +177 -0
- package/dist/tui/claudeAuth.d.ts +12 -17
- package/dist/tui/claudeAuth.js +37 -66
- package/package.json +13 -1
package/dist/core/claudeAuth.js
CHANGED
|
@@ -1,27 +1,140 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { claudeBinaryRequests, unpackedClaudeExecutable } from "./packaged.js";
|
|
5
|
+
/*
|
|
6
|
+
* Signing in to Claude without ever touching a credential (ADR-009, #90). Anthropic's terms
|
|
7
|
+
* (https://code.claude.com/docs/en/legal-and-compliance, "Authentication and credential use")
|
|
8
|
+
* say an Agent SDK developer may not collect, store or intermediate Claude.ai credentials or
|
|
9
|
+
* session tokens: a Claude account signs in through Anthropic's own flow, in the unmodified
|
|
10
|
+
* Claude Code binary. The SDK ships that binary, so the kit runs its `auth status` and
|
|
11
|
+
* `auth login` and nothing else: the binary opens Anthropic's page and keeps the credentials
|
|
12
|
+
* itself (`~/.claude/.credentials.json`, the Keychain on macOS), where the SDK's sessions find
|
|
13
|
+
* them. The kit never reads, returns or stores a token. An API key (`ANTHROPIC_API_KEY`) is
|
|
14
|
+
* allowed, and so is a person's own `CLAUDE_CODE_OAUTH_TOKEN`: both are their setting, not
|
|
15
|
+
* something the kit collects.
|
|
16
|
+
*/
|
|
1
17
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Checks, in order: `ANTHROPIC_API_KEY` (a complete authentication path on its own — API
|
|
7
|
-
* billing instead of subscription, so nothing else is checked when it's set),
|
|
8
|
-
* `CLAUDE_CODE_OAUTH_TOKEN`, then `config.claudeCodeOAuthToken` if the caller passed one.
|
|
9
|
-
* Returns `true` and (for the `config` case) sets `CLAUDE_CODE_OAUTH_TOKEN` on the process
|
|
10
|
-
* as a side effect once a token is found — the SDK's own subprocess reads that env var
|
|
11
|
-
* directly, never anything this kit passes it explicitly; `false` when none of the three
|
|
12
|
-
* has one. Getting a token in that case (e.g. by walking the user through
|
|
13
|
-
* `claude setup-token`) is a terminal-UX concern, not this function's — see
|
|
14
|
-
* `ensureClaudeAuth()` in tui/claudeAuth.ts, which calls this first and only falls back to
|
|
15
|
-
* that interactive flow when it returns `false`.
|
|
18
|
+
* The Claude Code binary the Agent SDK ships (`@anthropic-ai/claude-agent-sdk-<platform>-<arch>`),
|
|
19
|
+
* unmodified: the copy unpacked next to the archive when the agent runs packaged, or else the
|
|
20
|
+
* one resolved from `node_modules` as the SDK resolves it. `undefined` when it isn't installed.
|
|
21
|
+
* `resolve` and the platform are replaceable for tests.
|
|
16
22
|
*/
|
|
17
|
-
export function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
export function claudeExecutable(resolve = createRequire(import.meta.url).resolve, platform = process.platform, arch = process.arch) {
|
|
24
|
+
const unpacked = unpackedClaudeExecutable(resolve, platform, arch);
|
|
25
|
+
if (unpacked)
|
|
26
|
+
return unpacked;
|
|
27
|
+
for (const request of claudeBinaryRequests(platform, arch)) {
|
|
28
|
+
try {
|
|
29
|
+
const file = resolve(request);
|
|
30
|
+
if (existsSync(file))
|
|
31
|
+
return file;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// That package isn't installed: the next one.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/** The binary and its arguments as a process to spawn: a JavaScript file runs with this Node. */
|
|
40
|
+
export function claudeCommand(executable, args) {
|
|
41
|
+
return /\.(c|m)?js$/i.test(executable) ? { file: process.execPath, args: [executable, ...args] } : { file: executable, args };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether the agent can reach Claude: true when the person's environment has `ANTHROPIC_API_KEY`
|
|
45
|
+
* or `CLAUDE_CODE_OAUTH_TOKEN` (their own setting); otherwise what the Claude Code binary says
|
|
46
|
+
* (`claude auth status --json`, its `loggedIn`). Never reads the credentials file, and prints
|
|
47
|
+
* nothing: safe from a server or an Electron main process. False when there is no binary.
|
|
48
|
+
*/
|
|
49
|
+
export async function claudeSignedIn(options = {}) {
|
|
50
|
+
if (process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN)
|
|
24
51
|
return true;
|
|
52
|
+
const executable = options.executable ?? claudeExecutable();
|
|
53
|
+
if (!executable)
|
|
54
|
+
return false;
|
|
55
|
+
const command = claudeCommand(executable, ["auth", "status", "--json"]);
|
|
56
|
+
// Exit code 1 when signed out, with the same JSON (confirmed empirically): read it either way.
|
|
57
|
+
const output = await new Promise((resolve) => {
|
|
58
|
+
execFile(command.file, command.args, { timeout: 30_000, windowsHide: true }, (_error, stdout) => resolve(String(stdout ?? "")));
|
|
59
|
+
});
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(output).loggedIn === true;
|
|
25
62
|
}
|
|
26
|
-
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Signs the person in to their Claude account through Anthropic's own flow, for a host without a
|
|
69
|
+
* terminal (a web page, a desktop app): runs `claude auth login` (`console: true` for an
|
|
70
|
+
* Anthropic Console account, billed by API usage) with plain pipes. The binary opens the browser
|
|
71
|
+
* itself; the host shows `url` as a link in case it didn't, and a box for the code Anthropic's
|
|
72
|
+
* page shows (`sendCode`). The binary keeps the credentials: the kit never sees a token. Throws
|
|
73
|
+
* when the binary isn't installed.
|
|
74
|
+
*
|
|
75
|
+
* Confirmed empirically (Claude Code 2.1.310): without a TTY, `auth login` prints "If the browser
|
|
76
|
+
* didn't open, visit: <url>" (the URL may come wrapped in OSC 8 hyperlink escapes) and then waits
|
|
77
|
+
* at "Paste code here if prompted >".
|
|
78
|
+
*/
|
|
79
|
+
export function startClaudeSignIn(options = {}) {
|
|
80
|
+
const executable = options.executable ?? claudeExecutable();
|
|
81
|
+
if (!executable)
|
|
82
|
+
throw new Error("Claude Code's binary, which the Agent SDK ships (@anthropic-ai/claude-agent-sdk-<platform>-<arch>), isn't installed.");
|
|
83
|
+
const command = claudeCommand(executable, ["auth", "login", ...(options.console ? ["--console"] : [])]);
|
|
84
|
+
const child = spawn(command.file, command.args, { stdio: ["pipe", "pipe", "pipe"], windowsHide: true });
|
|
85
|
+
let output = "";
|
|
86
|
+
let resolveUrl;
|
|
87
|
+
const url = new Promise((resolve) => (resolveUrl = resolve));
|
|
88
|
+
const read = (chunk) => {
|
|
89
|
+
output += chunk.toString("utf8");
|
|
90
|
+
// Anthropic's page as soon as it's all there; any other URL only once the binary has ended.
|
|
91
|
+
const found = signInUrl(output);
|
|
92
|
+
if (found?.includes("/oauth/authorize"))
|
|
93
|
+
resolveUrl(found);
|
|
94
|
+
};
|
|
95
|
+
child.stdout?.on("data", read);
|
|
96
|
+
child.stderr?.on("data", read);
|
|
97
|
+
// A write after the binary ended fails on the stream: nothing to tell then.
|
|
98
|
+
child.stdin?.on("error", () => { });
|
|
99
|
+
const ended = new Promise((resolve) => {
|
|
100
|
+
child.on("error", () => resolve());
|
|
101
|
+
child.on("close", () => resolve());
|
|
102
|
+
});
|
|
103
|
+
const done = ended.then(() => {
|
|
104
|
+
resolveUrl(signInUrl(output, true));
|
|
105
|
+
return claudeSignedIn(options);
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
url,
|
|
109
|
+
sendCode(code) {
|
|
110
|
+
if (child.stdin?.writable)
|
|
111
|
+
child.stdin.write(`${code.trim()}\n`);
|
|
112
|
+
},
|
|
113
|
+
done,
|
|
114
|
+
cancel() {
|
|
115
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
116
|
+
child.kill();
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// An OSC sequence (a hyperlink's `ESC ] 8 ; ; URL ESC \`, a title), ended by BEL or ST.
|
|
121
|
+
// eslint-disable-next-line no-control-regex -- ESC and BEL delimit terminal escapes, on purpose
|
|
122
|
+
const OSC = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
123
|
+
// A CSI sequence (colors, cursor moves).
|
|
124
|
+
// eslint-disable-next-line no-control-regex -- ESC starts the sequence
|
|
125
|
+
const CSI = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
126
|
+
// eslint-disable-next-line no-control-regex -- an OSC 8 hyperlink's target, between ESC/BEL delimiters
|
|
127
|
+
const HYPERLINK_TARGET = /\x1b\]8;[^;\x07\x1b]*;(https:\/\/[^\x07\x1b]+)(?:\x07|\x1b\\)/g;
|
|
128
|
+
/**
|
|
129
|
+
* The sign-in URL in what `claude auth login` printed so far: an OSC 8 hyperlink's target, or a
|
|
130
|
+
* URL in the text with its escapes removed, Anthropic's authorize page (`/oauth/authorize`)
|
|
131
|
+
* first. A URL in the text counts only once whitespace follows it (the rest may still be on its
|
|
132
|
+
* way), unless `complete` says the output is all there is.
|
|
133
|
+
*/
|
|
134
|
+
export function signInUrl(output, complete = false) {
|
|
135
|
+
const targets = [...output.matchAll(HYPERLINK_TARGET)].map((match) => match[1]);
|
|
136
|
+
const text = output.replace(OSC, " ").replace(CSI, "");
|
|
137
|
+
const inText = [...text.matchAll(complete ? /https:\/\/\S+/g : /https:\/\/\S+(?=\s)/g)].map((match) => match[0]);
|
|
138
|
+
const urls = [...targets, ...inText];
|
|
139
|
+
return urls.find((url) => url.includes("/oauth/authorize")) ?? urls[0];
|
|
27
140
|
}
|
|
@@ -9,8 +9,8 @@ export interface TranscriptLogger {
|
|
|
9
9
|
/**
|
|
10
10
|
* Logs every tool call (request + result) as one JSON line, redacting `secrets` (e.g. a
|
|
11
11
|
* domain-specific password — see `BaseSessionConfig.secrets`) plus
|
|
12
|
-
* `CLAUDE_CODE_OAUTH_TOKEN
|
|
13
|
-
*
|
|
12
|
+
* `CLAUDE_CODE_OAUTH_TOKEN` when the person's environment sets one, which this kit always
|
|
13
|
+
* scrubs regardless of domain (it never handles a token itself: see claudeAuth.ts).
|
|
14
14
|
*/
|
|
15
15
|
export declare function createTranscriptLogger(transcriptPath: string, secrets?: readonly string[]): TranscriptLogger;
|
|
16
16
|
/** Avoids bloating the transcript with large payloads (browser-tool screenshots/base64). */
|
|
@@ -10,8 +10,8 @@ function redact(text, secrets) {
|
|
|
10
10
|
/**
|
|
11
11
|
* Logs every tool call (request + result) as one JSON line, redacting `secrets` (e.g. a
|
|
12
12
|
* domain-specific password — see `BaseSessionConfig.secrets`) plus
|
|
13
|
-
* `CLAUDE_CODE_OAUTH_TOKEN
|
|
14
|
-
*
|
|
13
|
+
* `CLAUDE_CODE_OAUTH_TOKEN` when the person's environment sets one, which this kit always
|
|
14
|
+
* scrubs regardless of domain (it never handles a token itself: see claudeAuth.ts).
|
|
15
15
|
*/
|
|
16
16
|
export function createTranscriptLogger(transcriptPath, secrets = []) {
|
|
17
17
|
const allSecrets = [...secrets, process.env.CLAUDE_CODE_OAUTH_TOKEN].filter((s) => Boolean(s));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How deep in a chain of agents served over MCP this process is (ADR-027): an agent that serves
|
|
3
|
+
* itself reads it, and passes it on one higher to the servers it starts, so a loop (A asks B asks
|
|
4
|
+
* A) stops instead of spawning agents forever. An installed extension's server never gets it: its
|
|
5
|
+
* environment is stripped, and no server should depend on the kit's variables.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MCP_DEPTH_ENV = "AGENT_KIT_MCP_DEPTH";
|
|
8
|
+
/** The depth this process runs at: 0 when no agent served over MCP started it. */
|
|
9
|
+
export declare function mcpDepth(): number;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How deep in a chain of agents served over MCP this process is (ADR-027): an agent that serves
|
|
3
|
+
* itself reads it, and passes it on one higher to the servers it starts, so a loop (A asks B asks
|
|
4
|
+
* A) stops instead of spawning agents forever. An installed extension's server never gets it: its
|
|
5
|
+
* environment is stripped, and no server should depend on the kit's variables.
|
|
6
|
+
*/
|
|
7
|
+
export const MCP_DEPTH_ENV = "AGENT_KIT_MCP_DEPTH";
|
|
8
|
+
/** The depth this process runs at: 0 when no agent served over MCP started it. */
|
|
9
|
+
export function mcpDepth() {
|
|
10
|
+
const depth = Number(process.env[MCP_DEPTH_ENV]);
|
|
11
|
+
return Number.isInteger(depth) && depth > 0 ? depth : 0;
|
|
12
|
+
}
|
package/dist/core/messages/de.js
CHANGED
|
@@ -194,12 +194,14 @@ export const de = {
|
|
|
194
194
|
presentingPlan: "Plan vorlegen",
|
|
195
195
|
},
|
|
196
196
|
auth: {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
cantStart: "Der Agent kann ohne
|
|
201
|
-
|
|
202
|
-
browserWillOpen: "
|
|
203
|
-
|
|
197
|
+
notSignedIn: "Du bist nicht bei Claude angemeldet",
|
|
198
|
+
notSignedInDetail: "Melde dich mit deinem Claude-Konto auf Anthropics eigener Seite an: Das übernimmt Claude Code, das mit diesem Agenten kommt, und es speichert die Anmeldung selbst. Oder setze ANTHROPIC_API_KEY, um einen API-Schlüssel zu verwenden.",
|
|
199
|
+
signInQuestion: "Jetzt mit deinem Claude-Konto anmelden?",
|
|
200
|
+
cantStart: "Der Agent kann ohne Claude nicht starten. Starte ihn erneut und melde dich an, oder setze ANTHROPIC_API_KEY.",
|
|
201
|
+
signingInHeading: "=== Anmeldung bei Claude ===",
|
|
202
|
+
browserWillOpen: "Ein Browser öffnet sich mit Anthropics Anmeldeseite. Zeigt die Seite einen Code, füge ihn hier ein.",
|
|
203
|
+
signedIn: "Angemeldet. Claude Code speichert die Anmeldung, du wirst nicht erneut gefragt.",
|
|
204
|
+
notCompleted: "Die Anmeldung wurde nicht abgeschlossen. Starte den Agenten erneut, um es noch einmal zu versuchen, oder setze ANTHROPIC_API_KEY.",
|
|
205
|
+
noClaude: "Das Claude-Code-Programm, das mit dem Agent SDK kommt (@anthropic-ai/claude-agent-sdk-<Plattform>-<Architektur>), ist nicht installiert, daher ist keine Anmeldung möglich. Installiere den Agenten neu, oder setze ANTHROPIC_API_KEY.",
|
|
204
206
|
},
|
|
205
207
|
};
|
|
@@ -203,13 +203,15 @@ export interface Messages {
|
|
|
203
203
|
presentingPlan: string;
|
|
204
204
|
};
|
|
205
205
|
auth: {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
notSignedIn: string;
|
|
207
|
+
notSignedInDetail: string;
|
|
208
|
+
signInQuestion: string;
|
|
209
209
|
cantStart: string;
|
|
210
|
-
|
|
210
|
+
signingInHeading: string;
|
|
211
211
|
browserWillOpen: string;
|
|
212
|
-
|
|
212
|
+
signedIn: string;
|
|
213
|
+
notCompleted: string;
|
|
214
|
+
noClaude: string;
|
|
213
215
|
};
|
|
214
216
|
}
|
|
215
217
|
export declare const en: Messages;
|
package/dist/core/messages/en.js
CHANGED
|
@@ -193,12 +193,14 @@ export const en = {
|
|
|
193
193
|
presentingPlan: "Presenting the plan",
|
|
194
194
|
},
|
|
195
195
|
auth: {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
cantStart: "The agent can't start without
|
|
200
|
-
|
|
201
|
-
browserWillOpen: "A browser will open
|
|
202
|
-
|
|
196
|
+
notSignedIn: "You aren't signed in to Claude",
|
|
197
|
+
notSignedInDetail: "Sign in with your Claude account on Anthropic's own page: Claude Code, which comes with this agent, does it and keeps the sign-in itself. Or set ANTHROPIC_API_KEY to use an API key.",
|
|
198
|
+
signInQuestion: "Sign in with your Claude account now?",
|
|
199
|
+
cantStart: "The agent can't start without Claude. Run it again and sign in, or set ANTHROPIC_API_KEY.",
|
|
200
|
+
signingInHeading: "=== Signing in to Claude ===",
|
|
201
|
+
browserWillOpen: "A browser will open on Anthropic's sign-in page. If the page shows a code, paste it here.",
|
|
202
|
+
signedIn: "Signed in. Claude Code keeps the sign-in, so this won't be asked again.",
|
|
203
|
+
notCompleted: "The sign-in didn't complete. Run the agent again to retry, or set ANTHROPIC_API_KEY.",
|
|
204
|
+
noClaude: "Claude Code's binary, which comes with the Agent SDK (@anthropic-ai/claude-agent-sdk-<platform>-<arch>), isn't installed, so there's no way to sign in. Reinstall the agent, or set ANTHROPIC_API_KEY.",
|
|
203
205
|
},
|
|
204
206
|
};
|
package/dist/core/messages/es.js
CHANGED
|
@@ -194,12 +194,14 @@ export const es = {
|
|
|
194
194
|
presentingPlan: "Presentando el plan",
|
|
195
195
|
},
|
|
196
196
|
auth: {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
cantStart: "El agente no puede arrancar sin
|
|
201
|
-
|
|
202
|
-
browserWillOpen: "Se abrirá un navegador
|
|
203
|
-
|
|
197
|
+
notSignedIn: "No has iniciado sesión en Claude",
|
|
198
|
+
notSignedInDetail: "Inicia sesión con tu cuenta de Claude en la propia página de Anthropic: lo hace Claude Code, que viene con este agente, y guarda él mismo la sesión. O define ANTHROPIC_API_KEY para usar una API key.",
|
|
199
|
+
signInQuestion: "¿Iniciar sesión ahora con tu cuenta de Claude?",
|
|
200
|
+
cantStart: "El agente no puede arrancar sin Claude. Vuelve a ejecutarlo e inicia sesión, o define ANTHROPIC_API_KEY.",
|
|
201
|
+
signingInHeading: "=== Iniciando sesión en Claude ===",
|
|
202
|
+
browserWillOpen: "Se abrirá un navegador en la página de inicio de sesión de Anthropic. Si la página muestra un código, pégalo aquí.",
|
|
203
|
+
signedIn: "Sesión iniciada. Claude Code la guarda, así que no se volverá a pedir.",
|
|
204
|
+
notCompleted: "El inicio de sesión no se completó. Vuelve a ejecutar el agente para reintentarlo, o define ANTHROPIC_API_KEY.",
|
|
205
|
+
noClaude: "No está instalado el binario de Claude Code que trae el Agent SDK (@anthropic-ai/claude-agent-sdk-<plataforma>-<arquitectura>), así que no hay cómo iniciar sesión. Reinstala el agente, o define ANTHROPIC_API_KEY.",
|
|
204
206
|
},
|
|
205
207
|
};
|
package/dist/core/messages/fr.js
CHANGED
|
@@ -194,12 +194,14 @@ export const fr = {
|
|
|
194
194
|
presentingPlan: "Présentation du plan",
|
|
195
195
|
},
|
|
196
196
|
auth: {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
cantStart: "L'agent ne peut pas démarrer sans
|
|
201
|
-
|
|
202
|
-
browserWillOpen: "Un navigateur va s'ouvrir
|
|
203
|
-
|
|
197
|
+
notSignedIn: "Tu n'es pas connecté à Claude",
|
|
198
|
+
notSignedInDetail: "Connecte-toi avec ton compte Claude sur la page d'Anthropic : c'est Claude Code, fourni avec cet agent, qui s'en charge et garde lui-même la connexion. Ou définis ANTHROPIC_API_KEY pour utiliser une clé d'API.",
|
|
199
|
+
signInQuestion: "Se connecter maintenant avec ton compte Claude ?",
|
|
200
|
+
cantStart: "L'agent ne peut pas démarrer sans Claude. Relance-le et connecte-toi, ou définis ANTHROPIC_API_KEY.",
|
|
201
|
+
signingInHeading: "=== Connexion à Claude ===",
|
|
202
|
+
browserWillOpen: "Un navigateur va s'ouvrir sur la page de connexion d'Anthropic. Si la page affiche un code, colle-le ici.",
|
|
203
|
+
signedIn: "Connecté. Claude Code garde la connexion : on ne te la redemandera pas.",
|
|
204
|
+
notCompleted: "La connexion n'a pas abouti. Relance l'agent pour réessayer, ou définis ANTHROPIC_API_KEY.",
|
|
205
|
+
noClaude: "Le binaire de Claude Code fourni avec l'Agent SDK (@anthropic-ai/claude-agent-sdk-<plateforme>-<architecture>) n'est pas installé : impossible de se connecter. Réinstalle l'agent, ou définis ANTHROPIC_API_KEY.",
|
|
204
206
|
},
|
|
205
207
|
};
|
package/dist/core/packaged.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** A path inside `…/app.asar/…`, as the unpacked copy next to it (`…/app.asar.unpacked/…`); any other path as it is. */
|
|
2
2
|
export declare function outsideArchive(file: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* The module requests that name the SDK's CLI binary on this platform, in the SDK's order:
|
|
5
|
+
* `@anthropic-ai/claude-agent-sdk-<platform>-<arch>/claude[.exe]`, both libcs on Linux (musl
|
|
6
|
+
* first when the system runs on it, as the SDK decides).
|
|
7
|
+
*/
|
|
8
|
+
export declare function claudeBinaryRequests(platform?: NodeJS.Platform, arch?: string, musl?: boolean): string[];
|
|
3
9
|
/**
|
|
4
10
|
* The SDK's CLI binary unpacked next to the archive, when the kit runs packaged: the SDK would
|
|
5
11
|
* resolve it inside `app.asar`, where it can't be executed, and its first turn would hang with
|
package/dist/core/packaged.js
CHANGED
|
@@ -11,6 +11,22 @@ const ARCHIVE = /([\\/])app\.asar([\\/])/;
|
|
|
11
11
|
export function outsideArchive(file) {
|
|
12
12
|
return file.replace(ARCHIVE, "$1app.asar.unpacked$2");
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The module requests that name the SDK's CLI binary on this platform, in the SDK's order:
|
|
16
|
+
* `@anthropic-ai/claude-agent-sdk-<platform>-<arch>/claude[.exe]`, both libcs on Linux (musl
|
|
17
|
+
* first when the system runs on it, as the SDK decides).
|
|
18
|
+
*/
|
|
19
|
+
export function claudeBinaryRequests(platform = process.platform, arch = process.arch, musl = platform === "linux" && runsOnMusl()) {
|
|
20
|
+
const packages = platform === "linux" ? (musl ? [`linux-${arch}-musl`, `linux-${arch}`] : [`linux-${arch}`, `linux-${arch}-musl`]) : [`${platform}-${arch}`];
|
|
21
|
+
return packages.map((suffix) => `@anthropic-ai/claude-agent-sdk-${suffix}/claude${platform === "win32" ? ".exe" : ""}`);
|
|
22
|
+
}
|
|
23
|
+
/** A Linux whose C library is musl (Alpine): Node's report names no glibc version there. */
|
|
24
|
+
function runsOnMusl() {
|
|
25
|
+
if (process.platform !== "linux")
|
|
26
|
+
return false;
|
|
27
|
+
const report = process.report?.getReport();
|
|
28
|
+
return Boolean(report?.header) && report?.header?.glibcVersionRuntime === undefined;
|
|
29
|
+
}
|
|
14
30
|
/**
|
|
15
31
|
* The SDK's CLI binary unpacked next to the archive, when the kit runs packaged: the SDK would
|
|
16
32
|
* resolve it inside `app.asar`, where it can't be executed, and its first turn would hang with
|
|
@@ -19,11 +35,10 @@ export function outsideArchive(file) {
|
|
|
19
35
|
* when not packaged, or when the build didn't unpack it: then the SDK finds its own.
|
|
20
36
|
*/
|
|
21
37
|
export function unpackedClaudeExecutable(resolve = createRequire(import.meta.url).resolve, platform = process.platform, arch = process.arch) {
|
|
22
|
-
const
|
|
23
|
-
for (const suffix of packages) {
|
|
38
|
+
for (const request of claudeBinaryRequests(platform, arch)) {
|
|
24
39
|
let file;
|
|
25
40
|
try {
|
|
26
|
-
file = resolve(
|
|
41
|
+
file = resolve(request);
|
|
27
42
|
}
|
|
28
43
|
catch {
|
|
29
44
|
continue;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { setTheme, getTheme, DEFAULT_THEME, type Theme, type ThemeColor } from "
|
|
|
3
3
|
export { isExitPromptError } from "./tui/promptErrors.js";
|
|
4
4
|
export { allowAnyMcpTool } from "./core/mcpPermissions.js";
|
|
5
5
|
export { createPromptLoader } from "./core/promptTemplate.js";
|
|
6
|
-
export {
|
|
6
|
+
export { claudeExecutable, claudeSignedIn, startClaudeSignIn, type ClaudeAuthOptions, type ClaudeSignIn } from "./core/claudeAuth.js";
|
|
7
7
|
export { ensureClaudeAuth } from "./tui/claudeAuth.js";
|
|
8
8
|
export { createFriendlyToolLabel, truncate, truncatePath, withToolLabels, withToolPhrases, withToolResults, quietResults, type ToolResult, type ToolDescriber, type ToolLabel, type ToolLabels, } from "./core/toolLabels.js";
|
|
9
9
|
export type { Mode, BaseSessionConfig, AgentSpec, AgentIdentity, PlanModeSpec, ExtensionGateSpec } from "./core/agentSpec.js";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { setTheme, getTheme, DEFAULT_THEME } from "./tui/theme.js";
|
|
|
5
5
|
export { isExitPromptError } from "./tui/promptErrors.js";
|
|
6
6
|
export { allowAnyMcpTool } from "./core/mcpPermissions.js";
|
|
7
7
|
export { createPromptLoader } from "./core/promptTemplate.js";
|
|
8
|
-
export {
|
|
8
|
+
export { claudeExecutable, claudeSignedIn, startClaudeSignIn } from "./core/claudeAuth.js";
|
|
9
9
|
export { ensureClaudeAuth } from "./tui/claudeAuth.js";
|
|
10
10
|
export { createFriendlyToolLabel, truncate, truncatePath, withToolLabels, withToolPhrases, withToolResults, quietResults, } from "./core/toolLabels.js";
|
|
11
11
|
export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, togglePlanMode } from "./core/session.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { serveMcp, type ServeMcpSettings } from "./serveMcp.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { serveMcp } from "./serveMcp.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
2
|
+
import type { AgentIdentity } from "../core/agentSpec.js";
|
|
3
|
+
import { type ChatSettings } from "../chat/chatController.js";
|
|
4
|
+
import { type SessionOpener } from "../chat/runs.js";
|
|
5
|
+
/** What `serveMcp()` needs besides how to open a session. */
|
|
6
|
+
export interface ServeMcpSettings {
|
|
7
|
+
/** Where the runs are: a `session_id` names one of them. */
|
|
8
|
+
runsDir: string;
|
|
9
|
+
/** Who the agent is: the server's name and version, and what the `ask` tool says it does. */
|
|
10
|
+
identity: AgentIdentity;
|
|
11
|
+
/** How many agents served over MCP may chain before `ask` refuses (default 3). */
|
|
12
|
+
maxDepth?: number;
|
|
13
|
+
/** How often a turn reports it's still running, so a client's tool timeout doesn't cut it (default 15 s). */
|
|
14
|
+
progressIntervalMs?: number;
|
|
15
|
+
/** The transport: stdio by default. */
|
|
16
|
+
transport?: Transport;
|
|
17
|
+
/** Runs the agent: the SDK's by default; another for tests. */
|
|
18
|
+
runQuery?: ChatSettings["runQuery"];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Serves an agent as an MCP server (ADR-027), so another agent (Claude Code, Claude Desktop,
|
|
22
|
+
* another one on the kit) drives it whole: its prompt, extensions and guardrails stay in its own
|
|
23
|
+
* process. One tool, `ask`; a `session_id` names a run, so a conversation goes on across calls.
|
|
24
|
+
*
|
|
25
|
+
* Autonomous mode only: nobody can answer a checkpoint here, and the calling model must never
|
|
26
|
+
* answer for the human. Resolves when the client disconnects.
|
|
27
|
+
*/
|
|
28
|
+
export declare function serveMcp(opener: SessionOpener, settings: ServeMcpSettings): Promise<void>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { getInteractionPort, setInteractionPort } from "../core/interaction.js";
|
|
6
|
+
import { MCP_DEPTH_ENV, mcpDepth } from "../core/mcpDepth.js";
|
|
7
|
+
import { createRunFolder, readRunSession } from "../core/runs.js";
|
|
8
|
+
import { createChatController } from "../chat/chatController.js";
|
|
9
|
+
import { openSession } from "../chat/runs.js";
|
|
10
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
11
|
+
const DEFAULT_PROGRESS_INTERVAL_MS = 15_000;
|
|
12
|
+
/** Sessions kept open between turns: each is a process. One closed is opened again by its `session_id`. */
|
|
13
|
+
const MAX_OPEN_SESSIONS = 4;
|
|
14
|
+
/**
|
|
15
|
+
* Serves an agent as an MCP server (ADR-027), so another agent (Claude Code, Claude Desktop,
|
|
16
|
+
* another one on the kit) drives it whole: its prompt, extensions and guardrails stay in its own
|
|
17
|
+
* process. One tool, `ask`; a `session_id` names a run, so a conversation goes on across calls.
|
|
18
|
+
*
|
|
19
|
+
* Autonomous mode only: nobody can answer a checkpoint here, and the calling model must never
|
|
20
|
+
* answer for the human. Resolves when the client disconnects.
|
|
21
|
+
*/
|
|
22
|
+
export async function serveMcp(opener, settings) {
|
|
23
|
+
const { runsDir, identity } = settings;
|
|
24
|
+
const maxDepth = settings.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
25
|
+
const interval = settings.progressIntervalMs ?? DEFAULT_PROGRESS_INTERVAL_MS;
|
|
26
|
+
const depth = mcpDepth();
|
|
27
|
+
// What this function takes over in the process, given back when it ends. Over stdio, stdout
|
|
28
|
+
// belongs to the transport: whatever else prints goes to stderr.
|
|
29
|
+
const previousDepth = process.env[MCP_DEPTH_ENV];
|
|
30
|
+
const previousPort = getInteractionPort();
|
|
31
|
+
const { log, info, debug } = console;
|
|
32
|
+
const stdio = !settings.transport;
|
|
33
|
+
process.env[MCP_DEPTH_ENV] = String(depth + 1);
|
|
34
|
+
setInteractionPort(null); // the response file alone: no keyboard here
|
|
35
|
+
if (stdio)
|
|
36
|
+
console.log = console.info = console.debug = console.error;
|
|
37
|
+
const restore = () => {
|
|
38
|
+
if (previousDepth === undefined)
|
|
39
|
+
delete process.env[MCP_DEPTH_ENV];
|
|
40
|
+
else
|
|
41
|
+
process.env[MCP_DEPTH_ENV] = previousDepth;
|
|
42
|
+
setInteractionPort(previousPort);
|
|
43
|
+
if (stdio)
|
|
44
|
+
Object.assign(console, { log, info, debug });
|
|
45
|
+
};
|
|
46
|
+
async function open(run) {
|
|
47
|
+
const opened = await openSession(opener, run);
|
|
48
|
+
if (opened.modeControl?.mode !== "autonomous")
|
|
49
|
+
throw new Error(`serveMcp() serves autonomous mode only, and this session opened in ${opened.modeControl?.mode ?? "no known mode"}: nobody can answer a checkpoint over MCP.`);
|
|
50
|
+
return opened;
|
|
51
|
+
}
|
|
52
|
+
// Opened at the start to refuse a wrong mode at once; the first new conversation uses it.
|
|
53
|
+
let spare;
|
|
54
|
+
try {
|
|
55
|
+
spare = await open(await createRunFolder(runsDir));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
restore();
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
const sessions = new Map();
|
|
62
|
+
const busy = new Set();
|
|
63
|
+
async function controllerOf(opened) {
|
|
64
|
+
return await createChatController(opened.options, {
|
|
65
|
+
sessionLogPath: path.join(opened.run.dir, "session.log"),
|
|
66
|
+
promptSuggestions: false,
|
|
67
|
+
...(opened.modeControl ? { modeControl: opened.modeControl } : {}),
|
|
68
|
+
...(opened.toolLabels ? { toolLabels: opened.toolLabels } : {}),
|
|
69
|
+
...(settings.runQuery ? { runQuery: settings.runQuery } : {}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** The session a `session_id` names, opened again if it isn't: only a run of this agent's. */
|
|
73
|
+
async function reopen(id) {
|
|
74
|
+
const running = sessions.get(id);
|
|
75
|
+
if (running)
|
|
76
|
+
return running;
|
|
77
|
+
const dir = path.join(runsDir, id);
|
|
78
|
+
const sessionId = /^[\w-]+$/.test(id) ? await readRunSession(dir) : null;
|
|
79
|
+
if (!sessionId)
|
|
80
|
+
throw new Error(`Unknown session_id: ${id}.`);
|
|
81
|
+
return await controllerOf(await open({ dir, sessionId }));
|
|
82
|
+
}
|
|
83
|
+
function closeIdleSessions() {
|
|
84
|
+
for (const [id, controller] of sessions) {
|
|
85
|
+
if (sessions.size <= MAX_OPEN_SESSIONS)
|
|
86
|
+
return;
|
|
87
|
+
if (busy.has(id))
|
|
88
|
+
continue;
|
|
89
|
+
controller.close();
|
|
90
|
+
sessions.delete(id);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const server = new McpServer({ name: identity.name, version: identity.version ?? "0.0.0" });
|
|
94
|
+
server.registerTool("ask", {
|
|
95
|
+
description: `Ask ${identity.name}${identity.description ? `, ${identity.description},` : ""} to do something, and get its reply. Without session_id it starts a new conversation; with the session_id of an earlier reply it goes on with that one.`,
|
|
96
|
+
inputSchema: { message: z.string().min(1).describe("What to ask, in plain words."), session_id: z.string().optional().describe("The session_id of an earlier reply, to go on with that conversation.") },
|
|
97
|
+
}, async ({ message, session_id: requested }, extra) => {
|
|
98
|
+
if (depth >= maxDepth)
|
|
99
|
+
throw new Error(`Too many agents chained over MCP (the limit is ${maxDepth}).`);
|
|
100
|
+
if (requested !== undefined && busy.has(requested))
|
|
101
|
+
throw new Error(`Session ${requested} is busy with another turn; wait for it to end.`);
|
|
102
|
+
if (requested !== undefined)
|
|
103
|
+
busy.add(requested);
|
|
104
|
+
let id = requested;
|
|
105
|
+
try {
|
|
106
|
+
let controller;
|
|
107
|
+
if (requested !== undefined)
|
|
108
|
+
controller = await reopen(requested);
|
|
109
|
+
else {
|
|
110
|
+
const opened = spare ?? (await open(await createRunFolder(runsDir)));
|
|
111
|
+
spare = null;
|
|
112
|
+
id = path.basename(opened.run.dir);
|
|
113
|
+
busy.add(id);
|
|
114
|
+
controller = await controllerOf(opened);
|
|
115
|
+
}
|
|
116
|
+
// Last in the map: the least recently used are closed first.
|
|
117
|
+
sessions.delete(id);
|
|
118
|
+
sessions.set(id, controller);
|
|
119
|
+
const token = extra._meta?.progressToken;
|
|
120
|
+
let progress = 0;
|
|
121
|
+
let activity;
|
|
122
|
+
const report = () => {
|
|
123
|
+
if (token === undefined)
|
|
124
|
+
return;
|
|
125
|
+
void extra.sendNotification({ method: "notifications/progress", params: { progressToken: token, progress: ++progress, ...(activity ? { message: activity } : {}) } }).catch(() => undefined);
|
|
126
|
+
};
|
|
127
|
+
let failure = null;
|
|
128
|
+
const stopListening = controller.onEvent((chat) => {
|
|
129
|
+
if (chat.type !== "agent")
|
|
130
|
+
return;
|
|
131
|
+
const event = chat.event;
|
|
132
|
+
if (event.type === "action" || event.type === "subagent-action") {
|
|
133
|
+
activity = controller.formatAction(event.toolName, event.input);
|
|
134
|
+
report();
|
|
135
|
+
}
|
|
136
|
+
else if (event.type === "turn-end" && event.failed)
|
|
137
|
+
failure = event.errorText || "The turn failed.";
|
|
138
|
+
});
|
|
139
|
+
const heartbeat = setInterval(report, interval);
|
|
140
|
+
const interrupt = () => controller.interrupt();
|
|
141
|
+
extra.signal.addEventListener("abort", interrupt, { once: true });
|
|
142
|
+
try {
|
|
143
|
+
await controller.turn(message);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
clearInterval(heartbeat);
|
|
147
|
+
stopListening();
|
|
148
|
+
extra.signal.removeEventListener("abort", interrupt);
|
|
149
|
+
}
|
|
150
|
+
if (failure)
|
|
151
|
+
throw new Error(failure);
|
|
152
|
+
return { content: [{ type: "text", text: controller.lastReply() }, { type: "text", text: `session_id: ${id}` }] };
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (id !== undefined)
|
|
156
|
+
busy.delete(id);
|
|
157
|
+
closeIdleSessions();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return await new Promise((resolve, reject) => {
|
|
161
|
+
server.server.onclose = () => {
|
|
162
|
+
for (const controller of sessions.values())
|
|
163
|
+
controller.close();
|
|
164
|
+
sessions.clear();
|
|
165
|
+
restore();
|
|
166
|
+
resolve();
|
|
167
|
+
};
|
|
168
|
+
const transport = settings.transport ?? new StdioServerTransport();
|
|
169
|
+
// The stdio transport doesn't see its client leave: stdin ending is how it does.
|
|
170
|
+
if (stdio)
|
|
171
|
+
process.stdin.once("end", () => void server.close());
|
|
172
|
+
server.connect(transport).catch((error) => {
|
|
173
|
+
restore();
|
|
174
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|