@falkenslab/agent-kit 0.16.1 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/assets/agent-help/SKILL.md +57 -0
- package/assets/knowledge-plugin/commands/ingest.md +1 -1
- package/assets/knowledge-plugin/skills/knowledge-ingest/SKILL.md +8 -3
- package/assets/knowledge-plugin/skills/knowledge-lint/SKILL.md +12 -5
- package/dist/core/agentHelp.d.ts +30 -0
- package/dist/core/agentHelp.js +55 -0
- package/dist/core/agentSpec.d.ts +37 -16
- package/dist/core/fileKnowledgeStore.d.ts +1 -3
- package/dist/core/fileKnowledgeStore.js +23 -29
- package/dist/core/hooks/fileScopeGate.d.ts +17 -2
- package/dist/core/hooks/fileScopeGate.js +45 -10
- package/dist/core/knowledge.d.ts +20 -17
- package/dist/core/knowledge.js +32 -60
- package/dist/core/knowledgeStore.d.ts +4 -9
- package/dist/core/knowledgeStore.js +16 -2
- package/dist/core/language.js +1 -1
- package/dist/core/messages/de.js +2 -2
- package/dist/core/messages/en.d.ts +2 -1
- package/dist/core/messages/en.js +2 -2
- package/dist/core/messages/es.js +2 -2
- package/dist/core/messages/fr.js +2 -2
- package/dist/core/runner.d.ts +5 -0
- package/dist/core/runner.js +1 -0
- package/dist/core/session.d.ts +12 -0
- package/dist/core/session.js +121 -40
- package/dist/core/sources.d.ts +24 -31
- package/dist/core/sources.js +45 -108
- package/dist/core/tools/humanApproval.js +3 -3
- package/dist/core/tools/knowledgeTools.d.ts +6 -6
- package/dist/core/tools/knowledgeTools.js +18 -53
- package/dist/core/tools/saveToSources.d.ts +6 -2
- package/dist/core/tools/saveToSources.js +17 -12
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/tui/ink/SessionView.d.ts +1 -0
- package/dist/tui/ink/SessionView.js +17 -4
- package/dist/tui/ink/runChatInk.d.ts +7 -0
- package/dist/tui/ink/runChatInk.js +6 -4
- package/package.json +1 -1
- package/assets/knowledge-plugin-files/.claude-plugin/plugin.json +0 -4
- package/assets/knowledge-plugin-files/commands/ingest.md +0 -7
- package/assets/knowledge-plugin-files/commands/lint.md +0 -5
- package/assets/knowledge-plugin-files/commands/query.md +0 -5
- package/assets/knowledge-plugin-files/skills/knowledge-ingest/SKILL.md +0 -46
- package/assets/knowledge-plugin-files/skills/knowledge-lint/SKILL.md +0 -26
- package/assets/knowledge-plugin-files/skills/knowledge-pages/SKILL.md +0 -137
- package/assets/knowledge-plugin-files/skills/knowledge-query/SKILL.md +0 -14
package/dist/core/session.js
CHANGED
|
@@ -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";
|
|
@@ -16,6 +18,7 @@ import { createFileKnowledgeStore } from "./fileKnowledgeStore.js";
|
|
|
16
18
|
import { createKnowledgeServer } from "./tools/knowledgeTools.js";
|
|
17
19
|
import { allowAnyMcpTool } from "./mcpPermissions.js";
|
|
18
20
|
import { knowledgePluginRoot, knowledgePromptSection } from "./knowledge.js";
|
|
21
|
+
import { AGENT_HELP_SKILL, identityPromptSection, writeAgentHelpPlugin } from "./agentHelp.js";
|
|
19
22
|
import { replyLanguageInstruction } from "./language.js";
|
|
20
23
|
import { chooseLanguage } from "./messages/index.js";
|
|
21
24
|
import { createRunStore } from "./runs.js";
|
|
@@ -61,40 +64,57 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
61
64
|
// The built-in knowledge base (rules in the system prompt + its skills/commands as a plugin) needs
|
|
62
65
|
// somewhere to keep the wiki, so it follows `knowledgeDir`; `spec.knowledgeBase: false` opts out.
|
|
63
66
|
const includeKnowledgeBase = Boolean(config.knowledgeDir) && spec.knowledgeBase !== false;
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
const notesDir =
|
|
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;
|
|
68
71
|
const extraDirs = config.extraWritableDirs ?? [];
|
|
69
|
-
|
|
72
|
+
const readableExtras = config.extraReadableDirs ?? [];
|
|
73
|
+
// With the knowledge base, the file tools are only for the originals (reading) and the extra
|
|
70
74
|
// folders; without either, none.
|
|
71
75
|
const fileTools = !includeFileTools
|
|
72
76
|
? []
|
|
73
|
-
: !
|
|
77
|
+
: !includeKnowledgeBase
|
|
74
78
|
? ["Read", "Write", "Edit", "Glob", "Grep"]
|
|
75
|
-
: [...(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"] : [])];
|
|
76
80
|
// Where Write/Edit may act and Grep may search (see hooks/fileScopeGate.ts): the
|
|
77
81
|
// project's own folders, never the whole cwd — which would include whatever else the
|
|
78
82
|
// project directory holds (the user's config...). `sourcesDir` is searchable but
|
|
79
83
|
// deliberately NOT writable: originals stay as obtained, and the only way to add to it is
|
|
80
84
|
// the `save_to_sources` tool (which never overwrites).
|
|
81
85
|
const writableDirs = [notesDir, ...extraDirs].filter((d) => Boolean(d));
|
|
82
|
-
const searchableDirs = [notesDir, config.sourcesDir, ...extraDirs].filter((d) => Boolean(d));
|
|
86
|
+
const searchableDirs = [notesDir, config.sourcesDir, ...extraDirs, ...readableExtras].filter((d) => Boolean(d));
|
|
83
87
|
const readOnlyDirs = config.sourcesDir ? [config.sourcesDir] : [];
|
|
84
88
|
// Skills/commands/plugins are a separate concern from the file tools: an agent that
|
|
85
89
|
// wants a plugin-provided skill/command but has no notes or sources folder shouldn't have
|
|
86
90
|
// to invent one just to get `cwd`/`skills: "all"`/`plugins` wired up. Gated on either
|
|
87
91
|
// signal, not on `pluginRoots` alone, so an agent with file tools keeps getting the SDK's
|
|
88
92
|
// own project-level `.claude/skills`/`.claude/commands` discovery.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
// With an identity, the agent-help skill (agentHelp.ts): written into the run's folder, since
|
|
94
|
+
// it carries this session's facts and the agent's own guide.
|
|
95
|
+
const helpPlugin = spec.identity
|
|
96
|
+
? await writeAgentHelpPlugin(path.join(runDir, "agent-help"), {
|
|
97
|
+
mode,
|
|
98
|
+
switchable: modeControl.switchable,
|
|
99
|
+
knowledgeBase: includeKnowledgeBase,
|
|
100
|
+
...(config.sourcesDir ? { sourcesFolder: `${path.relative(config.projectDir, config.sourcesDir).split(path.sep).join("/")}/` } : {}),
|
|
101
|
+
}, spec.helpGuide)
|
|
102
|
+
: undefined;
|
|
103
|
+
const pluginRoots = [
|
|
104
|
+
...spec.pluginRoots(config),
|
|
105
|
+
...(includeKnowledgeBase ? [knowledgePluginRoot()] : []),
|
|
106
|
+
...(helpPlugin ? [helpPlugin] : []),
|
|
107
|
+
];
|
|
108
|
+
const knowledgeStore = includeKnowledgeBase && config.knowledgeDir
|
|
109
|
+
? (spec.knowledgeStore?.(config) ?? createFileKnowledgeStore(config.knowledgeDir, { pageTypes: spec.knowledgePageTypes }))
|
|
92
110
|
: undefined;
|
|
93
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;
|
|
94
114
|
const skillTools = includeSkillsAndPlugins ? ["Skill"] : [];
|
|
95
115
|
// Whatever opt-in subagents `spec` wants for this config, or undefined if none apply.
|
|
96
|
-
//
|
|
97
|
-
// only by the subagent itself — see createSubagentBashGate() below, which denies Bash
|
|
116
|
+
// Subagents need the Agent tool (to delegate to them), and Bash when one of them uses it
|
|
117
|
+
// (used only by the subagent itself — see createSubagentBashGate() below, which denies Bash
|
|
98
118
|
// to the main agent regardless of which one put it in `tools`) — Bash has to be present
|
|
99
119
|
// in this session's own tools for any subagent to be allowed to use it at all
|
|
100
120
|
// (confirmed empirically the SDK refuses to spawn a subagent whose own tools list names
|
|
@@ -106,6 +126,12 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
106
126
|
? Object.fromEntries(Object.entries(subagents.agents).map(([name, agent]) => [name, { ...agent, prompt: withReplyLine(agent.prompt) }]))
|
|
107
127
|
: undefined;
|
|
108
128
|
const includeSubagentTools = subagents !== undefined;
|
|
129
|
+
// Bash only when a subagent can use it: one that lists it, or one without `tools`, which
|
|
130
|
+
// inherits every session tool. Its definition costs about 1.9k input tokens on every call
|
|
131
|
+
// (measured), and the main agent can't use it anyway.
|
|
132
|
+
const subagentTools = includeSubagentTools
|
|
133
|
+
? ["Agent", ...(Object.values(subagents.agents).some((agent) => !agent.tools || agent.tools.includes("Bash")) ? ["Bash"] : [])]
|
|
134
|
+
: [];
|
|
109
135
|
// The exact set of subagent_type values the Agent tool may spawn in this session — see
|
|
110
136
|
// createSubagentTypeGate() below for why this can't just be "whatever's in `agents`
|
|
111
137
|
// below": the SDK's own built-in "general-purpose" type is spawnable regardless of that
|
|
@@ -114,10 +140,21 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
114
140
|
const allowedSubagentTypes = subagents?.allowedSubagentTypes ?? [];
|
|
115
141
|
const transcriptPath = path.join(runDir, "transcript.jsonl");
|
|
116
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
|
+
: [];
|
|
147
|
+
const promptSections = [
|
|
148
|
+
spec.buildSystemPrompt(config),
|
|
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)] : []),
|
|
152
|
+
...(includeKnowledgeBase && config.knowledgeDir
|
|
153
|
+
? [knowledgePromptSection({ withSources: Boolean(config.sourcesDir), pageTypes: knowledgeStore?.types(), preferences })]
|
|
154
|
+
: []),
|
|
155
|
+
];
|
|
117
156
|
const sdkOptions = {
|
|
118
|
-
systemPrompt: withReplyLine(
|
|
119
|
-
? `${spec.buildSystemPrompt(config)}\n\n${knowledgePromptSection(config.projectDir, config.knowledgeDir, config.sourcesDir, { tools: knowledgeViaStore, pageTypes: knowledgeStore?.types() })}`
|
|
120
|
-
: spec.buildSystemPrompt(config)),
|
|
157
|
+
systemPrompt: withReplyLine(promptSections.join("\n\n")),
|
|
121
158
|
// Not the SDK's default (every source): "user" hands the agent the runner's own Claude
|
|
122
159
|
// Code configuration, and its `language` setting outranked the agent's prompt.
|
|
123
160
|
settingSources: spec.settingSources ?? ["project"],
|
|
@@ -127,8 +164,16 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
127
164
|
// whose own memory is the knowledge base. includeGitInstructions: false leaves out the
|
|
128
165
|
// CLI's commit workflow and git context (the runner's git user name among it), which no
|
|
129
166
|
// agent here needs and which pulled replies towards the runner's language (confirmed
|
|
130
|
-
// empirically, see ADR-018).
|
|
131
|
-
|
|
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
|
+
},
|
|
132
177
|
// No built-in tools except, if applicable, Read/Write/Edit/Glob/Grep scoped to the
|
|
133
178
|
// project's own folders (fileScopeGate below), and WebFetch/WebSearch
|
|
134
179
|
// with no domain restriction (not conditioned on includeFileTools: it's read-only,
|
|
@@ -139,8 +184,8 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
139
184
|
// so the model ends up looking for SKILL.md files by hand (confirmed empirically).
|
|
140
185
|
// TodoWrite: the SDK's task list for long jobs, in every session (the chats show the list
|
|
141
186
|
// instead of the calls; see todos.ts). Accepted in an explicit list (confirmed empirically).
|
|
142
|
-
tools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", TODO_TOOL, ...
|
|
143
|
-
allowedTools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", TODO_TOOL, ...
|
|
187
|
+
tools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", TODO_TOOL, ...subagentTools],
|
|
188
|
+
allowedTools: [...fileTools, ...skillTools, "WebFetch", "WebSearch", TODO_TOOL, ...subagentTools],
|
|
144
189
|
// Every mcp__* tool (whatever spec.buildMcpServers() registers, approvals/manualLogin
|
|
145
190
|
// when enabled below, sourceFiles, and any server a project's own .mcp.json
|
|
146
191
|
// declares) is approved generically here rather than listed one by one — see
|
|
@@ -170,8 +215,8 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
170
215
|
// "skills" acts as a name whitelist, not an addition: "all" enables both the
|
|
171
216
|
// SDK's own official skills (pdf/docx), the project's own custom ones, and the
|
|
172
217
|
// plugin-provided built-ins below. A spec's own list gets the knowledge base's
|
|
173
|
-
// skills added, so it only names its own.
|
|
174
|
-
skills: skillList(
|
|
218
|
+
// skills and agent-help added, so it only names its own.
|
|
219
|
+
skills: skillList(offeredSkills, includeKnowledgeBase, Boolean(helpPlugin)),
|
|
175
220
|
plugins: pluginRoots.map((pluginPath) => ({ type: "local", path: pluginPath, skipMcpDiscovery: true })),
|
|
176
221
|
}
|
|
177
222
|
: {}),
|
|
@@ -187,7 +232,6 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
187
232
|
...(config.sourcesDir
|
|
188
233
|
? {
|
|
189
234
|
sourceFiles: createSaveToSourcesServer(runDir, config.sourcesDir, spec.saveToSourcesDescription, {
|
|
190
|
-
knowledgeDir: config.knowledgeDir,
|
|
191
235
|
projectDir: config.projectDir,
|
|
192
236
|
interactive: mode !== "autonomous",
|
|
193
237
|
}),
|
|
@@ -196,13 +240,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
196
240
|
// The knowledge base's own tools, over its store (tools/knowledgeTools.ts, ADR-024).
|
|
197
241
|
...(knowledgeStore
|
|
198
242
|
? {
|
|
199
|
-
knowledge: createKnowledgeServer(knowledgeStore, {
|
|
200
|
-
runDir,
|
|
201
|
-
interactive: mode !== "autonomous",
|
|
202
|
-
sourcesDir: config.sourcesDir,
|
|
203
|
-
projectDir: config.projectDir,
|
|
204
|
-
knowledgeDir: config.knowledgeDir,
|
|
205
|
-
}),
|
|
243
|
+
knowledge: createKnowledgeServer(knowledgeStore, { runDir, interactive: mode !== "autonomous" }),
|
|
206
244
|
}
|
|
207
245
|
: {}),
|
|
208
246
|
// The date and date arithmetic, in every session and mode: only reads (tools/time.ts).
|
|
@@ -217,8 +255,16 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
217
255
|
writableDirs,
|
|
218
256
|
searchableDirs,
|
|
219
257
|
readOnlyDirs,
|
|
220
|
-
|
|
221
|
-
...(
|
|
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" }] } : {}),
|
|
222
268
|
}),
|
|
223
269
|
] }]
|
|
224
270
|
: []),
|
|
@@ -254,15 +300,50 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
254
300
|
return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language, ...(knowledgeStore ? { knowledgeStore } : {}) };
|
|
255
301
|
}
|
|
256
302
|
export { createModeControl, togglePlanMode } from "./modeControl.js";
|
|
257
|
-
/** The
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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) {
|
|
263
344
|
if (skills === undefined || skills === "all")
|
|
264
345
|
return "all";
|
|
265
|
-
return
|
|
346
|
+
return [...new Set([...skills, ...(knowledgeBase ? KNOWLEDGE_SKILLS : []), ...(agentHelp ? [AGENT_HELP_SKILL] : [])])];
|
|
266
347
|
}
|
|
267
348
|
/**
|
|
268
349
|
* User message queue backed by a single long-lived generator, for any multi-turn
|
package/dist/core/sources.d.ts
CHANGED
|
@@ -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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
/**
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
64
|
-
|
|
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
|
|
81
|
-
* the
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
114
|
-
*
|
|
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
|
|
115
|
+
export declare function retireSource(sourcesDir: string, source: string, why: "wrong" | "replaced", reason: string, replacedBy?: string): Promise<RetireResult>;
|
package/dist/core/sources.js
CHANGED
|
@@ -5,10 +5,11 @@ import path from "node:path";
|
|
|
5
5
|
/**
|
|
6
6
|
* The sources folder's bookkeeping: originals stay files (ADR-024), and the kit keeps a
|
|
7
7
|
* manifest next to them (`.agent-kit/sources.json`, inside `sourcesDir`, where the agent's
|
|
8
|
-
* file tools can't write) with each original's hash, size
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* file tools can't write) with each original's hash, size, provenance, the version it replaces
|
|
9
|
+
* and when its content last changed. From that it refuses duplicates, tells present, missing
|
|
10
|
+
* and retired originals apart, and retires an original into `.agent-kit/retired/` instead of
|
|
11
|
+
* deleting it. It knows nothing of the knowledge base: each owns its data, and the model
|
|
12
|
+
* connects them through their tools (#30). Hashes never leave it.
|
|
12
13
|
*/
|
|
13
14
|
export const KIT_DIR = ".agent-kit";
|
|
14
15
|
const MANIFEST = "sources.json";
|
|
@@ -70,41 +71,20 @@ export async function scanSources(sourcesDir) {
|
|
|
70
71
|
await walk(sourcesDir);
|
|
71
72
|
return found.sort();
|
|
72
73
|
}
|
|
73
|
-
/**
|
|
74
|
+
/**
|
|
75
|
+
* An entry for `file`, reusing the recorded hash when its size and modification time haven't
|
|
76
|
+
* changed; `changedAt` moves only when the content (its hash) did. Entries from before
|
|
77
|
+
* `changedAt` existed take their `addedAt`.
|
|
78
|
+
*/
|
|
74
79
|
async function freshEntry(file, known, origin) {
|
|
75
80
|
const info = await stat(file);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!link || /^[a-z]+:\/\//i.test(link))
|
|
84
|
-
return undefined;
|
|
85
|
-
return path.resolve(pageDir, decodeURI(link));
|
|
86
|
-
}
|
|
87
|
-
/** The summary pages (knowledge base layout: `summaries/*.md`) by the absolute path of the original their frontmatter `file:` points to. */
|
|
88
|
-
export async function summariesByOriginal(knowledgeDir) {
|
|
89
|
-
const byOriginal = new Map();
|
|
90
|
-
const dir = path.join(knowledgeDir, "summaries");
|
|
91
|
-
let names;
|
|
92
|
-
try {
|
|
93
|
-
names = await readdir(dir);
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return byOriginal;
|
|
97
|
-
}
|
|
98
|
-
for (const name of names.filter((n) => n.endsWith(".md"))) {
|
|
99
|
-
const page = path.join(dir, name);
|
|
100
|
-
const text = await readFile(page, "utf8");
|
|
101
|
-
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1] ?? "";
|
|
102
|
-
const value = /^file:\s*(.+)$/m.exec(frontmatter)?.[1];
|
|
103
|
-
const original = value ? linkedFile(value, dir) : undefined;
|
|
104
|
-
if (original)
|
|
105
|
-
byOriginal.set(path.normalize(original), [...(byOriginal.get(path.normalize(original)) ?? []), page]);
|
|
106
|
-
}
|
|
107
|
-
return byOriginal;
|
|
81
|
+
const previous = known ? { ...known, changedAt: known.changedAt ?? known.addedAt } : undefined;
|
|
82
|
+
if (previous && previous.size === info.size && previous.mtimeMs === info.mtimeMs)
|
|
83
|
+
return previous;
|
|
84
|
+
const hash = await hashFile(file);
|
|
85
|
+
const stamp = now();
|
|
86
|
+
const base = previous ?? { addedAt: stamp, origin, changedAt: stamp };
|
|
87
|
+
return { ...base, hash, size: info.size, mtimeMs: info.mtimeMs, changedAt: previous && previous.hash === hash ? previous.changedAt : stamp };
|
|
108
88
|
}
|
|
109
89
|
/** Pages of a PDF, read from its page tree without a parser; undefined when it can't tell (e.g. compressed object streams). */
|
|
110
90
|
export function pdfPageCount(bytes) {
|
|
@@ -124,31 +104,19 @@ export function pptxSlideCount(bytes) {
|
|
|
124
104
|
}
|
|
125
105
|
const typeOf = (file) => path.extname(file).slice(1).toLowerCase() || "file";
|
|
126
106
|
/**
|
|
127
|
-
* Every original with its status
|
|
128
|
-
* the
|
|
129
|
-
* folder there's no ingest to compare with: originals are "present".
|
|
107
|
+
* Every original with its status and when its content last changed, recording in the manifest
|
|
108
|
+
* the ones copied in by hand; then the missing ones and the retired ones.
|
|
130
109
|
*/
|
|
131
|
-
export function listSources(sourcesDir
|
|
132
|
-
return serialized(sourcesDir, () => listSourcesNow(sourcesDir
|
|
110
|
+
export function listSources(sourcesDir) {
|
|
111
|
+
return serialized(sourcesDir, () => listSourcesNow(sourcesDir));
|
|
133
112
|
}
|
|
134
|
-
async function listSourcesNow(sourcesDir
|
|
113
|
+
async function listSourcesNow(sourcesDir) {
|
|
135
114
|
const manifest = await loadManifest(sourcesDir);
|
|
136
|
-
const summaries = knowledgeDir ? await summariesByOriginal(knowledgeDir) : new Map();
|
|
137
115
|
const files = await scanSources(sourcesDir);
|
|
138
116
|
const listing = [];
|
|
139
117
|
for (const rel of files) {
|
|
140
118
|
const full = path.join(sourcesDir, rel);
|
|
141
119
|
const entry = await freshEntry(full, manifest.files[rel], { kind: "manual" });
|
|
142
|
-
const pagesOf = summaries.get(path.normalize(full)) ?? [];
|
|
143
|
-
let status = "present";
|
|
144
|
-
if (knowledgeDir) {
|
|
145
|
-
if (pagesOf.length === 0)
|
|
146
|
-
status = "new";
|
|
147
|
-
else {
|
|
148
|
-
entry.ingestedHash ??= entry.hash;
|
|
149
|
-
status = entry.ingestedHash === entry.hash ? "ingested" : "changed";
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
120
|
manifest.files[rel] = entry;
|
|
153
121
|
const type = typeOf(rel);
|
|
154
122
|
const counts = type === "pdf" || type === "pptx"
|
|
@@ -156,18 +124,28 @@ async function listSourcesNow(sourcesDir, knowledgeDir, projectDir) {
|
|
|
156
124
|
: {};
|
|
157
125
|
listing.push({
|
|
158
126
|
path: rel,
|
|
159
|
-
status,
|
|
127
|
+
status: "present",
|
|
160
128
|
type,
|
|
129
|
+
changedAt: entry.changedAt,
|
|
161
130
|
size: entry.size,
|
|
162
131
|
...counts,
|
|
163
132
|
origin: entry.origin,
|
|
164
133
|
...(entry.replaces ? { replaces: entry.replaces } : {}),
|
|
165
|
-
summaries: pagesOf.map((page) => posix(path.relative(projectDir, page))),
|
|
166
134
|
});
|
|
167
135
|
}
|
|
168
136
|
for (const [rel, entry] of Object.entries(manifest.files)) {
|
|
169
137
|
if (!files.includes(rel))
|
|
170
|
-
listing.push({ path: rel, status: "missing", type: typeOf(rel), origin: entry.origin
|
|
138
|
+
listing.push({ path: rel, status: "missing", type: typeOf(rel), origin: entry.origin });
|
|
139
|
+
}
|
|
140
|
+
for (const retired of manifest.retired) {
|
|
141
|
+
listing.push({
|
|
142
|
+
path: retired.path,
|
|
143
|
+
status: "retired",
|
|
144
|
+
type: typeOf(retired.path),
|
|
145
|
+
retiredAt: retired.retiredAt,
|
|
146
|
+
why: retired.why,
|
|
147
|
+
...(retired.replacedBy ? { replacedBy: retired.replacedBy } : {}),
|
|
148
|
+
});
|
|
171
149
|
}
|
|
172
150
|
await saveManifest(sourcesDir, manifest);
|
|
173
151
|
return listing;
|
|
@@ -219,11 +197,13 @@ async function addSourceNow(sourcesDir, file, destination, origin, extra) {
|
|
|
219
197
|
}
|
|
220
198
|
const rel = posix(path.relative(sourcesDir, target));
|
|
221
199
|
const info = await stat(target);
|
|
200
|
+
const stamp = now();
|
|
222
201
|
manifest.files[rel] = {
|
|
223
202
|
hash,
|
|
224
203
|
size: info.size,
|
|
225
204
|
mtimeMs: info.mtimeMs,
|
|
226
|
-
addedAt:
|
|
205
|
+
addedAt: stamp,
|
|
206
|
+
changedAt: stamp,
|
|
227
207
|
origin,
|
|
228
208
|
...(extra.replaces ? { replaces: posix(path.relative(sourcesDir, resolveWithin(sourcesDir, extra.replaces))) } : {}),
|
|
229
209
|
...(extra.derivedFrom ? { derivedFrom: extra.derivedFrom } : {}),
|
|
@@ -231,26 +211,15 @@ async function addSourceNow(sourcesDir, file, destination, origin, extra) {
|
|
|
231
211
|
await saveManifest(sourcesDir, manifest);
|
|
232
212
|
return { path: rel };
|
|
233
213
|
}
|
|
234
|
-
/** Adds frontmatter fields and a notice under the frontmatter of a markdown page. */
|
|
235
|
-
async function markPage(page, fields, notice) {
|
|
236
|
-
const text = await readFile(page, "utf8");
|
|
237
|
-
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
|
|
238
|
-
const lines = Object.entries(fields).map(([key, value]) => `${key}: ${value}`);
|
|
239
|
-
const updated = match
|
|
240
|
-
? `---\n${match[1].split(/\r?\n/).filter((line) => !Object.keys(fields).some((key) => line.startsWith(`${key}:`))).concat(lines).join("\n")}\n---\n\n> ${notice}\n\n${text.slice(match[0].length).replace(/^\s+/, "")}`
|
|
241
|
-
: `---\n${lines.join("\n")}\n---\n\n> ${notice}\n\n${text}`;
|
|
242
|
-
await writeFile(page, updated);
|
|
243
|
-
}
|
|
244
214
|
/**
|
|
245
|
-
* Moves an original to `.agent-kit/retired/` (never deleted: the person empties that folder)
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* when there is one.
|
|
215
|
+
* Moves an original to `.agent-kit/retired/` (never deleted: the person empties that folder) and
|
|
216
|
+
* records why. What was built from it (summary pages) is the knowledge base's to update: the
|
|
217
|
+
* model does it with its tools.
|
|
249
218
|
*/
|
|
250
|
-
export function retireSource(sourcesDir,
|
|
251
|
-
return serialized(sourcesDir, () => retireSourceNow(sourcesDir,
|
|
219
|
+
export function retireSource(sourcesDir, source, why, reason, replacedBy) {
|
|
220
|
+
return serialized(sourcesDir, () => retireSourceNow(sourcesDir, source, why, reason, replacedBy));
|
|
252
221
|
}
|
|
253
|
-
async function retireSourceNow(sourcesDir,
|
|
222
|
+
async function retireSourceNow(sourcesDir, source, why, reason, replacedBy) {
|
|
254
223
|
const full = resolveWithin(sourcesDir, source);
|
|
255
224
|
if (!full || !(await stat(full).catch(() => null))?.isFile())
|
|
256
225
|
throw new Error(`"${source}" isn't an original in sources/.`);
|
|
@@ -260,7 +229,6 @@ async function retireSourceNow(sourcesDir, knowledgeDir, projectDir, source, why
|
|
|
260
229
|
const replacement = replacedBy ? resolveWithin(sourcesDir, replacedBy) : undefined;
|
|
261
230
|
if (replacedBy && (!replacement || !(await stat(replacement).catch(() => null))))
|
|
262
231
|
throw new Error(`"${replacedBy}" isn't an original in sources/.`);
|
|
263
|
-
const summaries = knowledgeDir ? await summariesByOriginal(knowledgeDir) : new Map();
|
|
264
232
|
const manifest = await loadManifest(sourcesDir);
|
|
265
233
|
const stamp = now();
|
|
266
234
|
const movedTo = posix(path.join(KIT_DIR, RETIRED_DIR, `${stamp.replace(/[:.]/g, "-")}-${path.basename(rel)}`));
|
|
@@ -277,36 +245,5 @@ async function retireSourceNow(sourcesDir, knowledgeDir, projectDir, source, why
|
|
|
277
245
|
});
|
|
278
246
|
delete manifest.files[rel];
|
|
279
247
|
await saveManifest(sourcesDir, manifest);
|
|
280
|
-
|
|
281
|
-
const replacementSummary = replacement ? summaries.get(path.normalize(replacement))?.[0] : undefined;
|
|
282
|
-
const result = { movedTo, summaries: [] };
|
|
283
|
-
for (const page of summaries.get(path.normalize(full)) ?? []) {
|
|
284
|
-
if (why === "wrong") {
|
|
285
|
-
await markPage(page, { status: "retired", retired: date }, `Retired on ${date}: its original was wrong (${reason}). Don't rely on what it says.`);
|
|
286
|
-
result.summaries.push({ page: posix(path.relative(projectDir, page)), marked: "retired" });
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
const link = replacementSummary ? posix(path.relative(path.dirname(page), replacementSummary)) : undefined;
|
|
290
|
-
await markPage(page, { status: "superseded", superseded: date, ...(link ? { superseded_by: link } : {}) }, `Superseded on ${date}: its original was replaced (${reason})${link ? `; see [the new summary](${link})` : ""}.`);
|
|
291
|
-
result.summaries.push({ page: posix(path.relative(projectDir, page)), marked: "superseded", ...(replacementSummary ? { supersededBy: posix(path.relative(projectDir, replacementSummary)) } : {}) });
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return result;
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Records `source` (relative to `sourcesDir`) as ingested now, with its current hash: a
|
|
298
|
-
* summary page was just written from it, so "changed" means changed after this.
|
|
299
|
-
*/
|
|
300
|
-
export function recordIngest(sourcesDir, source) {
|
|
301
|
-
return serialized(sourcesDir, async () => {
|
|
302
|
-
const full = resolveWithin(sourcesDir, source);
|
|
303
|
-
if (!full || !(await stat(full).catch(() => null))?.isFile())
|
|
304
|
-
return;
|
|
305
|
-
const rel = posix(path.relative(sourcesDir, full));
|
|
306
|
-
const manifest = await loadManifest(sourcesDir);
|
|
307
|
-
const entry = await freshEntry(full, manifest.files[rel], { kind: "manual" });
|
|
308
|
-
entry.ingestedHash = entry.hash;
|
|
309
|
-
manifest.files[rel] = entry;
|
|
310
|
-
await saveManifest(sourcesDir, manifest);
|
|
311
|
-
});
|
|
248
|
+
return { movedTo };
|
|
312
249
|
}
|