@falkenslab/agent-kit 0.10.1
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/LICENSE +21 -0
- package/README.md +97 -0
- package/assets/knowledge-plugin/.claude-plugin/plugin.json +4 -0
- package/assets/knowledge-plugin/commands/ingest.md +7 -0
- package/assets/knowledge-plugin/commands/lint.md +5 -0
- package/assets/knowledge-plugin/commands/query.md +5 -0
- package/assets/knowledge-plugin/skills/knowledge-ingest/SKILL.md +42 -0
- package/assets/knowledge-plugin/skills/knowledge-lint/SKILL.md +26 -0
- package/assets/knowledge-plugin/skills/knowledge-pages/SKILL.md +137 -0
- package/assets/knowledge-plugin/skills/knowledge-query/SKILL.md +14 -0
- package/dist/core/agentSpec.d.ts +132 -0
- package/dist/core/agentSpec.js +1 -0
- package/dist/core/claudeAuth.d.ts +24 -0
- package/dist/core/claudeAuth.js +27 -0
- package/dist/core/hooks/fileScopeGate.d.ts +33 -0
- package/dist/core/hooks/fileScopeGate.js +68 -0
- package/dist/core/hooks/humanInput.d.ts +15 -0
- package/dist/core/hooks/humanInput.js +52 -0
- package/dist/core/hooks/stepGate.d.ts +11 -0
- package/dist/core/hooks/stepGate.js +47 -0
- package/dist/core/hooks/subagentBashGate.d.ts +28 -0
- package/dist/core/hooks/subagentBashGate.js +40 -0
- package/dist/core/hooks/subagentForegroundGate.d.ts +18 -0
- package/dist/core/hooks/subagentForegroundGate.js +34 -0
- package/dist/core/hooks/subagentTypeGate.d.ts +16 -0
- package/dist/core/hooks/subagentTypeGate.js +33 -0
- package/dist/core/hooks/transcriptLogger.d.ts +14 -0
- package/dist/core/hooks/transcriptLogger.js +66 -0
- package/dist/core/interaction.d.ts +29 -0
- package/dist/core/interaction.js +7 -0
- package/dist/core/knowledge.d.ts +15 -0
- package/dist/core/knowledge.js +54 -0
- package/dist/core/mcpPermissions.d.ts +23 -0
- package/dist/core/mcpPermissions.js +47 -0
- package/dist/core/promptTemplate.d.ts +6 -0
- package/dist/core/promptTemplate.js +20 -0
- package/dist/core/runner.d.ts +137 -0
- package/dist/core/runner.js +115 -0
- package/dist/core/session.d.ts +63 -0
- package/dist/core/session.js +222 -0
- package/dist/core/toolLabels.d.ts +23 -0
- package/dist/core/toolLabels.js +114 -0
- package/dist/core/tools/humanApproval.d.ts +14 -0
- package/dist/core/tools/humanApproval.js +37 -0
- package/dist/core/tools/manualLogin.d.ts +16 -0
- package/dist/core/tools/manualLogin.js +27 -0
- package/dist/core/tools/saveToSources.d.ts +10 -0
- package/dist/core/tools/saveToSources.js +105 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +32 -0
- package/dist/tui/chatTui.d.ts +105 -0
- package/dist/tui/chatTui.js +265 -0
- package/dist/tui/claudeAuth.d.ts +19 -0
- package/dist/tui/claudeAuth.js +96 -0
- package/dist/tui/consoleRenderer.d.ts +32 -0
- package/dist/tui/consoleRenderer.js +72 -0
- package/dist/tui/ink/PromptInput.d.ts +40 -0
- package/dist/tui/ink/PromptInput.js +333 -0
- package/dist/tui/ink/SessionView.d.ts +42 -0
- package/dist/tui/ink/SessionView.js +223 -0
- package/dist/tui/ink/fileMentions.d.ts +13 -0
- package/dist/tui/ink/fileMentions.js +50 -0
- package/dist/tui/ink/fullscreen.d.ts +79 -0
- package/dist/tui/ink/fullscreen.js +175 -0
- package/dist/tui/ink/header.d.ts +17 -0
- package/dist/tui/ink/header.js +24 -0
- package/dist/tui/ink/inkInteraction.d.ts +22 -0
- package/dist/tui/ink/inkInteraction.js +64 -0
- package/dist/tui/ink/lineBuffer.d.ts +28 -0
- package/dist/tui/ink/lineBuffer.js +87 -0
- package/dist/tui/ink/markdown.d.ts +12 -0
- package/dist/tui/ink/markdown.js +148 -0
- package/dist/tui/ink/progressView.d.ts +27 -0
- package/dist/tui/ink/progressView.js +65 -0
- package/dist/tui/ink/promptEditing.d.ts +54 -0
- package/dist/tui/ink/promptEditing.js +123 -0
- package/dist/tui/ink/runChatInk.d.ts +103 -0
- package/dist/tui/ink/runChatInk.js +298 -0
- package/dist/tui/ink/sessionModel.d.ts +85 -0
- package/dist/tui/ink/sessionModel.js +262 -0
- package/dist/tui/ink/terminalCursor.d.ts +32 -0
- package/dist/tui/ink/terminalCursor.js +68 -0
- package/dist/tui/ink/terminalStatus.d.ts +20 -0
- package/dist/tui/ink/terminalStatus.js +55 -0
- package/dist/tui/ink/toolGroup.d.ts +36 -0
- package/dist/tui/ink/toolGroup.js +77 -0
- package/dist/tui/ink/wizard.d.ts +62 -0
- package/dist/tui/ink/wizard.js +167 -0
- package/dist/tui/promptErrors.d.ts +2 -0
- package/dist/tui/promptErrors.js +4 -0
- package/dist/tui/terminalInteraction.d.ts +18 -0
- package/dist/tui/terminalInteraction.js +85 -0
- package/dist/tui/ui.d.ts +21 -0
- package/dist/tui/ui.js +25 -0
- package/package.json +75 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns a raw tool name + input into a short, readable console description, instead of
|
|
3
|
+
* the tool's technical name (e.g. "mcp__playwright__browser_click"). Covers the tools
|
|
4
|
+
* this kit itself provides (Read/Write/Edit/Glob/Grep/Bash/Agent/WebFetch/WebSearch/Skill,
|
|
5
|
+
* plus its own human-approval/manual-intervention/save-to-sources MCP tools)
|
|
6
|
+
* generically; a
|
|
7
|
+
* concrete agent supplies a `describe` callback for its own domain-specific tools (e.g.
|
|
8
|
+
* Playwright's browser_* cases) via `createFriendlyToolLabel()`.
|
|
9
|
+
*/
|
|
10
|
+
export function truncate(text, max = 60) {
|
|
11
|
+
return text.length > max ? `${text.slice(0, max)}…` : text;
|
|
12
|
+
}
|
|
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
|
+
export function truncatePath(text, max = 60) {
|
|
17
|
+
return text.length > max ? `…${text.slice(text.length - max + 1)}` : text;
|
|
18
|
+
}
|
|
19
|
+
/** Returns undefined for anything it doesn't specifically recognize — the caller decides the fallback. */
|
|
20
|
+
function describeCore(shortName, input) {
|
|
21
|
+
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
|
+
}
|
|
26
|
+
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
|
+
}
|
|
56
|
+
// The SDK's built-in subagent-delegation tool. Confirmed empirically (real
|
|
57
|
+
// transcript) its input is { description, prompt, subagent_type }, not
|
|
58
|
+
// { subagent_type, task } as its own tool description might suggest.
|
|
59
|
+
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)}"`;
|
|
73
|
+
}
|
|
74
|
+
// The SDK's own tool for invoking a skill (see session.ts for why it's listed
|
|
75
|
+
// 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
|
+
}
|
|
80
|
+
default:
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Builds a `friendlyToolLabel(toolName, toolInput)` — `describe` lets the host agent
|
|
86
|
+
* layer its own domain-specific cases (e.g. Playwright's browser_* tools) on top of this
|
|
87
|
+
* kit's generic ones; `extraLocalServers` names any *additional* MCP server (beyond this
|
|
88
|
+
* kit's own "approvals"/"manualLogin"/"sourceFiles") whose tools should be unwrapped
|
|
89
|
+
* without a "[server] " prefix, e.g. "playwright".
|
|
90
|
+
*/
|
|
91
|
+
export function createFriendlyToolLabel(options = {}) {
|
|
92
|
+
const localServers = new Set(["approvals", "manualLogin", "sourceFiles", ...(options.extraLocalServers ?? [])]);
|
|
93
|
+
const describe = (shortName, input) => options.describe?.(shortName, input) ?? describeCore(shortName, input) ?? shortName.replace(/_/g, " ");
|
|
94
|
+
return (toolName, toolInput) => {
|
|
95
|
+
const input = toolInput && typeof toolInput === "object" ? toolInput : {};
|
|
96
|
+
if (!toolName.startsWith("mcp__"))
|
|
97
|
+
return describe(toolName, input);
|
|
98
|
+
// MCP tool names are "mcp__<server>__<tool>" — split on the first literal "__" after
|
|
99
|
+
// the prefix rather than on every single "_", since both the server name and the
|
|
100
|
+
// tool name can themselves contain single underscores (e.g. "browser_click").
|
|
101
|
+
const rest = toolName.slice("mcp__".length);
|
|
102
|
+
const separatorIndex = rest.indexOf("__");
|
|
103
|
+
if (separatorIndex === -1)
|
|
104
|
+
return describe(rest, input);
|
|
105
|
+
const serverName = rest.slice(0, separatorIndex);
|
|
106
|
+
const shortName = rest.slice(separatorIndex + 2);
|
|
107
|
+
if (localServers.has(serverName))
|
|
108
|
+
return describe(shortName, input);
|
|
109
|
+
// describe() never has a specific case for a custom server's tools, so this is
|
|
110
|
+
// always its default-case cleanup — prefix it with the server name so the console
|
|
111
|
+
// still says where it came from, e.g. "[podman] container list".
|
|
112
|
+
return `[${serverName}] ${describe(shortName, input)}`;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface HumanApprovalTexts {
|
|
2
|
+
description: string;
|
|
3
|
+
approved: string;
|
|
4
|
+
rejected: string;
|
|
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. */
|
|
7
|
+
export declare const DEFAULT_HUMAN_APPROVAL_TEXTS: HumanApprovalTexts;
|
|
8
|
+
/**
|
|
9
|
+
* Checkpoint tool for "guided" mode: the agent explores freely, but
|
|
10
|
+
* must call this tool and wait for human approval right before any action that publishes
|
|
11
|
+
* something visible to others and that's hard to naturally undo. Everything else —
|
|
12
|
+
* browsing and reading — doesn't go through this.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createHumanApprovalServer(runDir: string, texts?: HumanApprovalTexts): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { askForDecision } from "../hooks/humanInput.js";
|
|
4
|
+
const APPROVED_ANSWERS = new Set(["", "y", "yes"]);
|
|
5
|
+
/** Domain-neutral defaults — a concrete agent should normally override these via `AgentSpec.humanApprovalTexts` with wording specific to what "publishing" means in its domain. */
|
|
6
|
+
export const DEFAULT_HUMAN_APPROVAL_TEXTS = {
|
|
7
|
+
description: "Ask for human confirmation right before an action that publishes something visible " +
|
|
8
|
+
"to others and is hard to naturally undo. Don't use it for anything else: not for " +
|
|
9
|
+
"browsing, not for reading, not before every intermediate step.",
|
|
10
|
+
approved: "Approved by the human. You may continue.",
|
|
11
|
+
rejected: "Rejected by the human. Don't proceed with that; ask what to do differently.",
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Checkpoint tool for "guided" mode: the agent explores freely, but
|
|
15
|
+
* must call this tool and wait for human approval right before any action that publishes
|
|
16
|
+
* something visible to others and that's hard to naturally undo. Everything else —
|
|
17
|
+
* browsing and reading — doesn't go through this.
|
|
18
|
+
*/
|
|
19
|
+
export function createHumanApprovalServer(runDir, texts = DEFAULT_HUMAN_APPROVAL_TEXTS) {
|
|
20
|
+
const requestHumanApproval = tool("request_human_approval", texts.description, {
|
|
21
|
+
summary: z.string().describe("Brief summary of what you're about to submit or publish"),
|
|
22
|
+
}, async (args) => {
|
|
23
|
+
const answer = await askForDecision(runDir, {
|
|
24
|
+
title: "Human confirmation required before publishing",
|
|
25
|
+
lines: [`Summary: ${args.summary}`],
|
|
26
|
+
});
|
|
27
|
+
const approved = APPROVED_ANSWERS.has(answer);
|
|
28
|
+
return {
|
|
29
|
+
content: [{ type: "text", text: approved ? texts.approved : texts.rejected }],
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
return createSdkMcpServer({
|
|
33
|
+
name: "approvals",
|
|
34
|
+
version: "1.0.0",
|
|
35
|
+
tools: [requestHumanApproval],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ManualInterventionTexts {
|
|
2
|
+
toolDescription: string;
|
|
3
|
+
confirmedMessage: string;
|
|
4
|
+
checkpointTitle: string;
|
|
5
|
+
checkpointLines: string[];
|
|
6
|
+
checkpointQuestion?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Checkpoint tool for when the agent hits something it can't do itself (typically a login
|
|
10
|
+
* it has no credentials for, or one that fails unexpectedly). Pauses execution until a
|
|
11
|
+
* human confirms they've intervened by hand in whatever live interface the agent is driving
|
|
12
|
+
* (a browser window, say), and then lets the agent continue. There is deliberately no
|
|
13
|
+
* default wording: what counts as a manual intervention is entirely the domain's, so the
|
|
14
|
+
* concrete agent always supplies `texts` (see `AgentSpec.manualInterventionTexts`).
|
|
15
|
+
*/
|
|
16
|
+
export declare function createManualLoginServer(runDir: string, texts: ManualInterventionTexts): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { askForManualIntervention } from "../hooks/humanInput.js";
|
|
3
|
+
/**
|
|
4
|
+
* Checkpoint tool for when the agent hits something it can't do itself (typically a login
|
|
5
|
+
* it has no credentials for, or one that fails unexpectedly). Pauses execution until a
|
|
6
|
+
* human confirms they've intervened by hand in whatever live interface the agent is driving
|
|
7
|
+
* (a browser window, say), and then lets the agent continue. There is deliberately no
|
|
8
|
+
* default wording: what counts as a manual intervention is entirely the domain's, so the
|
|
9
|
+
* concrete agent always supplies `texts` (see `AgentSpec.manualInterventionTexts`).
|
|
10
|
+
*/
|
|
11
|
+
export function createManualLoginServer(runDir, texts) {
|
|
12
|
+
const requestManualLogin = tool("request_manual_login", texts.toolDescription, {}, async () => {
|
|
13
|
+
await askForManualIntervention(runDir, {
|
|
14
|
+
title: texts.checkpointTitle,
|
|
15
|
+
lines: texts.checkpointLines,
|
|
16
|
+
question: texts.checkpointQuestion,
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
content: [{ type: "text", text: texts.confirmedMessage }],
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
return createSdkMcpServer({
|
|
23
|
+
name: "manualLogin",
|
|
24
|
+
version: "1.0.0",
|
|
25
|
+
tools: [requestManualLogin],
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `save_to_sources`: the only way the agent adds to `sourcesDir` (Write/Edit are scoped
|
|
3
|
+
* to the notes folder, see hooks/fileScopeGate.ts), so the originals stay as obtained.
|
|
4
|
+
* Deliberately just an fs.copyFile with COPYFILE_EXCL — no parsing, no format conversion,
|
|
5
|
+
* no overwriting. A document downloaded via a browser-automation tool typically lands in
|
|
6
|
+
* this run's own folder, which the file tools can't reach; this tool's only job is getting
|
|
7
|
+
* it into `sourcesDir`, where the SDK's own Read tool already knows how to interpret it
|
|
8
|
+
* (PDF/DOCX text extraction, multimodal images).
|
|
9
|
+
*/
|
|
10
|
+
export declare function createSaveToSourcesServer(runDir: string, sourcesDir: string, description?: string): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { constants, copyFile, mkdir, readdir, stat } from "node:fs/promises";
|
|
3
|
+
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
/** Resolves `candidate` against `base`; returns the absolute path only if it stays inside it. */
|
|
6
|
+
function resolveWithin(base, candidate) {
|
|
7
|
+
const resolved = path.isAbsolute(candidate) ? path.resolve(candidate) : path.resolve(base, candidate);
|
|
8
|
+
const relative = path.relative(base, resolved);
|
|
9
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)) ? resolved : undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `resolvedPath` as given if it exists; otherwise, a same-directory sibling whose
|
|
13
|
+
* filename is Unicode-equal under NFC normalization. Needed because a file downloaded via
|
|
14
|
+
* a real browser navigation can land on disk with an accented filename in NFD
|
|
15
|
+
* (decomposed) form, while the model's own `source` argument, being ordinary typed/read
|
|
16
|
+
* text, is normally NFC (composed). The two look character-for-character identical but
|
|
17
|
+
* differ byte-for-byte, so a literal `copyFile` throws ENOENT even though the file is
|
|
18
|
+
* right there under a name that reads identically.
|
|
19
|
+
*/
|
|
20
|
+
async function resolveExistingFileToleratingNormalization(resolvedPath) {
|
|
21
|
+
try {
|
|
22
|
+
await stat(resolvedPath);
|
|
23
|
+
return resolvedPath;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// fall through to the normalization-tolerant lookup below
|
|
27
|
+
}
|
|
28
|
+
const dir = path.dirname(resolvedPath);
|
|
29
|
+
const wantName = path.basename(resolvedPath).normalize("NFC");
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = await readdir(dir);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const match = entries.find((name) => name.normalize("NFC") === wantName);
|
|
38
|
+
return match ? path.join(dir, match) : undefined;
|
|
39
|
+
}
|
|
40
|
+
const DEFAULT_DESCRIPTION = "Copy a file from this run's own folder (e.g. something just downloaded) into a folder " +
|
|
41
|
+
"under sources/, where original files are kept as they were obtained, apart from your " +
|
|
42
|
+
"own notes. It becomes readable there with the SDK's own Read tool, which already " +
|
|
43
|
+
"extracts text from PDF/DOCX and interprets images directly. This tool does no parsing " +
|
|
44
|
+
"or conversion itself, it only relocates the file, and it never overwrites a file " +
|
|
45
|
+
"that's already in sources/. Use it right after downloading a document you're about to " +
|
|
46
|
+
"study in depth, so it's still there for later sessions instead of only existing for " +
|
|
47
|
+
"this run.";
|
|
48
|
+
/**
|
|
49
|
+
* `save_to_sources`: the only way the agent adds to `sourcesDir` (Write/Edit are scoped
|
|
50
|
+
* to the notes folder, see hooks/fileScopeGate.ts), so the originals stay as obtained.
|
|
51
|
+
* Deliberately just an fs.copyFile with COPYFILE_EXCL — no parsing, no format conversion,
|
|
52
|
+
* no overwriting. A document downloaded via a browser-automation tool typically lands in
|
|
53
|
+
* this run's own folder, which the file tools can't reach; this tool's only job is getting
|
|
54
|
+
* it into `sourcesDir`, where the SDK's own Read tool already knows how to interpret it
|
|
55
|
+
* (PDF/DOCX text extraction, multimodal images).
|
|
56
|
+
*/
|
|
57
|
+
export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFAULT_DESCRIPTION) {
|
|
58
|
+
const saveToSources = tool("save_to_sources", description, {
|
|
59
|
+
source: z.string().describe("Path to the file, relative to this run's own folder - not a URL"),
|
|
60
|
+
destination: z.string().describe('Path to save it under, relative to sources/, e.g. "topic-3/slides.pdf"'),
|
|
61
|
+
}, async (args) => {
|
|
62
|
+
const candidate = resolveWithin(runDir, args.source);
|
|
63
|
+
if (!candidate) {
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text: `"${args.source}" isn't inside this run's folder - refusing to read it.` }],
|
|
66
|
+
isError: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const resolvedSource = await resolveExistingFileToleratingNormalization(candidate);
|
|
70
|
+
if (!resolvedSource) {
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: `Couldn't find "${args.source}" in this run's folder (checked byte-for-byte and Unicode-normalized names) - double check the exact path, e.g. with Glob.` }],
|
|
73
|
+
isError: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const resolvedDestination = resolveWithin(sourcesDir, args.destination);
|
|
77
|
+
if (!resolvedDestination) {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: "text", text: `"${args.destination}" isn't inside sources/ - refusing to write there.` }],
|
|
80
|
+
isError: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
await mkdir(path.dirname(resolvedDestination), { recursive: true });
|
|
85
|
+
await copyFile(resolvedSource, resolvedDestination, constants.COPYFILE_EXCL);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const exists = error.code === "EEXIST";
|
|
89
|
+
return {
|
|
90
|
+
content: [{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: exists
|
|
93
|
+
? `"${args.destination}" already exists in sources/ - originals are never overwritten; pick another destination name.`
|
|
94
|
+
: `Couldn't copy "${args.source}" to sources/: ${error instanceof Error ? error.message : String(error)}`,
|
|
95
|
+
}],
|
|
96
|
+
isError: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const savedAs = path.relative(sourcesDir, resolvedDestination).split(path.sep).join("/");
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: "text", text: `Saved to sources/${savedAs} - read it from there with Read.` }],
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
return createSdkMcpServer({ name: "sourceFiles", version: "1.0.0", tools: [saveToSources] });
|
|
105
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export * as ui from "./tui/ui.js";
|
|
2
|
+
export { isExitPromptError } from "./tui/promptErrors.js";
|
|
3
|
+
export { allowAnyMcpTool } from "./core/mcpPermissions.js";
|
|
4
|
+
export { createPromptLoader } from "./core/promptTemplate.js";
|
|
5
|
+
export { resolveClaudeAuth, type ClaudeAuthConfig } from "./core/claudeAuth.js";
|
|
6
|
+
export { ensureClaudeAuth } from "./tui/claudeAuth.js";
|
|
7
|
+
export { createFriendlyToolLabel, truncate, truncatePath, type ToolDescriber, } from "./core/toolLabels.js";
|
|
8
|
+
export type { Mode, BaseSessionConfig, AgentSpec } from "./core/agentSpec.js";
|
|
9
|
+
export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, type ModeControl } from "./core/session.js";
|
|
10
|
+
export { runQuery, type AgentEvent, type AgentRun, type SessionUsage, type ContextUsage } from "./core/runner.js";
|
|
11
|
+
export { knowledgePluginRoot, knowledgePromptSection } from "./core/knowledge.js";
|
|
12
|
+
export type { Options, McpServerConfig, AgentDefinition } from "@anthropic-ai/claude-agent-sdk";
|
|
13
|
+
export { createTranscriptLogger, summarizeToolResponse, type TranscriptLogger } from "./core/hooks/transcriptLogger.js";
|
|
14
|
+
export { createStepGate } from "./core/hooks/stepGate.js";
|
|
15
|
+
export { createSubagentBashGate } from "./core/hooks/subagentBashGate.js";
|
|
16
|
+
export { createSubagentTypeGate } from "./core/hooks/subagentTypeGate.js";
|
|
17
|
+
export { createSubagentForegroundGate } from "./core/hooks/subagentForegroundGate.js";
|
|
18
|
+
export { createFileScopeGate, checkFileScope, type FileScope } from "./core/hooks/fileScopeGate.js";
|
|
19
|
+
export { askForDecision, askForManualIntervention } from "./core/hooks/humanInput.js";
|
|
20
|
+
export { setInteractionPort, getInteractionPort, type InteractionPort, type ApprovalPrompt } from "./core/interaction.js";
|
|
21
|
+
export { terminalInteractionPort, setSharedReadline, getSharedReadline } from "./tui/terminalInteraction.js";
|
|
22
|
+
export { createHumanApprovalServer, DEFAULT_HUMAN_APPROVAL_TEXTS, type HumanApprovalTexts } from "./core/tools/humanApproval.js";
|
|
23
|
+
export { createManualLoginServer, type ManualInterventionTexts } from "./core/tools/manualLogin.js";
|
|
24
|
+
export { createSaveToSourcesServer } from "./core/tools/saveToSources.js";
|
|
25
|
+
export { runChatTui, type ChatTuiOptions } from "./tui/chatTui.js";
|
|
26
|
+
export { createConsoleRenderer, type ConsoleRenderer, type ConsoleRendererOptions } from "./tui/consoleRenderer.js";
|
|
27
|
+
export { runChatInk, type InkChatOptions, type HeaderInfo } from "./tui/ink/runChatInk.js";
|
|
28
|
+
export type { ToolPhrase } from "./tui/ink/toolGroup.js";
|
|
29
|
+
export type { RenderApproval } from "./tui/ink/SessionView.js";
|
|
30
|
+
export { createProgressView, type ProgressView, type ProgressViewOptions } from "./tui/ink/progressView.js";
|
|
31
|
+
export { runWizard, type WizardStep, type SelectStep, type InputStep, type PasswordStep, type ConfirmStep, type WizardAnswers, type WizardOptions, } from "./tui/ink/wizard.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { setInteractionPort } from "./core/interaction.js";
|
|
2
|
+
import { terminalInteractionPort } from "./tui/terminalInteraction.js";
|
|
3
|
+
export * as ui from "./tui/ui.js";
|
|
4
|
+
export { isExitPromptError } from "./tui/promptErrors.js";
|
|
5
|
+
export { allowAnyMcpTool } from "./core/mcpPermissions.js";
|
|
6
|
+
export { createPromptLoader } from "./core/promptTemplate.js";
|
|
7
|
+
export { resolveClaudeAuth } from "./core/claudeAuth.js";
|
|
8
|
+
export { ensureClaudeAuth } from "./tui/claudeAuth.js";
|
|
9
|
+
export { createFriendlyToolLabel, truncate, truncatePath, } from "./core/toolLabels.js";
|
|
10
|
+
export { buildSessionOptions, createInputQueue, createDeferred, createModeControl } from "./core/session.js";
|
|
11
|
+
export { runQuery } from "./core/runner.js";
|
|
12
|
+
export { knowledgePluginRoot, knowledgePromptSection } from "./core/knowledge.js";
|
|
13
|
+
export { createTranscriptLogger, summarizeToolResponse } from "./core/hooks/transcriptLogger.js";
|
|
14
|
+
export { createStepGate } from "./core/hooks/stepGate.js";
|
|
15
|
+
export { createSubagentBashGate } from "./core/hooks/subagentBashGate.js";
|
|
16
|
+
export { createSubagentTypeGate } from "./core/hooks/subagentTypeGate.js";
|
|
17
|
+
export { createSubagentForegroundGate } from "./core/hooks/subagentForegroundGate.js";
|
|
18
|
+
export { createFileScopeGate, checkFileScope } from "./core/hooks/fileScopeGate.js";
|
|
19
|
+
export { askForDecision, askForManualIntervention } from "./core/hooks/humanInput.js";
|
|
20
|
+
export { setInteractionPort, getInteractionPort } from "./core/interaction.js";
|
|
21
|
+
export { terminalInteractionPort, setSharedReadline, getSharedReadline } from "./tui/terminalInteraction.js";
|
|
22
|
+
// The checkpoints answer on the terminal by default, as they always have; a non-terminal
|
|
23
|
+
// host replaces the port with its own (or null, to answer through the response file only).
|
|
24
|
+
setInteractionPort(terminalInteractionPort);
|
|
25
|
+
export { createHumanApprovalServer, DEFAULT_HUMAN_APPROVAL_TEXTS } from "./core/tools/humanApproval.js";
|
|
26
|
+
export { createManualLoginServer } from "./core/tools/manualLogin.js";
|
|
27
|
+
export { createSaveToSourcesServer } from "./core/tools/saveToSources.js";
|
|
28
|
+
export { runChatTui } from "./tui/chatTui.js";
|
|
29
|
+
export { createConsoleRenderer } from "./tui/consoleRenderer.js";
|
|
30
|
+
export { runChatInk } from "./tui/ink/runChatInk.js";
|
|
31
|
+
export { createProgressView } from "./tui/ink/progressView.js";
|
|
32
|
+
export { runWizard, } from "./tui/ink/wizard.js";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { type AgentEvent } from "../core/runner.js";
|
|
3
|
+
export interface ChatTuiOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Turns a tool call into a one-line console label. Defaults to `createFriendlyToolLabel()`
|
|
6
|
+
* with no domain overrides — pass your own (e.g.
|
|
7
|
+
* `createFriendlyToolLabel({ describe, extraLocalServers })`) to cover a concrete agent's
|
|
8
|
+
* own domain-specific tools (Playwright's `browser_*`, etc.).
|
|
9
|
+
*/
|
|
10
|
+
formatAction?: (toolName: string, input: unknown) => string;
|
|
11
|
+
/** Printed once before the first prompt, if given. */
|
|
12
|
+
welcomeMessage?: string;
|
|
13
|
+
/** Shown before the cursor at each prompt. */
|
|
14
|
+
promptLabel?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Shown once per turn, right before the agent's streamed reply text starts (not before
|
|
17
|
+
* any action lines that precede it) — and, when set, also colors that reply text via
|
|
18
|
+
* `ui.agent()`. Pass a pre-colored string (e.g. `ui.agent("Captain Whiskers>")`); omit to
|
|
19
|
+
* keep the plain, unlabeled/uncolored text streaming this kit had before.
|
|
20
|
+
*/
|
|
21
|
+
agentLabel?: string;
|
|
22
|
+
/** Lines (trimmed, case-insensitive) that end the chat. */
|
|
23
|
+
exitCommands?: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* When set, submitted as the very first turn — before the human is ever prompted —
|
|
26
|
+
* exactly as if they'd typed it themselves (mirrored to `sessionLogPath`/history like
|
|
27
|
+
* any other line). For an agent that should act proactively at the start of a chat (log
|
|
28
|
+
* into a site, load some state) instead of sitting idle at the prompt until the human
|
|
29
|
+
* says something first. Omit for the plain "wait at the prompt" behavior this kit had
|
|
30
|
+
* before.
|
|
31
|
+
*/
|
|
32
|
+
initialPrompt?: string;
|
|
33
|
+
/**
|
|
34
|
+
* When set, mirrors everything this loop prints — the welcome message, each prompt
|
|
35
|
+
* line the human types, the agent's streamed reply, action lines, and any error/warning
|
|
36
|
+
* — into this file as it happens (appended, plain text with ANSI color codes stripped),
|
|
37
|
+
* so a session leaves behind an exact, always-present transcript of what the terminal
|
|
38
|
+
* showed even when the agent never calls a single tool (unlike `transcriptPath` from
|
|
39
|
+
* `buildSessionOptions()`, which only gets written to on the first tool call). Omit to
|
|
40
|
+
* skip session logging entirely.
|
|
41
|
+
*/
|
|
42
|
+
sessionLogPath?: string;
|
|
43
|
+
/**
|
|
44
|
+
* When set, every non-empty line the human submits (including exit commands — a shell's
|
|
45
|
+
* own history keeps those too) is appended here as one JSON object per line
|
|
46
|
+
* (`{ text: string, timestamp: string }`, ISO 8601), trimmed to the most recent
|
|
47
|
+
* `historyLimit` entries (oldest dropped first). Loaded back at startup — including
|
|
48
|
+
* across separate runs of the same agent, since this path is normally a fixed file, not
|
|
49
|
+
* one under a per-run timestamped directory — into `readline`'s own history, so ↑/↓ at
|
|
50
|
+
* the prompt cycles through prior prompts. Omit to skip persistence entirely (↑/↓ still
|
|
51
|
+
* works for the current session alone, via readline's own in-memory default).
|
|
52
|
+
*/
|
|
53
|
+
historyPath?: string;
|
|
54
|
+
/** Max entries kept in `historyPath` (and in the in-session ↑/↓ list). Defaults to 100. */
|
|
55
|
+
historyLimit?: number;
|
|
56
|
+
}
|
|
57
|
+
interface HistoryEntry {
|
|
58
|
+
text: string;
|
|
59
|
+
timestamp: string;
|
|
60
|
+
}
|
|
61
|
+
/** Keeps only the most recent `limit` entries (oldest-first order in, oldest-first out). */
|
|
62
|
+
export declare function capHistory(entries: readonly HistoryEntry[], limit: number): HistoryEntry[];
|
|
63
|
+
/** Oldest-first. Missing file reads as empty — nothing to load yet is the normal case. */
|
|
64
|
+
export declare function loadHistory(filePath: string): Promise<HistoryEntry[]>;
|
|
65
|
+
/** Overwrites the whole file (not appended) so a prior prune (oldest entries dropped) sticks. */
|
|
66
|
+
export declare function saveHistory(filePath: string, entries: readonly HistoryEntry[]): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* The command name a typed line would invoke (e.g. "chiste" for both "/chiste" and
|
|
69
|
+
* "/chiste con salsa"), or `null` for a line that isn't a slash command at all (doesn't
|
|
70
|
+
* start with "/") or is just a bare "/" with nothing after it.
|
|
71
|
+
*/
|
|
72
|
+
export declare function slashCommandToken(line: string): string | null;
|
|
73
|
+
/**
|
|
74
|
+
* Reads `events` one turn at a time — up to and including a `turn-end` — via manual
|
|
75
|
+
* `.next()` calls, calling `onEvent` for each. Deliberately never uses
|
|
76
|
+
* `for await...of` + `break`: `run.events` (see runner.ts) is one long-lived generator
|
|
77
|
+
* spanning the *whole* multi-turn session, not one per turn, and breaking out of a
|
|
78
|
+
* `for-await-of` loop calls the generator's `.return()`, permanently closing it — the next
|
|
79
|
+
* turn's drain would then see `done: true` immediately and silently lose the rest of the
|
|
80
|
+
* conversation. Exported on its own (not part of the package's public API — see index.ts)
|
|
81
|
+
* so this exact behavior has a direct regression test.
|
|
82
|
+
*/
|
|
83
|
+
export declare function drainTurn(events: AsyncIterator<AgentEvent>, onEvent: (event: AgentEvent) => void): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* A ready-to-run interactive terminal chat loop, assembled from this kit's own chat
|
|
86
|
+
* primitives: `createInputQueue()` feeds typed lines into a multi-turn `query()` session,
|
|
87
|
+
* `runQuery()`'s normalized event stream is rendered to the console (streamed text,
|
|
88
|
+
* friendly action labels via `createFriendlyToolLabel()`, turn failures), and this loop's
|
|
89
|
+
* own `readline.Interface` is registered via `setSharedReadline()` so any human-in-the-loop
|
|
90
|
+
* checkpoint the session hits (the interactive-mode step gate, or the guided-mode approval/
|
|
91
|
+
* manual-login tools — see terminalInteraction.ts) prompts on the same interface instead of a
|
|
92
|
+
* second one fighting it for stdin's raw mode.
|
|
93
|
+
*
|
|
94
|
+
* Takes the SDK `Options` produced by `buildSessionOptions()` rather than an `AgentSpec`
|
|
95
|
+
* directly, so it stays reusable for an `Options` assembled any other way:
|
|
96
|
+
*
|
|
97
|
+
* const { options } = await buildSessionOptions(config, runDir, spec);
|
|
98
|
+
* await runChatTui(options, { welcomeMessage: "Ready. Type /exit to quit." });
|
|
99
|
+
*
|
|
100
|
+
* Resolves once the chat ends (an exit command, Ctrl+C/Ctrl+D at an idle prompt) — this
|
|
101
|
+
* function owns the whole session lifecycle, closing the underlying query and readline
|
|
102
|
+
* interface itself before returning.
|
|
103
|
+
*/
|
|
104
|
+
export declare function runChatTui(options: Options, tuiOptions?: ChatTuiOptions): Promise<void>;
|
|
105
|
+
export {};
|