@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.
- package/README.md +105 -97
- package/dist/core/agentSpec.d.ts +31 -1
- package/dist/core/claudeAuth.d.ts +1 -0
- package/dist/core/hooks/stepGate.js +3 -2
- package/dist/core/hooks/transcriptLogger.d.ts +1 -0
- package/dist/core/interaction.d.ts +3 -0
- package/dist/core/interaction.js +2 -0
- package/dist/core/language.d.ts +41 -0
- package/dist/core/language.js +60 -0
- package/dist/core/messages/de.d.ts +2 -0
- package/dist/core/messages/de.js +103 -0
- package/dist/core/messages/en.d.ts +90 -0
- package/dist/core/messages/en.js +102 -0
- package/dist/core/messages/es.d.ts +2 -0
- package/dist/core/messages/es.js +103 -0
- package/dist/core/messages/fr.d.ts +2 -0
- package/dist/core/messages/fr.js +103 -0
- package/dist/core/messages/index.d.ts +23 -0
- package/dist/core/messages/index.js +67 -0
- package/dist/core/runner.d.ts +2 -0
- package/dist/core/runs.d.ts +53 -0
- package/dist/core/runs.js +194 -0
- package/dist/core/session.d.ts +9 -0
- package/dist/core/session.js +43 -7
- package/dist/core/toolLabels.d.ts +8 -1
- package/dist/core/toolLabels.js +36 -54
- package/dist/core/tools/humanApproval.d.ts +1 -0
- package/dist/core/tools/humanApproval.js +3 -2
- package/dist/core/tools/manualLogin.d.ts +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/tui/chatTui.d.ts +31 -2
- package/dist/tui/chatTui.js +73 -7
- package/dist/tui/claudeAuth.d.ts +3 -1
- package/dist/tui/claudeAuth.js +12 -17
- package/dist/tui/consoleRenderer.d.ts +2 -0
- package/dist/tui/consoleRenderer.js +2 -1
- package/dist/tui/ink/PromptInput.d.ts +0 -2
- package/dist/tui/ink/PromptInput.js +5 -15
- package/dist/tui/ink/SessionView.js +17 -16
- package/dist/tui/ink/header.d.ts +1 -0
- package/dist/tui/ink/inkInteraction.js +4 -7
- package/dist/tui/ink/inkTheme.d.ts +9 -0
- package/dist/tui/ink/inkTheme.js +25 -0
- package/dist/tui/ink/markdown.js +49 -8
- package/dist/tui/ink/progressView.d.ts +15 -0
- package/dist/tui/ink/progressView.js +6 -1
- package/dist/tui/ink/promptEditing.d.ts +2 -5
- package/dist/tui/ink/promptEditing.js +11 -3
- package/dist/tui/ink/runChatInk.d.ts +26 -2
- package/dist/tui/ink/runChatInk.js +159 -48
- package/dist/tui/ink/sessionModel.d.ts +10 -0
- package/dist/tui/ink/sessionModel.js +27 -2
- package/dist/tui/ink/toolGroup.d.ts +2 -1
- package/dist/tui/ink/toolGroup.js +10 -21
- package/dist/tui/ink/wizard.d.ts +20 -0
- package/dist/tui/ink/wizard.js +8 -2
- package/dist/tui/language.d.ts +5 -0
- package/dist/tui/language.js +11 -0
- package/dist/tui/runs.d.ts +27 -0
- package/dist/tui/runs.js +30 -0
- package/dist/tui/terminalInteraction.d.ts +2 -0
- package/dist/tui/terminalInteraction.js +4 -2
- package/dist/tui/theme.d.ts +60 -0
- package/dist/tui/theme.js +65 -0
- package/dist/tui/ui.d.ts +23 -3
- package/dist/tui/ui.js +42 -18
- 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
|
+
}
|
package/dist/core/session.d.ts
CHANGED
|
@@ -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
|
package/dist/core/session.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
...(
|
|
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
|
package/dist/core/toolLabels.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
28
|
-
case "save_to_sources":
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
case "
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
case "
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
case "
|
|
41
|
-
|
|
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 =
|
|
61
|
-
const 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
|
-
|
|
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,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:
|
|
25
|
-
lines: [
|
|
25
|
+
title: t().approvalTitle,
|
|
26
|
+
lines: [t().summaryLine(args.summary)],
|
|
26
27
|
});
|
|
27
28
|
const approved = APPROVED_ANSWERS.has(answer);
|
|
28
29
|
return {
|
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";
|
package/dist/tui/chatTui.d.ts
CHANGED
|
@@ -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 {};
|