@falkenslab/agent-kit 0.16.0 → 0.16.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/README.md CHANGED
@@ -34,7 +34,9 @@ A foundation for building AI agents on top of the [Claude Agent SDK](https://www
34
34
 
35
35
  [Captain Whiskers](./examples/captain-whiskers) is a toy agent to see the kit in action: a retired pirate cat who tells jokes in the terminal, in your language. It starts full screen with its logo and has a small crew of subagents: one looks for new jokes on the web, another rates them before they're told, and a third tells the time with a tool of the captain's own. It lets you try the different levels of oversight and is the best starting point for an agent of your own. Its README explains how to run it.
36
36
 
37
- ![Captain Whiskers in Windows Terminal: its logo at the top, the conversation in the middle and the framed prompt at the bottom, with a suggestion for the next message](docs/assets/captain-whiskers.png)
37
+ ![Captain Whiskers in Windows Terminal: his logo with the mode and the agent-kit version at the top, his crew at work in the middle, a task list under the spinner, and the framed prompt and status bar at the bottom](docs/assets/captain-whiskers.png)
38
+
39
+ See [everything he can do](https://falkenslab.github.io/agent-kit/docs/next/examples/captain-whiskers#what-he-can-do), with screenshots.
38
40
 
39
41
  ### teacher-agent
40
42
 
@@ -13,7 +13,7 @@ A source is ingested once, and from then on the knowledge base holds what it tau
13
13
 
14
14
  ## 2. Read it for real
15
15
 
16
- Read the whole source, not its first page (a long PDF in parts: `list_sources` gives its pages). Its path is `sources/<path>` in the project folder, not in this skill's folder. `Read` reads PDFs and images; `extract_text` reads DOCX, PPTX (with its speaker notes) and XLSX. 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.
16
+ Read the whole source, not its first page (a long PDF in parts: `list_sources` gives its pages). Its path is the sources folder's (named in the system prompt) plus the path `list_sources` gives, from the project folder, not from this skill's folder. `Read` reads PDFs and images; `extract_text` reads DOCX, PPTX (with its speaker notes) and XLSX. 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.
17
17
 
18
18
  ## 3. Write the summary page
19
19
 
@@ -163,7 +163,12 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
163
163
  async function storedField(key, value, file) {
164
164
  if (key !== "file" || !options.sourcesDir || /^[a-z]+:\/\//i.test(value))
165
165
  return value;
166
- const original = path.resolve(options.sourcesDir, value.replace(/^sources\//, ""));
166
+ // Relative to the sources folder; a leading "sources/" or the folder's own name is dropped.
167
+ let relative = value.replace(/\\/g, "/").replace(/^\.\//, "");
168
+ for (const prefix of ["sources/", `${path.basename(options.sourcesDir)}/`])
169
+ if (relative.startsWith(prefix))
170
+ relative = relative.slice(prefix.length);
171
+ const original = path.resolve(options.sourcesDir, relative);
167
172
  if (!(await stat(original).catch(() => null))?.isFile())
168
173
  throw new Error(`"${value}" isn't an original in sources/ (list_sources lists them).`);
169
174
  return posix(path.relative(path.dirname(file), original));
@@ -321,6 +326,12 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
321
326
  .map(({ id, title, snippet }) => ({ id, title, snippet }));
322
327
  },
323
328
  index: indexText,
329
+ async recentLog(limit = 10) {
330
+ const text = await readFile(path.join(knowledgeDir, "log.md"), "utf8").catch(() => "");
331
+ // Entries start with "## [date] operation | what"; the newest are at the bottom.
332
+ const entries = text.split(/\r?\n(?=## )/).filter((entry) => entry.startsWith("## ")).map((entry) => entry.trim());
333
+ return entries.slice(-limit).reverse();
334
+ },
324
335
  async log(operation, what, pages = []) {
325
336
  await mkdir(knowledgeDir, { recursive: true });
326
337
  const file = path.join(knowledgeDir, "log.md");
@@ -331,6 +342,7 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
331
342
  async check() {
332
343
  await writeIndex(); // the one mechanical fix there is: an index.md out of date
333
344
  const { pages, byFile } = await pagesByFile();
345
+ const reservedFiles = new Set([...RESERVED].map((name) => path.normalize(path.join(knowledgeDir, `${name}.md`))));
334
346
  const known = new Map(pages.map((p) => [p.id, p]));
335
347
  const inbound = new Map();
336
348
  const report = { brokenLinks: [], orphans: [], linksToRetired: [], missingOriginals: [] };
@@ -338,11 +350,16 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
338
350
  if (statusOf(page) === "retired")
339
351
  continue;
340
352
  const body = fromFile(page.body, page.file, byFile);
341
- // Relative links to .md files that resolve to nothing are broken too.
353
+ // Relative links to .md files that resolve to nothing are broken too. The overview, the
354
+ // index and the log aren't pages but are files: a link to one that exists is fine (a
355
+ // knowledge base written with the file tools links the overview, #22).
342
356
  for (const m of page.body.matchAll(/\]\(([^)\s:]+\.md)(?:#[^)]*)?\)/g)) {
343
357
  const target = path.normalize(path.resolve(path.dirname(page.file), decodeURI(m[1])));
344
- if (!byFile.has(target) && target.startsWith(path.normalize(knowledgeDir)))
345
- report.brokenLinks.push({ page: page.id, target: posix(path.relative(knowledgeDir, target)) });
358
+ if (byFile.has(target) || !target.startsWith(path.normalize(knowledgeDir)))
359
+ continue;
360
+ if (reservedFiles.has(target) && (await stat(target).catch(() => null)))
361
+ continue;
362
+ report.brokenLinks.push({ page: page.id, target: posix(path.relative(knowledgeDir, target)) });
346
363
  }
347
364
  for (const id of new Set(linkedIds(body))) {
348
365
  const target = known.get(id);
@@ -30,7 +30,7 @@ function toolsSection(originals, pageTypes) {
30
30
  Your memory across sessions is an interlinked knowledge base of pages that you write and maintain yourself — a wiki, not a pile of notes. A future session only knows what is written there, so anything worth remembering must end up in a page, not just in this turn's reply. You reach it only through the \`knowledge_*\` tools. Write page content in the language you are using with the human.
31
31
 
32
32
  ### Layers
33
- ${originals ? originalsLine(originals) : ""}- **The knowledge base**: pages identified by type and slug (\`concept/spring-tides\`), plus the \`overview\`, a living synthesis of the whole. Page types: ${pageTypes.map((type) => `\`${type.type}\` (${type.description})`).join("; ")}.
33
+ ${originals ? originalsLine(originals) : ""}- **The knowledge base**: pages identified by type and slug (\`concept/spring-tides\`), plus the \`overview\`, a living synthesis of the whole, and the \`log\` of what was done to it (\`knowledge_read\` reads both). Page types: ${pageTypes.map((type) => `\`${type.type}\` (${type.description})`).join("; ")}.
34
34
  - **The schema**: these rules plus the \`knowledge-ingest\`, \`knowledge-query\` and \`knowledge-lint\` skills. \`knowledge_create\` without content gives a type's template.
35
35
 
36
36
  ### Working rules
@@ -114,6 +114,11 @@ export interface KnowledgeStore {
114
114
  index(): Promise<string>;
115
115
  /** Adds an entry to the operation log, dated today. */
116
116
  log(operation: string, what: string, pages?: readonly string[]): Promise<void>;
117
+ /**
118
+ * The latest entries of the operation log, newest first, as markdown (e.g. "## [2026-10-04]
119
+ * ingest | …"). Optional: a store without it can't show the log to the agent.
120
+ */
121
+ recentLog?(limit?: number): Promise<string[]>;
117
122
  /** The mechanical problems. */
118
123
  check(): Promise<CheckReport>;
119
124
  /** The overview: a living synthesis of the whole knowledge base ("" if none yet). */
@@ -70,12 +70,12 @@ export const de = {
70
70
  keepPlanning: "Weiter planen",
71
71
  cancelPlan: "Abbrechen",
72
72
  planCommentQuestion: "Was soll sich ändern? (Enter zum Überspringen): ",
73
- retireLines: (source, why, reason, replacedBy) => [
73
+ retireLines: (source, why, reason, replacedBy, folder) => [
74
74
  `Quelle: ${source}`,
75
75
  why === "wrong" ? "Warum: sie ist falsch; ihre Zusammenfassung wird zurückgezogen" : "Warum: sie wird ersetzt; ihre Zusammenfassung wird als ersetzt markiert",
76
76
  `Grund: ${reason}`,
77
77
  ...(replacedBy ? [`Ersetzt durch: ${replacedBy}`] : []),
78
- "Sie wird nach sources/.agent-kit/retired/ verschoben, nicht gelöscht.",
78
+ `Sie wird nach ${folder}.agent-kit/retired/ verschoben, nicht gelöscht.`,
79
79
  ],
80
80
  toolPhrases: {
81
81
  Read: ["hat {n} Datei gelesen", "hat {n} Dateien gelesen"],
@@ -120,7 +120,7 @@ export const de = {
120
120
  labels: {
121
121
  askingApproval: (summary) => `Bittet um menschliche Genehmigung: ${summary}`,
122
122
  waitingManual: "Wartet auf einen manuellen Eingriff",
123
- savingToSources: (destination) => `Speichert eine Datei in sources/${destination}`,
123
+ savingToSources: (destination) => `Speichert ${destination} in den Quellen`,
124
124
  checkingTime: "Datum und Uhrzeit abfragen",
125
125
  calculatingDates: "Daten berechnen",
126
126
  reading: (file) => `Liest ${file}`,
@@ -67,7 +67,7 @@ export interface Messages {
67
67
  keepPlanning: string;
68
68
  cancelPlan: string;
69
69
  planCommentQuestion: string;
70
- retireLines(source: string, why: "wrong" | "replaced", reason: string, replacedBy?: string): string[];
70
+ retireLines(source: string, why: "wrong" | "replaced", reason: string, replacedBy: string | undefined, folder: string): string[];
71
71
  toolPhrases: Record<string, ToolPhrase>;
72
72
  otherTools: ToolPhrase;
73
73
  noOutput: string;
@@ -69,12 +69,12 @@ export const en = {
69
69
  keepPlanning: "Keep planning",
70
70
  cancelPlan: "Cancel",
71
71
  planCommentQuestion: "What should change? (Enter to skip): ",
72
- retireLines: (source, why, reason, replacedBy) => [
72
+ retireLines: (source, why, reason, replacedBy, folder) => [
73
73
  `Source: ${source}`,
74
74
  why === "wrong" ? "Why: it's wrong; its summary will be retired" : "Why: it's replaced; its summary will be marked superseded",
75
75
  `Reason: ${reason}`,
76
76
  ...(replacedBy ? [`Replaced by: ${replacedBy}`] : []),
77
- "It's moved to sources/.agent-kit/retired/, not deleted.",
77
+ `It's moved to ${folder}.agent-kit/retired/, not deleted.`,
78
78
  ],
79
79
  toolPhrases: {
80
80
  Read: ["read {n} file", "read {n} files"],
@@ -119,7 +119,7 @@ export const en = {
119
119
  labels: {
120
120
  askingApproval: (summary) => `Asking for human approval: ${summary}`,
121
121
  waitingManual: "Waiting for a human to intervene manually",
122
- savingToSources: (destination) => `Saving a file to sources/${destination}`,
122
+ savingToSources: (destination) => `Saving ${destination} to the sources`,
123
123
  checkingTime: "Checking the date and time",
124
124
  calculatingDates: "Calculating dates",
125
125
  reading: (file) => `Reading ${file}`,
@@ -70,12 +70,12 @@ export const es = {
70
70
  keepPlanning: "Seguir planificando",
71
71
  cancelPlan: "Cancelar",
72
72
  planCommentQuestion: "¿Qué debería cambiar? (Enter para omitir): ",
73
- retireLines: (source, why, reason, replacedBy) => [
73
+ retireLines: (source, why, reason, replacedBy, folder) => [
74
74
  `Fuente: ${source}`,
75
75
  why === "wrong" ? "Motivo: es errónea; su resumen se retirará" : "Motivo: se sustituye; su resumen se marcará como sustituido",
76
76
  `Razón: ${reason}`,
77
77
  ...(replacedBy ? [`Sustituida por: ${replacedBy}`] : []),
78
- "Se mueve a sources/.agent-kit/retired/, no se borra.",
78
+ `Se mueve a ${folder}.agent-kit/retired/, no se borra.`,
79
79
  ],
80
80
  toolPhrases: {
81
81
  Read: ["leyó {n} fichero", "leyó {n} ficheros"],
@@ -120,7 +120,7 @@ export const es = {
120
120
  labels: {
121
121
  askingApproval: (summary) => `Pidiendo aprobación humana: ${summary}`,
122
122
  waitingManual: "Esperando a que una persona intervenga a mano",
123
- savingToSources: (destination) => `Guardando un fichero en sources/${destination}`,
123
+ savingToSources: (destination) => `Guardando ${destination} en las fuentes`,
124
124
  checkingTime: "Consultando la fecha y la hora",
125
125
  calculatingDates: "Calculando fechas",
126
126
  reading: (file) => `Leyendo ${file}`,
@@ -70,12 +70,12 @@ export const fr = {
70
70
  keepPlanning: "Continuer à planifier",
71
71
  cancelPlan: "Annuler",
72
72
  planCommentQuestion: "Que faut-il changer ? (Entrée pour passer) : ",
73
- retireLines: (source, why, reason, replacedBy) => [
73
+ retireLines: (source, why, reason, replacedBy, folder) => [
74
74
  `Source : ${source}`,
75
75
  why === "wrong" ? "Motif : elle est erronée ; son résumé sera retiré" : "Motif : elle est remplacée ; son résumé sera marqué comme remplacé",
76
76
  `Raison : ${reason}`,
77
77
  ...(replacedBy ? [`Remplacée par : ${replacedBy}`] : []),
78
- "Elle est déplacée dans sources/.agent-kit/retired/, pas supprimée.",
78
+ `Elle est déplacée dans ${folder}.agent-kit/retired/, pas supprimée.`,
79
79
  ],
80
80
  toolPhrases: {
81
81
  Read: ["a lu {n} fichier", "a lu {n} fichiers"],
@@ -120,7 +120,7 @@ export const fr = {
120
120
  labels: {
121
121
  askingApproval: (summary) => `Demande d'approbation humaine : ${summary}`,
122
122
  waitingManual: "En attente d'une intervention manuelle",
123
- savingToSources: (destination) => `Enregistrement d'un fichier dans sources/${destination}`,
123
+ savingToSources: (destination) => `Enregistrement de ${destination} dans les sources`,
124
124
  checkingTime: "Consultation de la date et de l'heure",
125
125
  calculatingDates: "Calcul de dates",
126
126
  reading: (file) => `Lecture de ${file}`,
@@ -148,6 +148,11 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
148
148
  // name isn't known ahead of time.
149
149
  canUseTool: allowAnyMcpTool,
150
150
  disallowedTools: spec.disallowedTools ?? [],
151
+ // TodoWrite (todos.ts) only exists with CLAUDE_CODE_ENABLE_TASKS=0: by default the CLI
152
+ // offers its TaskCreate/TaskUpdate/TaskList/TaskGet tools instead, and drops TodoWrite from
153
+ // the session even when `tools` names it (confirmed empirically). `env` replaces the
154
+ // subprocess environment, so the rest is passed on as it is.
155
+ env: { ...process.env, CLAUDE_CODE_ENABLE_TASKS: "0" },
151
156
  // Real text streaming instead of silently waiting for the turn's complete message —
152
157
  // see the caller's message loop.
153
158
  includePartialMessages: true,
@@ -22,6 +22,7 @@ async function attempt(work) {
22
22
  }
23
23
  }
24
24
  const OVERVIEW = "overview";
25
+ const LOG = "log";
25
26
  /** The `knowledge` MCP server over `store`. */
26
27
  export function createKnowledgeServer(store, options) {
27
28
  const typeNames = store.types().map((t) => t.type);
@@ -49,9 +50,15 @@ export function createKnowledgeServer(store, options) {
49
50
  const hits = await store.search(args.query, args.limit ?? 10);
50
51
  return hits.length ? hits.map((hit) => `- ${hit.id} — ${hit.title}: ${hit.snippet}`).join("\n") : "No page matches.";
51
52
  }), { annotations: { readOnlyHint: true } });
52
- const read = tool("knowledge_read", 'A page by its id ("concept/bowline"), or "overview": its fields, its content (links to other pages as ids) and the pages that link to it.', { page: z.string().describe('The page id, or "overview"') }, async (args) => attempt(async () => {
53
+ const read = tool("knowledge_read", 'A page by its id ("concept/bowline"): its fields, its content (links to other pages as ids) and the pages that link to it. Or "overview" (the living synthesis of the whole knowledge base), or "log" (the latest entries of the operation log, newest first: what was done to the knowledge base, and when).', { page: z.string().describe('The page id, "overview" or "log"') }, async (args) => attempt(async () => {
53
54
  if (args.page === OVERVIEW)
54
55
  return (await store.overview()) || "There's no overview yet: write it with knowledge_rewrite (page \"overview\").";
56
+ if (args.page === LOG) {
57
+ if (!store.recentLog)
58
+ return "This knowledge base's store can't show its log.";
59
+ const entries = await store.recentLog(10);
60
+ return entries.length ? entries.join("\n\n") : "The log is empty: nothing has been logged yet.";
61
+ }
55
62
  const page = await store.read(args.page);
56
63
  if (!page)
57
64
  throw new Error(`There's no page "${args.page}". knowledge_search or knowledge_index finds the right one.`);
@@ -46,8 +46,6 @@ const DEFAULT_DESCRIPTION = "Copy a file from this run's own folder (e.g. someth
46
46
  "downloading a document you're about to study in depth, so later sessions have it too.";
47
47
  /** The most `download_to_sources` and `request_file` copy in. */
48
48
  export const MAX_SOURCE_BYTES = 50 * 1024 * 1024;
49
- const ok = (text) => ({ content: [{ type: "text", text }] });
50
- const fail = (text) => ({ content: [{ type: "text", text }], isError: true });
51
49
  const message = (error) => (error instanceof Error ? error.message : String(error));
52
50
  function added(result, extra = "") {
53
51
  return result.duplicateOf
@@ -116,6 +114,20 @@ export async function downloadToFile(url, file, maxBytes) {
116
114
  export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFAULT_DESCRIPTION, options = {}) {
117
115
  const projectDir = options.projectDir ?? path.dirname(sourcesDir);
118
116
  const maxBytes = options.maxBytes ?? MAX_SOURCE_BYTES;
117
+ // The texts say "sources/"; the model reads the folder's real name (e.g. "treasure/"),
118
+ // since it reads and searches the originals by that path. Paths it gives may start with
119
+ // either.
120
+ const folder = `${path.relative(projectDir, sourcesDir).split(path.sep).join("/") || "."}/`;
121
+ const named = (text) => text.replaceAll("sources/", folder);
122
+ const inFolder = (relative) => {
123
+ const clean = relative.replace(/\\/g, "/").replace(/^\.\//, "");
124
+ for (const prefix of [folder, "sources/"])
125
+ if (clean.startsWith(prefix))
126
+ return clean.slice(prefix.length);
127
+ return clean;
128
+ };
129
+ const ok = (text) => ({ content: [{ type: "text", text: named(text) }] });
130
+ const fail = (text) => ({ content: [{ type: "text", text: named(text) }], isError: true });
119
131
  const replaces = z.string().optional().describe("For a new version: the original it replaces, relative to sources/ (kept, never overwritten)");
120
132
  const saveToSources = tool("save_to_sources", description, {
121
133
  source: z.string().describe("Path to the file, relative to this run's own folder - not a URL"),
@@ -131,7 +143,7 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
131
143
  }
132
144
  try {
133
145
  const relative = path.relative(runDir, resolvedSource).split(path.sep).join("/");
134
- return ok(added(await addSource(sourcesDir, resolvedSource, args.destination, { kind: "run", from: relative }, { replaces: args.replaces })));
146
+ return ok(added(await addSource(sourcesDir, resolvedSource, inFolder(args.destination), { kind: "run", from: relative }, { replaces: args.replaces && inFolder(args.replaces) })));
135
147
  }
136
148
  catch (error) {
137
149
  return fail(message(error));
@@ -150,7 +162,7 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
150
162
  from: z.number().int().positive().optional().describe("First slide (PPTX)"),
151
163
  to: z.number().int().positive().optional().describe("Last slide (PPTX)"),
152
164
  }, async (args) => {
153
- const file = resolveWithin(sourcesDir, args.source);
165
+ const file = resolveWithin(sourcesDir, inFolder(args.source));
154
166
  if (!file || !(await stat(file).catch(() => null))?.isFile())
155
167
  return fail(`"${args.source}" isn't an original in sources/ (list_sources lists them).`);
156
168
  if (!EXTRACTABLE.has(path.extname(file).slice(1).toLowerCase()))
@@ -171,7 +183,7 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
171
183
  try {
172
184
  const type = await downloadToFile(args.url, temp, maxBytes);
173
185
  const name = decodeURIComponent(new URL(args.url).pathname.split("/").filter(Boolean).pop() ?? "download");
174
- const result = await addSource(sourcesDir, temp, args.destination, { kind: "url", from: args.url }, { replaces: args.replaces, name });
186
+ const result = await addSource(sourcesDir, temp, inFolder(args.destination), { kind: "url", from: args.url }, { replaces: args.replaces && inFolder(args.replaces), name });
175
187
  let extra = "";
176
188
  if (!result.duplicateOf && /html/i.test(type)) {
177
189
  const html = await readFile(temp, "utf8");
@@ -217,7 +229,7 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
217
229
  if (info.size > maxBytes)
218
230
  return fail(`"${answer}" is ${info.size} bytes, over the ${maxBytes}-byte limit.`);
219
231
  try {
220
- return ok(added(await addSource(sourcesDir, file, args.destination ?? path.basename(file), { kind: "person", from: path.basename(file) }, { replaces: args.replaces })));
232
+ return ok(added(await addSource(sourcesDir, file, args.destination ? inFolder(args.destination) : path.basename(file), { kind: "person", from: path.basename(file) }, { replaces: args.replaces && inFolder(args.replaces) })));
221
233
  }
222
234
  catch (error) {
223
235
  return fail(message(error));
@@ -229,11 +241,11 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
229
241
  reason: z.string().describe("Why, in one sentence, in the person's language"),
230
242
  replacedBy: z.string().optional().describe('For "replaced": the original that replaces it, relative to sources/'),
231
243
  }, async (args) => {
232
- const answer = await askForDecision(runDir, { title: t().retireTitle, lines: t().retireLines(args.source, args.why, args.reason, args.replacedBy) });
244
+ const answer = await askForDecision(runDir, { title: t().retireTitle, lines: t().retireLines(inFolder(args.source), args.why, args.reason, args.replacedBy && inFolder(args.replacedBy), folder) });
233
245
  if (answer !== "" && answer !== "y" && answer !== "yes")
234
246
  return ok(answer === "q" ? "The person stopped: don't retire it, and stop what you were doing." : "The person said no: the original stays.");
235
247
  try {
236
- const result = await retireSource(sourcesDir, options.knowledgeDir, projectDir, args.source, args.why, args.reason, args.replacedBy);
248
+ const result = await retireSource(sourcesDir, options.knowledgeDir, projectDir, inFolder(args.source), args.why, args.reason, args.replacedBy && inFolder(args.replacedBy));
237
249
  const pages = result.summaries.map((s) => `${s.page} marked ${s.marked}${s.supersededBy ? ` (see ${s.supersededBy})` : ""}`);
238
250
  return ok([
239
251
  `Retired: moved to sources/${result.movedTo}.`,
@@ -245,9 +257,11 @@ export function createSaveToSourcesServer(runDir, sourcesDir, description = DEFA
245
257
  return fail(message(error));
246
258
  }
247
259
  });
248
- return createSdkMcpServer({
249
- name: "sourceFiles",
250
- version: "1.0.0",
251
- tools: [saveToSources, listSourcesTool, extractTextTool, downloadToSources, ...(options.interactive ? [requestFile, retire] : [])],
252
- });
260
+ // The descriptions (the tool's and its parameters') with the folder's real name too.
261
+ const tools = [saveToSources, listSourcesTool, extractTextTool, downloadToSources, ...(options.interactive ? [requestFile, retire] : [])].map((definition) => ({
262
+ ...definition,
263
+ description: named(definition.description),
264
+ inputSchema: Object.fromEntries(Object.entries(definition.inputSchema).map(([key, schema]) => [key, schema.description ? schema.describe(named(schema.description)) : schema])),
265
+ }));
266
+ return createSdkMcpServer({ name: "sourceFiles", version: "1.0.0", tools: tools });
253
267
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The version of agent-kit in use, read from its own `package.json` (next to `dist/`, or
3
+ * `src/` under tsx), e.g. to show it in an agent's header or log.
4
+ */
5
+ export declare function agentKitVersion(): string;
@@ -0,0 +1,12 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ let cached;
5
+ /**
6
+ * The version of agent-kit in use, read from its own `package.json` (next to `dist/`, or
7
+ * `src/` under tsx), e.g. to show it in an agent's header or log.
8
+ */
9
+ export function agentKitVersion() {
10
+ cached ??= JSON.parse(readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json"), "utf8")).version;
11
+ return cached;
12
+ }
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export type { Mode, BaseSessionConfig, AgentSpec, PlanModeSpec } from "./core/ag
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";
13
+ export { agentKitVersion } from "./core/version.js";
13
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";
14
15
  export { createFileKnowledgeStore, type FileKnowledgeStoreOptions } from "./core/fileKnowledgeStore.js";
15
16
  export { createKnowledgeServer, type KnowledgeToolsOptions } from "./core/tools/knowledgeTools.js";
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ export { createFriendlyToolLabel, truncate, truncatePath, } from "./core/toolLab
11
11
  export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, togglePlanMode } from "./core/session.js";
12
12
  export { runQuery } from "./core/runner.js";
13
13
  export { knowledgePluginRoot, knowledgePromptSection } from "./core/knowledge.js";
14
+ export { agentKitVersion } from "./core/version.js";
14
15
  export { BUILT_IN_PAGE_TYPES, isSlug, parseId, linkedIds, } from "./core/knowledgeStore.js";
15
16
  export { createFileKnowledgeStore } from "./core/fileKnowledgeStore.js";
16
17
  export { createKnowledgeServer } from "./core/tools/knowledgeTools.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falkenslab/agent-kit",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Library for building an agent on top of the Claude Agent SDK",
5
5
  "author": "Francisco Vargas Ruiz",
6
6
  "license": "MIT",