@falkenslab/agent-kit 0.10.2 → 0.12.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 +91 -97
- package/dist/core/agentSpec.d.ts +31 -1
- package/dist/core/hooks/stepGate.js +3 -2
- package/dist/core/language.d.ts +39 -0
- package/dist/core/language.js +59 -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/runs.d.ts +51 -0
- package/dist/core/runs.js +194 -0
- package/dist/core/session.d.ts +8 -0
- package/dist/core/session.js +42 -7
- package/dist/core/toolLabels.d.ts +2 -1
- package/dist/core/toolLabels.js +31 -51
- package/dist/core/tools/humanApproval.js +3 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +6 -0
- package/dist/tui/chatTui.d.ts +23 -2
- package/dist/tui/chatTui.js +71 -7
- package/dist/tui/claudeAuth.d.ts +3 -1
- package/dist/tui/claudeAuth.js +12 -17
- 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 +16 -15
- package/dist/tui/ink/inkInteraction.js +4 -7
- package/dist/tui/ink/progressView.d.ts +6 -0
- package/dist/tui/ink/progressView.js +2 -0
- package/dist/tui/ink/promptEditing.d.ts +2 -5
- package/dist/tui/ink/promptEditing.js +11 -3
- package/dist/tui/ink/runChatInk.d.ts +25 -2
- package/dist/tui/ink/runChatInk.js +156 -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 +5 -18
- package/dist/tui/ink/wizard.d.ts +6 -0
- package/dist/tui/ink/wizard.js +4 -1
- 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.js +2 -2
- package/package.json +1 -1
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
|
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,7 +177,14 @@ 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
|
}
|
|
154
189
|
export function createModeControl(initial) {
|
|
155
190
|
let current = initial;
|
|
@@ -5,7 +5,8 @@
|
|
|
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
|
*/
|
|
10
11
|
export declare function truncate(text: string, max?: number): string;
|
|
11
12
|
export declare function truncatePath(text: string, max?: number): string;
|
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,7 +6,8 @@
|
|
|
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
|
*/
|
|
10
12
|
export function truncate(text, max = 60) {
|
|
11
13
|
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
@@ -18,65 +20,43 @@ export function truncatePath(text, max = 60) {
|
|
|
18
20
|
}
|
|
19
21
|
/** Returns undefined for anything it doesn't specifically recognize — the caller decides the fallback. */
|
|
20
22
|
function describeCore(shortName, input) {
|
|
23
|
+
const labels = t().labels;
|
|
24
|
+
const text = (value, fallback) => (typeof value === "string" ? value : fallback);
|
|
21
25
|
switch (shortName) {
|
|
22
|
-
case "request_human_approval":
|
|
23
|
-
|
|
24
|
-
return `Asking for human approval: ${truncate(summary, 120)}`;
|
|
25
|
-
}
|
|
26
|
+
case "request_human_approval":
|
|
27
|
+
return labels.askingApproval(truncate(text(input.summary, ""), 120));
|
|
26
28
|
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
|
-
}
|
|
29
|
+
return labels.waitingManual;
|
|
30
|
+
case "save_to_sources":
|
|
31
|
+
return labels.savingToSources(truncatePath(text(input.destination, ""), 70));
|
|
32
|
+
case "Read":
|
|
33
|
+
return labels.reading(truncatePath(text(input.file_path, labels.aFile), 80));
|
|
34
|
+
case "Write":
|
|
35
|
+
return labels.writing(truncatePath(text(input.file_path, labels.aFile), 80));
|
|
36
|
+
case "Edit":
|
|
37
|
+
return labels.editing(truncatePath(text(input.file_path, labels.aFile), 80));
|
|
38
|
+
case "Glob":
|
|
39
|
+
return labels.findingFiles(truncate(text(input.pattern, "*"), 60));
|
|
40
|
+
case "Grep":
|
|
41
|
+
return labels.searchingContents(truncate(text(input.pattern, ""), 60));
|
|
42
|
+
case "Bash":
|
|
43
|
+
return labels.running(truncate(text(input.command, ""), 80));
|
|
56
44
|
// The SDK's built-in subagent-delegation tool. Confirmed empirically (real
|
|
57
45
|
// transcript) its input is { description, prompt, subagent_type }, not
|
|
58
46
|
// { subagent_type, task } as its own tool description might suggest.
|
|
59
47
|
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)}"`;
|
|
48
|
+
const subagentType = text(input.subagent_type, labels.aSubagent);
|
|
49
|
+
const description = text(input.description, "");
|
|
50
|
+
return description ? labels.delegatingTask(subagentType, truncate(description, 80)) : labels.delegating(subagentType);
|
|
73
51
|
}
|
|
52
|
+
case "WebFetch":
|
|
53
|
+
return labels.fetching(truncate(text(input.url, labels.aPage), 80));
|
|
54
|
+
case "WebSearch":
|
|
55
|
+
return labels.searchingWeb(truncate(text(input.query, ""), 80));
|
|
74
56
|
// The SDK's own tool for invoking a skill (see session.ts for why it's listed
|
|
75
57
|
// explicitly in `tools`). Confirmed empirically: its input is `{ skill: "name" }`.
|
|
76
|
-
case "Skill":
|
|
77
|
-
|
|
78
|
-
return `Applying the "${skill}" skill`;
|
|
79
|
-
}
|
|
58
|
+
case "Skill":
|
|
59
|
+
return labels.applyingSkill(text(input.skill, labels.aSkill));
|
|
80
60
|
default:
|
|
81
61
|
return undefined;
|
|
82
62
|
}
|
|
@@ -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
|
@@ -9,7 +9,11 @@ export type { Mode, BaseSessionConfig, AgentSpec } from "./core/agentSpec.js";
|
|
|
9
9
|
export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, type ModeControl } from "./core/session.js";
|
|
10
10
|
export { runQuery, type AgentEvent, type AgentRun, type SessionUsage, type ContextUsage } from "./core/runner.js";
|
|
11
11
|
export { knowledgePluginRoot, knowledgePromptSection } from "./core/knowledge.js";
|
|
12
|
+
export { resolveLanguage, detectLanguage, SUPPORTED_LANGUAGES, type Language, type LanguageSources, type ResolvedLanguage, } from "./core/language.js";
|
|
13
|
+
export { setLanguage, getLanguage, messagesFor, type Messages } from "./core/messages/index.js";
|
|
14
|
+
export { createRunStore, createRunFolder, listRuns, readConversation, type RunFolder, type RunSummary, type ConversationMessage, } from "./core/runs.js";
|
|
12
15
|
export type { Options, McpServerConfig, AgentDefinition } from "@anthropic-ai/claude-agent-sdk";
|
|
16
|
+
export { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
13
17
|
export { createTranscriptLogger, summarizeToolResponse, type TranscriptLogger } from "./core/hooks/transcriptLogger.js";
|
|
14
18
|
export { createStepGate } from "./core/hooks/stepGate.js";
|
|
15
19
|
export { createSubagentBashGate } from "./core/hooks/subagentBashGate.js";
|
|
@@ -23,6 +27,7 @@ export { createHumanApprovalServer, DEFAULT_HUMAN_APPROVAL_TEXTS, type HumanAppr
|
|
|
23
27
|
export { createManualLoginServer, type ManualInterventionTexts } from "./core/tools/manualLogin.js";
|
|
24
28
|
export { createSaveToSourcesServer } from "./core/tools/saveToSources.js";
|
|
25
29
|
export { runChatTui, type ChatTuiOptions } from "./tui/chatTui.js";
|
|
30
|
+
export type { SessionOpener } from "./tui/runs.js";
|
|
26
31
|
export { createConsoleRenderer, type ConsoleRenderer, type ConsoleRendererOptions } from "./tui/consoleRenderer.js";
|
|
27
32
|
export { runChatInk, type InkChatOptions, type HeaderInfo } from "./tui/ink/runChatInk.js";
|
|
28
33
|
export type { ToolPhrase } from "./tui/ink/toolGroup.js";
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,12 @@ export { createFriendlyToolLabel, truncate, truncatePath, } from "./core/toolLab
|
|
|
10
10
|
export { buildSessionOptions, createInputQueue, createDeferred, createModeControl } from "./core/session.js";
|
|
11
11
|
export { runQuery } from "./core/runner.js";
|
|
12
12
|
export { knowledgePluginRoot, knowledgePromptSection } from "./core/knowledge.js";
|
|
13
|
+
export { resolveLanguage, detectLanguage, SUPPORTED_LANGUAGES, } from "./core/language.js";
|
|
14
|
+
export { setLanguage, getLanguage, messagesFor } from "./core/messages/index.js";
|
|
15
|
+
export { createRunStore, createRunFolder, listRuns, readConversation, } from "./core/runs.js";
|
|
16
|
+
// And the two functions an agent needs for tools of its own (an in-process MCP server,
|
|
17
|
+
// returned from AgentSpec.buildMcpServers()), so it doesn't need the SDK for those either.
|
|
18
|
+
export { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
13
19
|
export { createTranscriptLogger, summarizeToolResponse } from "./core/hooks/transcriptLogger.js";
|
|
14
20
|
export { createStepGate } from "./core/hooks/stepGate.js";
|
|
15
21
|
export { createSubagentBashGate } from "./core/hooks/subagentBashGate.js";
|
package/dist/tui/chatTui.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import { type AgentEvent } from "../core/runner.js";
|
|
3
|
+
import { type SessionOpener } from "./runs.js";
|
|
3
4
|
export interface ChatTuiOptions {
|
|
4
5
|
/**
|
|
5
6
|
* Turns a tool call into a one-line console label. Defaults to `createFriendlyToolLabel()`
|
|
@@ -37,7 +38,7 @@ export interface ChatTuiOptions {
|
|
|
37
38
|
* so a session leaves behind an exact, always-present transcript of what the terminal
|
|
38
39
|
* showed even when the agent never calls a single tool (unlike `transcriptPath` from
|
|
39
40
|
* `buildSessionOptions()`, which only gets written to on the first tool call). Omit to
|
|
40
|
-
* skip session logging entirely.
|
|
41
|
+
* skip session logging entirely. With `runsDir`, the run's own `session.log` is used instead.
|
|
41
42
|
*/
|
|
42
43
|
sessionLogPath?: string;
|
|
43
44
|
/**
|
|
@@ -53,6 +54,22 @@ export interface ChatTuiOptions {
|
|
|
53
54
|
historyPath?: string;
|
|
54
55
|
/** Max entries kept in `historyPath` (and in the in-session ↑/↓ list). Defaults to 100. */
|
|
55
56
|
historyLimit?: number;
|
|
57
|
+
/**
|
|
58
|
+
* The language of the kit's texts ("en", "es", "fr", "de"). `--language=<code>` on the
|
|
59
|
+
* command line wins; without either, the one `buildSessionOptions()` chose from
|
|
60
|
+
* `config.language`, or else the system's.
|
|
61
|
+
*/
|
|
62
|
+
language?: string;
|
|
63
|
+
/**
|
|
64
|
+
* The agent's runs folder, with a session opener in place of the options (e.g.
|
|
65
|
+
* `(run) => buildSessionOptions(config, run.dir, spec, { run })`): each run gets its own
|
|
66
|
+
* folder (`<runsDir>/<timestamp>/`) keeping its conversation, its session log
|
|
67
|
+
* (`session.log`, instead of `sessionLogPath`) and its transcript, and can be resumed:
|
|
68
|
+
* `--continue` on the command line starts with the latest run, and `/resume` lists them
|
|
69
|
+
* (date and the human's last message) to switch to one. The runs folder holds the whole
|
|
70
|
+
* conversation, tool results included and not redacted: keep it out of version control.
|
|
71
|
+
*/
|
|
72
|
+
runsDir?: string;
|
|
56
73
|
}
|
|
57
74
|
interface HistoryEntry {
|
|
58
75
|
text: string;
|
|
@@ -97,9 +114,13 @@ export declare function drainTurn(events: AsyncIterator<AgentEvent>, onEvent: (e
|
|
|
97
114
|
* const { options } = await buildSessionOptions(config, runDir, spec);
|
|
98
115
|
* await runChatTui(options, { welcomeMessage: "Ready. Type /exit to quit." });
|
|
99
116
|
*
|
|
117
|
+
* or, to keep each run's conversation in its folder and resume it (see `runsDir`):
|
|
118
|
+
*
|
|
119
|
+
* await runChatTui((run) => buildSessionOptions(config, run.dir, spec, { run }), { runsDir });
|
|
120
|
+
*
|
|
100
121
|
* Resolves once the chat ends (an exit command, Ctrl+C/Ctrl+D at an idle prompt) — this
|
|
101
122
|
* function owns the whole session lifecycle, closing the underlying query and readline
|
|
102
123
|
* interface itself before returning.
|
|
103
124
|
*/
|
|
104
|
-
export declare function runChatTui(options: Options, tuiOptions?: ChatTuiOptions): Promise<void>;
|
|
125
|
+
export declare function runChatTui(options: Options | SessionOpener, tuiOptions?: ChatTuiOptions): Promise<void>;
|
|
105
126
|
export {};
|
package/dist/tui/chatTui.js
CHANGED
|
@@ -5,9 +5,13 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { createInputQueue } from "../core/session.js";
|
|
7
7
|
import { runQuery } from "../core/runner.js";
|
|
8
|
+
import { listRuns, readConversation } from "../core/runs.js";
|
|
8
9
|
import { isSharedQuestionActive, setSharedReadline } from "./terminalInteraction.js";
|
|
9
10
|
import * as ui from "./ui.js";
|
|
11
|
+
import { t } from "../core/messages/index.js";
|
|
12
|
+
import { applyLanguage } from "./language.js";
|
|
10
13
|
import { createConsoleRenderer } from "./consoleRenderer.js";
|
|
14
|
+
import { firstRun, openSession, runDate, runFolderOf, runLabel } from "./runs.js";
|
|
11
15
|
// Only strips picocolors' own SGR sequences (`\x1b[<codes>m`) — the only kind this file
|
|
12
16
|
// ever writes to the console — not a general-purpose ANSI stripper for arbitrary escape
|
|
13
17
|
// sequences (cursor movement, OSC, ...) this kit never emits.
|
|
@@ -92,16 +96,27 @@ export async function drainTurn(events, onEvent) {
|
|
|
92
96
|
* const { options } = await buildSessionOptions(config, runDir, spec);
|
|
93
97
|
* await runChatTui(options, { welcomeMessage: "Ready. Type /exit to quit." });
|
|
94
98
|
*
|
|
99
|
+
* or, to keep each run's conversation in its folder and resume it (see `runsDir`):
|
|
100
|
+
*
|
|
101
|
+
* await runChatTui((run) => buildSessionOptions(config, run.dir, spec, { run }), { runsDir });
|
|
102
|
+
*
|
|
95
103
|
* Resolves once the chat ends (an exit command, Ctrl+C/Ctrl+D at an idle prompt) — this
|
|
96
104
|
* function owns the whole session lifecycle, closing the underlying query and readline
|
|
97
105
|
* interface itself before returning.
|
|
98
106
|
*/
|
|
99
107
|
export async function runChatTui(options, tuiOptions = {}) {
|
|
108
|
+
applyLanguage(tuiOptions.language);
|
|
100
109
|
const exitCommands = new Set((tuiOptions.exitCommands ?? DEFAULT_EXIT_COMMANDS).map((c) => c.toLowerCase()));
|
|
101
110
|
const promptLabel = tuiOptions.promptLabel ?? DEFAULT_PROMPT_LABEL;
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
111
|
+
const opener = typeof options === "function" ? options : null;
|
|
112
|
+
const runsDir = opener ? tuiOptions.runsDir : undefined;
|
|
113
|
+
if (opener && !runsDir)
|
|
114
|
+
throw new Error("runChatTui(): a session opener needs `runsDir`.");
|
|
115
|
+
// The session in use: with runs, the run's folder, rebuilt on /resume.
|
|
116
|
+
let current = opener && runsDir ? await openSession(opener, await firstRun(runsDir)) : { options: options, run: null };
|
|
117
|
+
let queue = createInputQueue();
|
|
118
|
+
let run = runQuery(queue.iterable, current.options);
|
|
119
|
+
let events = run.events[Symbol.asyncIterator]();
|
|
105
120
|
const historyLimit = tuiOptions.historyLimit ?? DEFAULT_HISTORY_LIMIT;
|
|
106
121
|
let historyEntries = tuiOptions.historyPath ? capHistory(await loadHistory(tuiOptions.historyPath), historyLimit) : [];
|
|
107
122
|
const rl = readline.createInterface({
|
|
@@ -113,7 +128,14 @@ export async function runChatTui(options, tuiOptions = {}) {
|
|
|
113
128
|
historySize: historyLimit,
|
|
114
129
|
});
|
|
115
130
|
setSharedReadline(rl);
|
|
116
|
-
|
|
131
|
+
// With runs, the session log is the run's own; it moves with /resume.
|
|
132
|
+
let sessionLog = null;
|
|
133
|
+
function openLog() {
|
|
134
|
+
sessionLog?.end();
|
|
135
|
+
const file = current.run ? path.join(current.run.dir, "session.log") : tuiOptions.sessionLogPath;
|
|
136
|
+
sessionLog = file ? createWriteStream(file, { flags: "a" }) : null;
|
|
137
|
+
}
|
|
138
|
+
openLog();
|
|
117
139
|
// Mirrors to the log file only — for text already visible on the terminal some other
|
|
118
140
|
// way (readline's own prompt + the human's local echo), so it isn't printed twice.
|
|
119
141
|
function mirror(text) {
|
|
@@ -182,7 +204,7 @@ export async function runChatTui(options, tuiOptions = {}) {
|
|
|
182
204
|
return;
|
|
183
205
|
turnInterrupted = true;
|
|
184
206
|
void run.interrupt();
|
|
185
|
-
writeLine(ui.warn(
|
|
207
|
+
writeLine(ui.warn(t().interrupted));
|
|
186
208
|
}
|
|
187
209
|
rl.on("SIGINT", () => {
|
|
188
210
|
if (turnInFlight) {
|
|
@@ -203,10 +225,48 @@ export async function runChatTui(options, tuiOptions = {}) {
|
|
|
203
225
|
interruptTurn();
|
|
204
226
|
};
|
|
205
227
|
stdin.on("keypress", onKeypress);
|
|
228
|
+
/** Prints the run's earlier conversation (when it has one) and says it was resumed. */
|
|
229
|
+
async function showConversation(folder) {
|
|
230
|
+
if (!folder.sessionId)
|
|
231
|
+
return;
|
|
232
|
+
for (const message of await readConversation(folder.dir)) {
|
|
233
|
+
// Printed, not logged: the run's session log already has it.
|
|
234
|
+
stdout.write(message.role === "user" ? `${promptLabel}${message.text}\n` : `\n${tuiOptions.agentLabel ? `${tuiOptions.agentLabel} ` : ""}${message.text}\n`);
|
|
235
|
+
}
|
|
236
|
+
const [summary] = (await listRuns(path.dirname(folder.dir))).filter((r) => path.resolve(r.dir) === path.resolve(folder.dir));
|
|
237
|
+
writeLine(ui.dim(`\n${t().resumed(runDate(summary?.updatedAt ?? new Date()))}`));
|
|
238
|
+
}
|
|
239
|
+
/** /resume: lists the runs, asks for one and switches the session to it. */
|
|
240
|
+
async function resume() {
|
|
241
|
+
if (!opener || !runsDir)
|
|
242
|
+
return;
|
|
243
|
+
const runs = await listRuns(runsDir);
|
|
244
|
+
if (runs.length === 0) {
|
|
245
|
+
writeLine(ui.dim(t().noEarlierRuns));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
writeLine(ui.heading(t().resumeTitle));
|
|
249
|
+
runs.forEach((summary, i) => writeLine(` ${i + 1}. ${runLabel(summary, current.run ?? undefined)}`));
|
|
250
|
+
const answer = (await rl.question(t().resumeQuestion)).trim();
|
|
251
|
+
const summary = runs[Number(answer) - 1];
|
|
252
|
+
if (!answer || !summary || (current.run && path.resolve(summary.dir) === path.resolve(current.run.dir)))
|
|
253
|
+
return;
|
|
254
|
+
queue.end();
|
|
255
|
+
run.close();
|
|
256
|
+
current = await openSession(opener, await runFolderOf(summary));
|
|
257
|
+
openLog();
|
|
258
|
+
queue = createInputQueue();
|
|
259
|
+
run = runQuery(queue.iterable, current.options);
|
|
260
|
+
events = run.events[Symbol.asyncIterator]();
|
|
261
|
+
knownCommandTokens = null;
|
|
262
|
+
await showConversation(current.run);
|
|
263
|
+
}
|
|
206
264
|
if (tuiOptions.welcomeMessage)
|
|
207
265
|
writeLine(tuiOptions.welcomeMessage);
|
|
208
266
|
try {
|
|
209
|
-
if (
|
|
267
|
+
if (current.run)
|
|
268
|
+
await showConversation(current.run);
|
|
269
|
+
if (tuiOptions.initialPrompt && !current.run?.sessionId) {
|
|
210
270
|
mirror(`${promptLabel}${tuiOptions.initialPrompt}\n`);
|
|
211
271
|
queue.push(tuiOptions.initialPrompt);
|
|
212
272
|
turnInFlight = true;
|
|
@@ -233,6 +293,10 @@ export async function runChatTui(options, tuiOptions = {}) {
|
|
|
233
293
|
}
|
|
234
294
|
if (exitCommands.has(line.toLowerCase()))
|
|
235
295
|
break;
|
|
296
|
+
if (runsDir && line.toLowerCase() === "/resume") {
|
|
297
|
+
await resume();
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
236
300
|
// A line that *looks* like a slash command but matches nothing registered (a typo,
|
|
237
301
|
// an unnamespaced plugin command, ...) would otherwise reach the model as literal
|
|
238
302
|
// text with no special handling — confirmed empirically (captain-whiskers'
|
|
@@ -242,7 +306,7 @@ export async function runChatTui(options, tuiOptions = {}) {
|
|
|
242
306
|
// silence on unrecognized input might mean.
|
|
243
307
|
const commandToken = slashCommandToken(line);
|
|
244
308
|
if (commandToken && !(await isKnownSlashCommand(commandToken))) {
|
|
245
|
-
writeLine(ui.warn(
|
|
309
|
+
writeLine(ui.warn(t().unknownCommand(commandToken)));
|
|
246
310
|
continue;
|
|
247
311
|
}
|
|
248
312
|
queue.push(line);
|
package/dist/tui/claudeAuth.d.ts
CHANGED
|
@@ -16,4 +16,6 @@ import { type ClaudeAuthConfig } from "../core/claudeAuth.js";
|
|
|
16
16
|
* Only makes sense with a real terminal in front of a human (a `runWizard()` confirm, colored console
|
|
17
17
|
* output), which is why it lives here rather than in the non-interactive core.
|
|
18
18
|
*/
|
|
19
|
-
export declare function ensureClaudeAuth(config?: ClaudeAuthConfig
|
|
19
|
+
export declare function ensureClaudeAuth(config?: ClaudeAuthConfig, options?: {
|
|
20
|
+
language?: string;
|
|
21
|
+
}): Promise<string | undefined>;
|
package/dist/tui/claudeAuth.js
CHANGED
|
@@ -3,6 +3,8 @@ import { resolveClaudeAuth } from "../core/claudeAuth.js";
|
|
|
3
3
|
import { isExitPromptError } from "./promptErrors.js";
|
|
4
4
|
import * as ui from "./ui.js";
|
|
5
5
|
import { runWizard } from "./ink/wizard.js";
|
|
6
|
+
import { applyLanguage } from "./language.js";
|
|
7
|
+
import { t } from "../core/messages/index.js";
|
|
6
8
|
// The real format of "claude setup-token" tokens ("sk-ant-oat01-...", seen in the
|
|
7
9
|
// installed @anthropic-ai/claude-code binary) — used to extract the token from its output.
|
|
8
10
|
const TOKEN_PATTERN = /sk-ant-oat\d{2}-[A-Za-z0-9_-]+/;
|
|
@@ -61,23 +63,17 @@ function runSetupToken() {
|
|
|
61
63
|
* Only makes sense with a real terminal in front of a human (a `runWizard()` confirm, colored console
|
|
62
64
|
* output), which is why it lives here rather than in the non-interactive core.
|
|
63
65
|
*/
|
|
64
|
-
export async function ensureClaudeAuth(config = {}) {
|
|
66
|
+
export async function ensureClaudeAuth(config = {}, options = {}) {
|
|
65
67
|
if (resolveClaudeAuth(config))
|
|
66
68
|
return undefined;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
applyLanguage(options.language);
|
|
70
|
+
const texts = t().auth;
|
|
71
|
+
console.log(ui.warn(`\n${texts.noToken}`));
|
|
72
|
+
console.log(ui.dim(texts.noTokenDetail));
|
|
69
73
|
try {
|
|
70
|
-
const { generate } = await runWizard([
|
|
71
|
-
{
|
|
72
|
-
type: "confirm",
|
|
73
|
-
name: "generate",
|
|
74
|
-
message: 'Generate one now with "claude setup-token" (requires a Claude Pro/Max subscription)?',
|
|
75
|
-
default: true,
|
|
76
|
-
},
|
|
77
|
-
]);
|
|
74
|
+
const { generate } = await runWizard([{ type: "confirm", name: "generate", message: texts.generateQuestion, default: true }]);
|
|
78
75
|
if (!generate) {
|
|
79
|
-
console.log(ui.error(
|
|
80
|
-
"(or run this again and accept generating it) and try again."));
|
|
76
|
+
console.log(ui.error(`\n${texts.cantStart}`));
|
|
81
77
|
process.exit(0);
|
|
82
78
|
}
|
|
83
79
|
}
|
|
@@ -86,11 +82,10 @@ export async function ensureClaudeAuth(config = {}) {
|
|
|
86
82
|
process.exit(0);
|
|
87
83
|
throw error;
|
|
88
84
|
}
|
|
89
|
-
console.log(ui.heading(
|
|
90
|
-
console.log(ui.dim(
|
|
91
|
-
"below.\n"));
|
|
85
|
+
console.log(ui.heading(`\n${texts.generatingHeading}`));
|
|
86
|
+
console.log(ui.dim(`${texts.browserWillOpen}\n`));
|
|
92
87
|
const token = await runSetupToken();
|
|
93
88
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
|
|
94
|
-
console.log(ui.success(
|
|
89
|
+
console.log(ui.success(`\n${texts.generated}\n`));
|
|
95
90
|
return token;
|
|
96
91
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { stdout } from "node:process";
|
|
2
2
|
import { createFriendlyToolLabel } from "../core/toolLabels.js";
|
|
3
3
|
import * as ui from "./ui.js";
|
|
4
|
+
import { t } from "../core/messages/index.js";
|
|
4
5
|
/**
|
|
5
6
|
* The console rendering of `runQuery()`'s event stream, shared by `runChatTui()` and by any
|
|
6
7
|
* one-shot run that prints the same events. Streamed text rarely ends in a newline, so
|
|
@@ -46,7 +47,7 @@ export function createConsoleRenderer(options = {}) {
|
|
|
46
47
|
case "prompt-suggestion":
|
|
47
48
|
return;
|
|
48
49
|
case "mcp-error":
|
|
49
|
-
writeLine(ui.warn(
|
|
50
|
+
writeLine(ui.warn(t().mcpFailed(event.failedServers.join(", "))));
|
|
50
51
|
return;
|
|
51
52
|
case "info":
|
|
52
53
|
writeLine((event.level === "warning" ? ui.warn : ui.dim)(`(${event.text})`));
|
|
@@ -27,8 +27,6 @@ export interface PromptInputProps {
|
|
|
27
27
|
/** Project files for `@` mentions (paths relative to the session's folder). */
|
|
28
28
|
files?: readonly string[];
|
|
29
29
|
}
|
|
30
|
-
/** The shortcuts panel, opened with "?" on an empty prompt. */
|
|
31
|
-
export declare const SHORTCUTS: string[];
|
|
32
30
|
/**
|
|
33
31
|
* The chat's prompt: history (↑/↓), "/command" completion and suggestions (Tab), pasted
|
|
34
32
|
* blocks folded into a token, multi-line input (`\` + Enter or Ctrl+J), word and line
|