@falkenslab/agent-kit 0.16.1 → 0.18.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 +3 -3
- package/assets/agent-help/SKILL.md +57 -0
- package/assets/knowledge-plugin/commands/ingest.md +1 -1
- package/assets/knowledge-plugin/skills/knowledge-ingest/SKILL.md +8 -3
- package/assets/knowledge-plugin/skills/knowledge-lint/SKILL.md +12 -5
- package/dist/core/agentHelp.d.ts +30 -0
- package/dist/core/agentHelp.js +55 -0
- package/dist/core/agentSpec.d.ts +37 -16
- package/dist/core/fileKnowledgeStore.d.ts +1 -3
- package/dist/core/fileKnowledgeStore.js +23 -29
- package/dist/core/hooks/fileScopeGate.d.ts +17 -2
- package/dist/core/hooks/fileScopeGate.js +45 -10
- package/dist/core/knowledge.d.ts +20 -17
- package/dist/core/knowledge.js +32 -60
- package/dist/core/knowledgeStore.d.ts +4 -9
- package/dist/core/knowledgeStore.js +16 -2
- package/dist/core/language.js +1 -1
- package/dist/core/messages/de.js +2 -2
- package/dist/core/messages/en.d.ts +2 -1
- package/dist/core/messages/en.js +2 -2
- package/dist/core/messages/es.js +2 -2
- package/dist/core/messages/fr.js +2 -2
- package/dist/core/runner.d.ts +5 -0
- package/dist/core/runner.js +1 -0
- package/dist/core/session.d.ts +12 -0
- package/dist/core/session.js +121 -40
- package/dist/core/sources.d.ts +24 -31
- package/dist/core/sources.js +45 -108
- package/dist/core/tools/humanApproval.js +3 -3
- package/dist/core/tools/knowledgeTools.d.ts +6 -6
- package/dist/core/tools/knowledgeTools.js +18 -53
- package/dist/core/tools/saveToSources.d.ts +6 -2
- package/dist/core/tools/saveToSources.js +17 -12
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/tui/ink/SessionView.d.ts +1 -0
- package/dist/tui/ink/SessionView.js +17 -4
- package/dist/tui/ink/runChatInk.d.ts +7 -0
- package/dist/tui/ink/runChatInk.js +6 -4
- package/package.json +1 -1
- package/assets/knowledge-plugin-files/.claude-plugin/plugin.json +0 -4
- package/assets/knowledge-plugin-files/commands/ingest.md +0 -7
- package/assets/knowledge-plugin-files/commands/lint.md +0 -5
- package/assets/knowledge-plugin-files/commands/query.md +0 -5
- package/assets/knowledge-plugin-files/skills/knowledge-ingest/SKILL.md +0 -46
- package/assets/knowledge-plugin-files/skills/knowledge-lint/SKILL.md +0 -26
- package/assets/knowledge-plugin-files/skills/knowledge-pages/SKILL.md +0 -137
- package/assets/knowledge-plugin-files/skills/knowledge-query/SKILL.md +0 -14
|
@@ -34,8 +34,8 @@ export function createHumanApprovalServer(runDir, texts = DEFAULT_HUMAN_APPROVAL
|
|
|
34
34
|
};
|
|
35
35
|
});
|
|
36
36
|
const askHuman = tool("ask_human", "Ask the person to choose between options, in a panel, and get the answer in this same turn, instead of ending your turn with a written question. Use it when you need their decision to go on (which approach, which of several items). They can also type their own answer.", {
|
|
37
|
-
question: z.string().describe("The question, in one sentence, in the
|
|
38
|
-
options: z.array(z.string()).min(2).max(8).describe("The options, short, in the
|
|
37
|
+
question: z.string().describe("The question, in one sentence, in the language you reply in"),
|
|
38
|
+
options: z.array(z.string()).min(2).max(8).describe("The options, short, in the language you reply in (2 to 8; an \"Other\" for their own answer is added)"),
|
|
39
39
|
multiple: z.boolean().optional().describe("Whether they can pick several (default: one)"),
|
|
40
40
|
}, async (args) => {
|
|
41
41
|
const answer = await askForChoice(runDir, { title: t().questionTitle, lines: [args.question] }, args.options, args.multiple ?? false);
|
|
@@ -46,7 +46,7 @@ export function createHumanApprovalServer(runDir, texts = DEFAULT_HUMAN_APPROVAL
|
|
|
46
46
|
return ok(parts.length ? parts.join("\n") : "The person didn't answer. Go on with what you can, or ask in your reply.");
|
|
47
47
|
});
|
|
48
48
|
const control = options.modeControl;
|
|
49
|
-
const presentPlan = tool("present_plan", "In plan mode, when the plan is ready: show it to the person, who decides to run it (plan mode ends and you carry it out right away, in this turn), to keep planning (with a comment on what to change) or to cancel. Outside plan mode it's refused.", { plan: z.string().describe("The whole plan, in markdown, in the
|
|
49
|
+
const presentPlan = tool("present_plan", "In plan mode, when the plan is ready: show it to the person, who decides to run it (plan mode ends and you carry it out right away, in this turn), to keep planning (with a comment on what to change) or to cancel. Outside plan mode it's refused.", { plan: z.string().describe("The whole plan, in markdown, in the language you reply in: steps, what changes, what to check") }, async (args) => {
|
|
50
50
|
if (!control || control.mode !== "plan")
|
|
51
51
|
return { ...ok("present_plan only works in plan mode: you aren't in it, so just go ahead."), isError: true };
|
|
52
52
|
const choices = [t().runPlan, t().keepPlanning, t().cancelPlan];
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { KnowledgeStore } from "../knowledgeStore.js";
|
|
2
|
+
/**
|
|
3
|
+
* Text as the model meant it: a body that arrives with escaped newlines ("\\n") and no real
|
|
4
|
+
* one gets them back. A model sometimes double-escapes a long markdown argument, and the page
|
|
5
|
+
* would be stored as one line with literal "\n"s (found by a real session).
|
|
6
|
+
*/
|
|
7
|
+
export declare function unescapedText(text: string): string;
|
|
2
8
|
/** Options of `createKnowledgeServer()`. */
|
|
3
9
|
export interface KnowledgeToolsOptions {
|
|
4
10
|
/** This run's folder (for the response file of `knowledge_retire`'s approval). */
|
|
5
11
|
runDir: string;
|
|
6
12
|
/** A person can be asked (not autonomous): adds `knowledge_retire`, which asks first. */
|
|
7
13
|
interactive?: boolean;
|
|
8
|
-
/** The sources folder: summaries record their original's hash, and `knowledge_check` reports new, changed and missing originals. */
|
|
9
|
-
sourcesDir?: string;
|
|
10
|
-
/** The project root, for `list_sources`' paths. */
|
|
11
|
-
projectDir?: string;
|
|
12
|
-
/** The knowledge folder, for `list_sources`' ingest status. */
|
|
13
|
-
knowledgeDir?: string;
|
|
14
14
|
}
|
|
15
15
|
/** The `knowledge` MCP server over `store`. */
|
|
16
16
|
export declare function createKnowledgeServer(store: KnowledgeStore, options: KnowledgeToolsOptions): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
3
2
|
import { z } from "zod";
|
|
4
|
-
import { listSources, loadManifest, recordIngest } from "../sources.js";
|
|
5
3
|
import { askForDecision } from "../hooks/humanInput.js";
|
|
6
4
|
import { t } from "../messages/index.js";
|
|
7
5
|
/**
|
|
@@ -21,6 +19,14 @@ async function attempt(work) {
|
|
|
21
19
|
return fail(message(error));
|
|
22
20
|
}
|
|
23
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Text as the model meant it: a body that arrives with escaped newlines ("\\n") and no real
|
|
24
|
+
* one gets them back. A model sometimes double-escapes a long markdown argument, and the page
|
|
25
|
+
* would be stored as one line with literal "\n"s (found by a real session).
|
|
26
|
+
*/
|
|
27
|
+
export function unescapedText(text) {
|
|
28
|
+
return !text.includes("\n") && text.includes("\\n") ? text.replace(/\\r\\n|\\n/g, "\n").replace(/\\t/g, "\t") : text;
|
|
29
|
+
}
|
|
24
30
|
const OVERVIEW = "overview";
|
|
25
31
|
const LOG = "log";
|
|
26
32
|
/** The `knowledge` MCP server over `store`. */
|
|
@@ -31,20 +37,8 @@ export function createKnowledgeServer(store, options) {
|
|
|
31
37
|
const fields = z
|
|
32
38
|
.array(z.object({ name: z.string(), value: z.string().nullable() }))
|
|
33
39
|
.optional()
|
|
34
|
-
.describe('Frontmatter fields to set (value null removes one), e.g. [{"name": "aliases", "value": "tide range, tidal range"}]; for a summary, "file" is its original
|
|
40
|
+
.describe('Frontmatter fields to set (value null removes one), e.g. [{"name": "aliases", "value": "tide range, tidal range"}]; for a summary, "file" is its original as list_sources names it (its "ingested" is set for you)');
|
|
35
41
|
const changes = (pairs) => pairs ? Object.fromEntries(pairs.map(({ name, value }) => [name, value])) : undefined;
|
|
36
|
-
/** A summary written from an original: record its hash, so "changed" means changed after this. */
|
|
37
|
-
async function ingested(id) {
|
|
38
|
-
if (!options.sourcesDir || !id.startsWith("summary/"))
|
|
39
|
-
return;
|
|
40
|
-
const page = await store.read(id);
|
|
41
|
-
const file = page?.fields.file;
|
|
42
|
-
if (!file || /^[a-z]+:\/\//i.test(file))
|
|
43
|
-
return;
|
|
44
|
-
const knowledgeDir = options.knowledgeDir;
|
|
45
|
-
const original = knowledgeDir ? path.resolve(knowledgeDir, "summaries", file) : path.resolve(options.sourcesDir, file);
|
|
46
|
-
await recordIngest(options.sourcesDir, path.relative(options.sourcesDir, original));
|
|
47
|
-
}
|
|
48
42
|
const index = tool("knowledge_index", "The knowledge base's catalog: every page, one line each (title, id, what it is), by section. Start here, then read only the pages the task needs.", {}, async () => attempt(async () => (await store.list()).length ? await store.index() : "The knowledge base is empty."), { annotations: { readOnlyHint: true } });
|
|
49
43
|
const search = tool("knowledge_search", "Find pages by words: in titles, aliases and content, best first, with the line that matched. Search before creating a page, so you don't duplicate one under another name.", { query: z.string().describe("Words to look for"), limit: z.number().int().positive().max(50).optional() }, async (args) => attempt(async () => {
|
|
50
44
|
const hits = await store.search(args.query, args.limit ?? 10);
|
|
@@ -71,7 +65,7 @@ export function createKnowledgeServer(store, options) {
|
|
|
71
65
|
.join("; ")}. Without content, returns the type's template: fill it and call again. Links to other pages are ids, [Bowline](concept/bowline), and must exist, or be created in the same call (\`also\`); the index is kept for you.`, {
|
|
72
66
|
type: z.enum(typeNames),
|
|
73
67
|
slug: z.string().describe('Lowercase ASCII words joined by hyphens, e.g. "spring-tides"'),
|
|
74
|
-
title: z.string().describe("The page's title, in the
|
|
68
|
+
title: z.string().describe("The page's title, in the language you reply in"),
|
|
75
69
|
content: z.string().optional().describe("The page's markdown body (under its title), following the type's template"),
|
|
76
70
|
fields,
|
|
77
71
|
also: z
|
|
@@ -87,34 +81,30 @@ export function createKnowledgeServer(store, options) {
|
|
|
87
81
|
}, async (args) => attempt(async () => {
|
|
88
82
|
const type = store.types().find((t) => t.type === args.type);
|
|
89
83
|
if (!args.content)
|
|
90
|
-
return `Template for a ${type.type} page (${type.description}):\n\n${type.template}\n\nFill it in the
|
|
84
|
+
return `Template for a ${type.type} page (${type.description}):\n\n${type.template}\n\nFill it in the language you reply in and call knowledge_create again with content.`;
|
|
91
85
|
const ids = await store.createMany([
|
|
92
|
-
{ type: args.type, slug: args.slug, title: args.title, content: args.content, fields: changes(args.fields) },
|
|
93
|
-
...(args.also ?? []).map((page) => ({ ...page, fields: changes(page.fields) })),
|
|
86
|
+
{ type: args.type, slug: args.slug, title: args.title, content: unescapedText(args.content), fields: changes(args.fields) },
|
|
87
|
+
...(args.also ?? []).map((page) => ({ ...page, content: unescapedText(page.content), fields: changes(page.fields) })),
|
|
94
88
|
]);
|
|
95
|
-
for (const id of ids)
|
|
96
|
-
await ingested(id);
|
|
97
89
|
return `Created ${ids.join(", ")}.`;
|
|
98
90
|
}));
|
|
99
91
|
const edit = tool("knowledge_edit", "Change one fragment of a page: oldText (exactly as knowledge_read returned it, unique in the page) becomes newText. Prefer it to rewriting a page.", { page: z.string(), oldText: z.string(), newText: z.string(), fields }, async (args) => attempt(async () => {
|
|
100
|
-
await store.edit(args.page, args.oldText, args.newText, changes(args.fields));
|
|
101
|
-
await ingested(args.page);
|
|
92
|
+
await store.edit(args.page, unescapedText(args.oldText), unescapedText(args.newText), changes(args.fields));
|
|
102
93
|
return `Edited ${args.page}.`;
|
|
103
94
|
}));
|
|
104
95
|
const rewrite = tool("knowledge_rewrite", 'Replace a page\'s whole content, keeping its id and the links to it: for a page redone from a better source, or the "overview" (the living synthesis of the whole knowledge base). For small changes use knowledge_edit.', { page: z.string().describe('The page id, or "overview"'), content: z.string(), fields }, async (args) => attempt(async () => {
|
|
105
96
|
if (args.page === OVERVIEW) {
|
|
106
|
-
await store.writeOverview(args.content);
|
|
97
|
+
await store.writeOverview(unescapedText(args.content));
|
|
107
98
|
return "Rewrote the overview.";
|
|
108
99
|
}
|
|
109
|
-
await store.rewrite(args.page, args.content, changes(args.fields));
|
|
110
|
-
await ingested(args.page);
|
|
100
|
+
await store.rewrite(args.page, unescapedText(args.content), changes(args.fields));
|
|
111
101
|
return `Rewrote ${args.page}.`;
|
|
112
102
|
}));
|
|
113
103
|
const supersede = tool("knowledge_supersede", "Mark a page superseded by another one (it stays, with a notice pointing to the new one). Pages are never deleted or renamed.", { page: z.string(), by: z.string().describe("The page that replaces it"), reason: z.string().optional() }, async (args) => attempt(async () => {
|
|
114
104
|
await store.supersede(args.page, args.by, args.reason);
|
|
115
105
|
return `${args.page} is now superseded by ${args.by}. Repoint the links that should go to the new page.`;
|
|
116
106
|
}));
|
|
117
|
-
const retire = tool("knowledge_retire", "Retire a page whose knowledge was wrong (e.g. learned from a wrong original)
|
|
107
|
+
const retire = tool("knowledge_retire", "Retire a page whose knowledge was wrong (e.g. learned from a wrong original). It asks the person itself, with a panel: don't ask them first. Retired, it leaves the index and the search but is kept. Then fix the pages that link to it (knowledge_check lists them).", { page: z.string(), reason: z.string().describe("Why, in one sentence, in the language you reply in") }, async (args) => attempt(async () => {
|
|
118
108
|
const page = await store.read(args.page);
|
|
119
109
|
if (!page)
|
|
120
110
|
throw new Error(`There's no page "${args.page}".`);
|
|
@@ -134,32 +124,7 @@ export function createKnowledgeServer(store, options) {
|
|
|
134
124
|
await store.log(args.operation, args.what, args.pages ?? []);
|
|
135
125
|
return "Logged.";
|
|
136
126
|
}));
|
|
137
|
-
const check = tool("knowledge_check", "The knowledge base's mechanical problems in one call: broken links, orphan pages
|
|
138
|
-
const report = { ...(await store.check()) };
|
|
139
|
-
if (options.sourcesDir) {
|
|
140
|
-
const sources = await listSources(options.sourcesDir, options.knowledgeDir, options.projectDir ?? path.dirname(options.sourcesDir));
|
|
141
|
-
report.newOriginals = sources.filter((s) => s.status === "new").map((s) => s.path);
|
|
142
|
-
report.changedOriginals = sources.filter((s) => s.status === "changed").map((s) => ({ path: s.path, summaries: s.summaries }));
|
|
143
|
-
report.missingFromSources = sources.filter((s) => s.status === "missing").map((s) => s.path);
|
|
144
|
-
const retired = (await loadManifest(options.sourcesDir)).retired;
|
|
145
|
-
if (retired.length) {
|
|
146
|
-
const citations = [];
|
|
147
|
-
for (const info of await store.list()) {
|
|
148
|
-
const page = await store.read(info.id);
|
|
149
|
-
for (const original of retired) {
|
|
150
|
-
const name = path.basename(original.path);
|
|
151
|
-
for (const line of (page?.content ?? "").split(/\r?\n/))
|
|
152
|
-
if (line.includes(name))
|
|
153
|
-
citations.push({ original: original.path, page: info.id, passage: line.trim() });
|
|
154
|
-
if (page?.fields.file?.endsWith(name) && info.status === "active")
|
|
155
|
-
citations.push({ original: original.path, page: info.id, passage: `file: ${page.fields.file}` });
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
report.citesRetiredOriginals = citations;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return JSON.stringify(report, null, 2);
|
|
162
|
-
}), { annotations: { readOnlyHint: true } });
|
|
127
|
+
const check = tool("knowledge_check", "The knowledge base's mechanical problems in one call: broken links, orphan pages and links to retired pages. Fix what's mechanical; report what needs the person. Whether its summaries are up to date with their originals is a comparison you make: list_sources' changedAt against each summary's ingested (knowledge_index shows them): both ISO 8601 in UTC, the later one sorts after as text.", {}, async () => attempt(async () => JSON.stringify(await store.check(), null, 2)), { annotations: { readOnlyHint: true } });
|
|
163
128
|
return createSdkMcpServer({
|
|
164
129
|
name: "knowledge",
|
|
165
130
|
version: "1.0.0",
|
|
@@ -8,10 +8,14 @@ export declare const MAX_SOURCE_BYTES: number;
|
|
|
8
8
|
export declare function htmlToMarkdown(html: string, url: string): Promise<string | null>;
|
|
9
9
|
/** Downloads `url` into `file`, refusing other schemes and anything over `maxBytes`; returns its content type. */
|
|
10
10
|
export declare function downloadToFile(url: string, file: string, maxBytes: number): Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* The "Sources" section appended to the system prompt whenever there's a sources folder, with or
|
|
13
|
+
* without a knowledge base: what the originals are and how they come and go. `projectDir` and
|
|
14
|
+
* `sourcesDir` give the folder's name as the model reads it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function sourcesPromptSection(projectDir: string, sourcesDir: string): string;
|
|
11
17
|
/** Options of `createSaveToSourcesServer()`. */
|
|
12
18
|
export interface SourceToolsOptions {
|
|
13
|
-
/** The knowledge folder, to tell which originals have a summary page (`list_sources`). */
|
|
14
|
-
knowledgeDir?: string;
|
|
15
19
|
/** The project root: paths in the tools' answers are relative to it. Defaults to the parent of `sourcesDir`. */
|
|
16
20
|
projectDir?: string;
|
|
17
21
|
/** A person can be asked (not autonomous): adds `request_file` and `retire_source`, which ask first. */
|
|
@@ -104,6 +104,16 @@ export async function downloadToFile(url, file, maxBytes) {
|
|
|
104
104
|
await writeFile(file, Buffer.concat(chunks));
|
|
105
105
|
return response.headers.get("content-type") ?? "";
|
|
106
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* The "Sources" section appended to the system prompt whenever there's a sources folder, with or
|
|
109
|
+
* without a knowledge base: what the originals are and how they come and go. `projectDir` and
|
|
110
|
+
* `sourcesDir` give the folder's name as the model reads it.
|
|
111
|
+
*/
|
|
112
|
+
export function sourcesPromptSection(projectDir, sourcesDir) {
|
|
113
|
+
const folder = `${path.relative(projectDir, sourcesDir).split(path.sep).join("/") || "."}/`;
|
|
114
|
+
return `## Sources
|
|
115
|
+
- **Originals (read-only for you)**: \`${folder}\`. Never rewrite one; build on them. \`list_sources\` lists them with when each one's content last changed (\`changedAt\`), and the missing and retired ones; add a file only with \`save_to_sources\`, \`download_to_sources\` or by asking the person (\`request_file\`), and take out a wrong or superseded one only with \`retire_source\`. \`Read\` reads PDFs and images; \`extract_text\` reads DOCX, PPTX (with speaker notes) and XLSX.`;
|
|
116
|
+
}
|
|
107
117
|
/**
|
|
108
118
|
* The tools that keep `sourcesDir`, the originals the agent reads but never edits (Write/Edit
|
|
109
119
|
* are scoped to the notes folder, see hooks/fileScopeGate.ts): `save_to_sources`,
|
|
@@ -149,9 +159,9 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
|
|
|
149
159
|
return fail(message(error));
|
|
150
160
|
}
|
|
151
161
|
});
|
|
152
|
-
const listSourcesTool = tool("list_sources", "List the originals in sources
|
|
162
|
+
const listSourcesTool = tool("list_sources", "List the originals in sources/: present, missing (deleted by hand) or retired (when, why, what replaced it); with changedAt (when its content last changed, ISO 8601 in UTC: a later date sorts after as text, to compare with when something was built from it), type, size, pages of a PDF or slides of a PPTX (read long ones in parts), provenance and the version it replaces. Use it instead of listing the folder.", {}, async () => {
|
|
153
163
|
try {
|
|
154
|
-
return ok(JSON.stringify(await listSources(sourcesDir
|
|
164
|
+
return ok(JSON.stringify(await listSources(sourcesDir), null, 2));
|
|
155
165
|
}
|
|
156
166
|
catch (error) {
|
|
157
167
|
return fail(message(error));
|
|
@@ -215,7 +225,7 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
|
|
|
215
225
|
}
|
|
216
226
|
});
|
|
217
227
|
const requestFile = tool("request_file", "Ask the person for a file you need (a syllabus, a document they have) and copy it into sources/ with its provenance. They give a path or say they don't have it; never overwrites, and doesn't copy a file identical to one already there.", {
|
|
218
|
-
description: z.string().describe("What file you need and why, in one or two sentences, in the
|
|
228
|
+
description: z.string().describe("What file you need and why, in one or two sentences, in the language you reply in"),
|
|
219
229
|
destination: z.string().optional().describe("Path to save it under, relative to sources/; the file's own name if omitted"),
|
|
220
230
|
replaces,
|
|
221
231
|
}, async (args) => {
|
|
@@ -235,23 +245,18 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
|
|
|
235
245
|
return fail(message(error));
|
|
236
246
|
}
|
|
237
247
|
});
|
|
238
|
-
const retire = tool("retire_source", 'Retire an original that was wrong ("wrong") or replaced by a better one ("replaced")
|
|
248
|
+
const retire = tool("retire_source", 'Retire an original that was wrong ("wrong") or replaced by a better one ("replaced"). It asks the person itself, with a panel: don\'t ask them first. Approved, it\'s moved to sources/.agent-kit/retired/ (never deleted), and list_sources shows it as retired. What you built from it is yours to review afterwards.', {
|
|
239
249
|
source: z.string().describe("The original, relative to sources/"),
|
|
240
250
|
why: z.enum(["wrong", "replaced"]),
|
|
241
|
-
reason: z.string().describe("Why, in one sentence, in the
|
|
251
|
+
reason: z.string().describe("Why, in one sentence, in the language you reply in"),
|
|
242
252
|
replacedBy: z.string().optional().describe('For "replaced": the original that replaces it, relative to sources/'),
|
|
243
253
|
}, async (args) => {
|
|
244
254
|
const answer = await askForDecision(runDir, { title: t().retireTitle, lines: t().retireLines(inFolder(args.source), args.why, args.reason, args.replacedBy && inFolder(args.replacedBy), folder) });
|
|
245
255
|
if (answer !== "" && answer !== "y" && answer !== "yes")
|
|
246
256
|
return ok(answer === "q" ? "The person stopped: don't retire it, and stop what you were doing." : "The person said no: the original stays.");
|
|
247
257
|
try {
|
|
248
|
-
const result = await retireSource(sourcesDir,
|
|
249
|
-
|
|
250
|
-
return ok([
|
|
251
|
-
`Retired: moved to sources/${result.movedTo}.`,
|
|
252
|
-
...(pages.length ? pages : ["It had no summary page."]),
|
|
253
|
-
"Now update index.md, and correct or re-ground the pages that cite it (search the knowledge base for its file name).",
|
|
254
|
-
].join("\n"));
|
|
258
|
+
const result = await retireSource(sourcesDir, inFolder(args.source), args.why, args.reason, args.replacedBy && inFolder(args.replacedBy));
|
|
259
|
+
return ok(`Retired: moved to sources/${result.movedTo}. If anything was built from it (a summary, notes that cite it), review it now: ${args.why === "wrong" ? "what it taught was wrong" : `it was replaced${args.replacedBy ? ` by sources/${inFolder(args.replacedBy)}` : ""}`}.`);
|
|
255
260
|
}
|
|
256
261
|
catch (error) {
|
|
257
262
|
return fail(message(error));
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { createPromptLoader } from "./core/promptTemplate.js";
|
|
|
6
6
|
export { resolveClaudeAuth, type ClaudeAuthConfig } from "./core/claudeAuth.js";
|
|
7
7
|
export { ensureClaudeAuth } from "./tui/claudeAuth.js";
|
|
8
8
|
export { createFriendlyToolLabel, truncate, truncatePath, type ToolDescriber, } from "./core/toolLabels.js";
|
|
9
|
-
export type { Mode, BaseSessionConfig, AgentSpec, PlanModeSpec } from "./core/agentSpec.js";
|
|
9
|
+
export type { Mode, BaseSessionConfig, AgentSpec, AgentIdentity, PlanModeSpec } from "./core/agentSpec.js";
|
|
10
10
|
export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, togglePlanMode, type ModeControl } from "./core/session.js";
|
|
11
11
|
export { runQuery, type AgentEvent, type AgentRun, type SessionUsage, type ContextUsage } from "./core/runner.js";
|
|
12
12
|
export { knowledgePluginRoot, knowledgePromptSection } from "./core/knowledge.js";
|
|
@@ -31,7 +31,7 @@ export { setInteractionPort, getInteractionPort, type InteractionPort, type Appr
|
|
|
31
31
|
export { terminalInteractionPort, setSharedReadline, getSharedReadline } from "./tui/terminalInteraction.js";
|
|
32
32
|
export { createHumanApprovalServer, DEFAULT_HUMAN_APPROVAL_TEXTS, type HumanApprovalTexts } from "./core/tools/humanApproval.js";
|
|
33
33
|
export { createManualLoginServer, type ManualInterventionTexts } from "./core/tools/manualLogin.js";
|
|
34
|
-
export { createSaveToSourcesServer, type SourceToolsOptions } from "./core/tools/saveToSources.js";
|
|
34
|
+
export { createSaveToSourcesServer, sourcesPromptSection, type SourceToolsOptions } from "./core/tools/saveToSources.js";
|
|
35
35
|
export { runChatTui, type ChatTuiOptions } from "./tui/chatTui.js";
|
|
36
36
|
export type { SessionOpener } from "./tui/runs.js";
|
|
37
37
|
export { createConsoleRenderer, type ConsoleRenderer, type ConsoleRendererOptions } from "./tui/consoleRenderer.js";
|
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ export { terminalInteractionPort, setSharedReadline, getSharedReadline } from ".
|
|
|
36
36
|
setInteractionPort(terminalInteractionPort);
|
|
37
37
|
export { createHumanApprovalServer, DEFAULT_HUMAN_APPROVAL_TEXTS } from "./core/tools/humanApproval.js";
|
|
38
38
|
export { createManualLoginServer } from "./core/tools/manualLogin.js";
|
|
39
|
-
export { createSaveToSourcesServer } from "./core/tools/saveToSources.js";
|
|
39
|
+
export { createSaveToSourcesServer, sourcesPromptSection } from "./core/tools/saveToSources.js";
|
|
40
40
|
export { runChatTui } from "./tui/chatTui.js";
|
|
41
41
|
export { createConsoleRenderer } from "./tui/consoleRenderer.js";
|
|
42
42
|
export { runChatInk } from "./tui/ink/runChatInk.js";
|
|
@@ -19,6 +19,7 @@ export declare function previewLines(lines: readonly string[], terminalRows: num
|
|
|
19
19
|
export declare function statusText(mode: Mode | undefined, turns: number, usage: SessionUsage | null, extra?: {
|
|
20
20
|
contextPercent?: number | null;
|
|
21
21
|
modeSwitchable?: boolean;
|
|
22
|
+
width?: number;
|
|
22
23
|
}): string;
|
|
23
24
|
export interface SessionViewProps {
|
|
24
25
|
model: SessionModel;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import wrapAnsi from "wrap-ansi";
|
|
3
|
+
import stringWidth from "string-width";
|
|
3
4
|
import { useEffect, useReducer, useRef, useState, useSyncExternalStore } from "react";
|
|
4
5
|
import { Box, measureElement, Static, Text, useInput, useStdout } from "ink";
|
|
5
6
|
import { MultiSelect, Select, TextInput } from "@inkjs/ui";
|
|
@@ -86,13 +87,24 @@ function formatTokens(count) {
|
|
|
86
87
|
return count >= 1000 ? `${(count / 1000).toFixed(1)}k` : String(count);
|
|
87
88
|
}
|
|
88
89
|
export function statusText(mode, turns, usage, extra = {}) {
|
|
89
|
-
const
|
|
90
|
+
const text = (withCached) => statusParts(mode, turns, usage, extra, withCached).join(" · ");
|
|
91
|
+
const full = text(true);
|
|
92
|
+
// The cached part goes first when the terminal is too narrow for it.
|
|
93
|
+
return extra.width !== undefined && stringWidth(full) > extra.width ? text(false) : full;
|
|
94
|
+
}
|
|
95
|
+
function statusParts(mode, turns, usage, extra, withCached) {
|
|
96
|
+
return [
|
|
90
97
|
...(mode ? [`⏵⏵ ${t().mode(mode)}${extra.modeSwitchable ? ` (${t().switchModeKey})` : ""}`] : []),
|
|
91
98
|
t().turns(turns),
|
|
92
|
-
|
|
99
|
+
// The new input apart from what was read again from the cache: the total grows with every
|
|
100
|
+
// call (each one sends the whole context again), which looked like the context growing.
|
|
101
|
+
...(usage
|
|
102
|
+
? [
|
|
103
|
+
t().tokens(formatTokens(usage.inputTokens - (usage.cacheReadTokens ?? 0)), formatTokens(usage.outputTokens), withCached && usage.cacheReadTokens ? formatTokens(usage.cacheReadTokens) : undefined),
|
|
104
|
+
]
|
|
105
|
+
: []),
|
|
93
106
|
...(extra.contextPercent != null ? [t().context(Math.round(extra.contextPercent))] : []),
|
|
94
107
|
];
|
|
95
|
-
return parts.join(" · ");
|
|
96
108
|
}
|
|
97
109
|
// No emoji-capable characters: Windows Terminal draws ✳ (U+2733) as a green emoji.
|
|
98
110
|
const WORKING_GLYPHS = ["·", "✢", "✱", "✶", "✻", "✽", "✻", "✶", "✱", "✢"];
|
|
@@ -110,7 +122,8 @@ function Working({ label, startedAt, width }) {
|
|
|
110
122
|
/** Line in progress, checkpoint panel or spinner, the input, and the status bar. */
|
|
111
123
|
function LiveArea({ session, checkpoint, renderApproval, mode, width, statusExtra, modeSwitchable, reservedRows, children }) {
|
|
112
124
|
// The mode in the spinner's color; the rest dim.
|
|
113
|
-
const
|
|
125
|
+
const extraText = statusExtra ? ` · ${statusExtra}` : "";
|
|
126
|
+
const plain = statusText(mode, session.turns, session.usage, { contextPercent: session.contextPercent, modeSwitchable, width: width - stringWidth(extraText) }) + extraText;
|
|
114
127
|
const cut = mode ? plain.indexOf(" · ") : -1;
|
|
115
128
|
const status = cut > 0 ? ui.working(plain.slice(0, cut)) + ui.dim(plain.slice(cut)) : ui.dim(plain);
|
|
116
129
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, marginTop: session.liveGap ? 1 : 0, children: [stripAnsi(session.live) ? _jsx(Text, { children: session.live }) : null, checkpoint ? (_jsx(CheckpointPanel, { checkpoint: checkpoint, renderApproval: renderApproval, reservedRows: reservedRows, width: width })) : session.busy ? (
|
|
@@ -74,6 +74,13 @@ export interface InkChatOptions extends ChatTuiOptions {
|
|
|
74
74
|
*/
|
|
75
75
|
firstPromptSuggestion?: string;
|
|
76
76
|
}
|
|
77
|
+
/** The chat's own commands, never sent to the model (the agent-help skill lists them; a test keeps both in step). */
|
|
78
|
+
export declare const DEFAULT_EXIT_COMMANDS: readonly string[];
|
|
79
|
+
export declare const LOCAL_COMMANDS: {
|
|
80
|
+
readonly copy: "/copy";
|
|
81
|
+
readonly resume: "/resume";
|
|
82
|
+
readonly plan: "/plan";
|
|
83
|
+
};
|
|
77
84
|
/** A choice asked in place of the prompt (the /resume list). */
|
|
78
85
|
export interface ChatPicker {
|
|
79
86
|
title: string;
|
|
@@ -27,7 +27,9 @@ import { createTerminalStatus, focusFromReport } from "./terminalStatus.js";
|
|
|
27
27
|
import { listProjectFiles } from "./fileMentions.js";
|
|
28
28
|
import { headerLines } from "./header.js";
|
|
29
29
|
const DEFAULT_PROMPT_LABEL = "\n> ";
|
|
30
|
-
|
|
30
|
+
/** The chat's own commands, never sent to the model (the agent-help skill lists them; a test keeps both in step). */
|
|
31
|
+
export const DEFAULT_EXIT_COMMANDS = ["/exit", "/quit"];
|
|
32
|
+
export const LOCAL_COMMANDS = { copy: "/copy", resume: "/resume", plan: "/plan" };
|
|
31
33
|
const DEFAULT_HISTORY_LIMIT = 100;
|
|
32
34
|
// The prompt's frame: a border and one column of padding on each side.
|
|
33
35
|
const PROMPT_FRAME_COLUMNS = 4;
|
|
@@ -382,19 +384,19 @@ export async function runChatInk(options, tuiOptions = {}) {
|
|
|
382
384
|
if (exitCommands.has(line.toLowerCase()))
|
|
383
385
|
break;
|
|
384
386
|
// A local command, never sent to the model: the last reply to the clipboard (OSC 52).
|
|
385
|
-
if (status && line.toLowerCase() ===
|
|
387
|
+
if (status && line.toLowerCase() === LOCAL_COMMANDS.copy) {
|
|
386
388
|
const reply = model.lastReply();
|
|
387
389
|
if (reply)
|
|
388
390
|
stdout.write(clipboardSequence(reply));
|
|
389
391
|
model.writeLine(ui.dim(reply ? t().copiedReply(reply.length) : t().nothingToCopy));
|
|
390
392
|
continue;
|
|
391
393
|
}
|
|
392
|
-
if (runsDir && line.toLowerCase() ===
|
|
394
|
+
if (runsDir && line.toLowerCase() === LOCAL_COMMANDS.resume) {
|
|
393
395
|
await resume();
|
|
394
396
|
continue;
|
|
395
397
|
}
|
|
396
398
|
// A local command too: into plan mode, or back to the mode it was entered from.
|
|
397
|
-
if (line.toLowerCase() ===
|
|
399
|
+
if (line.toLowerCase() === LOCAL_COMMANDS.plan) {
|
|
398
400
|
const control = modeControl();
|
|
399
401
|
const next = control ? togglePlanMode(control) : null;
|
|
400
402
|
if (next)
|
package/package.json
CHANGED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Ingest material into the knowledge base right now - the files or topic given, or everything in sources/ not ingested yet
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Apply the `knowledge-ingest` skill right now to: $ARGUMENTS
|
|
6
|
-
|
|
7
|
-
If nothing is given above, ingest every original `list_sources` reports as new, and update the summaries of the ones it reports as changed. When you're done, summarize which pages you created and updated, and anything you couldn't read.
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Health-check the knowledge base right now - broken links, orphans, duplicates, contradictions, gaps
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
Apply the `knowledge-lint` skill right now over the whole knowledge base. Fix what's mechanical (links, index, missing back-links) and report the rest: what needs the human's decision, and the gaps worth filling next.
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: knowledge-ingest
|
|
3
|
-
description: Ingest one source into the knowledge base - a file in sources/, a document, a page - creating its summary page, creating or updating the concept and entity pages it touches, linking everything both ways, and updating index.md and log.md.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Ingesting a source into the knowledge base
|
|
7
|
-
|
|
8
|
-
A source is ingested once, and from then on the knowledge base holds what it taught: later sessions read the pages, not the original again. Ingesting is not summarizing a file in isolation - its value is in how it connects to what the knowledge base already knows. Load the `knowledge-pages` skill first for the page templates.
|
|
9
|
-
|
|
10
|
-
## 1. Check it isn't already ingested
|
|
11
|
-
|
|
12
|
-
`list_sources` gives each original's status and summary pages: *new* needs ingesting; *ingested* has a page; *changed* changed after its ingest. For a document from elsewhere, look for it in `index.md` (Summaries) or `Grep` its URL inside the knowledge base. If it already has a page:
|
|
13
|
-
- unchanged source: nothing to do;
|
|
14
|
-
- the source changed or the page is thin: update that page instead of creating another.
|
|
15
|
-
|
|
16
|
-
## 2. Read it for real
|
|
17
|
-
|
|
18
|
-
Read the whole source, not its first page (a long PDF in parts: `list_sources` gives its pages). If it exists only outside the sources folder, keep the original first: `download_to_sources` for a URL (a web page is kept as markdown too: read that one), `save_to_sources` for a file in the run's folder, `request_file` to ask the person for one. `Read` reads PDFs and images; read DOCX, PPTX (with its speaker notes) and XLSX with `extract_text`.
|
|
19
|
-
|
|
20
|
-
## 3. Write the summary page
|
|
21
|
-
|
|
22
|
-
Create `summaries/<slug>.md` with the template: summary, key points (literal wording where exactness matters), frontmatter `file`/`url` pointing to the original.
|
|
23
|
-
|
|
24
|
-
## 4. Update concepts and entities
|
|
25
|
-
|
|
26
|
-
For every concept or entity the source explains or relies on:
|
|
27
|
-
- no page yet: create it;
|
|
28
|
-
- page exists: add what is new (a better definition, an example, a nuance, a fact) with `Edit`, and link the summary under "Sources".
|
|
29
|
-
|
|
30
|
-
Check first whether it exists under another name (`Grep` the knowledge base for the term and look at `aliases`): a duplicate page is worse than none.
|
|
31
|
-
|
|
32
|
-
## 5. Link both ways and look for conflicts
|
|
33
|
-
|
|
34
|
-
Every link you added from page A to page B needs its counterpart in B. If the source contradicts an existing page, record both versions with attribution in both pages - don't overwrite.
|
|
35
|
-
|
|
36
|
-
## 6. Close
|
|
37
|
-
|
|
38
|
-
- `index.md`: new pages, and one-line descriptions that changed.
|
|
39
|
-
- `log.md`: `## [date] ingest | <source title>` with the pages created and updated.
|
|
40
|
-
- `overview.md`: only if the source changes the big picture.
|
|
41
|
-
|
|
42
|
-
When ingesting several sources in a row, do steps 1-5 for each and close once at the end.
|
|
43
|
-
|
|
44
|
-
## An original that was wrong or replaced
|
|
45
|
-
|
|
46
|
-
`retire_source` (the person approves it) moves it aside and marks its summary retired (wrong) or superseded (replaced by a better one). Then update `index.md`, and fix the pages that cite it (`Grep` its file name): after a wrong one, correct or remove what it taught; after a replaced one, ingest the new original and re-ground those passages on it.
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: knowledge-lint
|
|
3
|
-
description: Health-check the knowledge base - broken links, pages missing from index.md, orphan pages, one-way links, duplicated concepts, mentioned-but-missing concepts, contradictions, gaps - fix what's mechanical and report the rest.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Linting the knowledge base
|
|
7
|
-
|
|
8
|
-
A knowledge base maintained across sessions drifts: links break, pages go unlisted, the same concept appears under two names. Lint it when asked and after a large ingest. Load `knowledge-pages` first for the templates.
|
|
9
|
-
|
|
10
|
-
## Checks
|
|
11
|
-
|
|
12
|
-
Use `Glob` to list pages and `Grep` (always with `path` inside the knowledge base) to find links, instead of reading every page in full.
|
|
13
|
-
|
|
14
|
-
1. **Broken links**: a relative link whose target file doesn't exist. Fix it if the right target is obvious (a typo, a page under a close name); otherwise report it.
|
|
15
|
-
2. **Index**: every page is listed in `index.md` and every entry there points to an existing page. Fix it.
|
|
16
|
-
3. **Orphans**: pages nothing links to (`Grep` for the file name). Link them from where they belong, or report why they don't belong anywhere.
|
|
17
|
-
4. **One-way links**: a summary that feeds a concept which doesn't list it back... Add the missing side.
|
|
18
|
-
5. **Duplicates**: two pages for the same idea under different names. Merge the content into one, mark the other as superseded (never delete it) and repoint links.
|
|
19
|
-
6. **Missing pages**: terms that several pages treat as concepts or entities but have no page. Create them if the material is there; otherwise list them.
|
|
20
|
-
7. **Contradictions**: claims that disagree across pages without being recorded as such. Record both sides, attributed.
|
|
21
|
-
8. **Provenance**: claims with no traceable source. Add the link, or mark them as unsourced.
|
|
22
|
-
9. **Gaps**: originals `list_sources` reports as new (no summary yet), changed (their summary may be stale) or missing (deleted by hand: their summaries point nowhere), and concepts mentioned but never explained by any source. Report them - they are what to ingest, update or find next.
|
|
23
|
-
|
|
24
|
-
## Close
|
|
25
|
-
|
|
26
|
-
Append `## [date] lint | <summary>` to `log.md` listing what was fixed. Then report to the human: what you fixed, and what needs their decision or more material. Don't make judgement calls silently - merging pages that are only similar, or choosing between contradictory sources, goes in the report.
|