@falkenslab/agent-kit 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/assets/agent-help/SKILL.md +1 -1
  2. package/assets/knowledge-plugin/commands/ingest.md +1 -1
  3. package/assets/knowledge-plugin/skills/knowledge-ingest/SKILL.md +8 -3
  4. package/assets/knowledge-plugin/skills/knowledge-lint/SKILL.md +12 -5
  5. package/dist/core/agentSpec.d.ts +16 -16
  6. package/dist/core/fileKnowledgeStore.d.ts +1 -3
  7. package/dist/core/fileKnowledgeStore.js +23 -29
  8. package/dist/core/hooks/fileScopeGate.d.ts +17 -2
  9. package/dist/core/hooks/fileScopeGate.js +45 -10
  10. package/dist/core/knowledge.d.ts +20 -17
  11. package/dist/core/knowledge.js +32 -60
  12. package/dist/core/knowledgeStore.d.ts +4 -9
  13. package/dist/core/knowledgeStore.js +16 -2
  14. package/dist/core/language.js +1 -1
  15. package/dist/core/messages/de.js +1 -1
  16. package/dist/core/messages/en.js +1 -1
  17. package/dist/core/messages/es.js +1 -1
  18. package/dist/core/messages/fr.js +1 -1
  19. package/dist/core/session.d.ts +12 -0
  20. package/dist/core/session.js +88 -33
  21. package/dist/core/sources.d.ts +24 -31
  22. package/dist/core/sources.js +45 -108
  23. package/dist/core/tools/humanApproval.js +3 -3
  24. package/dist/core/tools/knowledgeTools.d.ts +6 -6
  25. package/dist/core/tools/knowledgeTools.js +18 -53
  26. package/dist/core/tools/saveToSources.d.ts +6 -2
  27. package/dist/core/tools/saveToSources.js +17 -12
  28. package/dist/index.d.ts +1 -1
  29. package/dist/index.js +1 -1
  30. package/package.json +1 -1
  31. package/assets/knowledge-plugin-files/.claude-plugin/plugin.json +0 -4
  32. package/assets/knowledge-plugin-files/commands/ingest.md +0 -7
  33. package/assets/knowledge-plugin-files/commands/lint.md +0 -5
  34. package/assets/knowledge-plugin-files/commands/query.md +0 -5
  35. package/assets/knowledge-plugin-files/skills/knowledge-ingest/SKILL.md +0 -46
  36. package/assets/knowledge-plugin-files/skills/knowledge-lint/SKILL.md +0 -26
  37. package/assets/knowledge-plugin-files/skills/knowledge-pages/SKILL.md +0 -137
  38. package/assets/knowledge-plugin-files/skills/knowledge-query/SKILL.md +0 -14
@@ -5,7 +5,7 @@
5
5
  * skills. A page is identified by its type and slug, `concept/bowline`, and links between
6
6
  * pages use that id too: `[Bowline](concept/bowline)`.
7
7
  */
8
- /** A kind of page: the kit's four, or one an agent declares (`AgentSpec.knowledgePageTypes`). */
8
+ /** A kind of page: one of the kit's, or one an agent declares (`AgentSpec.knowledgePageTypes`). */
9
9
  export interface PageType {
10
10
  /** Its name, lowercase: `concept`, `topic`. Page ids start with it. */
11
11
  type: string;
@@ -57,18 +57,13 @@ export interface CheckReport {
57
57
  page: string;
58
58
  target: string;
59
59
  }[];
60
- /** Pages nothing links to (summaries and syntheses aside, which the index reaches). */
60
+ /** Pages nothing links to (summaries, syntheses and preferences aside, which the index reaches). */
61
61
  orphans: string[];
62
62
  /** Links to retired pages. */
63
63
  linksToRetired: {
64
64
  page: string;
65
65
  target: string;
66
66
  }[];
67
- /** Summary pages whose original is gone from the sources folder. */
68
- missingOriginals: {
69
- page: string;
70
- file: string;
71
- }[];
72
67
  }
73
68
  /** A page to create. */
74
69
  export interface NewPage {
@@ -110,7 +105,7 @@ export interface KnowledgeStore {
110
105
  retire(id: string, reason: string): Promise<void>;
111
106
  /** Pages matching words of `query` in their title, aliases or body, best first. */
112
107
  search(query: string, limit?: number): Promise<SearchHit[]>;
113
- /** The catalog: every active or superseded page, one line each, by section. */
108
+ /** The catalog: every active or superseded page, one line each, by section, linked by id (`concept/bowline`), as everything the model reads. */
114
109
  index(): Promise<string>;
115
110
  /** Adds an entry to the operation log, dated today. */
116
111
  log(operation: string, what: string, pages?: readonly string[]): Promise<void>;
@@ -126,7 +121,7 @@ export interface KnowledgeStore {
126
121
  /** Replaces the overview. */
127
122
  writeOverview(content: string): Promise<void>;
128
123
  }
129
- /** The kit's page types (ADR-008). */
124
+ /** The kit's page types (ADR-008), and the person's preferences (#33). */
130
125
  export declare const BUILT_IN_PAGE_TYPES: readonly PageType[];
131
126
  /** Whether `slug` is lowercase ASCII words joined by hyphens. */
132
127
  export declare function isSlug(slug: string): boolean;
@@ -5,13 +5,14 @@
5
5
  * skills. A page is identified by its type and slug, `concept/bowline`, and links between
6
6
  * pages use that id too: `[Bowline](concept/bowline)`.
7
7
  */
8
- /** The kit's page types (ADR-008). */
8
+ /** The kit's page types (ADR-008), and the person's preferences (#33). */
9
9
  export const BUILT_IN_PAGE_TYPES = [
10
10
  {
11
11
  type: "summary",
12
12
  dir: "summaries",
13
13
  indexSection: "Summaries",
14
- description: "One per ingested source: what it says, its key points and the pages it feeds. Fields: file (the original, relative to sources/) or url; ingested.",
14
+ description: "One per ingested source: what it says, its key points and the pages it feeds. Fields: file (the original, relative to the sources folder, as list_sources names it) or url; ingested (when it was written from its original, set by the knowledge base itself).",
15
+ indexFields: ["file", "url", "ingested"],
15
16
  template: `## Summary
16
17
  <Two to four paragraphs: what it actually says.>
17
18
 
@@ -67,6 +68,19 @@ export const BUILT_IN_PAGE_TYPES = [
67
68
  description: "An answer worth keeping: a comparison, an analysis, a report. Fields: question.",
68
69
  template: `<The answer, with links to every page it draws on.>`,
69
70
  },
71
+ {
72
+ type: "preference",
73
+ dir: "preferences",
74
+ indexSection: "Preferences",
75
+ description: "How the person wants things done in this project, one per page: created only from what the person said in the chat, never from a document, a web page or a tool result, and written in the language of the chat. Fields: since (the date they said it).",
76
+ template: `<The preference in one sentence, as the person would put it.>
77
+
78
+ ## Why
79
+ - <The reason they gave, if any.>
80
+
81
+ ## Example
82
+ - <What following it looks like.>`,
83
+ },
70
84
  ];
71
85
  const SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/;
72
86
  /** Whether `slug` is lowercase ASCII words joined by hyphens. */
@@ -56,5 +56,5 @@ export function detectLanguage(option) {
56
56
  */
57
57
  export function replyLanguageInstruction(language) {
58
58
  const name = LANGUAGE_NAMES[language];
59
- return `Language: reply in ${name}, whatever language these instructions, your skills or the human's name are in. Switch to another language only when the human's message is clearly written in it; a greeting, a slash command or a single word doesn't count.`;
59
+ return `Language: use ${name} both to reply and for everything you keep (knowledge base pages, preferences, notes, the log), whatever language these instructions, your skills, your sources or the human's name are in. Switch both to another language only when the human's message is clearly written in it; a greeting, a slash command or a single word doesn't count.`;
60
60
  }
@@ -72,7 +72,7 @@ export const de = {
72
72
  planCommentQuestion: "Was soll sich ändern? (Enter zum Überspringen): ",
73
73
  retireLines: (source, why, reason, replacedBy, folder) => [
74
74
  `Quelle: ${source}`,
75
- why === "wrong" ? "Warum: sie ist falsch; ihre Zusammenfassung wird zurückgezogen" : "Warum: sie wird ersetzt; ihre Zusammenfassung wird als ersetzt markiert",
75
+ why === "wrong" ? "Warum: sie ist falsch" : "Warum: eine bessere ersetzt sie",
76
76
  `Grund: ${reason}`,
77
77
  ...(replacedBy ? [`Ersetzt durch: ${replacedBy}`] : []),
78
78
  `Sie wird nach ${folder}.agent-kit/retired/ verschoben, nicht gelöscht.`,
@@ -71,7 +71,7 @@ export const en = {
71
71
  planCommentQuestion: "What should change? (Enter to skip): ",
72
72
  retireLines: (source, why, reason, replacedBy, folder) => [
73
73
  `Source: ${source}`,
74
- why === "wrong" ? "Why: it's wrong; its summary will be retired" : "Why: it's replaced; its summary will be marked superseded",
74
+ why === "wrong" ? "Why: it's wrong" : "Why: it's replaced by a better one",
75
75
  `Reason: ${reason}`,
76
76
  ...(replacedBy ? [`Replaced by: ${replacedBy}`] : []),
77
77
  `It's moved to ${folder}.agent-kit/retired/, not deleted.`,
@@ -72,7 +72,7 @@ export const es = {
72
72
  planCommentQuestion: "¿Qué debería cambiar? (Enter para omitir): ",
73
73
  retireLines: (source, why, reason, replacedBy, folder) => [
74
74
  `Fuente: ${source}`,
75
- why === "wrong" ? "Motivo: es errónea; su resumen se retirará" : "Motivo: se sustituye; su resumen se marcará como sustituido",
75
+ why === "wrong" ? "Motivo: es errónea" : "Motivo: la sustituye una mejor",
76
76
  `Razón: ${reason}`,
77
77
  ...(replacedBy ? [`Sustituida por: ${replacedBy}`] : []),
78
78
  `Se mueve a ${folder}.agent-kit/retired/, no se borra.`,
@@ -72,7 +72,7 @@ export const fr = {
72
72
  planCommentQuestion: "Que faut-il changer ? (Entrée pour passer) : ",
73
73
  retireLines: (source, why, reason, replacedBy, folder) => [
74
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é",
75
+ why === "wrong" ? "Motif : elle est erronée" : "Motif : une meilleure la remplace",
76
76
  `Raison : ${reason}`,
77
77
  ...(replacedBy ? [`Remplacée par : ${replacedBy}`] : []),
78
78
  `Elle est déplacée dans ${folder}.agent-kit/retired/, pas supprimée.`,
@@ -37,6 +37,18 @@ export declare function buildSessionOptions<TConfig extends BaseSessionConfig>(c
37
37
  knowledgeStore?: KnowledgeStore;
38
38
  }>;
39
39
  export { createModeControl, togglePlanMode, type ModeControl } from "./modeControl.js";
40
+ /**
41
+ * The CLI's folder for a project, where it keeps each session's transcript and large tool
42
+ * results (`<session>/tool-results/`): the project's path with every character but letters
43
+ * and digits as "-" (confirmed empirically).
44
+ */
45
+ export declare function claudeProjectDir(projectDir: string): string;
46
+ /**
47
+ * The skills of the plugins in `roots`, as the SDK names them: `<plugin>:<folder>`, the
48
+ * plugin's name from its `.claude-plugin/plugin.json` and the skill's folder under `skills/`,
49
+ * whatever its frontmatter's `name` says (confirmed empirically).
50
+ */
51
+ export declare function pluginSkills(roots: readonly string[]): Promise<string[]>;
40
52
  /**
41
53
  * User message queue backed by a single long-lived generator, for any multi-turn
42
54
  * (chat-shaped) caller of `query()`.
@@ -1,3 +1,5 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import os from "node:os";
1
3
  import path from "node:path";
2
4
  import { createTranscriptLogger } from "./hooks/transcriptLogger.js";
3
5
  import { createStepGate } from "./hooks/stepGate.js";
@@ -8,7 +10,7 @@ import { createFileScopeGate } from "./hooks/fileScopeGate.js";
8
10
  import { createPlanGate } from "./hooks/planGate.js";
9
11
  import { createHumanApprovalServer } from "./tools/humanApproval.js";
10
12
  import { createManualLoginServer } from "./tools/manualLogin.js";
11
- import { createSaveToSourcesServer } from "./tools/saveToSources.js";
13
+ import { createSaveToSourcesServer, sourcesPromptSection } from "./tools/saveToSources.js";
12
14
  import { createTimeServer } from "./tools/time.js";
13
15
  import { TODO_TOOL } from "./todos.js";
14
16
  import { createModeControl } from "./modeControl.js";
@@ -62,25 +64,26 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
62
64
  // The built-in knowledge base (rules in the system prompt + its skills/commands as a plugin) needs
63
65
  // somewhere to keep the wiki, so it follows `knowledgeDir`; `spec.knowledgeBase: false` opts out.
64
66
  const includeKnowledgeBase = Boolean(config.knowledgeDir) && spec.knowledgeBase !== false;
65
- // By default the agent reaches it only through the `knowledge_*` tools, over a store, never
66
- // with the file tools (ADR-024); `spec.knowledgeTools: "files"` keeps the file tools on it.
67
- const knowledgeViaStore = includeKnowledgeBase && (spec.knowledgeTools ?? "store") === "store";
68
- const notesDir = knowledgeViaStore ? undefined : config.knowledgeDir;
67
+ // The agent reaches it only through the `knowledge_*` tools, over a store, never with the file
68
+ // tools (ADR-024). Without it (`knowledgeBase: false`), `knowledgeDir` is a folder of the
69
+ // agent's own notes, kept with the file tools under its own rules.
70
+ const notesDir = includeKnowledgeBase ? undefined : config.knowledgeDir;
69
71
  const extraDirs = config.extraWritableDirs ?? [];
70
- // With the store, the file tools are only for the originals (reading) and the extra writable
72
+ const readableExtras = config.extraReadableDirs ?? [];
73
+ // With the knowledge base, the file tools are only for the originals (reading) and the extra
71
74
  // folders; without either, none.
72
75
  const fileTools = !includeFileTools
73
76
  ? []
74
- : !knowledgeViaStore
77
+ : !includeKnowledgeBase
75
78
  ? ["Read", "Write", "Edit", "Glob", "Grep"]
76
- : [...(config.sourcesDir || extraDirs.length ? ["Read", "Glob", "Grep"] : []), ...(extraDirs.length ? ["Write", "Edit"] : [])];
79
+ : [...(config.sourcesDir || extraDirs.length || readableExtras.length ? ["Read", "Glob", "Grep"] : []), ...(extraDirs.length ? ["Write", "Edit"] : [])];
77
80
  // Where Write/Edit may act and Grep may search (see hooks/fileScopeGate.ts): the
78
81
  // project's own folders, never the whole cwd — which would include whatever else the
79
82
  // project directory holds (the user's config...). `sourcesDir` is searchable but
80
83
  // deliberately NOT writable: originals stay as obtained, and the only way to add to it is
81
84
  // the `save_to_sources` tool (which never overwrites).
82
85
  const writableDirs = [notesDir, ...extraDirs].filter((d) => Boolean(d));
83
- const searchableDirs = [notesDir, config.sourcesDir, ...extraDirs].filter((d) => Boolean(d));
86
+ const searchableDirs = [notesDir, config.sourcesDir, ...extraDirs, ...readableExtras].filter((d) => Boolean(d));
84
87
  const readOnlyDirs = config.sourcesDir ? [config.sourcesDir] : [];
85
88
  // Skills/commands/plugins are a separate concern from the file tools: an agent that
86
89
  // wants a plugin-provided skill/command but has no notes or sources folder shouldn't have
@@ -99,13 +102,15 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
99
102
  : undefined;
100
103
  const pluginRoots = [
101
104
  ...spec.pluginRoots(config),
102
- ...(includeKnowledgeBase ? [knowledgePluginRoot(knowledgeViaStore ? "tools" : "files")] : []),
105
+ ...(includeKnowledgeBase ? [knowledgePluginRoot()] : []),
103
106
  ...(helpPlugin ? [helpPlugin] : []),
104
107
  ];
105
- const knowledgeStore = knowledgeViaStore && config.knowledgeDir
106
- ? (spec.knowledgeStore?.(config) ?? createFileKnowledgeStore(config.knowledgeDir, { pageTypes: spec.knowledgePageTypes, sourcesDir: config.sourcesDir }))
108
+ const knowledgeStore = includeKnowledgeBase && config.knowledgeDir
109
+ ? (spec.knowledgeStore?.(config) ?? createFileKnowledgeStore(config.knowledgeDir, { pageTypes: spec.knowledgePageTypes }))
107
110
  : undefined;
108
111
  const includeSkillsAndPlugins = includeFileTools || pluginRoots.length > 0;
112
+ // "plugins": the skills of the plugins loaded, named as the SDK names them.
113
+ const offeredSkills = spec.skills === "plugins" ? await pluginSkills(pluginRoots) : spec.skills;
109
114
  const skillTools = includeSkillsAndPlugins ? ["Skill"] : [];
110
115
  // Whatever opt-in subagents `spec` wants for this config, or undefined if none apply.
111
116
  // Subagents need the Agent tool (to delegate to them), and Bash when one of them uses it
@@ -135,11 +140,17 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
135
140
  const allowedSubagentTypes = subagents?.allowedSubagentTypes ?? [];
136
141
  const transcriptPath = path.join(runDir, "transcript.jsonl");
137
142
  const transcriptLogger = createTranscriptLogger(transcriptPath, config.secrets ?? []);
143
+ // The person's preferences, by title, for the knowledge base's section (#33).
144
+ const preferences = knowledgeStore
145
+ ? (await knowledgeStore.list()).filter((page) => page.type === "preference" && page.status === "active").map(({ id, title }) => ({ id, title }))
146
+ : [];
138
147
  const promptSections = [
139
148
  spec.buildSystemPrompt(config),
140
149
  ...(spec.identity ? [identityPromptSection(spec.identity)] : []),
150
+ // The sources' own section, with or without a knowledge base (#30).
151
+ ...(config.sourcesDir ? [sourcesPromptSection(config.projectDir, config.sourcesDir)] : []),
141
152
  ...(includeKnowledgeBase && config.knowledgeDir
142
- ? [knowledgePromptSection(config.projectDir, config.knowledgeDir, config.sourcesDir, { tools: knowledgeViaStore, pageTypes: knowledgeStore?.types() })]
153
+ ? [knowledgePromptSection({ withSources: Boolean(config.sourcesDir), pageTypes: knowledgeStore?.types(), preferences })]
143
154
  : []),
144
155
  ];
145
156
  const sdkOptions = {
@@ -153,8 +164,16 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
153
164
  // whose own memory is the knowledge base. includeGitInstructions: false leaves out the
154
165
  // CLI's commit workflow and git context (the runner's git user name among it), which no
155
166
  // agent here needs and which pulled replies towards the runner's language (confirmed
156
- // empirically, see ADR-018).
157
- settings: { autoCompactEnabled: options.autoCompactEnabled ?? true, autoMemoryEnabled: false, includeGitInstructions: false },
167
+ // empirically, see ADR-018). disableClaudeAiConnectors: true keeps out the claude.ai
168
+ // connectors of the account the session runs with (Drive, Gmail…), which the CLI fetches
169
+ // whatever settingSources says (confirmed empirically): the runner's integrations, not the
170
+ // agent's. An MCP server the agent passes itself is unaffected.
171
+ settings: {
172
+ autoCompactEnabled: options.autoCompactEnabled ?? true,
173
+ autoMemoryEnabled: false,
174
+ includeGitInstructions: false,
175
+ disableClaudeAiConnectors: true,
176
+ },
158
177
  // No built-in tools except, if applicable, Read/Write/Edit/Glob/Grep scoped to the
159
178
  // project's own folders (fileScopeGate below), and WebFetch/WebSearch
160
179
  // with no domain restriction (not conditioned on includeFileTools: it's read-only,
@@ -197,7 +216,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
197
216
  // SDK's own official skills (pdf/docx), the project's own custom ones, and the
198
217
  // plugin-provided built-ins below. A spec's own list gets the knowledge base's
199
218
  // skills and agent-help added, so it only names its own.
200
- skills: skillList(spec.skills, includeKnowledgeBase ? (knowledgeViaStore ? "tools" : "files") : null, Boolean(helpPlugin)),
219
+ skills: skillList(offeredSkills, includeKnowledgeBase, Boolean(helpPlugin)),
201
220
  plugins: pluginRoots.map((pluginPath) => ({ type: "local", path: pluginPath, skipMcpDiscovery: true })),
202
221
  }
203
222
  : {}),
@@ -213,7 +232,6 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
213
232
  ...(config.sourcesDir
214
233
  ? {
215
234
  sourceFiles: createSaveToSourcesServer(runDir, config.sourcesDir, spec.saveToSourcesDescription, {
216
- knowledgeDir: config.knowledgeDir,
217
235
  projectDir: config.projectDir,
218
236
  interactive: mode !== "autonomous",
219
237
  }),
@@ -222,13 +240,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
222
240
  // The knowledge base's own tools, over its store (tools/knowledgeTools.ts, ADR-024).
223
241
  ...(knowledgeStore
224
242
  ? {
225
- knowledge: createKnowledgeServer(knowledgeStore, {
226
- runDir,
227
- interactive: mode !== "autonomous",
228
- sourcesDir: config.sourcesDir,
229
- projectDir: config.projectDir,
230
- knowledgeDir: config.knowledgeDir,
231
- }),
243
+ knowledge: createKnowledgeServer(knowledgeStore, { runDir, interactive: mode !== "autonomous" }),
232
244
  }
233
245
  : {}),
234
246
  // The date and date arithmetic, in every session and mode: only reads (tools/time.ts).
@@ -243,8 +255,16 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
243
255
  writableDirs,
244
256
  searchableDirs,
245
257
  readOnlyDirs,
246
- deniedPaths: config.deniedPaths ?? [],
247
- ...(knowledgeViaStore && config.knowledgeDir ? { toolOnlyDirs: [{ dir: config.knowledgeDir, instead: "the knowledge_* tools" }] } : {}),
258
+ // The SDK's own credentials, whatever the agent denies.
259
+ deniedPaths: [...(config.deniedPaths ?? []), path.join(claudeConfigDir(), ".credentials.json")],
260
+ // Read and Glob only in the agent's folders, plus what only the kit knows it
261
+ // needs: the run folder, the plugins, the project's .claude/ (when loaded) and
262
+ // the SDK's large tool results. Never the rest of the disk (~/.ssh, other
263
+ // projects' .env...), which an agent reading untrusted content could be talked into.
264
+ readableDirs: searchableDirs,
265
+ alsoReadable: [runDir, ...pluginRoots, ...((spec.settingSources ?? ["project"]).includes("project") ? [path.join(config.projectDir, ".claude")] : [])],
266
+ toolResultsRoot: claudeProjectDir(config.projectDir),
267
+ ...(includeKnowledgeBase && config.knowledgeDir ? { toolOnlyDirs: [{ dir: config.knowledgeDir, instead: "the knowledge_* tools" }] } : {}),
248
268
  }),
249
269
  ] }]
250
270
  : []),
@@ -280,15 +300,50 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
280
300
  return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language, ...(knowledgeStore ? { knowledgeStore } : {}) };
281
301
  }
282
302
  export { createModeControl, togglePlanMode } from "./modeControl.js";
283
- /** 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). */
284
- const KNOWLEDGE_SKILLS = {
285
- tools: ["knowledge-ingest", "knowledge-query", "knowledge-lint"].map((skill) => `knowledge:${skill}`),
286
- files: ["knowledge-pages", "knowledge-ingest", "knowledge-query", "knowledge-lint"].map((skill) => `knowledge:${skill}`),
287
- };
288
- function skillList(skills, knowledgeVariant, agentHelp) {
303
+ /** The Claude Code configuration folder the SDK's CLI uses: `CLAUDE_CONFIG_DIR`, or `~/.claude`. */
304
+ function claudeConfigDir() {
305
+ return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
306
+ }
307
+ /**
308
+ * The CLI's folder for a project, where it keeps each session's transcript and large tool
309
+ * results (`<session>/tool-results/`): the project's path with every character but letters
310
+ * and digits as "-" (confirmed empirically).
311
+ */
312
+ export function claudeProjectDir(projectDir) {
313
+ return path.join(claudeConfigDir(), "projects", path.resolve(projectDir).replace(/[^a-zA-Z0-9]/g, "-"));
314
+ }
315
+ /** The knowledge plugin's skills, as the SDK names a plugin's skills ("plugin:skill"). */
316
+ const KNOWLEDGE_SKILLS = ["knowledge-ingest", "knowledge-query", "knowledge-lint"].map((skill) => `knowledge:${skill}`);
317
+ /**
318
+ * The skills of the plugins in `roots`, as the SDK names them: `<plugin>:<folder>`, the
319
+ * plugin's name from its `.claude-plugin/plugin.json` and the skill's folder under `skills/`,
320
+ * whatever its frontmatter's `name` says (confirmed empirically).
321
+ */
322
+ export async function pluginSkills(roots) {
323
+ const names = [];
324
+ for (const root of roots) {
325
+ let plugin;
326
+ try {
327
+ plugin = JSON.parse(await readFile(path.join(root, ".claude-plugin", "plugin.json"), "utf8")).name;
328
+ }
329
+ catch {
330
+ continue; // not a plugin the SDK would load
331
+ }
332
+ if (!plugin)
333
+ continue;
334
+ const folders = await readdir(path.join(root, "skills"), { withFileTypes: true }).catch(() => []);
335
+ for (const folder of folders.filter((entry) => entry.isDirectory())) {
336
+ const hasSkill = await readFile(path.join(root, "skills", folder.name, "SKILL.md")).then(() => true, () => false);
337
+ if (hasSkill)
338
+ names.push(`${plugin}:${folder.name}`);
339
+ }
340
+ }
341
+ return names;
342
+ }
343
+ function skillList(skills, knowledgeBase, agentHelp) {
289
344
  if (skills === undefined || skills === "all")
290
345
  return "all";
291
- return [...new Set([...skills, ...(knowledgeVariant ? KNOWLEDGE_SKILLS[knowledgeVariant] : []), ...(agentHelp ? [AGENT_HELP_SKILL] : [])])];
346
+ return [...new Set([...skills, ...(knowledgeBase ? KNOWLEDGE_SKILLS : []), ...(agentHelp ? [AGENT_HELP_SKILL] : [])])];
292
347
  }
293
348
  /**
294
349
  * User message queue backed by a single long-lived generator, for any multi-turn
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * The sources folder's bookkeeping: originals stay files (ADR-024), and the kit keeps a
3
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.
4
+ * file tools can't write) with each original's hash, size, provenance, the version it replaces
5
+ * and when its content last changed. From that it refuses duplicates, tells present, missing
6
+ * and retired originals apart, and retires an original into `.agent-kit/retired/` instead of
7
+ * deleting it. It knows nothing of the knowledge base: each owns its data, and the model
8
+ * connects them through their tools (#30). Hashes never leave it.
8
9
  */
9
10
  export declare const KIT_DIR = ".agent-kit";
10
11
  /** Where an original came from. */
@@ -33,8 +34,8 @@ export interface SourceEntry {
33
34
  replaces?: string;
34
35
  /** The page this one was generated from (a web page's markdown, from its HTML). */
35
36
  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;
37
+ /** When its content last changed (ISO 8601): when it was added, or when its hash last differed. Not when it was only touched. */
38
+ changedAt: string;
38
39
  }
39
40
  export interface RetiredSource {
40
41
  path: string;
@@ -50,18 +51,23 @@ export interface SourceManifest {
50
51
  files: Record<string, SourceEntry>;
51
52
  retired: RetiredSource[];
52
53
  }
53
- export type SourceStatus = "new" | "ingested" | "changed" | "missing" | "present";
54
+ /** Present in the folder; missing (in the manifest, deleted by hand); or retired with `retire_source`. */
55
+ export type SourceStatus = "present" | "missing" | "retired";
54
56
  export interface SourceListing {
55
57
  path: string;
56
58
  status: SourceStatus;
57
59
  type: string;
60
+ /** When its content last changed (ISO 8601), to compare with when something was built from it. */
61
+ changedAt?: string;
58
62
  size?: number;
59
63
  pages?: number;
60
64
  slides?: number;
61
65
  origin?: SourceOrigin;
62
66
  replaces?: string;
63
- /** The summary pages that point to it (paths relative to the project). */
64
- summaries: string[];
67
+ /** For a retired one: when, why, and what replaced it. */
68
+ retiredAt?: string;
69
+ why?: "wrong" | "replaced";
70
+ replacedBy?: string;
65
71
  }
66
72
  export declare function hashFile(file: string): Promise<string>;
67
73
  /** Resolves `candidate` against `base`; the absolute path only if it stays inside it. */
@@ -70,18 +76,15 @@ export declare function loadManifest(sourcesDir: string): Promise<SourceManifest
70
76
  export declare function saveManifest(sourcesDir: string, manifest: SourceManifest): Promise<void>;
71
77
  /** Every original under `sourcesDir` (paths relative to it), leaving out hidden files and folders (the kit's own among them). */
72
78
  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
79
  /** Pages of a PDF, read from its page tree without a parser; undefined when it can't tell (e.g. compressed object streams). */
76
80
  export declare function pdfPageCount(bytes: Buffer): number | undefined;
77
81
  /** Slides of a PPTX, from the file names in the zip (stored uncompressed). */
78
82
  export declare function pptxSlideCount(bytes: Buffer): number | undefined;
79
83
  /**
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".
84
+ * Every original with its status and when its content last changed, recording in the manifest
85
+ * the ones copied in by hand; then the missing ones and the retired ones.
83
86
  */
84
- export declare function listSources(sourcesDir: string, knowledgeDir: string | undefined, projectDir: string): Promise<SourceListing[]>;
87
+ export declare function listSources(sourcesDir: string): Promise<SourceListing[]>;
85
88
  export interface AddedSource {
86
89
  /** Where it is, relative to `sourcesDir`. */
87
90
  path: string;
@@ -99,24 +102,14 @@ export declare function addSource(sourcesDir: string, file: string, destination:
99
102
  derivedFrom?: string;
100
103
  name?: string;
101
104
  }): Promise<AddedSource>;
102
- /** What retiring an original did to its summary pages (paths relative to the project). */
105
+ /** Where a retired original went. */
103
106
  export interface RetireResult {
107
+ /** Relative to `sourcesDir`. */
104
108
  movedTo: string;
105
- summaries: {
106
- page: string;
107
- marked: "retired" | "superseded";
108
- supersededBy?: string;
109
- }[];
110
109
  }
111
110
  /**
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.
111
+ * Moves an original to `.agent-kit/retired/` (never deleted: the person empties that folder) and
112
+ * records why. What was built from it (summary pages) is the knowledge base's to update: the
113
+ * model does it with its tools.
121
114
  */
122
- export declare function recordIngest(sourcesDir: string, source: string): Promise<void>;
115
+ export declare function retireSource(sourcesDir: string, source: string, why: "wrong" | "replaced", reason: string, replacedBy?: string): Promise<RetireResult>;