@falkenslab/agent-kit 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/assets/extension-launcher.mjs +24 -0
  2. package/dist/chat/chatController.d.ts +264 -0
  3. package/dist/chat/chatController.js +510 -0
  4. package/dist/chat/extensions.d.ts +14 -0
  5. package/dist/chat/extensions.js +41 -0
  6. package/dist/chat/history.d.ts +17 -0
  7. package/dist/chat/history.js +39 -0
  8. package/dist/{tui → chat}/runs.d.ts +8 -1
  9. package/dist/{tui → chat}/runs.js +1 -1
  10. package/dist/core/agentSpec.d.ts +41 -27
  11. package/dist/core/extensions.d.ts +119 -0
  12. package/dist/core/extensions.js +97 -0
  13. package/dist/core/externalExtensions.d.ts +131 -0
  14. package/dist/core/externalExtensions.js +340 -0
  15. package/dist/core/hooks/fileScopeGate.d.ts +17 -2
  16. package/dist/core/hooks/fileScopeGate.js +45 -10
  17. package/dist/core/hooks/planGate.d.ts +2 -0
  18. package/dist/core/hooks/planGate.js +1 -10
  19. package/dist/core/hooks/stepGate.d.ts +1 -1
  20. package/dist/core/hooks/stepGate.js +3 -3
  21. package/dist/core/language.js +1 -1
  22. package/dist/core/messages/de.js +12 -33
  23. package/dist/core/messages/en.d.ts +13 -16
  24. package/dist/core/messages/en.js +12 -33
  25. package/dist/core/messages/es.js +12 -33
  26. package/dist/core/messages/fr.js +12 -33
  27. package/dist/core/messages/index.d.ts +6 -0
  28. package/dist/core/messages/index.js +12 -1
  29. package/dist/core/packaged.d.ts +10 -0
  30. package/dist/core/packaged.js +38 -0
  31. package/dist/core/pluginAgents.d.ts +20 -0
  32. package/dist/core/pluginAgents.js +76 -0
  33. package/dist/core/runner.js +16 -9
  34. package/dist/core/session.d.ts +42 -5
  35. package/dist/core/session.js +203 -99
  36. package/dist/core/sessionFacts.d.ts +80 -0
  37. package/dist/core/sessionFacts.js +48 -0
  38. package/dist/core/toolLabels.d.ts +24 -6
  39. package/dist/core/toolLabels.js +24 -39
  40. package/dist/core/tools/humanApproval.js +3 -3
  41. package/dist/extensions/awareness/index.d.ts +13 -0
  42. package/dist/extensions/awareness/index.js +39 -0
  43. package/dist/extensions/awareness/labels.d.ts +3 -0
  44. package/dist/extensions/awareness/labels.js +12 -0
  45. package/dist/extensions/awareness/tools.d.ts +5 -0
  46. package/dist/extensions/awareness/tools.js +54 -0
  47. package/dist/{core → extensions/knowledge}/fileKnowledgeStore.d.ts +1 -3
  48. package/dist/{core → extensions/knowledge}/fileKnowledgeStore.js +23 -29
  49. package/dist/extensions/knowledge/index.d.ts +8 -0
  50. package/dist/extensions/knowledge/index.js +35 -0
  51. package/dist/{core → extensions/knowledge}/knowledgeStore.d.ts +5 -9
  52. package/dist/{core → extensions/knowledge}/knowledgeStore.js +16 -2
  53. package/dist/extensions/knowledge/labels.d.ts +3 -0
  54. package/dist/extensions/knowledge/labels.js +119 -0
  55. package/dist/extensions/knowledge/prompt.d.ts +28 -0
  56. package/dist/extensions/knowledge/prompt.js +59 -0
  57. package/dist/{core/tools/knowledgeTools.d.ts → extensions/knowledge/tools.d.ts} +7 -7
  58. package/dist/{core/tools/knowledgeTools.js → extensions/knowledge/tools.js} +20 -55
  59. package/dist/extensions/memory/index.d.ts +12 -0
  60. package/dist/extensions/memory/index.js +56 -0
  61. package/dist/extensions/memory/labels.d.ts +3 -0
  62. package/dist/extensions/memory/labels.js +58 -0
  63. package/dist/extensions/memory/memoryStore.d.ts +53 -0
  64. package/dist/extensions/memory/memoryStore.js +88 -0
  65. package/dist/extensions/memory/tools.d.ts +9 -0
  66. package/dist/extensions/memory/tools.js +82 -0
  67. package/dist/extensions/sources/index.d.ts +9 -0
  68. package/dist/extensions/sources/index.js +40 -0
  69. package/dist/extensions/sources/labels.d.ts +3 -0
  70. package/dist/extensions/sources/labels.js +82 -0
  71. package/dist/{core → extensions/sources}/sources.d.ts +24 -31
  72. package/dist/{core → extensions/sources}/sources.js +45 -108
  73. package/dist/{core/tools/saveToSources.d.ts → extensions/sources/tools.d.ts} +7 -3
  74. package/dist/{core/tools/saveToSources.js → extensions/sources/tools.js} +22 -17
  75. package/dist/index.d.ts +14 -9
  76. package/dist/index.js +11 -7
  77. package/dist/tui/chatTui.d.ts +11 -20
  78. package/dist/tui/chatTui.js +75 -230
  79. package/dist/tui/extensionCommand.d.ts +21 -0
  80. package/dist/tui/extensionCommand.js +133 -0
  81. package/dist/tui/ink/runChatInk.d.ts +4 -3
  82. package/dist/tui/ink/runChatInk.js +101 -217
  83. package/extensions/awareness/.claude-plugin/plugin.json +10 -0
  84. package/{assets/agent-help → extensions/awareness/skills/help}/SKILL.md +4 -3
  85. package/{assets/knowledge-plugin → extensions/knowledge}/.claude-plugin/plugin.json +7 -1
  86. package/extensions/knowledge/commands/ingest.md +7 -0
  87. package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-ingest/SKILL.md +8 -3
  88. package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-lint/SKILL.md +12 -5
  89. package/extensions/memory/.claude-plugin/plugin.json +10 -0
  90. package/extensions/memory/commands/forget.md +8 -0
  91. package/extensions/memory/commands/list.md +5 -0
  92. package/extensions/sources/.claude-plugin/plugin.json +10 -0
  93. package/package.json +3 -2
  94. package/assets/knowledge-plugin/commands/ingest.md +0 -7
  95. package/assets/knowledge-plugin/commands/query.md +0 -5
  96. package/assets/knowledge-plugin-files/.claude-plugin/plugin.json +0 -4
  97. package/assets/knowledge-plugin-files/commands/ingest.md +0 -7
  98. package/assets/knowledge-plugin-files/commands/lint.md +0 -5
  99. package/assets/knowledge-plugin-files/skills/knowledge-ingest/SKILL.md +0 -46
  100. package/assets/knowledge-plugin-files/skills/knowledge-lint/SKILL.md +0 -26
  101. package/assets/knowledge-plugin-files/skills/knowledge-pages/SKILL.md +0 -137
  102. package/assets/knowledge-plugin-files/skills/knowledge-query/SKILL.md +0 -14
  103. package/dist/core/agentHelp.d.ts +0 -30
  104. package/dist/core/agentHelp.js +0 -55
  105. package/dist/core/knowledge.d.ts +0 -25
  106. package/dist/core/knowledge.js +0 -84
  107. /package/dist/{core → extensions/sources}/extractText.d.ts +0 -0
  108. /package/dist/{core → extensions/sources}/extractText.js +0 -0
  109. /package/{assets/knowledge-plugin → extensions/knowledge}/commands/lint.md +0 -0
  110. /package/{assets/knowledge-plugin-files → extensions/knowledge}/commands/query.md +0 -0
  111. /package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-query/SKILL.md +0 -0
@@ -5,10 +5,11 @@ import path from "node:path";
5
5
  /**
6
6
  * The sources folder's bookkeeping: originals stay files (ADR-024), and the kit keeps a
7
7
  * manifest next to them (`.agent-kit/sources.json`, inside `sourcesDir`, where the agent's
8
- * file tools can't write) with each original's hash, size and provenance, the version it
9
- * replaces, and the hash it had when a summary page first pointed to it. From that it tells
10
- * new, ingested, changed and missing originals apart, refuses duplicates, and retires an
11
- * original into `.agent-kit/retired/` instead of deleting it.
8
+ * file tools can't write) with each original's hash, size, provenance, the version it replaces
9
+ * and when its content last changed. From that it refuses duplicates, tells present, missing
10
+ * and retired originals apart, and retires an original into `.agent-kit/retired/` instead of
11
+ * deleting it. It knows nothing of the knowledge base: each owns its data, and the model
12
+ * connects them through their tools (#30). Hashes never leave it.
12
13
  */
13
14
  export const KIT_DIR = ".agent-kit";
14
15
  const MANIFEST = "sources.json";
@@ -70,41 +71,20 @@ export async function scanSources(sourcesDir) {
70
71
  await walk(sourcesDir);
71
72
  return found.sort();
72
73
  }
73
- /** An entry for `file`, reusing the recorded hash when its size and modification time haven't changed. */
74
+ /**
75
+ * An entry for `file`, reusing the recorded hash when its size and modification time haven't
76
+ * changed; `changedAt` moves only when the content (its hash) did. Entries from before
77
+ * `changedAt` existed take their `addedAt`.
78
+ */
74
79
  async function freshEntry(file, known, origin) {
75
80
  const info = await stat(file);
76
- if (known && known.size === info.size && known.mtimeMs === info.mtimeMs)
77
- return known;
78
- return { ...(known ?? { addedAt: now(), origin }), hash: await hashFile(file), size: info.size, mtimeMs: info.mtimeMs };
79
- }
80
- /** The original a frontmatter `file:` value points to (a relative path or a markdown link), resolved from the page's folder. */
81
- function linkedFile(value, pageDir) {
82
- const link = /\]\(([^)]+)\)/.exec(value)?.[1] ?? value.trim().replace(/^["']|["']$/g, "");
83
- if (!link || /^[a-z]+:\/\//i.test(link))
84
- return undefined;
85
- return path.resolve(pageDir, decodeURI(link));
86
- }
87
- /** The summary pages (knowledge base layout: `summaries/*.md`) by the absolute path of the original their frontmatter `file:` points to. */
88
- export async function summariesByOriginal(knowledgeDir) {
89
- const byOriginal = new Map();
90
- const dir = path.join(knowledgeDir, "summaries");
91
- let names;
92
- try {
93
- names = await readdir(dir);
94
- }
95
- catch {
96
- return byOriginal;
97
- }
98
- for (const name of names.filter((n) => n.endsWith(".md"))) {
99
- const page = path.join(dir, name);
100
- const text = await readFile(page, "utf8");
101
- const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1] ?? "";
102
- const value = /^file:\s*(.+)$/m.exec(frontmatter)?.[1];
103
- const original = value ? linkedFile(value, dir) : undefined;
104
- if (original)
105
- byOriginal.set(path.normalize(original), [...(byOriginal.get(path.normalize(original)) ?? []), page]);
106
- }
107
- return byOriginal;
81
+ const previous = known ? { ...known, changedAt: known.changedAt ?? known.addedAt } : undefined;
82
+ if (previous && previous.size === info.size && previous.mtimeMs === info.mtimeMs)
83
+ return previous;
84
+ const hash = await hashFile(file);
85
+ const stamp = now();
86
+ const base = previous ?? { addedAt: stamp, origin, changedAt: stamp };
87
+ return { ...base, hash, size: info.size, mtimeMs: info.mtimeMs, changedAt: previous && previous.hash === hash ? previous.changedAt : stamp };
108
88
  }
109
89
  /** Pages of a PDF, read from its page tree without a parser; undefined when it can't tell (e.g. compressed object streams). */
110
90
  export function pdfPageCount(bytes) {
@@ -124,31 +104,19 @@ export function pptxSlideCount(bytes) {
124
104
  }
125
105
  const typeOf = (file) => path.extname(file).slice(1).toLowerCase() || "file";
126
106
  /**
127
- * Every original with its status, recording in the manifest the ones copied in by hand and,
128
- * the first time a summary page points to one, its hash at ingest. Without a knowledge
129
- * folder there's no ingest to compare with: originals are "present".
107
+ * Every original with its status and when its content last changed, recording in the manifest
108
+ * the ones copied in by hand; then the missing ones and the retired ones.
130
109
  */
131
- export function listSources(sourcesDir, knowledgeDir, projectDir) {
132
- return serialized(sourcesDir, () => listSourcesNow(sourcesDir, knowledgeDir, projectDir));
110
+ export function listSources(sourcesDir) {
111
+ return serialized(sourcesDir, () => listSourcesNow(sourcesDir));
133
112
  }
134
- async function listSourcesNow(sourcesDir, knowledgeDir, projectDir) {
113
+ async function listSourcesNow(sourcesDir) {
135
114
  const manifest = await loadManifest(sourcesDir);
136
- const summaries = knowledgeDir ? await summariesByOriginal(knowledgeDir) : new Map();
137
115
  const files = await scanSources(sourcesDir);
138
116
  const listing = [];
139
117
  for (const rel of files) {
140
118
  const full = path.join(sourcesDir, rel);
141
119
  const entry = await freshEntry(full, manifest.files[rel], { kind: "manual" });
142
- const pagesOf = summaries.get(path.normalize(full)) ?? [];
143
- let status = "present";
144
- if (knowledgeDir) {
145
- if (pagesOf.length === 0)
146
- status = "new";
147
- else {
148
- entry.ingestedHash ??= entry.hash;
149
- status = entry.ingestedHash === entry.hash ? "ingested" : "changed";
150
- }
151
- }
152
120
  manifest.files[rel] = entry;
153
121
  const type = typeOf(rel);
154
122
  const counts = type === "pdf" || type === "pptx"
@@ -156,18 +124,28 @@ async function listSourcesNow(sourcesDir, knowledgeDir, projectDir) {
156
124
  : {};
157
125
  listing.push({
158
126
  path: rel,
159
- status,
127
+ status: "present",
160
128
  type,
129
+ changedAt: entry.changedAt,
161
130
  size: entry.size,
162
131
  ...counts,
163
132
  origin: entry.origin,
164
133
  ...(entry.replaces ? { replaces: entry.replaces } : {}),
165
- summaries: pagesOf.map((page) => posix(path.relative(projectDir, page))),
166
134
  });
167
135
  }
168
136
  for (const [rel, entry] of Object.entries(manifest.files)) {
169
137
  if (!files.includes(rel))
170
- listing.push({ path: rel, status: "missing", type: typeOf(rel), origin: entry.origin, summaries: [] });
138
+ listing.push({ path: rel, status: "missing", type: typeOf(rel), origin: entry.origin });
139
+ }
140
+ for (const retired of manifest.retired) {
141
+ listing.push({
142
+ path: retired.path,
143
+ status: "retired",
144
+ type: typeOf(retired.path),
145
+ retiredAt: retired.retiredAt,
146
+ why: retired.why,
147
+ ...(retired.replacedBy ? { replacedBy: retired.replacedBy } : {}),
148
+ });
171
149
  }
172
150
  await saveManifest(sourcesDir, manifest);
173
151
  return listing;
@@ -219,11 +197,13 @@ async function addSourceNow(sourcesDir, file, destination, origin, extra) {
219
197
  }
220
198
  const rel = posix(path.relative(sourcesDir, target));
221
199
  const info = await stat(target);
200
+ const stamp = now();
222
201
  manifest.files[rel] = {
223
202
  hash,
224
203
  size: info.size,
225
204
  mtimeMs: info.mtimeMs,
226
- addedAt: now(),
205
+ addedAt: stamp,
206
+ changedAt: stamp,
227
207
  origin,
228
208
  ...(extra.replaces ? { replaces: posix(path.relative(sourcesDir, resolveWithin(sourcesDir, extra.replaces))) } : {}),
229
209
  ...(extra.derivedFrom ? { derivedFrom: extra.derivedFrom } : {}),
@@ -231,26 +211,15 @@ async function addSourceNow(sourcesDir, file, destination, origin, extra) {
231
211
  await saveManifest(sourcesDir, manifest);
232
212
  return { path: rel };
233
213
  }
234
- /** Adds frontmatter fields and a notice under the frontmatter of a markdown page. */
235
- async function markPage(page, fields, notice) {
236
- const text = await readFile(page, "utf8");
237
- const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
238
- const lines = Object.entries(fields).map(([key, value]) => `${key}: ${value}`);
239
- const updated = match
240
- ? `---\n${match[1].split(/\r?\n/).filter((line) => !Object.keys(fields).some((key) => line.startsWith(`${key}:`))).concat(lines).join("\n")}\n---\n\n> ${notice}\n\n${text.slice(match[0].length).replace(/^\s+/, "")}`
241
- : `---\n${lines.join("\n")}\n---\n\n> ${notice}\n\n${text}`;
242
- await writeFile(page, updated);
243
- }
244
214
  /**
245
- * Moves an original to `.agent-kit/retired/` (never deleted: the person empties that folder)
246
- * and records why. Its summary pages are marked: retired if the original was wrong (what it
247
- * taught was wrong), superseded if it was replaced, pointing to the replacement's summary
248
- * when there is one.
215
+ * Moves an original to `.agent-kit/retired/` (never deleted: the person empties that folder) and
216
+ * records why. What was built from it (summary pages) is the knowledge base's to update: the
217
+ * model does it with its tools.
249
218
  */
250
- export function retireSource(sourcesDir, knowledgeDir, projectDir, source, why, reason, replacedBy) {
251
- return serialized(sourcesDir, () => retireSourceNow(sourcesDir, knowledgeDir, projectDir, source, why, reason, replacedBy));
219
+ export function retireSource(sourcesDir, source, why, reason, replacedBy) {
220
+ return serialized(sourcesDir, () => retireSourceNow(sourcesDir, source, why, reason, replacedBy));
252
221
  }
253
- async function retireSourceNow(sourcesDir, knowledgeDir, projectDir, source, why, reason, replacedBy) {
222
+ async function retireSourceNow(sourcesDir, source, why, reason, replacedBy) {
254
223
  const full = resolveWithin(sourcesDir, source);
255
224
  if (!full || !(await stat(full).catch(() => null))?.isFile())
256
225
  throw new Error(`"${source}" isn't an original in sources/.`);
@@ -260,7 +229,6 @@ async function retireSourceNow(sourcesDir, knowledgeDir, projectDir, source, why
260
229
  const replacement = replacedBy ? resolveWithin(sourcesDir, replacedBy) : undefined;
261
230
  if (replacedBy && (!replacement || !(await stat(replacement).catch(() => null))))
262
231
  throw new Error(`"${replacedBy}" isn't an original in sources/.`);
263
- const summaries = knowledgeDir ? await summariesByOriginal(knowledgeDir) : new Map();
264
232
  const manifest = await loadManifest(sourcesDir);
265
233
  const stamp = now();
266
234
  const movedTo = posix(path.join(KIT_DIR, RETIRED_DIR, `${stamp.replace(/[:.]/g, "-")}-${path.basename(rel)}`));
@@ -277,36 +245,5 @@ async function retireSourceNow(sourcesDir, knowledgeDir, projectDir, source, why
277
245
  });
278
246
  delete manifest.files[rel];
279
247
  await saveManifest(sourcesDir, manifest);
280
- const date = stamp.slice(0, 10);
281
- const replacementSummary = replacement ? summaries.get(path.normalize(replacement))?.[0] : undefined;
282
- const result = { movedTo, summaries: [] };
283
- for (const page of summaries.get(path.normalize(full)) ?? []) {
284
- if (why === "wrong") {
285
- await markPage(page, { status: "retired", retired: date }, `Retired on ${date}: its original was wrong (${reason}). Don't rely on what it says.`);
286
- result.summaries.push({ page: posix(path.relative(projectDir, page)), marked: "retired" });
287
- }
288
- else {
289
- const link = replacementSummary ? posix(path.relative(path.dirname(page), replacementSummary)) : undefined;
290
- await markPage(page, { status: "superseded", superseded: date, ...(link ? { superseded_by: link } : {}) }, `Superseded on ${date}: its original was replaced (${reason})${link ? `; see [the new summary](${link})` : ""}.`);
291
- result.summaries.push({ page: posix(path.relative(projectDir, page)), marked: "superseded", ...(replacementSummary ? { supersededBy: posix(path.relative(projectDir, replacementSummary)) } : {}) });
292
- }
293
- }
294
- return result;
295
- }
296
- /**
297
- * Records `source` (relative to `sourcesDir`) as ingested now, with its current hash: a
298
- * summary page was just written from it, so "changed" means changed after this.
299
- */
300
- export function recordIngest(sourcesDir, source) {
301
- return serialized(sourcesDir, async () => {
302
- const full = resolveWithin(sourcesDir, source);
303
- if (!full || !(await stat(full).catch(() => null))?.isFile())
304
- return;
305
- const rel = posix(path.relative(sourcesDir, full));
306
- const manifest = await loadManifest(sourcesDir);
307
- const entry = await freshEntry(full, manifest.files[rel], { kind: "manual" });
308
- entry.ingestedHash = entry.hash;
309
- manifest.files[rel] = entry;
310
- await saveManifest(sourcesDir, manifest);
311
- });
248
+ return { movedTo };
312
249
  }
@@ -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. */
@@ -24,6 +28,6 @@ export interface SourceToolsOptions {
24
28
  * are scoped to the notes folder, see hooks/fileScopeGate.ts): `save_to_sources`,
25
29
  * `list_sources` and `download_to_sources`, plus `request_file` and `retire_source` when a
26
30
  * person can be asked. Nothing is ever overwritten or deleted; the bookkeeping is in
27
- * sources.ts.
31
+ * sources.ts (this extension's).
28
32
  */
29
33
  export declare function createSaveToSourcesServer(runDir: string, sourcesDir: string, description?: string, options?: SourceToolsOptions): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
@@ -2,10 +2,10 @@ import path from "node:path";
2
2
  import { mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
4
4
  import { z } from "zod";
5
- import { addSource, listSources, resolveWithin, retireSource } from "../sources.js";
6
- import { askForDecision, askForText } from "../hooks/humanInput.js";
7
- import { t } from "../messages/index.js";
8
- import { EXTRACTABLE, extractText } from "../extractText.js";
5
+ import { addSource, listSources, resolveWithin, retireSource } from "./sources.js";
6
+ import { askForDecision, askForText } from "../../core/hooks/humanInput.js";
7
+ import { t } from "../../core/messages/index.js";
8
+ import { EXTRACTABLE, extractText } from "./extractText.js";
9
9
  /**
10
10
  * `resolvedPath` as given if it exists; otherwise, a same-directory sibling whose
11
11
  * filename is Unicode-equal under NFC normalization. Needed because a file downloaded via
@@ -104,12 +104,22 @@ 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`,
110
120
  * `list_sources` and `download_to_sources`, plus `request_file` and `retire_source` when a
111
121
  * person can be asked. Nothing is ever overwritten or deleted; the bookkeeping is in
112
- * sources.ts.
122
+ * sources.ts (this extension's).
113
123
  */
114
124
  export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFAULT_DESCRIPTION, options = {}) {
115
125
  const projectDir = options.projectDir ?? path.dirname(sourcesDir);
@@ -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/ with their status: new (no summary page points to it: ingest it), ingested, changed (it changed after its ingest: update its summary), missing (deleted by hand) or present (no knowledge base to compare with); with type, size, pages of a PDF or slides of a PPTX (read long ones in parts), provenance, the version it replaces and its summary pages. Use it instead of listing the folder.", {}, async () => {
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, options.knowledgeDir, projectDir), null, 2));
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 person's language"),
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"), after the person approves it: it\'s moved to sources/.agent-kit/retired/ (never deleted), and its summary page is marked retired (wrong) or superseded, pointing to the replacement\'s summary (replaced). Afterwards, update the index and correct or re-ground the pages that cited it.', {
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 person's language"),
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, options.knowledgeDir, projectDir, inFolder(args.source), args.why, args.reason, args.replacedBy && inFolder(args.replacedBy));
249
- const pages = result.summaries.map((s) => `${s.page} marked ${s.marked}${s.supersededBy ? ` (see ${s.supersededBy})` : ""}`);
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
@@ -5,17 +5,21 @@ export { allowAnyMcpTool } from "./core/mcpPermissions.js";
5
5
  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
- export { createFriendlyToolLabel, truncate, truncatePath, type ToolDescriber, } from "./core/toolLabels.js";
8
+ export { createFriendlyToolLabel, truncate, truncatePath, withToolLabels, withToolPhrases, type ToolDescriber, type ToolLabel, type ToolLabels, } from "./core/toolLabels.js";
9
9
  export type { Mode, BaseSessionConfig, AgentSpec, AgentIdentity, PlanModeSpec } from "./core/agentSpec.js";
10
- export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, togglePlanMode, type ModeControl } from "./core/session.js";
10
+ export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, togglePlanMode, type ModeControl, type ExtensionsStatus } from "./core/session.js";
11
11
  export { runQuery, type AgentEvent, type AgentRun, type SessionUsage, type ContextUsage } from "./core/runner.js";
12
- export { knowledgePluginRoot, knowledgePromptSection } from "./core/knowledge.js";
12
+ export type { SessionFacts } from "./core/sessionFacts.js";
13
+ export { knowledgePluginRoot, knowledgePromptSection } from "./extensions/knowledge/prompt.js";
14
+ export { readExtensionManifest, type Extension, type ExtensionContext, type ExtensionContribution, type ExtensionManifest, } from "./core/extensions.js";
15
+ export { addExtension, removeExtension, setExtensionEnabled, listInstalled, readExternalManifest, type ExtensionAuthor, type ExtensionDirs, type ExtensionScope, type ExternalManifest, type ExternalToolLabel, type PluginServer, type InstalledExtension, type LockEntry, } from "./core/externalExtensions.js";
16
+ export { runExtensionCommand } from "./tui/extensionCommand.js";
13
17
  export { agentKitVersion } from "./core/version.js";
14
- export { BUILT_IN_PAGE_TYPES, isSlug, parseId, linkedIds, type KnowledgeStore, type PageType, type KnowledgePage, type PageInfo, type SearchHit, type CheckReport, type FieldChanges, type NewPage, } from "./core/knowledgeStore.js";
15
- export { createFileKnowledgeStore, type FileKnowledgeStoreOptions } from "./core/fileKnowledgeStore.js";
16
- export { createKnowledgeServer, type KnowledgeToolsOptions } from "./core/tools/knowledgeTools.js";
18
+ export { BUILT_IN_PAGE_TYPES, isSlug, parseId, linkedIds, type KnowledgeStore, type PageType, type KnowledgePage, type PageInfo, type SearchHit, type CheckReport, type FieldChanges, type NewPage, } from "./extensions/knowledge/knowledgeStore.js";
19
+ export { createFileKnowledgeStore, type FileKnowledgeStoreOptions } from "./extensions/knowledge/fileKnowledgeStore.js";
20
+ export { createKnowledgeServer, type KnowledgeToolsOptions } from "./extensions/knowledge/tools.js";
17
21
  export { resolveLanguage, detectLanguage, SUPPORTED_LANGUAGES, type Language, type LanguageSources, type ResolvedLanguage, } from "./core/language.js";
18
- export { setLanguage, getLanguage, messagesFor, type Messages } from "./core/messages/index.js";
22
+ export { setLanguage, switchLanguage, getLanguage, messagesFor, type Messages } from "./core/messages/index.js";
19
23
  export { createRunStore, createRunFolder, listRuns, readConversation, type RunFolder, type RunSummary, type ConversationMessage, } from "./core/runs.js";
20
24
  export type { Options, McpServerConfig, AgentDefinition } from "@anthropic-ai/claude-agent-sdk";
21
25
  export { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
@@ -31,9 +35,10 @@ export { setInteractionPort, getInteractionPort, type InteractionPort, type Appr
31
35
  export { terminalInteractionPort, setSharedReadline, getSharedReadline } from "./tui/terminalInteraction.js";
32
36
  export { createHumanApprovalServer, DEFAULT_HUMAN_APPROVAL_TEXTS, type HumanApprovalTexts } from "./core/tools/humanApproval.js";
33
37
  export { createManualLoginServer, type ManualInterventionTexts } from "./core/tools/manualLogin.js";
34
- export { createSaveToSourcesServer, type SourceToolsOptions } from "./core/tools/saveToSources.js";
38
+ export { createSaveToSourcesServer, sourcesPromptSection, type SourceToolsOptions } from "./extensions/sources/tools.js";
35
39
  export { runChatTui, type ChatTuiOptions } from "./tui/chatTui.js";
36
- export type { SessionOpener } from "./tui/runs.js";
40
+ export type { SessionOpener } from "./chat/runs.js";
41
+ export { createChatController, type ChatController, type ChatSettings, type ChatState, type ChatEvent, type ChatPanel, type ChatQuestion, type TranscriptEntry, type TranscriptCall, type NoticeTone, } from "./chat/chatController.js";
37
42
  export { createConsoleRenderer, type ConsoleRenderer, type ConsoleRendererOptions } from "./tui/consoleRenderer.js";
38
43
  export { runChatInk, type InkChatOptions, type HeaderInfo } from "./tui/ink/runChatInk.js";
39
44
  export type { ToolPhrase, ToolDetail, ResultFormatter } from "./tui/ink/toolGroup.js";
package/dist/index.js CHANGED
@@ -7,16 +7,19 @@ export { allowAnyMcpTool } from "./core/mcpPermissions.js";
7
7
  export { createPromptLoader } from "./core/promptTemplate.js";
8
8
  export { resolveClaudeAuth } from "./core/claudeAuth.js";
9
9
  export { ensureClaudeAuth } from "./tui/claudeAuth.js";
10
- export { createFriendlyToolLabel, truncate, truncatePath, } from "./core/toolLabels.js";
10
+ export { createFriendlyToolLabel, truncate, truncatePath, withToolLabels, withToolPhrases, } from "./core/toolLabels.js";
11
11
  export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, togglePlanMode } from "./core/session.js";
12
12
  export { runQuery } from "./core/runner.js";
13
- export { knowledgePluginRoot, knowledgePromptSection } from "./core/knowledge.js";
13
+ export { knowledgePluginRoot, knowledgePromptSection } from "./extensions/knowledge/prompt.js";
14
+ export { readExtensionManifest, } from "./core/extensions.js";
15
+ export { addExtension, removeExtension, setExtensionEnabled, listInstalled, readExternalManifest, } from "./core/externalExtensions.js";
16
+ export { runExtensionCommand } from "./tui/extensionCommand.js";
14
17
  export { agentKitVersion } from "./core/version.js";
15
- export { BUILT_IN_PAGE_TYPES, isSlug, parseId, linkedIds, } from "./core/knowledgeStore.js";
16
- export { createFileKnowledgeStore } from "./core/fileKnowledgeStore.js";
17
- export { createKnowledgeServer } from "./core/tools/knowledgeTools.js";
18
+ export { BUILT_IN_PAGE_TYPES, isSlug, parseId, linkedIds, } from "./extensions/knowledge/knowledgeStore.js";
19
+ export { createFileKnowledgeStore } from "./extensions/knowledge/fileKnowledgeStore.js";
20
+ export { createKnowledgeServer } from "./extensions/knowledge/tools.js";
18
21
  export { resolveLanguage, detectLanguage, SUPPORTED_LANGUAGES, } from "./core/language.js";
19
- export { setLanguage, getLanguage, messagesFor } from "./core/messages/index.js";
22
+ export { setLanguage, switchLanguage, getLanguage, messagesFor } from "./core/messages/index.js";
20
23
  export { createRunStore, createRunFolder, listRuns, readConversation, } from "./core/runs.js";
21
24
  // And the two functions an agent needs for tools of its own (an in-process MCP server,
22
25
  // returned from AgentSpec.buildMcpServers()), so it doesn't need the SDK for those either.
@@ -36,8 +39,9 @@ export { terminalInteractionPort, setSharedReadline, getSharedReadline } from ".
36
39
  setInteractionPort(terminalInteractionPort);
37
40
  export { createHumanApprovalServer, DEFAULT_HUMAN_APPROVAL_TEXTS } from "./core/tools/humanApproval.js";
38
41
  export { createManualLoginServer } from "./core/tools/manualLogin.js";
39
- export { createSaveToSourcesServer } from "./core/tools/saveToSources.js";
42
+ export { createSaveToSourcesServer, sourcesPromptSection } from "./extensions/sources/tools.js";
40
43
  export { runChatTui } from "./tui/chatTui.js";
44
+ export { createChatController, } from "./chat/chatController.js";
41
45
  export { createConsoleRenderer } from "./tui/consoleRenderer.js";
42
46
  export { runChatInk } from "./tui/ink/runChatInk.js";
43
47
  export { createProgressView } from "./tui/ink/progressView.js";
@@ -1,8 +1,10 @@
1
1
  import type { Options } from "@anthropic-ai/claude-agent-sdk";
2
- import { type ModeControl } from "../core/session.js";
3
- import { type AgentEvent } from "../core/runner.js";
2
+ import type { ModeControl } from "../core/session.js";
3
+ import type { AgentEvent } from "../core/runner.js";
4
4
  import { type Theme } from "./theme.js";
5
- import { type SessionOpener } from "./runs.js";
5
+ import type { ToolLabels } from "../core/toolLabels.js";
6
+ import { type SessionOpener } from "../chat/runs.js";
7
+ export { capHistory, loadHistory, saveHistory, slashCommandToken } from "../chat/history.js";
6
8
  /** Options of the chats (`runChatTui()`, and `runChatInk()` through `InkChatOptions`). */
7
9
  export interface ChatTuiOptions {
8
10
  /**
@@ -80,6 +82,12 @@ export interface ChatTuiOptions {
80
82
  * returns is used instead.
81
83
  */
82
84
  modeControl?: ModeControl;
85
+ /**
86
+ * How the chat shows the extensions' tools (`buildSessionOptions()` returns them as
87
+ * `toolLabels`), before `formatAction`. With a session opener, the ones it returns are used
88
+ * instead.
89
+ */
90
+ toolLabels?: ToolLabels;
83
91
  /**
84
92
  * Colors for the kit's roles (see `Theme`), on top of the kit's defaults: only the roles
85
93
  * given change, e.g. `{ toolResult: "yellow", selection: "#00ff00" }`. One theme per
@@ -87,22 +95,6 @@ export interface ChatTuiOptions {
87
95
  */
88
96
  theme?: Partial<Theme>;
89
97
  }
90
- interface HistoryEntry {
91
- text: string;
92
- timestamp: string;
93
- }
94
- /** Keeps only the most recent `limit` entries (oldest-first order in, oldest-first out). */
95
- export declare function capHistory(entries: readonly HistoryEntry[], limit: number): HistoryEntry[];
96
- /** Oldest-first. Missing file reads as empty — nothing to load yet is the normal case. */
97
- export declare function loadHistory(filePath: string): Promise<HistoryEntry[]>;
98
- /** Overwrites the whole file (not appended) so a prior prune (oldest entries dropped) sticks. */
99
- export declare function saveHistory(filePath: string, entries: readonly HistoryEntry[]): Promise<void>;
100
- /**
101
- * The command name a typed line would invoke (e.g. "chiste" for both "/chiste" and
102
- * "/chiste con salsa"), or `null` for a line that isn't a slash command at all (doesn't
103
- * start with "/") or is just a bare "/" with nothing after it.
104
- */
105
- export declare function slashCommandToken(line: string): string | null;
106
98
  /**
107
99
  * Reads `events` one turn at a time — up to and including a `turn-end` — via manual
108
100
  * `.next()` calls, calling `onEvent` for each. Deliberately never uses
@@ -139,4 +131,3 @@ export declare function drainTurn(events: AsyncIterator<AgentEvent>, onEvent: (e
139
131
  * interface itself before returning.
140
132
  */
141
133
  export declare function runChatTui(options: Options | SessionOpener, tuiOptions?: ChatTuiOptions): Promise<void>;
142
- export {};