@falkenslab/agent-kit 0.15.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.
Files changed (68) hide show
  1. package/README.md +3 -1
  2. package/assets/knowledge-plugin/.claude-plugin/plugin.json +1 -1
  3. package/assets/knowledge-plugin/commands/ingest.md +1 -1
  4. package/assets/knowledge-plugin/commands/lint.md +1 -1
  5. package/assets/knowledge-plugin/skills/knowledge-ingest/SKILL.md +13 -19
  6. package/assets/knowledge-plugin/skills/knowledge-lint/SKILL.md +18 -14
  7. package/assets/knowledge-plugin/skills/knowledge-query/SKILL.md +5 -5
  8. package/assets/knowledge-plugin-files/.claude-plugin/plugin.json +4 -0
  9. package/assets/knowledge-plugin-files/commands/ingest.md +7 -0
  10. package/assets/knowledge-plugin-files/commands/lint.md +5 -0
  11. package/assets/knowledge-plugin-files/commands/query.md +5 -0
  12. package/assets/knowledge-plugin-files/skills/knowledge-ingest/SKILL.md +46 -0
  13. package/assets/knowledge-plugin-files/skills/knowledge-lint/SKILL.md +26 -0
  14. package/assets/knowledge-plugin-files/skills/knowledge-query/SKILL.md +14 -0
  15. package/dist/core/agentSpec.d.ts +27 -2
  16. package/dist/core/extractText.d.ts +13 -0
  17. package/dist/core/extractText.js +147 -0
  18. package/dist/core/fileKnowledgeStore.d.ts +10 -0
  19. package/dist/core/fileKnowledgeStore.js +395 -0
  20. package/dist/core/hooks/fileScopeGate.d.ts +8 -0
  21. package/dist/core/hooks/fileScopeGate.js +20 -0
  22. package/dist/core/hooks/humanInput.d.ts +18 -0
  23. package/dist/core/hooks/humanInput.js +30 -2
  24. package/dist/core/hooks/planGate.js +14 -5
  25. package/dist/core/hooks/stepGate.js +6 -0
  26. package/dist/core/interaction.d.ts +20 -0
  27. package/dist/core/knowledge.d.ts +14 -4
  28. package/dist/core/knowledge.js +36 -6
  29. package/dist/core/knowledgeStore.d.ts +139 -0
  30. package/dist/core/knowledgeStore.js +84 -0
  31. package/dist/core/messages/de.js +65 -1
  32. package/dist/core/messages/en.d.ts +39 -0
  33. package/dist/core/messages/en.js +65 -1
  34. package/dist/core/messages/es.js +65 -1
  35. package/dist/core/messages/fr.js +65 -1
  36. package/dist/core/modeControl.d.ts +36 -0
  37. package/dist/core/modeControl.js +61 -0
  38. package/dist/core/session.d.ts +6 -31
  39. package/dist/core/session.js +81 -71
  40. package/dist/core/sources.d.ts +122 -0
  41. package/dist/core/sources.js +312 -0
  42. package/dist/core/todos.d.ts +23 -0
  43. package/dist/core/todos.js +33 -0
  44. package/dist/core/toolLabels.d.ts +1 -1
  45. package/dist/core/toolLabels.js +42 -2
  46. package/dist/core/tools/humanApproval.d.ts +9 -5
  47. package/dist/core/tools/humanApproval.js +39 -7
  48. package/dist/core/tools/knowledgeTools.d.ts +16 -0
  49. package/dist/core/tools/knowledgeTools.js +168 -0
  50. package/dist/core/tools/saveToSources.d.ts +27 -8
  51. package/dist/core/tools/saveToSources.js +217 -55
  52. package/dist/core/tools/time.d.ts +29 -0
  53. package/dist/core/tools/time.js +129 -0
  54. package/dist/core/version.d.ts +5 -0
  55. package/dist/core/version.js +12 -0
  56. package/dist/index.d.ts +7 -3
  57. package/dist/index.js +5 -1
  58. package/dist/tui/chatTui.js +14 -1
  59. package/dist/tui/consoleRenderer.js +16 -2
  60. package/dist/tui/ink/SessionView.js +67 -17
  61. package/dist/tui/ink/inkInteraction.d.ts +4 -2
  62. package/dist/tui/ink/inkInteraction.js +20 -2
  63. package/dist/tui/ink/runChatInk.js +5 -0
  64. package/dist/tui/ink/sessionModel.d.ts +3 -0
  65. package/dist/tui/ink/sessionModel.js +14 -1
  66. package/dist/tui/terminalInteraction.js +2 -0
  67. package/package.json +31 -1
  68. /package/assets/{knowledge-plugin → knowledge-plugin-files}/skills/knowledge-pages/SKILL.md +0 -0
@@ -54,6 +54,29 @@ export const fr = {
54
54
  parametersLine: (parameters) => `Paramètres : ${parameters}`,
55
55
  approvalTitle: "Confirmation humaine requise avant de publier",
56
56
  summaryLine: (summary) => `Résumé : ${summary}`,
57
+ noAnswer: "(pas de réponse)",
58
+ textAnswerHint: "tapez la réponse",
59
+ textAnswerKeys: "Entrée pour envoyer · Échap pour ne pas répondre",
60
+ fileRequestTitle: "L'agent demande un fichier",
61
+ fileRequestQuestion: "Chemin du fichier (glissez-le ici), ou Entrée si vous ne l'avez pas : ",
62
+ retireTitle: "Retirer une source ?",
63
+ retirePageTitle: "Retirer une page de la base de connaissances ?",
64
+ choiceQuestion: (multiple) => (multiple ? "Numéros séparés par des virgules, ou votre propre réponse : " : "Numéro, ou votre propre réponse : "),
65
+ otherOption: "Autre (tapez votre réponse)",
66
+ choiceKeys: (multiple) => (multiple ? "Espace pour cocher · Entrée pour envoyer" : "↑/↓ choisir · Entrée pour envoyer"),
67
+ questionTitle: "L'agent demande",
68
+ planTitle: "Le plan, pour votre approbation",
69
+ runPlan: "L'exécuter",
70
+ keepPlanning: "Continuer à planifier",
71
+ cancelPlan: "Annuler",
72
+ planCommentQuestion: "Que faut-il changer ? (Entrée pour passer) : ",
73
+ retireLines: (source, why, reason, replacedBy, folder) => [
74
+ `Source : ${source}`,
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
+ `Raison : ${reason}`,
77
+ ...(replacedBy ? [`Remplacée par : ${replacedBy}`] : []),
78
+ `Elle est déplacée dans ${folder}.agent-kit/retired/, pas supprimée.`,
79
+ ],
57
80
  toolPhrases: {
58
81
  Read: ["a lu {n} fichier", "a lu {n} fichiers"],
59
82
  Write: ["a écrit {n} fichier", "a écrit {n} fichiers"],
@@ -66,6 +89,26 @@ export const fr = {
66
89
  Skill: ["a utilisé {n} skill", "a utilisé {n} skills"],
67
90
  Agent: ["a lancé {n} sous-agent", "a lancé {n} sous-agents"],
68
91
  Task: ["a lancé {n} sous-agent", "a lancé {n} sous-agents"],
92
+ mcp__time__current_time: ["a consulté l'heure", "a consulté l'heure {n} fois"],
93
+ mcp__time__date_math: ["a calculé des dates", "a calculé des dates {n} fois"],
94
+ mcp__sourceFiles__list_sources: ["a examiné les sources", "a examiné les sources {n} fois"],
95
+ mcp__sourceFiles__extract_text: ["a lu {n} document", "a lu {n} documents"],
96
+ mcp__sourceFiles__save_to_sources: ["a enregistré {n} source", "a enregistré {n} sources"],
97
+ mcp__sourceFiles__download_to_sources: ["a téléchargé {n} source", "a téléchargé {n} sources"],
98
+ mcp__sourceFiles__request_file: ["a demandé {n} fichier", "a demandé {n} fichiers"],
99
+ mcp__sourceFiles__retire_source: ["a retiré {n} source", "a retiré {n} sources"],
100
+ mcp__knowledge__knowledge_index: ["a lu l'index", "a lu l'index {n} fois"],
101
+ mcp__knowledge__knowledge_search: ["a cherché dans la base", "a cherché dans la base {n} fois"],
102
+ mcp__knowledge__knowledge_read: ["a lu {n} page", "a lu {n} pages"],
103
+ mcp__knowledge__knowledge_create: ["a créé {n} page", "a créé {n} pages"],
104
+ mcp__knowledge__knowledge_edit: ["a modifié {n} page", "a modifié {n} pages"],
105
+ mcp__knowledge__knowledge_rewrite: ["a réécrit {n} page", "a réécrit {n} pages"],
106
+ mcp__knowledge__knowledge_supersede: ["a remplacé {n} page", "a remplacé {n} pages"],
107
+ mcp__knowledge__knowledge_retire: ["a retiré {n} page", "a retiré {n} pages"],
108
+ mcp__knowledge__knowledge_log: ["a mis à jour le journal", "a mis à jour le journal {n} fois"],
109
+ mcp__knowledge__knowledge_check: ["a vérifié la base", "a vérifié la base {n} fois"],
110
+ mcp__approvals__ask_human: ["a posé {n} question", "a posé {n} questions"],
111
+ mcp__approvals__present_plan: ["a présenté le plan", "a présenté le plan {n} fois"],
69
112
  },
70
113
  otherTools: ["a utilisé {n} outil", "a utilisé {n} outils"],
71
114
  noOutput: "(aucune sortie)",
@@ -73,10 +116,13 @@ export const fr = {
73
116
  toolFailed: "Impossible de terminer",
74
117
  moreResultLines: (count) => `(+${count} ${plural(count, "ligne", "lignes")})`,
75
118
  earlierCalls: (count) => `… ${count} ${plural(count, "précédent", "précédents")}`,
119
+ moreTodos: (count) => `… ${count} ${plural(count, "tâche", "tâches")} de plus`,
76
120
  labels: {
77
121
  askingApproval: (summary) => `Demande d'approbation humaine : ${summary}`,
78
122
  waitingManual: "En attente d'une intervention manuelle",
79
- savingToSources: (destination) => `Enregistrement d'un fichier dans sources/${destination}`,
123
+ savingToSources: (destination) => `Enregistrement de ${destination} dans les sources`,
124
+ checkingTime: "Consultation de la date et de l'heure",
125
+ calculatingDates: "Calcul de dates",
80
126
  reading: (file) => `Lecture de ${file}`,
81
127
  writing: (file) => `Écriture dans ${file}`,
82
128
  editing: (file) => `Modification de ${file}`,
@@ -92,6 +138,24 @@ export const fr = {
92
138
  searchingWeb: (query) => `Recherche sur le web de "${query}"`,
93
139
  applyingSkill: (skill) => `Application de la skill "${skill}"`,
94
140
  aSkill: "une skill",
141
+ updatingTasks: "Mise à jour de la liste des tâches",
142
+ listingSources: "Examen des sources",
143
+ extractingText: (source) => `Lecture de ${source}`,
144
+ downloading: (url) => `Téléchargement de ${url} dans les sources`,
145
+ requestingFile: (description) => `Demande d'un fichier : ${description}`,
146
+ retiringSource: (source) => `Retrait de la source ${source}`,
147
+ knowledgeIndex: "Lecture de l'index de la base de connaissances",
148
+ knowledgeSearch: (query) => `Recherche de "${query}" dans la base de connaissances`,
149
+ knowledgeRead: (page) => `Lecture de ${page}`,
150
+ knowledgeCreate: (page) => `Création de ${page}`,
151
+ knowledgeEdit: (page) => `Modification de ${page}`,
152
+ knowledgeRewrite: (page) => `Réécriture de ${page}`,
153
+ knowledgeSupersede: (page) => `${page} marquée comme remplacée`,
154
+ knowledgeRetire: (page) => `Retrait de ${page}`,
155
+ knowledgeLog: "Mise à jour du journal de la base de connaissances",
156
+ knowledgeCheck: "Vérification de la base de connaissances",
157
+ askingHuman: (question) => `Question : ${question}`,
158
+ presentingPlan: "Présentation du plan",
95
159
  },
96
160
  auth: {
97
161
  noToken: "Aucun jeton d'authentification Claude n'a été trouvé",
@@ -0,0 +1,36 @@
1
+ import type { Mode } from "./agentSpec.js";
2
+ /**
3
+ * The supervision mode of a running session. "guided", "interactive" and "plan" switch into
4
+ * one another: they share the same tools (the approval tool), and the step gate and the plan
5
+ * gate are registered in all three, deciding only in their own mode. "autonomous" has no
6
+ * approval tool at all, and a tool can't appear or vanish mid-session, so a session that
7
+ * starts autonomous stays autonomous, and one that doesn't can't become autonomous. The
8
+ * system prompt keeps the mode it was built with, so entering or leaving plan mode is told
9
+ * to the model with the next message instead (`takeNotice()`).
10
+ */
11
+ export interface ModeControl {
12
+ readonly mode: Mode;
13
+ /** The modes this session can be in, in Shift+Tab's order; fewer than two means it can't switch. */
14
+ readonly switchable: readonly Mode[];
15
+ /** Switches to `next` if this session allows it; returns whether it did. */
16
+ set(next: Mode): boolean;
17
+ /**
18
+ * The note telling the model it entered or left plan mode, once per change it hasn't been
19
+ * told about yet; `undefined` otherwise. `createInputQueue({ modeControl })` puts it before
20
+ * the next message; a caller with its own queue prepends it itself.
21
+ */
22
+ takeNotice?(): string | undefined;
23
+ /**
24
+ * Calls `listener` with the new mode whenever it changes (Shift+Tab, `/plan`, or the agent
25
+ * leaving plan mode through `present_plan`), so a UI can show it; returns the unsubscribe.
26
+ */
27
+ subscribe?(listener: (mode: Mode) => void): () => void;
28
+ }
29
+ /** A `ModeControl` starting at `initial` (`buildSessionOptions()` returns one for its session). */
30
+ export declare function createModeControl(initial: Mode): ModeControl;
31
+ /**
32
+ * What the chats' `/plan` does: switches `control` into plan mode, or, when it's already
33
+ * there, back to the mode it was entered from ("guided" if it started in plan mode). Returns
34
+ * the new mode, or `null` when this session can't be in plan mode (an autonomous one).
35
+ */
36
+ export declare function togglePlanMode(control: ModeControl): Mode | null;
@@ -0,0 +1,61 @@
1
+ /** A `ModeControl` starting at `initial` (`buildSessionOptions()` returns one for its session). */
2
+ export function createModeControl(initial) {
3
+ let current = initial;
4
+ // The mode the model was last told about: a session starting in plan mode tells it with
5
+ // the first message, since the system prompt doesn't say.
6
+ let told = initial === "plan" ? undefined : initial;
7
+ const switchable = initial === "autonomous" ? ["autonomous"] : ["guided", "interactive", "plan"];
8
+ const listeners = new Set();
9
+ return {
10
+ get mode() {
11
+ return current;
12
+ },
13
+ switchable,
14
+ set(next) {
15
+ if (!switchable.includes(next))
16
+ return false;
17
+ const changed = next !== current;
18
+ current = next;
19
+ if (changed)
20
+ for (const listener of listeners)
21
+ listener(next);
22
+ return true;
23
+ },
24
+ takeNotice() {
25
+ const before = told;
26
+ told = current;
27
+ if (current === "plan" && before !== "plan")
28
+ return PLAN_MODE_ENTERED;
29
+ if (current !== "plan" && before === "plan")
30
+ return PLAN_MODE_LEFT;
31
+ return undefined;
32
+ },
33
+ subscribe(listener) {
34
+ listeners.add(listener);
35
+ return () => listeners.delete(listener);
36
+ },
37
+ };
38
+ }
39
+ // The mode each session was in when `/plan` took it into plan mode, to go back to.
40
+ const modeBeforePlan = new WeakMap();
41
+ /**
42
+ * What the chats' `/plan` does: switches `control` into plan mode, or, when it's already
43
+ * there, back to the mode it was entered from ("guided" if it started in plan mode). Returns
44
+ * the new mode, or `null` when this session can't be in plan mode (an autonomous one).
45
+ */
46
+ export function togglePlanMode(control) {
47
+ if (!control.switchable.includes("plan"))
48
+ return null;
49
+ if (control.mode === "plan") {
50
+ const back = modeBeforePlan.get(control) ?? "guided";
51
+ control.set(back);
52
+ return back;
53
+ }
54
+ modeBeforePlan.set(control, control.mode);
55
+ control.set("plan");
56
+ return "plan";
57
+ }
58
+ // What the model reads on a plan-mode switch, in English like the rest of what it reads
59
+ // (ADR-019); tagged so a resumed conversation doesn't show it as the human's words (runs.ts).
60
+ const PLAN_MODE_ENTERED = "<system-reminder>The user switched to plan mode. Only read, research and plan: nothing can be changed (files, tools that modify something) until they switch out of it. When the plan is ready, present it and wait for them.</system-reminder>";
61
+ const PLAN_MODE_LEFT = "<system-reminder>The user left plan mode: you can carry out the plan now.</system-reminder>";
@@ -1,6 +1,8 @@
1
1
  import type { Options, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { type TranscriptLogger } from "./hooks/transcriptLogger.js";
3
- import type { AgentSpec, BaseSessionConfig, Mode } from "./agentSpec.js";
3
+ import { type ModeControl } from "./modeControl.js";
4
+ import type { KnowledgeStore } from "./knowledgeStore.js";
5
+ import type { AgentSpec, BaseSessionConfig } from "./agentSpec.js";
4
6
  import { type Language } from "./language.js";
5
7
  import { type RunFolder } from "./runs.js";
6
8
  /**
@@ -31,37 +33,10 @@ export declare function buildSessionOptions<TConfig extends BaseSessionConfig>(c
31
33
  transcriptPath: string;
32
34
  modeControl: ModeControl;
33
35
  language: Language;
36
+ /** The knowledge base's store, when the agent reaches it through the `knowledge_*` tools. */
37
+ knowledgeStore?: KnowledgeStore;
34
38
  }>;
35
- /**
36
- * The supervision mode of a running session. "guided", "interactive" and "plan" switch into
37
- * one another: they share the same tools (the approval tool), and the step gate and the plan
38
- * gate are registered in all three, deciding only in their own mode. "autonomous" has no
39
- * approval tool at all, and a tool can't appear or vanish mid-session, so a session that
40
- * starts autonomous stays autonomous, and one that doesn't can't become autonomous. The
41
- * system prompt keeps the mode it was built with, so entering or leaving plan mode is told
42
- * to the model with the next message instead (`takeNotice()`).
43
- */
44
- export interface ModeControl {
45
- readonly mode: Mode;
46
- /** The modes this session can be in, in Shift+Tab's order; fewer than two means it can't switch. */
47
- readonly switchable: readonly Mode[];
48
- /** Switches to `next` if this session allows it; returns whether it did. */
49
- set(next: Mode): boolean;
50
- /**
51
- * The note telling the model it entered or left plan mode, once per change it hasn't been
52
- * told about yet; `undefined` otherwise. `createInputQueue({ modeControl })` puts it before
53
- * the next message; a caller with its own queue prepends it itself.
54
- */
55
- takeNotice?(): string | undefined;
56
- }
57
- /** A `ModeControl` starting at `initial` (`buildSessionOptions()` returns one for its session). */
58
- export declare function createModeControl(initial: Mode): ModeControl;
59
- /**
60
- * What the chats' `/plan` does: switches `control` into plan mode, or, when it's already
61
- * there, back to the mode it was entered from ("guided" if it started in plan mode). Returns
62
- * the new mode, or `null` when this session can't be in plan mode (an autonomous one).
63
- */
64
- export declare function togglePlanMode(control: ModeControl): Mode | null;
39
+ export { createModeControl, togglePlanMode, type ModeControl } from "./modeControl.js";
65
40
  /**
66
41
  * User message queue backed by a single long-lived generator, for any multi-turn
67
42
  * (chat-shaped) caller of `query()`.
@@ -9,6 +9,11 @@ import { createPlanGate } from "./hooks/planGate.js";
9
9
  import { createHumanApprovalServer } from "./tools/humanApproval.js";
10
10
  import { createManualLoginServer } from "./tools/manualLogin.js";
11
11
  import { createSaveToSourcesServer } from "./tools/saveToSources.js";
12
+ import { createTimeServer } from "./tools/time.js";
13
+ import { TODO_TOOL } from "./todos.js";
14
+ import { createModeControl } from "./modeControl.js";
15
+ import { createFileKnowledgeStore } from "./fileKnowledgeStore.js";
16
+ import { createKnowledgeServer } from "./tools/knowledgeTools.js";
12
17
  import { allowAnyMcpTool } from "./mcpPermissions.js";
13
18
  import { knowledgePluginRoot, knowledgePromptSection } from "./knowledge.js";
14
19
  import { replyLanguageInstruction } from "./language.js";
@@ -53,24 +58,38 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
53
58
  // (`knowledgeDir`) or for original files (`sourcesDir`) — there is no separate "context"
54
59
  // folder: material the user provides is just more of `sourcesDir`.
55
60
  const includeFileTools = Boolean(config.knowledgeDir || config.sourcesDir);
56
- const fileTools = includeFileTools ? ["Read", "Write", "Edit", "Glob", "Grep"] : [];
61
+ // The built-in knowledge base (rules in the system prompt + its skills/commands as a plugin) needs
62
+ // somewhere to keep the wiki, so it follows `knowledgeDir`; `spec.knowledgeBase: false` opts out.
63
+ const includeKnowledgeBase = Boolean(config.knowledgeDir) && spec.knowledgeBase !== false;
64
+ // By default the agent reaches it only through the `knowledge_*` tools, over a store, never
65
+ // with the file tools (ADR-024); `spec.knowledgeTools: "files"` keeps the file tools on it.
66
+ const knowledgeViaStore = includeKnowledgeBase && (spec.knowledgeTools ?? "store") === "store";
67
+ const notesDir = knowledgeViaStore ? undefined : config.knowledgeDir;
68
+ const extraDirs = config.extraWritableDirs ?? [];
69
+ // With the store, the file tools are only for the originals (reading) and the extra writable
70
+ // folders; without either, none.
71
+ const fileTools = !includeFileTools
72
+ ? []
73
+ : !knowledgeViaStore
74
+ ? ["Read", "Write", "Edit", "Glob", "Grep"]
75
+ : [...(config.sourcesDir || extraDirs.length ? ["Read", "Glob", "Grep"] : []), ...(extraDirs.length ? ["Write", "Edit"] : [])];
57
76
  // Where Write/Edit may act and Grep may search (see hooks/fileScopeGate.ts): the
58
77
  // project's own folders, never the whole cwd — which would include whatever else the
59
78
  // project directory holds (the user's config...). `sourcesDir` is searchable but
60
79
  // deliberately NOT writable: originals stay as obtained, and the only way to add to it is
61
80
  // the `save_to_sources` tool (which never overwrites).
62
- const writableDirs = [config.knowledgeDir, ...(config.extraWritableDirs ?? [])].filter((d) => Boolean(d));
63
- const searchableDirs = [config.knowledgeDir, config.sourcesDir, ...(config.extraWritableDirs ?? [])].filter((d) => Boolean(d));
81
+ const writableDirs = [notesDir, ...extraDirs].filter((d) => Boolean(d));
82
+ const searchableDirs = [notesDir, config.sourcesDir, ...extraDirs].filter((d) => Boolean(d));
64
83
  const readOnlyDirs = config.sourcesDir ? [config.sourcesDir] : [];
65
84
  // Skills/commands/plugins are a separate concern from the file tools: an agent that
66
85
  // wants a plugin-provided skill/command but has no notes or sources folder shouldn't have
67
86
  // to invent one just to get `cwd`/`skills: "all"`/`plugins` wired up. Gated on either
68
87
  // signal, not on `pluginRoots` alone, so an agent with file tools keeps getting the SDK's
69
88
  // own project-level `.claude/skills`/`.claude/commands` discovery.
70
- // The built-in knowledge base (rules in the system prompt + its skills/commands as a plugin) needs
71
- // somewhere to keep the wiki, so it follows `knowledgeDir`; `spec.knowledgeBase: false` opts out.
72
- const includeKnowledgeBase = Boolean(config.knowledgeDir) && spec.knowledgeBase !== false;
73
- const pluginRoots = [...spec.pluginRoots(config), ...(includeKnowledgeBase ? [knowledgePluginRoot()] : [])];
89
+ const pluginRoots = [...spec.pluginRoots(config), ...(includeKnowledgeBase ? [knowledgePluginRoot(knowledgeViaStore ? "tools" : "files")] : [])];
90
+ const knowledgeStore = knowledgeViaStore && config.knowledgeDir
91
+ ? (spec.knowledgeStore?.(config) ?? createFileKnowledgeStore(config.knowledgeDir, { pageTypes: spec.knowledgePageTypes, sourcesDir: config.sourcesDir }))
92
+ : undefined;
74
93
  const includeSkillsAndPlugins = includeFileTools || pluginRoots.length > 0;
75
94
  const skillTools = includeSkillsAndPlugins ? ["Skill"] : [];
76
95
  // Whatever opt-in subagents `spec` wants for this config, or undefined if none apply.
@@ -97,7 +116,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
97
116
  const transcriptLogger = createTranscriptLogger(transcriptPath, config.secrets ?? []);
98
117
  const sdkOptions = {
99
118
  systemPrompt: withReplyLine(includeKnowledgeBase && config.knowledgeDir
100
- ? `${spec.buildSystemPrompt(config)}\n\n${knowledgePromptSection(config.projectDir, config.knowledgeDir, config.sourcesDir)}`
119
+ ? `${spec.buildSystemPrompt(config)}\n\n${knowledgePromptSection(config.projectDir, config.knowledgeDir, config.sourcesDir, { tools: knowledgeViaStore, pageTypes: knowledgeStore?.types() })}`
101
120
  : spec.buildSystemPrompt(config)),
102
121
  // Not the SDK's default (every source): "user" hands the agent the runner's own Claude
103
122
  // Code configuration, and its `language` setting outranked the agent's prompt.
@@ -118,8 +137,10 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
118
137
  // "Skill" has to be listed explicitly: with an explicit `tools` list the SDK loads the
119
138
  // skills (they show up in the init message) but doesn't offer the tool to invoke them,
120
139
  // so the model ends up looking for SKILL.md files by hand (confirmed empirically).
121
- tools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", ...(includeSubagentTools ? ["Agent", "Bash"] : [])],
122
- allowedTools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", ...(includeSubagentTools ? ["Agent", "Bash"] : [])],
140
+ // TodoWrite: the SDK's task list for long jobs, in every session (the chats show the list
141
+ // instead of the calls; see todos.ts). Accepted in an explicit list (confirmed empirically).
142
+ tools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", TODO_TOOL, ...(includeSubagentTools ? ["Agent", "Bash"] : [])],
143
+ allowedTools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", TODO_TOOL, ...(includeSubagentTools ? ["Agent", "Bash"] : [])],
123
144
  // Every mcp__* tool (whatever spec.buildMcpServers() registers, approvals/manualLogin
124
145
  // when enabled below, sourceFiles, and any server a project's own .mcp.json
125
146
  // declares) is approved generically here rather than listed one by one — see
@@ -127,6 +148,11 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
127
148
  // name isn't known ahead of time.
128
149
  canUseTool: allowAnyMcpTool,
129
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" },
130
156
  // Real text streaming instead of silently waiting for the turn's complete message —
131
157
  // see the caller's message loop.
132
158
  includePartialMessages: true,
@@ -145,7 +171,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
145
171
  // SDK's own official skills (pdf/docx), the project's own custom ones, and the
146
172
  // plugin-provided built-ins below. A spec's own list gets the knowledge base's
147
173
  // skills added, so it only names its own.
148
- skills: skillList(spec.skills, includeKnowledgeBase),
174
+ skills: skillList(spec.skills, includeKnowledgeBase ? (knowledgeViaStore ? "tools" : "files") : null),
149
175
  plugins: pluginRoots.map((pluginPath) => ({ type: "local", path: pluginPath, skipMcpDiscovery: true })),
150
176
  }
151
177
  : {}),
@@ -154,15 +180,47 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
154
180
  ...(subagentDefinitions ? { agents: subagentDefinitions } : {}),
155
181
  mcpServers: {
156
182
  ...spec.buildMcpServers(config, runDir),
157
- ...(includeApprovalTool ? { approvals: createHumanApprovalServer(runDir, spec.humanApprovalTexts) } : {}),
183
+ ...(includeApprovalTool ? { approvals: createHumanApprovalServer(runDir, spec.humanApprovalTexts, { modeControl }) } : {}),
158
184
  ...(manualInterventionTexts ? { manualLogin: createManualLoginServer(runDir, manualInterventionTexts) } : {}),
159
- ...(config.sourcesDir ? { sourceFiles: createSaveToSourcesServer(runDir, config.sourcesDir, spec.saveToSourcesDescription) } : {}),
185
+ // The sources folder's tools (tools/saveToSources.ts); asking a person (request_file,
186
+ // retire_source) only where there is one.
187
+ ...(config.sourcesDir
188
+ ? {
189
+ sourceFiles: createSaveToSourcesServer(runDir, config.sourcesDir, spec.saveToSourcesDescription, {
190
+ knowledgeDir: config.knowledgeDir,
191
+ projectDir: config.projectDir,
192
+ interactive: mode !== "autonomous",
193
+ }),
194
+ }
195
+ : {}),
196
+ // The knowledge base's own tools, over its store (tools/knowledgeTools.ts, ADR-024).
197
+ ...(knowledgeStore
198
+ ? {
199
+ knowledge: createKnowledgeServer(knowledgeStore, {
200
+ runDir,
201
+ interactive: mode !== "autonomous",
202
+ sourcesDir: config.sourcesDir,
203
+ projectDir: config.projectDir,
204
+ knowledgeDir: config.knowledgeDir,
205
+ }),
206
+ }
207
+ : {}),
208
+ // The date and date arithmetic, in every session and mode: only reads (tools/time.ts).
209
+ time: createTimeServer(config.timeZone),
160
210
  },
161
211
  hooks: {
162
212
  PreToolUse: [
163
213
  { hooks: [transcriptLogger.preToolUse] },
164
214
  ...(includeFileTools
165
- ? [{ hooks: [createFileScopeGate({ projectDir: config.projectDir, writableDirs, searchableDirs, readOnlyDirs, deniedPaths: config.deniedPaths ?? [] })] }]
215
+ ? [{ hooks: [createFileScopeGate({
216
+ projectDir: config.projectDir,
217
+ writableDirs,
218
+ searchableDirs,
219
+ readOnlyDirs,
220
+ deniedPaths: config.deniedPaths ?? [],
221
+ ...(knowledgeViaStore && config.knowledgeDir ? { toolOnlyDirs: [{ dir: config.knowledgeDir, instead: "the knowledge_* tools" }] } : {}),
222
+ }),
223
+ ] }]
166
224
  : []),
167
225
  // Registered wherever the approval tool is, and asking only while the current mode is
168
226
  // "interactive": a guided session can switch to interactive and back (ModeControl).
@@ -193,67 +251,19 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
193
251
  PostToolUse: [{ hooks: [transcriptLogger.postToolUse] }],
194
252
  },
195
253
  };
196
- return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language };
254
+ return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language, ...(knowledgeStore ? { knowledgeStore } : {}) };
197
255
  }
198
- /** The knowledge plugin's skills, as the SDK names a plugin's skills ("plugin:skill"). */
199
- const KNOWLEDGE_SKILLS = ["knowledge-pages", "knowledge-ingest", "knowledge-query", "knowledge-lint"].map((skill) => `knowledge:${skill}`);
200
- function skillList(skills, includeKnowledgeBase) {
256
+ export { createModeControl, togglePlanMode } from "./modeControl.js";
257
+ /** The knowledge plugin's skills, as the SDK names a plugin's skills ("plugin:skill"): the tools variant has no page templates skill (knowledge_create gives them). */
258
+ const KNOWLEDGE_SKILLS = {
259
+ tools: ["knowledge-ingest", "knowledge-query", "knowledge-lint"].map((skill) => `knowledge:${skill}`),
260
+ files: ["knowledge-pages", "knowledge-ingest", "knowledge-query", "knowledge-lint"].map((skill) => `knowledge:${skill}`),
261
+ };
262
+ function skillList(skills, knowledgeVariant) {
201
263
  if (skills === undefined || skills === "all")
202
264
  return "all";
203
- return includeKnowledgeBase ? [...new Set([...skills, ...KNOWLEDGE_SKILLS])] : skills;
204
- }
205
- /** A `ModeControl` starting at `initial` (`buildSessionOptions()` returns one for its session). */
206
- export function createModeControl(initial) {
207
- let current = initial;
208
- // The mode the model was last told about: a session starting in plan mode tells it with
209
- // the first message, since the system prompt doesn't say.
210
- let told = initial === "plan" ? undefined : initial;
211
- const switchable = initial === "autonomous" ? ["autonomous"] : ["guided", "interactive", "plan"];
212
- return {
213
- get mode() {
214
- return current;
215
- },
216
- switchable,
217
- set(next) {
218
- if (!switchable.includes(next))
219
- return false;
220
- current = next;
221
- return true;
222
- },
223
- takeNotice() {
224
- const before = told;
225
- told = current;
226
- if (current === "plan" && before !== "plan")
227
- return PLAN_MODE_ENTERED;
228
- if (current !== "plan" && before === "plan")
229
- return PLAN_MODE_LEFT;
230
- return undefined;
231
- },
232
- };
233
- }
234
- // The mode each session was in when `/plan` took it into plan mode, to go back to.
235
- const modeBeforePlan = new WeakMap();
236
- /**
237
- * What the chats' `/plan` does: switches `control` into plan mode, or, when it's already
238
- * there, back to the mode it was entered from ("guided" if it started in plan mode). Returns
239
- * the new mode, or `null` when this session can't be in plan mode (an autonomous one).
240
- */
241
- export function togglePlanMode(control) {
242
- if (!control.switchable.includes("plan"))
243
- return null;
244
- if (control.mode === "plan") {
245
- const back = modeBeforePlan.get(control) ?? "guided";
246
- control.set(back);
247
- return back;
248
- }
249
- modeBeforePlan.set(control, control.mode);
250
- control.set("plan");
251
- return "plan";
265
+ return knowledgeVariant ? [...new Set([...skills, ...KNOWLEDGE_SKILLS[knowledgeVariant]])] : skills;
252
266
  }
253
- // What the model reads on a plan-mode switch, in English like the rest of what it reads
254
- // (ADR-019); tagged so a resumed conversation doesn't show it as the human's words (runs.ts).
255
- const PLAN_MODE_ENTERED = "<system-reminder>The user switched to plan mode. Only read, research and plan: nothing can be changed (files, tools that modify something) until they switch out of it. When the plan is ready, present it and wait for them.</system-reminder>";
256
- const PLAN_MODE_LEFT = "<system-reminder>The user left plan mode: you can carry out the plan now.</system-reminder>";
257
267
  /**
258
268
  * User message queue backed by a single long-lived generator, for any multi-turn
259
269
  * (chat-shaped) caller of `query()`.
@@ -0,0 +1,122 @@
1
+ /**
2
+ * The sources folder's bookkeeping: originals stay files (ADR-024), and the kit keeps a
3
+ * manifest next to them (`.agent-kit/sources.json`, inside `sourcesDir`, where the agent's
4
+ * file tools can't write) with each original's hash, size and provenance, the version it
5
+ * replaces, and the hash it had when a summary page first pointed to it. From that it tells
6
+ * new, ingested, changed and missing originals apart, refuses duplicates, and retires an
7
+ * original into `.agent-kit/retired/` instead of deleting it.
8
+ */
9
+ export declare const KIT_DIR = ".agent-kit";
10
+ /** Where an original came from. */
11
+ export type SourceOrigin = {
12
+ kind: "run";
13
+ from: string;
14
+ } | {
15
+ kind: "url";
16
+ from: string;
17
+ } | {
18
+ kind: "person";
19
+ from: string;
20
+ }
21
+ /** Copied in by hand: first seen by `listSources()`. */
22
+ | {
23
+ kind: "manual";
24
+ };
25
+ export interface SourceEntry {
26
+ hash: string;
27
+ size: number;
28
+ /** For hashing a file again only when it changed. */
29
+ mtimeMs: number;
30
+ addedAt: string;
31
+ origin: SourceOrigin;
32
+ /** The original this one is a new version of (a path relative to `sourcesDir`). */
33
+ replaces?: string;
34
+ /** The page this one was generated from (a web page's markdown, from its HTML). */
35
+ derivedFrom?: string;
36
+ /** The hash when a summary page first pointed to it: a different hash now means it changed after its ingest. */
37
+ ingestedHash?: string;
38
+ }
39
+ export interface RetiredSource {
40
+ path: string;
41
+ movedTo: string;
42
+ retiredAt: string;
43
+ why: "wrong" | "replaced";
44
+ reason: string;
45
+ replacedBy?: string;
46
+ entry?: SourceEntry;
47
+ }
48
+ export interface SourceManifest {
49
+ version: 1;
50
+ files: Record<string, SourceEntry>;
51
+ retired: RetiredSource[];
52
+ }
53
+ export type SourceStatus = "new" | "ingested" | "changed" | "missing" | "present";
54
+ export interface SourceListing {
55
+ path: string;
56
+ status: SourceStatus;
57
+ type: string;
58
+ size?: number;
59
+ pages?: number;
60
+ slides?: number;
61
+ origin?: SourceOrigin;
62
+ replaces?: string;
63
+ /** The summary pages that point to it (paths relative to the project). */
64
+ summaries: string[];
65
+ }
66
+ export declare function hashFile(file: string): Promise<string>;
67
+ /** Resolves `candidate` against `base`; the absolute path only if it stays inside it. */
68
+ export declare function resolveWithin(base: string, candidate: string): string | undefined;
69
+ export declare function loadManifest(sourcesDir: string): Promise<SourceManifest>;
70
+ export declare function saveManifest(sourcesDir: string, manifest: SourceManifest): Promise<void>;
71
+ /** Every original under `sourcesDir` (paths relative to it), leaving out hidden files and folders (the kit's own among them). */
72
+ export declare function scanSources(sourcesDir: string): Promise<string[]>;
73
+ /** The summary pages (knowledge base layout: `summaries/*.md`) by the absolute path of the original their frontmatter `file:` points to. */
74
+ export declare function summariesByOriginal(knowledgeDir: string): Promise<Map<string, string[]>>;
75
+ /** Pages of a PDF, read from its page tree without a parser; undefined when it can't tell (e.g. compressed object streams). */
76
+ export declare function pdfPageCount(bytes: Buffer): number | undefined;
77
+ /** Slides of a PPTX, from the file names in the zip (stored uncompressed). */
78
+ export declare function pptxSlideCount(bytes: Buffer): number | undefined;
79
+ /**
80
+ * Every original with its status, recording in the manifest the ones copied in by hand and,
81
+ * the first time a summary page points to one, its hash at ingest. Without a knowledge
82
+ * folder there's no ingest to compare with: originals are "present".
83
+ */
84
+ export declare function listSources(sourcesDir: string, knowledgeDir: string | undefined, projectDir: string): Promise<SourceListing[]>;
85
+ export interface AddedSource {
86
+ /** Where it is, relative to `sourcesDir`. */
87
+ path: string;
88
+ /** An identical file was already there: nothing was copied. */
89
+ duplicateOf?: string;
90
+ }
91
+ /**
92
+ * Adds `file` (an absolute path) to `sourcesDir` at `destination` (a folder: under `extra.name`,
93
+ * or the file's own name): never overwriting, and
94
+ * not at all if an identical file is already there (its path is returned). `replaces`
95
+ * records it as a new version of an original already in the folder.
96
+ */
97
+ export declare function addSource(sourcesDir: string, file: string, destination: string, origin: SourceOrigin, extra?: {
98
+ replaces?: string;
99
+ derivedFrom?: string;
100
+ name?: string;
101
+ }): Promise<AddedSource>;
102
+ /** What retiring an original did to its summary pages (paths relative to the project). */
103
+ export interface RetireResult {
104
+ movedTo: string;
105
+ summaries: {
106
+ page: string;
107
+ marked: "retired" | "superseded";
108
+ supersededBy?: string;
109
+ }[];
110
+ }
111
+ /**
112
+ * Moves an original to `.agent-kit/retired/` (never deleted: the person empties that folder)
113
+ * and records why. Its summary pages are marked: retired if the original was wrong (what it
114
+ * taught was wrong), superseded if it was replaced, pointing to the replacement's summary
115
+ * when there is one.
116
+ */
117
+ export declare function retireSource(sourcesDir: string, knowledgeDir: string | undefined, projectDir: string, source: string, why: "wrong" | "replaced", reason: string, replacedBy?: string): Promise<RetireResult>;
118
+ /**
119
+ * Records `source` (relative to `sourcesDir`) as ingested now, with its current hash: a
120
+ * summary page was just written from it, so "changed" means changed after this.
121
+ */
122
+ export declare function recordIngest(sourcesDir: string, source: string): Promise<void>;