@falkenslab/agent-kit 0.30.1 → 0.32.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 -111
- package/dist/chat/chatController.d.ts +8 -0
- package/dist/chat/chatController.js +4 -1
- package/dist/core/agentSpec.d.ts +4 -1
- package/dist/core/claudeAuth.d.ts +53 -21
- package/dist/core/claudeAuth.js +135 -22
- package/dist/core/externalExtensions.js +21 -12
- package/dist/core/gitClone.d.ts +13 -0
- package/dist/core/gitClone.js +117 -0
- package/dist/core/hooks/languageReminder.d.ts +16 -0
- package/dist/core/hooks/languageReminder.js +24 -0
- package/dist/core/hooks/stepGate.d.ts +6 -1
- package/dist/core/hooks/stepGate.js +11 -2
- package/dist/core/hooks/transcriptLogger.d.ts +2 -2
- package/dist/core/hooks/transcriptLogger.js +2 -2
- package/dist/core/marketplaces.d.ts +9 -0
- package/dist/core/marketplaces.js +3 -15
- 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/core/session.js +8 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/tui/chatTui.d.ts +2 -0
- package/dist/tui/chatTui.js +1 -0
- package/dist/tui/claudeAuth.d.ts +12 -17
- package/dist/tui/claudeAuth.js +37 -66
- package/dist/tui/ink/runChatInk.js +1 -0
- package/package.json +2 -1
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { createHash } from "node:crypto";
|
|
3
2
|
import { cp, mkdtemp, readdir, readFile, rm, stat, writeFile, mkdir } from "node:fs/promises";
|
|
4
3
|
import os from "node:os";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
6
|
import { capabilitiesProblem, parseCapabilities, parseDescriptions, providedBy } from "./extensions.js";
|
|
9
7
|
import { getLanguage } from "./messages/index.js";
|
|
10
8
|
import { truncate } from "./toolLabels.js";
|
|
@@ -14,6 +12,7 @@ import { extensionSecretsPath, fillOptions, missingOptions, optionsNamed, parseO
|
|
|
14
12
|
import { isTrusted } from "./workspaceTrust.js";
|
|
15
13
|
import { checkRequirements, describeMissing, findOnPath, parseRequirements, requirementsProblem } from "./extensionRequirements.js";
|
|
16
14
|
import { cachedPackageDir, cachedPackageProblem, cachePackage, npxServer, packageCacheDir, packageSpec } from "./npxPackages.js";
|
|
15
|
+
import { cloneRepository } from "./gitClone.js";
|
|
17
16
|
/** Whether a server is remote (#61). */
|
|
18
17
|
export function isRemoteServer(server) {
|
|
19
18
|
return "url" in server;
|
|
@@ -203,7 +202,20 @@ async function pluginServers(dir, raw) {
|
|
|
203
202
|
}
|
|
204
203
|
/** `${CLAUDE_PLUGIN_ROOT}`, `${CLAUDE_PLUGIN_DATA}` and `${VAR}` (the agent's variable) in a server's values. */
|
|
205
204
|
function expand(value, folders) {
|
|
206
|
-
return value.replace(/\$\{(\w+)\}/g, (_, name) => name === "CLAUDE_PLUGIN_ROOT" ? folders.root : name === "CLAUDE_PLUGIN_DATA" ? folders.data : (process.env[name] ?? ""));
|
|
205
|
+
return value.replace(/\$\{(\w+)\}/g, (_, name) => (name === "CLAUDE_PLUGIN_ROOT" ? folders.root : name === "CLAUDE_PLUGIN_DATA" ? folders.data : (process.env[name] ?? "")));
|
|
206
|
+
}
|
|
207
|
+
/** The POSIX locale for a session's language: what any program reads to speak the person's language. */
|
|
208
|
+
const LOCALES = { en: "en_US.UTF-8", es: "es_ES.UTF-8", fr: "fr_FR.UTF-8", de: "de_DE.UTF-8" };
|
|
209
|
+
/**
|
|
210
|
+
* A server's locale in the session's language (#86): `LANG` and `LC_MESSAGES`, and `LC_ALL` when the
|
|
211
|
+
* system sets one (it would win). The standard way, so a server asks in the person's language
|
|
212
|
+
* knowing nothing of the kit, and does the same under Claude Code or any other client.
|
|
213
|
+
*/
|
|
214
|
+
function localeEnv(language) {
|
|
215
|
+
const locale = language ? LOCALES[language] : undefined;
|
|
216
|
+
if (!locale)
|
|
217
|
+
return {};
|
|
218
|
+
return { LANG: locale, LC_MESSAGES: locale, ...(process.env.LC_ALL ? { LC_ALL: locale } : {}) };
|
|
207
219
|
}
|
|
208
220
|
/**
|
|
209
221
|
* Why one of an extension's servers can't run through the launcher (Node, its script first,
|
|
@@ -214,7 +226,8 @@ function serverLaunch(name, server, folders, packages = {}) {
|
|
|
214
226
|
const { root } = folders;
|
|
215
227
|
// Its folders as variables too, as Claude Code gives them to a plugin's servers.
|
|
216
228
|
const declared = Object.fromEntries(Object.entries(server.env ?? {}).map(([key, value]) => [key, expand(String(value), folders)]));
|
|
217
|
-
|
|
229
|
+
// And the session's language as its locale (#86): a server that reads it can ask and answer in it.
|
|
230
|
+
const env = { CLAUDE_PLUGIN_ROOT: root, CLAUDE_PLUGIN_DATA: folders.data, ...localeEnv(folders.language), ...declared };
|
|
218
231
|
const npx = npxServer(server);
|
|
219
232
|
if (typeof npx === "string")
|
|
220
233
|
return `${npx} (its server "${name}")`;
|
|
@@ -390,7 +403,7 @@ function externalExtension(installed, manifest, cache, unavailable = []) {
|
|
|
390
403
|
const remote = [];
|
|
391
404
|
if (Object.keys(manifest.servers).length)
|
|
392
405
|
await mkdir(installed.dataDir, { recursive: true });
|
|
393
|
-
const folders = { root: installed.dir, data: installed.dataDir };
|
|
406
|
+
const folders = { root: installed.dir, data: installed.dataDir, language: getLanguage() };
|
|
394
407
|
// A remote server's options go into its URL and headers here, when the session opens: the
|
|
395
408
|
// SDK fixes them then, so a changed value takes opening the session again (#61).
|
|
396
409
|
const values = Object.values(manifest.servers).some(isRemoteServer) ? await readOptionValues(installed.privateDir, installed.name, manifest.userConfig, installed.lock.options) : {};
|
|
@@ -525,7 +538,6 @@ async function problem(installed, cache) {
|
|
|
525
538
|
return options;
|
|
526
539
|
return (await serversProblem(manifest, installed.dir, { cache, locked: installed.lock.packages })) ?? { manifest, unavailable };
|
|
527
540
|
}
|
|
528
|
-
const exec = promisify(execFile);
|
|
529
541
|
/** Whether `source` names a git repository rather than a folder. */
|
|
530
542
|
export function isGitSource(source) {
|
|
531
543
|
return /^(https?:\/\/|git@|ssh:\/\/|git:\/\/|file:\/\/|github:)/.test(source) || /\.git(#.*)?$/.test(source);
|
|
@@ -549,12 +561,9 @@ export async function addExtension(source, scopeDir, options = {}) {
|
|
|
549
561
|
if (isGitSource(source)) {
|
|
550
562
|
const [url, ref] = source.replace(/^github:/, "https://github.com/").split("#");
|
|
551
563
|
clone = await mkdtemp(path.join(os.tmpdir(), "agent-kit-extension-"));
|
|
552
|
-
//
|
|
553
|
-
await
|
|
554
|
-
if (options.sha)
|
|
555
|
-
await exec("git", ["-C", clone, "checkout", "--quiet", options.sha]);
|
|
556
|
-
commit = (await exec("git", ["-C", clone, "rev-parse", "HEAD"])).stdout.trim();
|
|
557
|
-
if (options.sha && commit !== options.sha.toLowerCase())
|
|
564
|
+
// Without the git program (#89): a pinned commit takes the history, otherwise the tip is enough.
|
|
565
|
+
commit = await cloneRepository(url, clone, { ...(ref ? { ref } : {}), ...(options.sha ? { sha: options.sha } : {}) });
|
|
566
|
+
if (options.sha && !commit.startsWith(options.sha.toLowerCase()))
|
|
558
567
|
throw new Error(`${url} has no commit ${options.sha}.`);
|
|
559
568
|
from = clone;
|
|
560
569
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clones a git repository into `dir` (#89), without the `git` program: an extension or a
|
|
3
|
+
* marketplace from GitHub installs on a computer without Git, as in a desktop app on a clean
|
|
4
|
+
* Windows. Over HTTP(S) with isomorphic-git (pure JavaScript: Node and Electron); a `file://`
|
|
5
|
+
* repository is read where it is and its files written out, never changed; SSH (`git@…`,
|
|
6
|
+
* `ssh://`, `git://`), which isomorphic-git can't speak, goes to the system's `git` when there is
|
|
7
|
+
* one. `ref` is a branch or a tag (the default branch without it); `sha` pins a commit, which must
|
|
8
|
+
* be on that branch's history. Returns the commit checked out. `dir` keeps no `.git`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function cloneRepository(url: string, dir: string, options?: {
|
|
11
|
+
ref?: string;
|
|
12
|
+
sha?: string;
|
|
13
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import git from "isomorphic-git";
|
|
8
|
+
import http from "isomorphic-git/http/node";
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
/**
|
|
11
|
+
* Clones a git repository into `dir` (#89), without the `git` program: an extension or a
|
|
12
|
+
* marketplace from GitHub installs on a computer without Git, as in a desktop app on a clean
|
|
13
|
+
* Windows. Over HTTP(S) with isomorphic-git (pure JavaScript: Node and Electron); a `file://`
|
|
14
|
+
* repository is read where it is and its files written out, never changed; SSH (`git@…`,
|
|
15
|
+
* `ssh://`, `git://`), which isomorphic-git can't speak, goes to the system's `git` when there is
|
|
16
|
+
* one. `ref` is a branch or a tag (the default branch without it); `sha` pins a commit, which must
|
|
17
|
+
* be on that branch's history. Returns the commit checked out. `dir` keeps no `.git`.
|
|
18
|
+
*/
|
|
19
|
+
export async function cloneRepository(url, dir, options = {}) {
|
|
20
|
+
try {
|
|
21
|
+
if (/^(git@|ssh:\/\/|git:\/\/)/.test(url))
|
|
22
|
+
return await cloneWithGitProgram(url, dir, options);
|
|
23
|
+
if (/^https?:\/\//.test(url))
|
|
24
|
+
return await cloneOverHttp(url, dir, options);
|
|
25
|
+
// A local repository: a file:// URL, or a path (one ending in .git, perhaps bare).
|
|
26
|
+
return await exportLocal(url.startsWith("file://") ? fileURLToPath(url) : path.resolve(url), dir, options);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
throw new Error(explain(url, options, error), { cause: error });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Over HTTP(S): the tip of a branch or tag in one shallow fetch, or its history when a commit is pinned. */
|
|
33
|
+
async function cloneOverHttp(url, dir, { ref, sha }) {
|
|
34
|
+
await git.clone({ fs, http, dir, url, ...(ref ? { ref } : {}), singleBranch: true, ...(sha ? {} : { depth: 1 }), noTags: !ref });
|
|
35
|
+
if (sha)
|
|
36
|
+
await git.checkout({ fs, dir, ref: await expandCommit(path.join(dir, ".git"), sha), force: true });
|
|
37
|
+
const commit = await git.resolveRef({ fs, dir, ref: "HEAD" });
|
|
38
|
+
await fs.promises.rm(path.join(dir, ".git"), { recursive: true, force: true });
|
|
39
|
+
return commit;
|
|
40
|
+
}
|
|
41
|
+
/** A local repository: the commit's files written into `dir`, the repository itself untouched (no checkout, no index). */
|
|
42
|
+
async function exportLocal(repo, dir, { ref, sha }) {
|
|
43
|
+
// A working copy's .git, or the repository itself when it's bare.
|
|
44
|
+
const gitdir = fs.existsSync(path.join(repo, ".git")) ? path.join(repo, ".git") : repo;
|
|
45
|
+
const commit = sha ? await expandCommit(gitdir, sha) : await git.resolveRef({ fs, gitdir, ref: ref ?? "HEAD" });
|
|
46
|
+
const { commit: { tree } } = await git.readCommit({ fs, gitdir, oid: commit });
|
|
47
|
+
await git.walk({
|
|
48
|
+
fs,
|
|
49
|
+
gitdir,
|
|
50
|
+
trees: [git.TREE({ ref: tree })],
|
|
51
|
+
map: async (filepath, [entry]) => {
|
|
52
|
+
if (!entry || filepath === ".")
|
|
53
|
+
return true;
|
|
54
|
+
const type = await entry.type();
|
|
55
|
+
const target = path.join(dir, filepath);
|
|
56
|
+
if (type === "tree")
|
|
57
|
+
await mkdir(target, { recursive: true });
|
|
58
|
+
else if (type === "blob") {
|
|
59
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
60
|
+
await writeFile(target, (await entry.content()) ?? new Uint8Array());
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
return commit;
|
|
66
|
+
}
|
|
67
|
+
/** A commit given whole or abbreviated, as git takes it: the whole id, or an error when there's no such commit. */
|
|
68
|
+
async function expandCommit(gitdir, sha) {
|
|
69
|
+
const oid = sha.length === 40 ? sha.toLowerCase() : await git.expandOid({ fs, gitdir, oid: sha.toLowerCase() });
|
|
70
|
+
await git.readCommit({ fs, gitdir, oid });
|
|
71
|
+
return oid;
|
|
72
|
+
}
|
|
73
|
+
/** SSH, which isomorphic-git can't speak: the system's `git`, or a clear error without one. */
|
|
74
|
+
async function cloneWithGitProgram(url, dir, { ref, sha }) {
|
|
75
|
+
try {
|
|
76
|
+
await exec("git", ["clone", ...(sha ? [] : ["--depth", "1"]), ...(ref ? ["--branch", ref] : []), url, dir]);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error.code === "ENOENT")
|
|
80
|
+
throw Object.assign(new Error("no git"), { code: "NoGitForSsh" });
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
if (sha)
|
|
84
|
+
await exec("git", ["-C", dir, "checkout", "--quiet", sha]);
|
|
85
|
+
const commit = (await exec("git", ["-C", dir, "rev-parse", "HEAD"])).stdout.trim();
|
|
86
|
+
await fs.promises.rm(path.join(dir, ".git"), { recursive: true, force: true });
|
|
87
|
+
return commit;
|
|
88
|
+
}
|
|
89
|
+
/** What went wrong, for a person: which repository, and whether it's the network, the repository, the ref or the commit. */
|
|
90
|
+
function explain(url, { ref, sha }, error) {
|
|
91
|
+
const code = error.code ?? "";
|
|
92
|
+
const status = error.data?.statusCode;
|
|
93
|
+
let host = url;
|
|
94
|
+
try {
|
|
95
|
+
host = new URL(url).host || url;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Not a URL (git@host:…): the address as given.
|
|
99
|
+
}
|
|
100
|
+
if (code === "NoGitForSsh")
|
|
101
|
+
return `${url} is an SSH address, which needs the git program, and this computer has none: use its HTTPS address instead.`;
|
|
102
|
+
if (code === "HttpError" && (status === 401 || status === 403 || status === 404))
|
|
103
|
+
return `${url} isn't a repository this computer can reach: it doesn't exist, or it's private.`;
|
|
104
|
+
if (code === "HttpError")
|
|
105
|
+
return `${host} answered with an error (${status ?? "?"}) for ${url}.`;
|
|
106
|
+
if (["ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT"].includes(code))
|
|
107
|
+
return `Couldn't reach ${host} for ${url}: check the network.`;
|
|
108
|
+
if (code === "NotFoundError" || code === "AmbiguousError") {
|
|
109
|
+
if (sha)
|
|
110
|
+
return `${url} has no commit ${sha}${ref ? ` on ${ref}` : ""}.`;
|
|
111
|
+
if (ref)
|
|
112
|
+
return `${url} has no branch or tag ${ref}.`;
|
|
113
|
+
}
|
|
114
|
+
if (code === "NotFoundError" && !/^https?:\/\//.test(url))
|
|
115
|
+
return `${url} isn't a git repository.`;
|
|
116
|
+
return `Couldn't get ${url}${ref ? ` at ${ref}` : ""}: ${error instanceof Error ? error.message : String(error)}`;
|
|
117
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { type Language } from "../language.js";
|
|
3
|
+
/**
|
|
4
|
+
* The reply language, said again next to what the model just read (#85): with only the system
|
|
5
|
+
* prompt's line, far from the turn, its narration between tool calls drifted to English after an
|
|
6
|
+
* English result (a source, a skill, the kit's own messages). One short line, about fifteen tokens.
|
|
7
|
+
*/
|
|
8
|
+
export declare function languageReminderLine(language: Language): string;
|
|
9
|
+
/**
|
|
10
|
+
* Its hooks: after each of the main agent's tool calls (a subagent has its own prompt line, and
|
|
11
|
+
* its narration isn't the person's), and with the person's message.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createLanguageReminder(language: Language): {
|
|
14
|
+
postToolUse: HookCallback;
|
|
15
|
+
userPromptSubmit: HookCallback;
|
|
16
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { LANGUAGE_NAMES } from "../language.js";
|
|
2
|
+
/**
|
|
3
|
+
* The reply language, said again next to what the model just read (#85): with only the system
|
|
4
|
+
* prompt's line, far from the turn, its narration between tool calls drifted to English after an
|
|
5
|
+
* English result (a source, a skill, the kit's own messages). One short line, about fifteen tokens.
|
|
6
|
+
*/
|
|
7
|
+
export function languageReminderLine(language) {
|
|
8
|
+
return `Reply language: ${LANGUAGE_NAMES[language]}, your narration between tool calls too.`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Its hooks: after each of the main agent's tool calls (a subagent has its own prompt line, and
|
|
12
|
+
* its narration isn't the person's), and with the person's message.
|
|
13
|
+
*/
|
|
14
|
+
export function createLanguageReminder(language) {
|
|
15
|
+
const line = languageReminderLine(language);
|
|
16
|
+
return {
|
|
17
|
+
postToolUse: async (input) => {
|
|
18
|
+
if (input.agent_id)
|
|
19
|
+
return {};
|
|
20
|
+
return { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: line } };
|
|
21
|
+
},
|
|
22
|
+
userPromptSubmit: async () => ({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: line } }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { type ToolLabels } from "../toolLabels.js";
|
|
2
3
|
/**
|
|
3
4
|
* PreToolUse hook for "interactive" mode: pauses before every action and asks for
|
|
4
5
|
* confirmation (by keyboard or by file, see humanInput.ts), like reviewing a plan step
|
|
@@ -7,5 +8,9 @@ import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
|
|
7
8
|
* `isActive` lets one session switch between "guided" and "interactive" while it runs (see
|
|
8
9
|
* session.ts's `ModeControl`): the hook stays registered and, while inactive, gives no
|
|
9
10
|
* decision at all, so the tool call goes on exactly as if the hook weren't there.
|
|
11
|
+
*
|
|
12
|
+
* The panel says the call as the chat does (#88), in the person's language: the session's
|
|
13
|
+
* labels (`labels`: the extensions'), else the kit's for its own tools, then the tool's name and
|
|
14
|
+
* its parameters in one line, cut short; the person approving needn't read JSON.
|
|
10
15
|
*/
|
|
11
|
-
export declare function createStepGate(runDir: string, isActive?: () => boolean, selfAskingTools?: readonly string[]): HookCallback;
|
|
16
|
+
export declare function createStepGate(runDir: string, isActive?: () => boolean, selfAskingTools?: readonly string[], labels?: () => ToolLabels | undefined): HookCallback;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { askForDecision } from "./humanInput.js";
|
|
2
2
|
import { t } from "../messages/index.js";
|
|
3
|
+
import { createFriendlyToolLabel, withToolLabels } from "../toolLabels.js";
|
|
4
|
+
/** How much of a call's parameters the panel shows, as one line (#88). */
|
|
5
|
+
const MAX_PARAMETERS = 300;
|
|
3
6
|
const SELF_ASKING_SERVERS = ["mcp__approvals__", "mcp__manualLogin__"];
|
|
4
7
|
const SELF_ASKING_TOOLS = new Set(["TodoWrite"]);
|
|
5
8
|
/**
|
|
@@ -10,8 +13,13 @@ const SELF_ASKING_TOOLS = new Set(["TodoWrite"]);
|
|
|
10
13
|
* `isActive` lets one session switch between "guided" and "interactive" while it runs (see
|
|
11
14
|
* session.ts's `ModeControl`): the hook stays registered and, while inactive, gives no
|
|
12
15
|
* decision at all, so the tool call goes on exactly as if the hook weren't there.
|
|
16
|
+
*
|
|
17
|
+
* The panel says the call as the chat does (#88), in the person's language: the session's
|
|
18
|
+
* labels (`labels`: the extensions'), else the kit's for its own tools, then the tool's name and
|
|
19
|
+
* its parameters in one line, cut short; the person approving needn't read JSON.
|
|
13
20
|
*/
|
|
14
|
-
export function createStepGate(runDir, isActive = () => true, selfAskingTools = []) {
|
|
21
|
+
export function createStepGate(runDir, isActive = () => true, selfAskingTools = [], labels = () => undefined) {
|
|
22
|
+
const describe = withToolLabels(labels, createFriendlyToolLabel());
|
|
15
23
|
return async (input) => {
|
|
16
24
|
if (!isActive())
|
|
17
25
|
return {};
|
|
@@ -20,9 +28,10 @@ export function createStepGate(runDir, isActive = () => true, selfAskingTools =
|
|
|
20
28
|
// a source) or only keeps the task list isn't asked about first: that would be asking to ask.
|
|
21
29
|
if (SELF_ASKING_SERVERS.some((prefix) => pre.tool_name.startsWith(prefix)) || SELF_ASKING_TOOLS.has(pre.tool_name) || selfAskingTools.includes(pre.tool_name))
|
|
22
30
|
return {};
|
|
31
|
+
const compact = JSON.stringify(pre.tool_input ?? {});
|
|
23
32
|
const answer = await askForDecision(runDir, {
|
|
24
33
|
title: t().proposedAction,
|
|
25
|
-
lines: [t().toolLine(pre.tool_name), t().parametersLine(
|
|
34
|
+
lines: [describe(pre.tool_name, pre.tool_input), t().toolLine(pre.tool_name), t().parametersLine(compact.length > MAX_PARAMETERS ? `${compact.slice(0, MAX_PARAMETERS)}…` : compact)],
|
|
26
35
|
});
|
|
27
36
|
if (answer === "q") {
|
|
28
37
|
return {
|
|
@@ -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));
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marketplaces (ADR-025, #29): a repository of extensions is a Claude Code marketplace as it is,
|
|
3
|
+
* a folder or git repository with `.claude-plugin/marketplace.json` listing plugins and where
|
|
4
|
+
* each comes from. An agent knows the marketplaces added to it, kept in its extensions folder
|
|
5
|
+
* (`marketplaces.json`, and a copy of each in `.marketplaces/<name>/`), and installs from them
|
|
6
|
+
* by `<plugin>@<marketplace>` into either scope, through `addExtension()`: the same copy, hash and
|
|
7
|
+
* lock as any extension. Its official marketplace is known without asking; any other is added
|
|
8
|
+
* only by hand, after a warning (the command asks to type its name).
|
|
9
|
+
*/
|
|
1
10
|
/** Where a marketplace's plugin comes from, as `marketplace.json` says (Claude Code's forms). */
|
|
2
11
|
export type PluginSource = string | {
|
|
3
12
|
source: "github";
|
|
@@ -1,20 +1,9 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
2
|
import os from "node:os";
|
|
4
3
|
import path from "node:path";
|
|
5
|
-
import { promisify } from "node:util";
|
|
6
4
|
import { addExtension, isGitSource } from "./externalExtensions.js";
|
|
7
5
|
import { fetchArchivePlugin, fetchNpmPlugin } from "./pluginDownloads.js";
|
|
8
|
-
|
|
9
|
-
* Marketplaces (ADR-025, #29): a repository of extensions is a Claude Code marketplace as it is,
|
|
10
|
-
* a folder or git repository with `.claude-plugin/marketplace.json` listing plugins and where
|
|
11
|
-
* each comes from. An agent knows the marketplaces added to it, kept in its extensions folder
|
|
12
|
-
* (`marketplaces.json`, and a copy of each in `.marketplaces/<name>/`), and installs from them
|
|
13
|
-
* by `<plugin>@<marketplace>` into either scope, through `addExtension()`: the same copy, hash and
|
|
14
|
-
* lock as any extension. Its official marketplace is known without asking; any other is added
|
|
15
|
-
* only by hand, after a warning (the command asks to type its name).
|
|
16
|
-
*/
|
|
17
|
-
const exec = promisify(execFile);
|
|
6
|
+
import { cloneRepository } from "./gitClone.js";
|
|
18
7
|
const REGISTRY = "marketplaces.json";
|
|
19
8
|
const COPIES = ".marketplaces";
|
|
20
9
|
const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
@@ -77,9 +66,8 @@ async function fetchMarketplace(source) {
|
|
|
77
66
|
try {
|
|
78
67
|
if (await isGitMarketplace(source)) {
|
|
79
68
|
const [location, ref] = source.split("#");
|
|
80
|
-
|
|
81
|
-
const commit =
|
|
82
|
-
await rm(path.join(copy, ".git"), { recursive: true, force: true });
|
|
69
|
+
// Without the git program (#89).
|
|
70
|
+
const commit = await cloneRepository(gitUrl(location), copy, ref ? { ref } : {});
|
|
83
71
|
return { copy, commit };
|
|
84
72
|
}
|
|
85
73
|
await cp(path.resolve(source), copy, { recursive: true, filter: (file) => path.basename(file) !== ".git" });
|
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/core/session.js
CHANGED
|
@@ -31,6 +31,7 @@ import { createElicitationHandler } from "./elicitation.js";
|
|
|
31
31
|
import { createReadOnlyHints, createToolHints } from "./readOnlyHints.js";
|
|
32
32
|
import { projectServerDuplicates } from "./projectServers.js";
|
|
33
33
|
import { createSignInReminder, SIGN_IN_RULE } from "./hooks/signInReminder.js";
|
|
34
|
+
import { createLanguageReminder } from "./hooks/languageReminder.js";
|
|
34
35
|
import { agentKitVersion } from "./version.js";
|
|
35
36
|
/**
|
|
36
37
|
* Builds the `options` object passed to the Agent SDK's `query()` — everything about
|
|
@@ -58,6 +59,8 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
58
59
|
// for the interface too, so the agent answers in the language its interface is in.
|
|
59
60
|
const language = chooseLanguage(config.language);
|
|
60
61
|
const replyLine = spec.replyInLanguage === false ? null : replyLanguageInstruction(language);
|
|
62
|
+
// Said again next to what the model reads, so its narration doesn't drift to English (#85).
|
|
63
|
+
const languageReminder = spec.replyInLanguage !== false && spec.replyInLanguage !== "prompt-only" && language !== "en" ? createLanguageReminder(language) : undefined;
|
|
61
64
|
const withReplyLine = (prompt) => (replyLine ? `${prompt}\n\n${replyLine}` : prompt);
|
|
62
65
|
const modeControl = createModeControl(mode);
|
|
63
66
|
// The human approval server only exists outside autonomous mode: the point of
|
|
@@ -379,7 +382,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
379
382
|
: []),
|
|
380
383
|
// Registered wherever the approval tool is, and asking only while the current mode is
|
|
381
384
|
// "interactive": a guided session can switch to interactive and back (ModeControl).
|
|
382
|
-
...(includeApprovalTool ? [{ hooks: [createStepGate(runDir, () => modeControl.mode === "interactive", fromExtensions((contribution) => contribution.selfAskingTools))] }] : []),
|
|
385
|
+
...(includeApprovalTool ? [{ hooks: [createStepGate(runDir, () => modeControl.mode === "interactive", fromExtensions((contribution) => contribution.selfAskingTools), () => toolLabels)] }] : []),
|
|
383
386
|
// Likewise for "plan", which any non-autonomous session can switch into: the agent
|
|
384
387
|
// only reads and plans while it's on (hooks/planGate.ts).
|
|
385
388
|
...(includeApprovalTool
|
|
@@ -405,12 +408,14 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
405
408
|
]
|
|
406
409
|
: []),
|
|
407
410
|
],
|
|
408
|
-
PostToolUse: [{ hooks: [transcriptLogger.postToolUse] }],
|
|
411
|
+
PostToolUse: [{ hooks: [transcriptLogger.postToolUse] }, ...(languageReminder ? [{ hooks: [languageReminder.postToolUse] }] : [])],
|
|
409
412
|
// The person's message: it ends a batch they approved (#74), and carries the sign-in reminders (#82).
|
|
410
|
-
...(guidedGate || signInExtensions.length
|
|
413
|
+
...(guidedGate || signInExtensions.length || languageReminder
|
|
411
414
|
? {
|
|
412
415
|
UserPromptSubmit: [
|
|
413
416
|
...(guidedGate ? [{ hooks: [guidedGate.userPromptSubmit] }] : []),
|
|
417
|
+
// The reply language, next to the person's message too (#85).
|
|
418
|
+
...(languageReminder ? [{ hooks: [languageReminder.userPromptSubmit] }] : []),
|
|
414
419
|
// With the person's message, which extensions now wait for a sign-in (#82).
|
|
415
420
|
...(signInExtensions.length ? [{ hooks: [createSignInReminder({ extensions: () => signInExtensions, status: () => late.view?.controls().mcpServerStatus })] }] : []),
|
|
416
421
|
],
|
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";
|