@falkenslab/agent-kit 0.10.2 → 0.13.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.
Files changed (68) hide show
  1. package/README.md +105 -97
  2. package/dist/core/agentSpec.d.ts +31 -1
  3. package/dist/core/claudeAuth.d.ts +1 -0
  4. package/dist/core/hooks/stepGate.js +3 -2
  5. package/dist/core/hooks/transcriptLogger.d.ts +1 -0
  6. package/dist/core/interaction.d.ts +3 -0
  7. package/dist/core/interaction.js +2 -0
  8. package/dist/core/language.d.ts +41 -0
  9. package/dist/core/language.js +60 -0
  10. package/dist/core/messages/de.d.ts +2 -0
  11. package/dist/core/messages/de.js +103 -0
  12. package/dist/core/messages/en.d.ts +90 -0
  13. package/dist/core/messages/en.js +102 -0
  14. package/dist/core/messages/es.d.ts +2 -0
  15. package/dist/core/messages/es.js +103 -0
  16. package/dist/core/messages/fr.d.ts +2 -0
  17. package/dist/core/messages/fr.js +103 -0
  18. package/dist/core/messages/index.d.ts +23 -0
  19. package/dist/core/messages/index.js +67 -0
  20. package/dist/core/runner.d.ts +2 -0
  21. package/dist/core/runs.d.ts +53 -0
  22. package/dist/core/runs.js +194 -0
  23. package/dist/core/session.d.ts +9 -0
  24. package/dist/core/session.js +43 -7
  25. package/dist/core/toolLabels.d.ts +8 -1
  26. package/dist/core/toolLabels.js +36 -54
  27. package/dist/core/tools/humanApproval.d.ts +1 -0
  28. package/dist/core/tools/humanApproval.js +3 -2
  29. package/dist/core/tools/manualLogin.d.ts +1 -0
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.js +7 -0
  32. package/dist/tui/chatTui.d.ts +31 -2
  33. package/dist/tui/chatTui.js +73 -7
  34. package/dist/tui/claudeAuth.d.ts +3 -1
  35. package/dist/tui/claudeAuth.js +12 -17
  36. package/dist/tui/consoleRenderer.d.ts +2 -0
  37. package/dist/tui/consoleRenderer.js +2 -1
  38. package/dist/tui/ink/PromptInput.d.ts +0 -2
  39. package/dist/tui/ink/PromptInput.js +5 -15
  40. package/dist/tui/ink/SessionView.js +17 -16
  41. package/dist/tui/ink/header.d.ts +1 -0
  42. package/dist/tui/ink/inkInteraction.js +4 -7
  43. package/dist/tui/ink/inkTheme.d.ts +9 -0
  44. package/dist/tui/ink/inkTheme.js +25 -0
  45. package/dist/tui/ink/markdown.js +49 -8
  46. package/dist/tui/ink/progressView.d.ts +15 -0
  47. package/dist/tui/ink/progressView.js +6 -1
  48. package/dist/tui/ink/promptEditing.d.ts +2 -5
  49. package/dist/tui/ink/promptEditing.js +11 -3
  50. package/dist/tui/ink/runChatInk.d.ts +26 -2
  51. package/dist/tui/ink/runChatInk.js +159 -48
  52. package/dist/tui/ink/sessionModel.d.ts +10 -0
  53. package/dist/tui/ink/sessionModel.js +27 -2
  54. package/dist/tui/ink/toolGroup.d.ts +2 -1
  55. package/dist/tui/ink/toolGroup.js +10 -21
  56. package/dist/tui/ink/wizard.d.ts +20 -0
  57. package/dist/tui/ink/wizard.js +8 -2
  58. package/dist/tui/language.d.ts +5 -0
  59. package/dist/tui/language.js +11 -0
  60. package/dist/tui/runs.d.ts +27 -0
  61. package/dist/tui/runs.js +30 -0
  62. package/dist/tui/terminalInteraction.d.ts +2 -0
  63. package/dist/tui/terminalInteraction.js +4 -2
  64. package/dist/tui/theme.d.ts +60 -0
  65. package/dist/tui/theme.js +65 -0
  66. package/dist/tui/ui.d.ts +23 -3
  67. package/dist/tui/ui.js +42 -18
  68. package/package.json +79 -75
@@ -0,0 +1,194 @@
1
+ import { appendFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const CONVERSATION = "conversation.jsonl";
4
+ const SUBAGENTS = "subagents";
5
+ const SESSION = "session.json";
6
+ /** A folder name for a new run, e.g. "2026-09-09T16-50-12-345Z" (sorts by date). */
7
+ export function runFolderName(date = new Date()) {
8
+ return date.toISOString().replace(/[:.]/g, "-");
9
+ }
10
+ /** Creates a new, empty run folder under `runsDir`. */
11
+ export async function createRunFolder(runsDir) {
12
+ const dir = path.join(runsDir, runFolderName());
13
+ await mkdir(dir, { recursive: true });
14
+ return { dir, sessionId: null };
15
+ }
16
+ async function readSessionFile(dir) {
17
+ try {
18
+ const parsed = JSON.parse(await readFile(path.join(dir, SESSION), "utf8"));
19
+ return typeof parsed.sessionId === "string" ? { sessionId: parsed.sessionId, subagents: parsed.subagents ?? {} } : null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ /** The SDK session a run folder keeps, from its `session.json`, or null. */
26
+ export async function readRunSession(dir) {
27
+ return (await readSessionFile(dir))?.sessionId ?? null;
28
+ }
29
+ async function readEntries(file) {
30
+ let raw;
31
+ try {
32
+ raw = await readFile(file, "utf8");
33
+ }
34
+ catch (error) {
35
+ if (error.code === "ENOENT")
36
+ return null;
37
+ throw error;
38
+ }
39
+ return raw
40
+ .split("\n")
41
+ .filter((line) => line.trim())
42
+ .map((line) => JSON.parse(line));
43
+ }
44
+ /** A readable, unused file name for a subagent's transcript, from its key (e.g. "subagents/agent-a67…"). */
45
+ function subagentFileName(subpath, taken) {
46
+ const name = subpath.replace(/^subagents\//, "").replace(/[^A-Za-z0-9._-]+/g, "_") || "agent";
47
+ let file = `${SUBAGENTS}/${name}.jsonl`;
48
+ for (let n = 2; taken.includes(file); n++)
49
+ file = `${SUBAGENTS}/${name}-${n}.jsonl`;
50
+ return file;
51
+ }
52
+ /**
53
+ * A `SessionStore` (the SDK's adapter for keeping transcripts elsewhere) that keeps one
54
+ * session in one run folder: `conversation.jsonl` for the main transcript,
55
+ * `subagents/*.jsonl` for each subagent's, and `session.json` with the session's id and
56
+ * which file is which subagent's. The SDK appends to it as the transcript grows and loads
57
+ * from it before resuming (confirmed empirically: a resumed session remembers the
58
+ * conversation, its subagents' transcripts included, even with the CLI's own copy deleted).
59
+ * The CLI still writes that copy under `~/.claude/projects/`: it can't be turned off while a
60
+ * store is in use.
61
+ *
62
+ * Bound to one folder, so every key it receives is taken as this run's session: one run
63
+ * folder is one conversation, resumed as many times as wanted (the session keeps its id).
64
+ */
65
+ export function createRunStore(dir) {
66
+ const seen = new Set();
67
+ let session = null;
68
+ // Appends are chained: the SDK may call append() again before the last one has finished,
69
+ // and entries must keep their order.
70
+ let queue = Promise.resolve();
71
+ async function currentSession() {
72
+ session ??= await readSessionFile(dir);
73
+ return session;
74
+ }
75
+ async function saveSession(next) {
76
+ session = next;
77
+ await mkdir(dir, { recursive: true });
78
+ await writeFile(path.join(dir, SESSION), `${JSON.stringify(next, null, 2)}\n`, "utf8");
79
+ }
80
+ /** The file for a key; with `create`, registers the session (and a new subagent) in session.json. */
81
+ async function fileFor(key, create) {
82
+ const current = await currentSession();
83
+ if (!key.subpath) {
84
+ if (create && !current)
85
+ await saveSession({ sessionId: key.sessionId, subagents: {} });
86
+ return path.join(dir, CONVERSATION);
87
+ }
88
+ const known = current?.subagents[key.subpath];
89
+ if (known)
90
+ return path.join(dir, known);
91
+ if (!create)
92
+ return null;
93
+ const base = current ?? { sessionId: key.sessionId, subagents: {} };
94
+ const file = subagentFileName(key.subpath, Object.values(base.subagents));
95
+ await saveSession({ ...base, subagents: { ...base.subagents, [key.subpath]: file } });
96
+ return path.join(dir, file);
97
+ }
98
+ async function write(key, entries) {
99
+ // Retries and replays may send an entry again: its uuid, when it has one, says so.
100
+ const fresh = entries.filter((entry) => {
101
+ if (!entry.uuid)
102
+ return true;
103
+ const id = `${key.subpath ?? ""}:${entry.uuid}`;
104
+ if (seen.has(id))
105
+ return false;
106
+ seen.add(id);
107
+ return true;
108
+ });
109
+ if (fresh.length === 0)
110
+ return;
111
+ const file = (await fileFor(key, true));
112
+ await mkdir(path.dirname(file), { recursive: true });
113
+ await appendFile(file, fresh.map((entry) => `${JSON.stringify(entry)}\n`).join(""), "utf8");
114
+ }
115
+ return {
116
+ append(key, entries) {
117
+ queue = queue.then(() => write(key, entries));
118
+ return queue;
119
+ },
120
+ async load(key) {
121
+ const file = await fileFor(key, false);
122
+ const entries = file ? await readEntries(file) : null;
123
+ // What's already there counts as seen: resuming must not append it twice.
124
+ for (const entry of entries ?? [])
125
+ if (entry.uuid)
126
+ seen.add(`${key.subpath ?? ""}:${entry.uuid}`);
127
+ return entries;
128
+ },
129
+ async listSubkeys() {
130
+ return Object.keys((await currentSession())?.subagents ?? {});
131
+ },
132
+ };
133
+ }
134
+ /** A transcript entry's text when it's something the human wrote or the agent replied, or null. */
135
+ export function entryText(entry) {
136
+ if (entry.type !== "user" && entry.type !== "assistant")
137
+ return null;
138
+ if (entry.isMeta || entry.isSidechain)
139
+ return null;
140
+ const content = entry.message?.content;
141
+ let text = "";
142
+ if (typeof content === "string")
143
+ text = content;
144
+ else if (Array.isArray(content)) {
145
+ const parts = content;
146
+ // A user entry holding tool results isn't something the human wrote.
147
+ if (entry.type === "user" && parts.some((part) => part.type === "tool_result"))
148
+ return null;
149
+ text = parts
150
+ .filter((part) => part.type === "text")
151
+ .map((part) => part.text ?? "")
152
+ .join("");
153
+ }
154
+ // Slash commands and the CLI's own notes arrive as tagged text, not as the human's words.
155
+ if (!text.trim() || /^\s*<[a-z-]+>/.test(text))
156
+ return null;
157
+ return { role: entry.type, text };
158
+ }
159
+ /** The human's messages and the agent's replies in a run's conversation, in order. */
160
+ export async function readConversation(dir) {
161
+ const entries = (await readEntries(path.join(dir, CONVERSATION))) ?? [];
162
+ return entries.map(entryText).filter((message) => message !== null);
163
+ }
164
+ /** The runs under `runsDir` that kept a conversation, newest first; runs without `session.json` are left out. */
165
+ export async function listRuns(runsDir) {
166
+ let names;
167
+ try {
168
+ names = (await readdir(runsDir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
169
+ }
170
+ catch {
171
+ return [];
172
+ }
173
+ const runs = [];
174
+ for (const name of names) {
175
+ const dir = path.join(runsDir, name);
176
+ const sessionId = await readRunSession(dir);
177
+ if (!sessionId)
178
+ continue;
179
+ const entries = (await readEntries(path.join(dir, CONVERSATION))) ?? [];
180
+ const times = entries.map((entry) => Date.parse(entry.timestamp ?? "")).filter((ms) => !Number.isNaN(ms));
181
+ const human = entries.map(entryText).filter((message) => message?.role === "user");
182
+ runs.push({
183
+ dir,
184
+ sessionId,
185
+ updatedAt: new Date(times.length > 0 ? Math.max(...times) : 0),
186
+ lastMessage: (human.at(-1)?.text ?? "").replace(/\s+/g, " ").trim(),
187
+ });
188
+ }
189
+ return runs.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
190
+ }
191
+ /** `--continue` among the process's arguments: start by resuming the latest run. */
192
+ export function continueArgument(argv) {
193
+ return argv.includes("--continue");
194
+ }
@@ -1,6 +1,8 @@
1
1
  import type { Options, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { type TranscriptLogger } from "./hooks/transcriptLogger.js";
3
3
  import type { AgentSpec, BaseSessionConfig, Mode } from "./agentSpec.js";
4
+ import { type Language } from "./language.js";
5
+ import { type RunFolder } from "./runs.js";
4
6
  /**
5
7
  * Builds the `options` object passed to the Agent SDK's `query()` — everything about
6
8
  * *how* the agent runs a session (system prompt, tools, MCP servers, hooks), independent
@@ -15,14 +17,20 @@ import type { AgentSpec, BaseSessionConfig, Mode } from "./agentSpec.js";
15
17
  * The per-mode behavioral differences (whether the approval/manual-intervention tools
16
18
  * exist, whether the interactive step gate hook runs) all fall out of `config.mode`
17
19
  * alone.
20
+ *
21
+ * `options.run` keeps the SDK's transcript of the conversation in that run folder (see
22
+ * runs.ts's createRunStore()) and, when it has a session already, resumes it: the chat
23
+ * passes it when the agent gives it a runs folder (`InkChatOptions.runsDir`).
18
24
  */
19
25
  export declare function buildSessionOptions<TConfig extends BaseSessionConfig>(config: TConfig, runDir: string, spec: AgentSpec<TConfig>, options?: {
20
26
  autoCompactEnabled?: boolean;
27
+ run?: RunFolder;
21
28
  }): Promise<{
22
29
  options: Options;
23
30
  transcriptLogger: TranscriptLogger;
24
31
  transcriptPath: string;
25
32
  modeControl: ModeControl;
33
+ language: Language;
26
34
  }>;
27
35
  /**
28
36
  * The supervision mode of a running session. Only "guided" and "interactive" can switch into
@@ -39,6 +47,7 @@ export interface ModeControl {
39
47
  /** Switches to `next` if this session allows it; returns whether it did. */
40
48
  set(next: Mode): boolean;
41
49
  }
50
+ /** A `ModeControl` starting at `initial` (`buildSessionOptions()` returns one for its session). */
42
51
  export declare function createModeControl(initial: Mode): ModeControl;
43
52
  /**
44
53
  * User message queue backed by a single long-lived generator, for any multi-turn
@@ -10,6 +10,9 @@ import { createManualLoginServer } from "./tools/manualLogin.js";
10
10
  import { createSaveToSourcesServer } from "./tools/saveToSources.js";
11
11
  import { allowAnyMcpTool } from "./mcpPermissions.js";
12
12
  import { knowledgePluginRoot, knowledgePromptSection } from "./knowledge.js";
13
+ import { replyLanguageInstruction } from "./language.js";
14
+ import { chooseLanguage } from "./messages/index.js";
15
+ import { createRunStore } from "./runs.js";
13
16
  /**
14
17
  * Builds the `options` object passed to the Agent SDK's `query()` — everything about
15
18
  * *how* the agent runs a session (system prompt, tools, MCP servers, hooks), independent
@@ -24,9 +27,18 @@ import { knowledgePluginRoot, knowledgePromptSection } from "./knowledge.js";
24
27
  * The per-mode behavioral differences (whether the approval/manual-intervention tools
25
28
  * exist, whether the interactive step gate hook runs) all fall out of `config.mode`
26
29
  * alone.
30
+ *
31
+ * `options.run` keeps the SDK's transcript of the conversation in that run folder (see
32
+ * runs.ts's createRunStore()) and, when it has a session already, resumes it: the chat
33
+ * passes it when the agent gives it a runs folder (`InkChatOptions.runsDir`).
27
34
  */
28
35
  export async function buildSessionOptions(config, runDir, spec, options = {}) {
29
36
  const { mode } = config;
37
+ // The kit's language (`--language`, then config.language, then the system's), chosen here
38
+ // for the interface too, so the agent answers in the language its interface is in.
39
+ const language = chooseLanguage(config.language);
40
+ const replyLine = spec.replyInLanguage === false ? null : replyLanguageInstruction(language);
41
+ const withReplyLine = (prompt) => (replyLine ? `${prompt}\n\n${replyLine}` : prompt);
30
42
  const modeControl = createModeControl(mode);
31
43
  // The human approval server only exists outside autonomous mode: the point of
32
44
  // autonomous is that the agent has no channel to ask for human intervention at all.
@@ -69,6 +81,10 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
69
81
  // anything not already present here — which is why the hook, not just omitting "Bash"
70
82
  // from this list, is what actually confines it to subagent use).
71
83
  const subagents = spec.buildSubagents(config);
84
+ // Each subagent gets the reply line too: its prompt doesn't include the main one's.
85
+ const subagentDefinitions = subagents
86
+ ? Object.fromEntries(Object.entries(subagents.agents).map(([name, agent]) => [name, { ...agent, prompt: withReplyLine(agent.prompt) }]))
87
+ : undefined;
72
88
  const includeSubagentTools = subagents !== undefined;
73
89
  // The exact set of subagent_type values the Agent tool may spawn in this session — see
74
90
  // createSubagentTypeGate() below for why this can't just be "whatever's in `agents`
@@ -79,10 +95,20 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
79
95
  const transcriptPath = path.join(runDir, "transcript.jsonl");
80
96
  const transcriptLogger = createTranscriptLogger(transcriptPath, config.secrets ?? []);
81
97
  const sdkOptions = {
82
- systemPrompt: includeKnowledgeBase && config.knowledgeDir
98
+ systemPrompt: withReplyLine(includeKnowledgeBase && config.knowledgeDir
83
99
  ? `${spec.buildSystemPrompt(config)}\n\n${knowledgePromptSection(config.projectDir, config.knowledgeDir, config.sourcesDir)}`
84
- : spec.buildSystemPrompt(config),
85
- settings: { autoCompactEnabled: options.autoCompactEnabled ?? true },
100
+ : spec.buildSystemPrompt(config)),
101
+ // Not the SDK's default (every source): "user" hands the agent the runner's own Claude
102
+ // Code configuration, and its `language` setting outranked the agent's prompt.
103
+ settingSources: spec.settingSources ?? ["project"],
104
+ // autoMemoryEnabled: false keeps out the runner's own Claude Code memory for the project
105
+ // (~/.claude/projects/<repository>/memory/MEMORY.md), which the CLI loads whatever
106
+ // settingSources says (confirmed empirically): it's notes for them, not for this agent,
107
+ // whose own memory is the knowledge base. includeGitInstructions: false leaves out the
108
+ // CLI's commit workflow and git context (the runner's git user name among it), which no
109
+ // agent here needs and which pulled replies towards the runner's language (confirmed
110
+ // empirically, see ADR-018).
111
+ settings: { autoCompactEnabled: options.autoCompactEnabled ?? true, autoMemoryEnabled: false, includeGitInstructions: false },
86
112
  // No built-in tools except, if applicable, Read/Write/Edit/Glob/Grep scoped to the
87
113
  // project's own folders (fileScopeGate below), and WebFetch/WebSearch
88
114
  // with no domain restriction (not conditioned on includeFileTools: it's read-only,
@@ -116,13 +142,15 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
116
142
  additionalDirectories: searchableDirs,
117
143
  // "skills" acts as a name whitelist, not an addition: "all" enables both the
118
144
  // SDK's own official skills (pdf/docx), the project's own custom ones, and the
119
- // plugin-provided built-ins below.
120
- skills: "all",
145
+ // plugin-provided built-ins below. A spec's own list gets the knowledge base's
146
+ // skills added, so it only names its own.
147
+ skills: skillList(spec.skills, includeKnowledgeBase),
121
148
  plugins: pluginRoots.map((pluginPath) => ({ type: "local", path: pluginPath, skipMcpDiscovery: true })),
122
149
  }
123
150
  : {}),
124
151
  maxTurns: 400,
125
- ...(subagents ? { agents: subagents.agents } : {}),
152
+ ...(options.run ? { sessionStore: createRunStore(options.run.dir), ...(options.run.sessionId ? { resume: options.run.sessionId } : {}) } : {}),
153
+ ...(subagentDefinitions ? { agents: subagentDefinitions } : {}),
126
154
  mcpServers: {
127
155
  ...spec.buildMcpServers(config, runDir),
128
156
  ...(includeApprovalTool ? { approvals: createHumanApprovalServer(runDir, spec.humanApprovalTexts) } : {}),
@@ -149,8 +177,16 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
149
177
  PostToolUse: [{ hooks: [transcriptLogger.postToolUse] }],
150
178
  },
151
179
  };
152
- return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl };
180
+ return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language };
181
+ }
182
+ /** The knowledge plugin's skills, as the SDK names a plugin's skills ("plugin:skill"). */
183
+ const KNOWLEDGE_SKILLS = ["knowledge-pages", "knowledge-ingest", "knowledge-query", "knowledge-lint"].map((skill) => `knowledge:${skill}`);
184
+ function skillList(skills, includeKnowledgeBase) {
185
+ if (skills === undefined || skills === "all")
186
+ return "all";
187
+ return includeKnowledgeBase ? [...new Set([...skills, ...KNOWLEDGE_SKILLS])] : skills;
153
188
  }
189
+ /** A `ModeControl` starting at `initial` (`buildSessionOptions()` returns one for its session). */
154
190
  export function createModeControl(initial) {
155
191
  let current = initial;
156
192
  const switchable = initial === "autonomous" ? ["autonomous"] : ["guided", "interactive"];
@@ -5,10 +5,17 @@
5
5
  * plus its own human-approval/manual-intervention/save-to-sources MCP tools)
6
6
  * generically; a
7
7
  * concrete agent supplies a `describe` callback for its own domain-specific tools (e.g.
8
- * Playwright's browser_* cases) via `createFriendlyToolLabel()`.
8
+ * Playwright's browser_* cases) via `createFriendlyToolLabel()`. The kit's own labels are in
9
+ * the current language (see messages/).
9
10
  */
11
+ /** Cuts `text` to `max` characters, with "…" at the end. */
10
12
  export declare function truncate(text: string, max?: number): string;
13
+ /**
14
+ * Cuts a path to `max` characters from the front, with "…": unlike `truncate()` (which cuts
15
+ * the tail off long freeform text), a path's most useful part, the file name, is at the end.
16
+ */
11
17
  export declare function truncatePath(text: string, max?: number): string;
18
+ /** An agent's labels for its own tools: the tool's short name and input, to a label, or `undefined` for the kit's default. */
12
19
  export type ToolDescriber = (shortName: string, input: Record<string, unknown>) => string | undefined;
13
20
  /**
14
21
  * Builds a `friendlyToolLabel(toolName, toolInput)` — `describe` lets the host agent
@@ -1,3 +1,4 @@
1
+ import { t } from "./messages/index.js";
1
2
  /**
2
3
  * Turns a raw tool name + input into a short, readable console description, instead of
3
4
  * the tool's technical name (e.g. "mcp__playwright__browser_click"). Covers the tools
@@ -5,78 +6,59 @@
5
6
  * plus its own human-approval/manual-intervention/save-to-sources MCP tools)
6
7
  * generically; a
7
8
  * concrete agent supplies a `describe` callback for its own domain-specific tools (e.g.
8
- * Playwright's browser_* cases) via `createFriendlyToolLabel()`.
9
+ * Playwright's browser_* cases) via `createFriendlyToolLabel()`. The kit's own labels are in
10
+ * the current language (see messages/).
9
11
  */
12
+ /** Cuts `text` to `max` characters, with "…" at the end. */
10
13
  export function truncate(text, max = 60) {
11
14
  return text.length > max ? `${text.slice(0, max)}…` : text;
12
15
  }
13
- // Unlike truncate() above (which cuts the tail off long freeform text), a path's most
14
- // useful part — the filename — is at the *end*, so this keeps the tail and cuts the
15
- // front instead.
16
+ /**
17
+ * Cuts a path to `max` characters from the front, with "…": unlike `truncate()` (which cuts
18
+ * the tail off long freeform text), a path's most useful part, the file name, is at the end.
19
+ */
16
20
  export function truncatePath(text, max = 60) {
17
21
  return text.length > max ? `…${text.slice(text.length - max + 1)}` : text;
18
22
  }
19
23
  /** Returns undefined for anything it doesn't specifically recognize — the caller decides the fallback. */
20
24
  function describeCore(shortName, input) {
25
+ const labels = t().labels;
26
+ const text = (value, fallback) => (typeof value === "string" ? value : fallback);
21
27
  switch (shortName) {
22
- case "request_human_approval": {
23
- const summary = typeof input.summary === "string" ? input.summary : "";
24
- return `Asking for human approval: ${truncate(summary, 120)}`;
25
- }
28
+ case "request_human_approval":
29
+ return labels.askingApproval(truncate(text(input.summary, ""), 120));
26
30
  case "request_manual_login":
27
- return "Waiting for a human to intervene manually";
28
- case "save_to_sources": {
29
- const destination = typeof input.destination === "string" ? input.destination : "sources/";
30
- return `Saving a file to sources/${truncatePath(destination, 70)}`;
31
- }
32
- case "Read": {
33
- const file = typeof input.file_path === "string" ? input.file_path : "a file";
34
- return `Reading ${truncatePath(file, 80)}`;
35
- }
36
- case "Write": {
37
- const file = typeof input.file_path === "string" ? input.file_path : "a file";
38
- return `Writing to ${truncatePath(file, 80)}`;
39
- }
40
- case "Edit": {
41
- const file = typeof input.file_path === "string" ? input.file_path : "a file";
42
- return `Editing ${truncatePath(file, 80)}`;
43
- }
44
- case "Glob": {
45
- const pattern = typeof input.pattern === "string" ? input.pattern : "*";
46
- return `Searching for files matching "${truncate(pattern, 60)}"`;
47
- }
48
- case "Grep": {
49
- const pattern = typeof input.pattern === "string" ? input.pattern : "";
50
- return `Searching file contents for "${truncate(pattern, 60)}"`;
51
- }
52
- case "Bash": {
53
- const command = typeof input.command === "string" ? input.command : "";
54
- return `Running "${truncate(command, 80)}"`;
55
- }
31
+ return labels.waitingManual;
32
+ case "save_to_sources":
33
+ return labels.savingToSources(truncatePath(text(input.destination, ""), 70));
34
+ case "Read":
35
+ return labels.reading(truncatePath(text(input.file_path, labels.aFile), 80));
36
+ case "Write":
37
+ return labels.writing(truncatePath(text(input.file_path, labels.aFile), 80));
38
+ case "Edit":
39
+ return labels.editing(truncatePath(text(input.file_path, labels.aFile), 80));
40
+ case "Glob":
41
+ return labels.findingFiles(truncate(text(input.pattern, "*"), 60));
42
+ case "Grep":
43
+ return labels.searchingContents(truncate(text(input.pattern, ""), 60));
44
+ case "Bash":
45
+ return labels.running(truncate(text(input.command, ""), 80));
56
46
  // The SDK's built-in subagent-delegation tool. Confirmed empirically (real
57
47
  // transcript) its input is { description, prompt, subagent_type }, not
58
48
  // { subagent_type, task } as its own tool description might suggest.
59
49
  case "Agent": {
60
- const subagentType = typeof input.subagent_type === "string" ? input.subagent_type : "a subagent";
61
- const description = typeof input.description === "string" ? input.description : "";
62
- return description
63
- ? `Delegating to "${subagentType}": ${truncate(description, 80)}`
64
- : `Delegating to "${subagentType}"`;
65
- }
66
- case "WebFetch": {
67
- const url = typeof input.url === "string" ? input.url : "a page";
68
- return `Fetching ${truncate(url, 80)}`;
69
- }
70
- case "WebSearch": {
71
- const query = typeof input.query === "string" ? input.query : "";
72
- return `Searching the web for "${truncate(query, 80)}"`;
50
+ const subagentType = text(input.subagent_type, labels.aSubagent);
51
+ const description = text(input.description, "");
52
+ return description ? labels.delegatingTask(subagentType, truncate(description, 80)) : labels.delegating(subagentType);
73
53
  }
54
+ case "WebFetch":
55
+ return labels.fetching(truncate(text(input.url, labels.aPage), 80));
56
+ case "WebSearch":
57
+ return labels.searchingWeb(truncate(text(input.query, ""), 80));
74
58
  // The SDK's own tool for invoking a skill (see session.ts for why it's listed
75
59
  // explicitly in `tools`). Confirmed empirically: its input is `{ skill: "name" }`.
76
- case "Skill": {
77
- const skill = typeof input.skill === "string" ? input.skill : "a skill";
78
- return `Applying the "${skill}" skill`;
79
- }
60
+ case "Skill":
61
+ return labels.applyingSkill(text(input.skill, labels.aSkill));
80
62
  default:
81
63
  return undefined;
82
64
  }
@@ -1,3 +1,4 @@
1
+ /** The approval tool's texts: its description (when the model should call it) and what it returns when approved or rejected. */
1
2
  export interface HumanApprovalTexts {
2
3
  description: string;
3
4
  approved: string;
@@ -1,6 +1,7 @@
1
1
  import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { z } from "zod";
3
3
  import { askForDecision } from "../hooks/humanInput.js";
4
+ import { t } from "../messages/index.js";
4
5
  const APPROVED_ANSWERS = new Set(["", "y", "yes"]);
5
6
  /** Domain-neutral defaults — a concrete agent should normally override these via `AgentSpec.humanApprovalTexts` with wording specific to what "publishing" means in its domain. */
6
7
  export const DEFAULT_HUMAN_APPROVAL_TEXTS = {
@@ -21,8 +22,8 @@ export function createHumanApprovalServer(runDir, texts = DEFAULT_HUMAN_APPROVAL
21
22
  summary: z.string().describe("Brief summary of what you're about to submit or publish"),
22
23
  }, async (args) => {
23
24
  const answer = await askForDecision(runDir, {
24
- title: "Human confirmation required before publishing",
25
- lines: [`Summary: ${args.summary}`],
25
+ title: t().approvalTitle,
26
+ lines: [t().summaryLine(args.summary)],
26
27
  });
27
28
  const approved = APPROVED_ANSWERS.has(answer);
28
29
  return {
@@ -1,3 +1,4 @@
1
+ /** The manual-intervention tool's texts: for the model (`toolDescription`, `confirmedMessage`) and for the person (the checkpoint). */
1
2
  export interface ManualInterventionTexts {
2
3
  toolDescription: string;
3
4
  confirmedMessage: string;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * as ui from "./tui/ui.js";
2
+ export { setTheme, getTheme, DEFAULT_THEME, type Theme, type ThemeColor } from "./tui/theme.js";
2
3
  export { isExitPromptError } from "./tui/promptErrors.js";
3
4
  export { allowAnyMcpTool } from "./core/mcpPermissions.js";
4
5
  export { createPromptLoader } from "./core/promptTemplate.js";
@@ -9,7 +10,11 @@ export type { Mode, BaseSessionConfig, AgentSpec } from "./core/agentSpec.js";
9
10
  export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, type ModeControl } from "./core/session.js";
10
11
  export { runQuery, type AgentEvent, type AgentRun, type SessionUsage, type ContextUsage } from "./core/runner.js";
11
12
  export { knowledgePluginRoot, knowledgePromptSection } from "./core/knowledge.js";
13
+ export { resolveLanguage, detectLanguage, SUPPORTED_LANGUAGES, type Language, type LanguageSources, type ResolvedLanguage, } from "./core/language.js";
14
+ export { setLanguage, getLanguage, messagesFor, type Messages } from "./core/messages/index.js";
15
+ export { createRunStore, createRunFolder, listRuns, readConversation, type RunFolder, type RunSummary, type ConversationMessage, } from "./core/runs.js";
12
16
  export type { Options, McpServerConfig, AgentDefinition } from "@anthropic-ai/claude-agent-sdk";
17
+ export { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
13
18
  export { createTranscriptLogger, summarizeToolResponse, type TranscriptLogger } from "./core/hooks/transcriptLogger.js";
14
19
  export { createStepGate } from "./core/hooks/stepGate.js";
15
20
  export { createSubagentBashGate } from "./core/hooks/subagentBashGate.js";
@@ -23,6 +28,7 @@ export { createHumanApprovalServer, DEFAULT_HUMAN_APPROVAL_TEXTS, type HumanAppr
23
28
  export { createManualLoginServer, type ManualInterventionTexts } from "./core/tools/manualLogin.js";
24
29
  export { createSaveToSourcesServer } from "./core/tools/saveToSources.js";
25
30
  export { runChatTui, type ChatTuiOptions } from "./tui/chatTui.js";
31
+ export type { SessionOpener } from "./tui/runs.js";
26
32
  export { createConsoleRenderer, type ConsoleRenderer, type ConsoleRendererOptions } from "./tui/consoleRenderer.js";
27
33
  export { runChatInk, type InkChatOptions, type HeaderInfo } from "./tui/ink/runChatInk.js";
28
34
  export type { ToolPhrase } from "./tui/ink/toolGroup.js";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { setInteractionPort } from "./core/interaction.js";
2
2
  import { terminalInteractionPort } from "./tui/terminalInteraction.js";
3
3
  export * as ui from "./tui/ui.js";
4
+ export { setTheme, getTheme, DEFAULT_THEME } from "./tui/theme.js";
4
5
  export { isExitPromptError } from "./tui/promptErrors.js";
5
6
  export { allowAnyMcpTool } from "./core/mcpPermissions.js";
6
7
  export { createPromptLoader } from "./core/promptTemplate.js";
@@ -10,6 +11,12 @@ export { createFriendlyToolLabel, truncate, truncatePath, } from "./core/toolLab
10
11
  export { buildSessionOptions, createInputQueue, createDeferred, createModeControl } from "./core/session.js";
11
12
  export { runQuery } from "./core/runner.js";
12
13
  export { knowledgePluginRoot, knowledgePromptSection } from "./core/knowledge.js";
14
+ export { resolveLanguage, detectLanguage, SUPPORTED_LANGUAGES, } from "./core/language.js";
15
+ export { setLanguage, getLanguage, messagesFor } from "./core/messages/index.js";
16
+ export { createRunStore, createRunFolder, listRuns, readConversation, } from "./core/runs.js";
17
+ // And the two functions an agent needs for tools of its own (an in-process MCP server,
18
+ // returned from AgentSpec.buildMcpServers()), so it doesn't need the SDK for those either.
19
+ export { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
13
20
  export { createTranscriptLogger, summarizeToolResponse } from "./core/hooks/transcriptLogger.js";
14
21
  export { createStepGate } from "./core/hooks/stepGate.js";
15
22
  export { createSubagentBashGate } from "./core/hooks/subagentBashGate.js";
@@ -1,5 +1,8 @@
1
1
  import type { Options } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { type AgentEvent } from "../core/runner.js";
3
+ import { type Theme } from "./theme.js";
4
+ import { type SessionOpener } from "./runs.js";
5
+ /** Options of the chats (`runChatTui()`, and `runChatInk()` through `InkChatOptions`). */
3
6
  export interface ChatTuiOptions {
4
7
  /**
5
8
  * Turns a tool call into a one-line console label. Defaults to `createFriendlyToolLabel()`
@@ -37,7 +40,7 @@ export interface ChatTuiOptions {
37
40
  * so a session leaves behind an exact, always-present transcript of what the terminal
38
41
  * showed even when the agent never calls a single tool (unlike `transcriptPath` from
39
42
  * `buildSessionOptions()`, which only gets written to on the first tool call). Omit to
40
- * skip session logging entirely.
43
+ * skip session logging entirely. With `runsDir`, the run's own `session.log` is used instead.
41
44
  */
42
45
  sessionLogPath?: string;
43
46
  /**
@@ -53,6 +56,28 @@ export interface ChatTuiOptions {
53
56
  historyPath?: string;
54
57
  /** Max entries kept in `historyPath` (and in the in-session ↑/↓ list). Defaults to 100. */
55
58
  historyLimit?: number;
59
+ /**
60
+ * The language of the kit's texts ("en", "es", "fr", "de"). `--language=<code>` on the
61
+ * command line wins; without either, the one `buildSessionOptions()` chose from
62
+ * `config.language`, or else the system's.
63
+ */
64
+ language?: string;
65
+ /**
66
+ * The agent's runs folder, with a session opener in place of the options (e.g.
67
+ * `(run) => buildSessionOptions(config, run.dir, spec, { run })`): each run gets its own
68
+ * folder (`<runsDir>/<timestamp>/`) keeping its conversation, its session log
69
+ * (`session.log`, instead of `sessionLogPath`) and its transcript, and can be resumed:
70
+ * `--continue` on the command line starts with the latest run, and `/resume` lists them
71
+ * (date and the human's last message) to switch to one. The runs folder holds the whole
72
+ * conversation, tool results included and not redacted: keep it out of version control.
73
+ */
74
+ runsDir?: string;
75
+ /**
76
+ * Colors for the kit's roles (see `Theme`), on top of the kit's defaults: only the roles
77
+ * given change, e.g. `{ toolResult: "yellow", selection: "#00ff00" }`. One theme per
78
+ * process: without this option, the one already set stays.
79
+ */
80
+ theme?: Partial<Theme>;
56
81
  }
57
82
  interface HistoryEntry {
58
83
  text: string;
@@ -97,9 +122,13 @@ export declare function drainTurn(events: AsyncIterator<AgentEvent>, onEvent: (e
97
122
  * const { options } = await buildSessionOptions(config, runDir, spec);
98
123
  * await runChatTui(options, { welcomeMessage: "Ready. Type /exit to quit." });
99
124
  *
125
+ * or, to keep each run's conversation in its folder and resume it (see `runsDir`):
126
+ *
127
+ * await runChatTui((run) => buildSessionOptions(config, run.dir, spec, { run }), { runsDir });
128
+ *
100
129
  * Resolves once the chat ends (an exit command, Ctrl+C/Ctrl+D at an idle prompt) — this
101
130
  * function owns the whole session lifecycle, closing the underlying query and readline
102
131
  * interface itself before returning.
103
132
  */
104
- export declare function runChatTui(options: Options, tuiOptions?: ChatTuiOptions): Promise<void>;
133
+ export declare function runChatTui(options: Options | SessionOpener, tuiOptions?: ChatTuiOptions): Promise<void>;
105
134
  export {};