@falkenslab/agent-kit 0.19.0 → 0.21.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 (46) hide show
  1. package/assets/extension-launcher.mjs +9 -2
  2. package/dist/chat/chatController.d.ts +4 -1
  3. package/dist/chat/chatController.js +43 -17
  4. package/dist/chat/extensions.d.ts +7 -3
  5. package/dist/chat/extensions.js +12 -9
  6. package/dist/chat/runs.d.ts +3 -0
  7. package/dist/chat/runs.js +1 -1
  8. package/dist/core/agentSpec.d.ts +20 -56
  9. package/dist/core/extensions.d.ts +23 -12
  10. package/dist/core/extensions.js +8 -18
  11. package/dist/core/externalExtensions.d.ts +49 -9
  12. package/dist/core/externalExtensions.js +89 -18
  13. package/dist/core/liveExtensions.d.ts +65 -0
  14. package/dist/core/liveExtensions.js +121 -0
  15. package/dist/core/marketplaces.d.ts +118 -0
  16. package/dist/core/marketplaces.js +242 -0
  17. package/dist/core/messages/de.js +1 -0
  18. package/dist/core/messages/en.d.ts +2 -0
  19. package/dist/core/messages/en.js +1 -0
  20. package/dist/core/messages/es.js +1 -0
  21. package/dist/core/messages/fr.js +1 -0
  22. package/dist/core/pluginDownloads.d.ts +14 -0
  23. package/dist/core/pluginDownloads.js +141 -0
  24. package/dist/core/runner.js +6 -1
  25. package/dist/core/session.d.ts +11 -4
  26. package/dist/core/session.js +73 -21
  27. package/dist/core/sessionFacts.d.ts +5 -0
  28. package/dist/core/sessionFacts.js +1 -0
  29. package/dist/extensions/awareness/index.d.ts +12 -3
  30. package/dist/extensions/awareness/index.js +18 -15
  31. package/dist/extensions/awareness/tools.d.ts +1 -1
  32. package/dist/extensions/awareness/tools.js +3 -3
  33. package/dist/extensions/knowledge/index.d.ts +20 -6
  34. package/dist/extensions/knowledge/index.js +32 -29
  35. package/dist/extensions/knowledge/knowledgeStore.d.ts +1 -1
  36. package/dist/extensions/knowledge/prompt.d.ts +4 -4
  37. package/dist/extensions/knowledge/prompt.js +4 -4
  38. package/dist/extensions/memory/index.d.ts +15 -6
  39. package/dist/extensions/memory/index.js +34 -31
  40. package/dist/extensions/sources/index.d.ts +17 -5
  41. package/dist/extensions/sources/index.js +31 -29
  42. package/dist/index.d.ts +7 -2
  43. package/dist/index.js +6 -1
  44. package/dist/tui/extensionCommand.d.ts +14 -3
  45. package/dist/tui/extensionCommand.js +164 -12
  46. package/package.json +1 -1
@@ -22,6 +22,7 @@ import { replyLanguageInstruction } from "./language.js";
22
22
  import { chooseLanguage } from "./messages/index.js";
23
23
  import { createRunStore } from "./runs.js";
24
24
  import { createSessionView, registerSessionView } from "./sessionFacts.js";
25
+ import { createExtensionSwitch, mountPlugin } from "./liveExtensions.js";
25
26
  import { agentKitVersion } from "./version.js";
26
27
  /**
27
28
  * Builds the `options` object passed to the Agent SDK's `query()` — everything about
@@ -69,32 +70,39 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
69
70
  runDir,
70
71
  mode,
71
72
  interactive: mode !== "autonomous",
73
+ // What the active ones provide, once they're resolved below.
74
+ capabilities: new Set(),
72
75
  session: () => (late.view ? late.view.facts() : Promise.reject(new Error("The session isn't built yet: call session() from a tool, not from contribute()."))),
73
76
  };
74
77
  // The installed ones (#37), the project's over the agent's: those that can't load are off with
75
78
  // why, and so is one named like an extension the spec enables.
76
79
  const extensionDirs = config.extensionDirs ?? {};
77
80
  const external = await loadExternalExtensions(extensionDirs);
78
- const enabledNames = new Set((spec.extensions ?? []).map((entry) => (typeof entry === "string" ? entry : entry.name)));
81
+ const enabledNames = new Set((spec.extensions ?? []).map((extension) => extension.name));
79
82
  const resolved = resolveExtensions([...(spec.extensions ?? []), ...external.extensions.filter((extension) => !enabledNames.has(extension.name))], extensionContext);
80
83
  resolved.inactive.push(...external.off, ...external.extensions.filter((extension) => enabledNames.has(extension.name)).map((extension) => ({ name: extension.name, reason: "has the name of one this agent already has" })));
84
+ // Each installed one's plugin, mounted in the run's folder (a link, nothing copied): the running
85
+ // session unmounts and mounts it to turn the extension off and on (liveExtensions.ts), never
86
+ // touching what's installed.
87
+ const installedDirs = new Map();
88
+ for (const { extension } of resolved.active.filter(({ extension }) => extension.external)) {
89
+ const mount = path.join(runDir, "extensions", extension.name);
90
+ await mountPlugin(extension.plugin, mount);
91
+ installedDirs.set(extension.name, { dir: extension.plugin, scopeDir: extension.installedIn ?? path.dirname(extension.plugin) });
92
+ extension.plugin = mount;
93
+ }
81
94
  const externalPlugins = new Set(resolved.active.filter(({ extension }) => extension.external).map(({ extension }) => path.resolve(extension.plugin)));
95
+ extensionContext.capabilities = resolved.capabilities;
82
96
  const added = await Promise.all(resolved.active.map(({ extension }) => extension.contribute(extensionContext)));
83
97
  const extensionPlugins = resolved.active.map(({ extension }) => extension.plugin);
84
98
  const fromExtensions = (pick) => added.flatMap((contribution) => pick(contribution) ?? []);
85
- const knowledgeStore = added.map((contribution) => contribution.api?.knowledgeStore).find(Boolean);
86
- // An extension may claim `knowledgeDir` as reached only through its tools (the knowledge base,
87
- // ADR-024); otherwise it's a folder of the agent's own notes, kept with the file tools under
88
- // its own rules.
89
- const claimed = (dir) => fromExtensions((contribution) => contribution.toolOnlyDirs).some((only) => path.resolve(only.dir) === path.resolve(dir));
90
- const notesDir = config.knowledgeDir && !claimed(config.knowledgeDir) ? config.knowledgeDir : undefined;
91
99
  const extraDirs = config.extraWritableDirs ?? [];
92
100
  const readableExtras = config.extraReadableDirs ?? [];
93
- // The file tools the session needs: all five for the notes and the extra writable folders,
94
- // the reading ones for the extra readable folders, and whatever the extensions ask for (the
95
- // sources' reading ones); without any of them, none.
101
+ // The file tools the session needs: all five for the extra writable folders, the reading ones
102
+ // for the extra readable folders, and whatever the extensions ask for (the sources' reading
103
+ // ones); without any of them, none.
96
104
  const wantedFileTools = new Set([
97
- ...(notesDir || extraDirs.length ? FILE_TOOLS : []),
105
+ ...(extraDirs.length ? FILE_TOOLS : []),
98
106
  ...(readableExtras.length ? ["Read", "Glob", "Grep"] : []),
99
107
  ...fromExtensions((contribution) => contribution.fileTools),
100
108
  ]);
@@ -105,10 +113,10 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
105
113
  // searchable but deliberately NOT writable: the only way to add to them is its tools.
106
114
  const readOnlyDirs = fromExtensions((contribution) => contribution.readOnlyDirs);
107
115
  const toolOnlyDirs = fromExtensions((contribution) => contribution.toolOnlyDirs);
108
- const writableDirs = [notesDir, ...extraDirs].filter((d) => Boolean(d));
109
- const searchableDirs = [notesDir, ...readOnlyDirs, ...extraDirs, ...readableExtras].filter((d) => Boolean(d));
116
+ const writableDirs = extraDirs;
117
+ const searchableDirs = [...readOnlyDirs, ...extraDirs, ...readableExtras];
110
118
  // Skills/commands/plugins are a separate concern from the file tools: an agent that
111
- // wants a plugin-provided skill/command but has no notes or sources folder shouldn't have
119
+ // wants a plugin-provided skill/command but has no folder of its own shouldn't have
112
120
  // to invent one just to get `cwd`/`skills: "all"`/`plugins` wired up. Gated on either
113
121
  // signal, not on `pluginRoots` alone, so an agent with file tools keeps getting the SDK's
114
122
  // own project-level `.claude/skills`/`.claude/commands` discovery.
@@ -160,6 +168,10 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
160
168
  // map, and inherits the full session tools (Bash included) rather than the narrow list
161
169
  // each declared subagent actually declares.
162
170
  const allowedSubagentTypes = [...new Set([...(subagents?.allowedSubagentTypes ?? []), ...Object.keys(fromPlugins)])];
171
+ // The extensions' read-only tools, for the plan gate: a list the live switch changes (liveExtensions.ts).
172
+ const readOnlyTools = fromExtensions((contribution) => contribution.readOnlyTools);
173
+ // What no file tool may touch: the agent's denied paths and the SDK's own credentials.
174
+ const deniedPaths = [...(config.deniedPaths ?? []), path.join(claudeConfigDir(), ".credentials.json")];
163
175
  const transcriptPath = path.join(runDir, "transcript.jsonl");
164
176
  const transcriptLogger = createTranscriptLogger(transcriptPath, config.secrets ?? []);
165
177
  const promptSections = [
@@ -195,6 +207,9 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
195
207
  // An installed extension's plugin may carry hooks: none run (ADR-025). The kit's own,
196
208
  // passed in `hooks`, still do (confirmed empirically).
197
209
  ...(externalPlugins.size ? { disableAllHooks: true } : {}),
210
+ // The denied paths again, as the SDK's own rules: a second layer under the file scope gate,
211
+ // which a tool the gate doesn't know of can't pass either (#49).
212
+ permissions: { deny: denyRules(deniedPaths) },
198
213
  },
199
214
  // No built-in tools except, if applicable, Read/Write/Edit/Glob/Grep scoped to the
200
215
  // project's own folders (fileScopeGate below), and WebFetch/WebSearch
@@ -230,7 +245,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
230
245
  // skills, on top of the plugin-provided built-ins from spec.pluginRoots()
231
246
  // below. skipMcpDiscovery: true on those plugins is the caller's choice.
232
247
  cwd: config.projectDir,
233
- // Empty when there's no knowledgeDir/sourcesDir (the plugin-only case) — an
248
+ // Empty when there's no folder of the agent's or its extensions' (the plugin-only case) — an
234
249
  // empty array is a valid, no-op value for this SDK option, not omitted, since
235
250
  // this whole block is already conditioned on includeSkillsAndPlugins.
236
251
  additionalDirectories: searchableDirs,
@@ -264,7 +279,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
264
279
  searchableDirs,
265
280
  readOnlyDirs,
266
281
  // The SDK's own credentials, whatever the agent denies.
267
- deniedPaths: [...(config.deniedPaths ?? []), path.join(claudeConfigDir(), ".credentials.json")],
282
+ deniedPaths,
268
283
  // Read and Glob only in the agent's folders, plus what only the kit knows it
269
284
  // needs: the run folder, the plugins, the project's .claude/ (when loaded) and
270
285
  // the SDK's large tool results. Never the rest of the disk (~/.ssh, other
@@ -287,7 +302,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
287
302
  hooks: [
288
303
  createPlanGate({
289
304
  projectDir: config.projectDir,
290
- readOnlyTools: fromExtensions((contribution) => contribution.readOnlyTools),
305
+ readOnlyTools,
291
306
  ...(spec.planMode?.isPlanFile ? { isPlanFile: (filePath) => spec.planMode.isPlanFile(filePath, config) } : {}),
292
307
  ...(spec.planMode?.isReadOnlyTool ? { isReadOnlyTool: spec.planMode.isReadOnlyTool } : {}),
293
308
  }, () => modeControl.mode === "plan"),
@@ -321,7 +336,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
321
336
  inactive: resolved.inactive,
322
337
  };
323
338
  // What the session is, for the extensions that ask (sessionFacts.ts, #43).
324
- late.view = createSessionView({
339
+ const viewBase = {
325
340
  ...(spec.identity ? { identity: spec.identity } : {}),
326
341
  kitVersion: agentKitVersion(),
327
342
  language,
@@ -333,15 +348,39 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
333
348
  servers: Object.keys(added[index]?.mcpServers ?? {}),
334
349
  help: added[index]?.helpLines ?? [],
335
350
  })),
336
- inactive: resolved.inactive,
351
+ // Its own list: /extensions' status has another, and the live switch changes both.
352
+ inactive: [...resolved.inactive],
337
353
  },
338
354
  subagents: Object.entries(allAgents).map(([name, agent]) => ({ name, description: agent.description })),
339
355
  skills: sdkOptions.skills === "all" ? await pluginSkills(pluginRoots) : [...(sdkOptions.skills ?? [])],
340
356
  runDir,
341
- }, modeControl);
357
+ };
358
+ late.view = createSessionView(viewBase, modeControl);
342
359
  registerSessionView(sdkOptions, late.view);
360
+ // Turning an installed extension off and on while the session runs (#49).
361
+ const switchExtension = createExtensionSwitch(resolved.active.flatMap(({ extension, manifest }, index) => {
362
+ const installed = installedDirs.get(extension.name);
363
+ if (!installed)
364
+ return [];
365
+ const contribution = added[index];
366
+ return [
367
+ {
368
+ name: extension.name,
369
+ installedDir: installed.dir,
370
+ scopeDir: installed.scopeDir,
371
+ pluginMount: extension.plugin,
372
+ servers: Object.keys(contribution?.mcpServers ?? {}),
373
+ readOnlyTools: contribution?.readOnlyTools ?? [],
374
+ toolLabels: contribution?.toolLabels ?? {},
375
+ agents: Object.keys(fromPlugins).filter((agent) => agent.startsWith(`${extension.name}:`)),
376
+ description: manifest.description,
377
+ provides: manifest.provides,
378
+ help: contribution?.helpLines ?? [],
379
+ },
380
+ ];
381
+ }), { readOnlyTools, allowedSubagentTypes, toolLabels, status: extensions, facts: viewBase, controls: () => late.view?.controls() ?? {} });
343
382
  const apis = Object.fromEntries(resolved.active.flatMap(({ extension }, index) => (added[index]?.api ? [[extension.name, added[index].api]] : [])));
344
- return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language, toolLabels, extensions, apis, ...(knowledgeStore ? { knowledgeStore } : {}) };
383
+ return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language, toolLabels, extensions, apis, switchExtension };
345
384
  }
346
385
  export { createModeControl, togglePlanMode } from "./modeControl.js";
347
386
  /** The Claude Code configuration folder the SDK's CLI uses: `CLAUDE_CONFIG_DIR`, or `~/.claude`. */
@@ -356,6 +395,19 @@ function claudeConfigDir() {
356
395
  export function claudeProjectDir(projectDir) {
357
396
  return path.join(claudeConfigDir(), "projects", path.resolve(projectDir).replace(/[^a-zA-Z0-9]/g, "-"));
358
397
  }
398
+ /**
399
+ * The SDK's `permissions.deny` rules for paths no file tool may read or change, each one and
400
+ * whatever is inside it. An absolute path is written `//` and POSIX: `C:\Users\x` is
401
+ * `//c/Users/x` (confirmed empirically on Windows; on POSIX `/home/x` is `//home/x`). `Read`
402
+ * rules reach the tools that read, `Edit` rules those that write.
403
+ */
404
+ export function denyRules(paths) {
405
+ return paths.flatMap((denied) => {
406
+ const posix = path.resolve(denied).replace(/\\/g, "/").replace(/^([A-Za-z]):/, (_, drive) => `/${drive.toLowerCase()}`);
407
+ const rule = `/${posix}`;
408
+ return [`Read(${rule})`, `Read(${rule}/**)`, `Edit(${rule})`, `Edit(${rule}/**)`];
409
+ });
410
+ }
359
411
  /** The built-in file tools, in the order the session lists them. */
360
412
  const FILE_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep"];
361
413
  /**
@@ -58,6 +58,9 @@ export interface SessionLive {
58
58
  skills?: string[];
59
59
  supportedCommands?: () => Promise<SlashCommand[]>;
60
60
  contextUsage?: () => Promise<ContextUsage | null>;
61
+ /** The running session's controls an installed extension is turned off and on with (liveExtensions.ts). */
62
+ toggleMcpServer?: (server: string, enabled: boolean) => Promise<void>;
63
+ reloadPlugins?: () => Promise<unknown>;
61
64
  }
62
65
  /** The facts known when the options are built: everything but the mode and what only the running session knows. */
63
66
  export type StaticSessionFacts = Omit<SessionFacts, "mode" | "switchableModes" | "tools" | "commands" | "context" | "extensions"> & {
@@ -71,6 +74,8 @@ export type StaticSessionFacts = Omit<SessionFacts, "mode" | "switchableModes" |
71
74
  export interface SessionView {
72
75
  facts(): Promise<SessionFacts>;
73
76
  attach(live: SessionLive): void;
77
+ /** What the running session has attached so far. */
78
+ controls(): SessionLive;
74
79
  }
75
80
  /** The view of a session: its static facts, the mode read from `modeControl` on every call, and what the running session attaches. */
76
81
  export declare function createSessionView(base: StaticSessionFacts, modeControl: ModeControl): SessionView;
@@ -8,6 +8,7 @@ export function createSessionView(base, modeControl) {
8
8
  attach(more) {
9
9
  live = { ...live, ...more };
10
10
  },
11
+ controls: () => live,
11
12
  async facts() {
12
13
  const tools = live.tools ?? [];
13
14
  const [commands, context] = await Promise.all([
@@ -4,10 +4,19 @@ import type { Extension } from "../../core/extensions.js";
4
4
  export declare function awarenessPluginRoot(): string;
5
5
  /** The "Who you are" section of the system prompt: name, version, what it is, the kit's version, and where to look. */
6
6
  export declare function identityPromptSection(identity: AgentIdentity): string;
7
+ /** The awareness extension's options. */
8
+ export interface AwarenessOptions {
9
+ /**
10
+ * Absolute path of a markdown guide to the agent's own domain (its commands, configuration,
11
+ * folders), which `about_me` gives the agent when the person asks.
12
+ */
13
+ guide?: string;
14
+ }
7
15
  /**
8
16
  * The awareness (#43): the agent knows who it is and what it is at each moment of a session,
9
17
  * and the person can ask it. A "Who you are" section, `about_me` (the session's facts, read
10
- * anew on every call through `ExtensionContext.session()`, and the agent's `helpGuide`), and
11
- * the `help` skill, how the kit's chat is used, which doesn't change.
18
+ * anew on every call through `ExtensionContext.session()`, and the agent's `guide`), and
19
+ * the `help` skill, how the kit's chat is used, which doesn't change. It needs the spec's
20
+ * `identity`.
12
21
  */
13
- export declare const awarenessExtension: Extension;
22
+ export declare function awareness(options?: AwarenessOptions): Extension;
@@ -21,19 +21,22 @@ You are ${who}. You are built on agent-kit ${agentKitVersion()}, which provides
21
21
  /**
22
22
  * The awareness (#43): the agent knows who it is and what it is at each moment of a session,
23
23
  * and the person can ask it. A "Who you are" section, `about_me` (the session's facts, read
24
- * anew on every call through `ExtensionContext.session()`, and the agent's `helpGuide`), and
25
- * the `help` skill, how the kit's chat is used, which doesn't change.
24
+ * anew on every call through `ExtensionContext.session()`, and the agent's `guide`), and
25
+ * the `help` skill, how the kit's chat is used, which doesn't change. It needs the spec's
26
+ * `identity`.
26
27
  */
27
- export const awarenessExtension = {
28
- name: "awareness",
29
- plugin: awarenessPluginRoot(),
30
- missing: ({ spec }) => (spec.identity ? undefined : "needs `identity` in the spec"),
31
- async contribute({ spec, session }) {
32
- return {
33
- mcpServers: { awareness: createAwarenessServer(session, spec.helpGuide) },
34
- promptSection: identityPromptSection(spec.identity),
35
- readOnlyTools: ["mcp__awareness__about_me"],
36
- toolLabels: awarenessToolLabels(),
37
- };
38
- },
39
- };
28
+ export function awareness(options = {}) {
29
+ return {
30
+ name: "awareness",
31
+ plugin: awarenessPluginRoot(),
32
+ missing: ({ spec }) => (spec.identity ? undefined : "needs `identity` in the spec"),
33
+ async contribute({ spec, session }) {
34
+ return {
35
+ mcpServers: { awareness: createAwarenessServer(session, options.guide) },
36
+ promptSection: identityPromptSection(spec.identity),
37
+ readOnlyTools: ["mcp__awareness__about_me"],
38
+ toolLabels: awarenessToolLabels(),
39
+ };
40
+ },
41
+ };
42
+ }
@@ -2,4 +2,4 @@ import type { SessionFacts } from "../../core/sessionFacts.js";
2
2
  /** The session's facts as the model reads them: markdown, in English (ADR-019). */
3
3
  export declare function describeSession(facts: SessionFacts): string;
4
4
  /** The awareness's MCP server: `about_me`, from the session's facts and the agent's guide. */
5
- export declare function createAwarenessServer(session: () => Promise<SessionFacts>, helpGuide?: string): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
5
+ export declare function createAwarenessServer(session: () => Promise<SessionFacts>, guide?: string): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
@@ -41,12 +41,12 @@ export function describeSession(facts) {
41
41
  return sections.join("\n\n");
42
42
  }
43
43
  /** The awareness's MCP server: `about_me`, from the session's facts and the agent's guide. */
44
- export function createAwarenessServer(session, helpGuide) {
44
+ export function createAwarenessServer(session, guide) {
45
45
  const aboutMe = tool("about_me", "What you are right now: your mode and the ones the person can switch to, your extensions with their tools and the ones off (with why), your subagents, the commands the person can type, how full your context is. With `part: \"guide\"`, your own guide to your domain (your commands, configuration, folders). It changes during a session: call it whenever the person asks about you, rather than answering from memory.", { part: z.enum(["now", "guide"]).optional().describe("`now` (the default): what you are right now. `guide`: your own guide to your domain.") }, async ({ part }) => {
46
46
  if (part === "guide") {
47
- if (!helpGuide)
47
+ if (!guide)
48
48
  return ok("You have no guide of your own: for your domain, say only what your instructions tell you, and that the rest isn't documented.");
49
- return ok(await readFile(helpGuide, "utf8"));
49
+ return ok(await readFile(guide, "utf8"));
50
50
  }
51
51
  return ok(describeSession(await session()));
52
52
  }, { annotations: { readOnlyHint: true } });
@@ -1,8 +1,22 @@
1
- import type { Extension } from "../../core/extensions.js";
1
+ import type { BaseSessionConfig } from "../../core/agentSpec.js";
2
+ import { type Extension, type FolderOption } from "../../core/extensions.js";
3
+ import type { KnowledgeStore, PageType } from "./knowledgeStore.js";
4
+ /** The knowledge base's options. */
5
+ export interface KnowledgeOptions<TConfig extends BaseSessionConfig = BaseSessionConfig> {
6
+ /** The knowledge base's folder, reached only through its tools, never the file tools. Without a folder the extension is off. */
7
+ dir: FolderOption<TConfig>;
8
+ /**
9
+ * The agent's own page types, besides the kit's (summary, concept, entity, synthesis,
10
+ * preference): each with its folder (`""` for the root), index section, description (told to
11
+ * the model) and template.
12
+ */
13
+ pageTypes?: PageType[];
14
+ /** Its store, instead of the kit's over markdown files in `dir` (e.g. a database or a vector store implementing `KnowledgeStore`). */
15
+ store?(config: TConfig, dir: string): KnowledgeStore;
16
+ }
2
17
  /**
3
- * The built-in knowledge base (`config.knowledgeDir`, ADR-008, ADR-024): an interlinked wiki the
4
- * agent keeps only through its `knowledge_*` tools, over a `KnowledgeStore` (the kit's over
5
- * markdown files, or the agent's own). Not enabled, `knowledgeDir` is left to the core as a
6
- * folder of the agent's own notes.
18
+ * The built-in knowledge base (ADR-008, ADR-024): an interlinked wiki the agent keeps only through
19
+ * its `knowledge_*` tools, over a `KnowledgeStore` (the kit's over markdown files, or the agent's
20
+ * own). The host reads it through `apis.knowledge.knowledgeStore`.
7
21
  */
8
- export declare const knowledgeExtension: Extension;
22
+ export declare function knowledge<TConfig extends BaseSessionConfig = BaseSessionConfig>(options: KnowledgeOptions<TConfig>): Extension;
@@ -1,35 +1,38 @@
1
+ import { folderOf } from "../../core/extensions.js";
1
2
  import { createFileKnowledgeStore } from "./fileKnowledgeStore.js";
2
3
  import { knowledgeToolLabels } from "./labels.js";
3
4
  import { knowledgePluginRoot, knowledgePromptSection } from "./prompt.js";
4
5
  import { createKnowledgeServer } from "./tools.js";
5
6
  /**
6
- * The built-in knowledge base (`config.knowledgeDir`, ADR-008, ADR-024): an interlinked wiki the
7
- * agent keeps only through its `knowledge_*` tools, over a `KnowledgeStore` (the kit's over
8
- * markdown files, or the agent's own). Not enabled, `knowledgeDir` is left to the core as a
9
- * folder of the agent's own notes.
7
+ * The built-in knowledge base (ADR-008, ADR-024): an interlinked wiki the agent keeps only through
8
+ * its `knowledge_*` tools, over a `KnowledgeStore` (the kit's over markdown files, or the agent's
9
+ * own). The host reads it through `apis.knowledge.knowledgeStore`.
10
10
  */
11
- export const knowledgeExtension = {
12
- name: "knowledge",
13
- plugin: knowledgePluginRoot(),
14
- missing: ({ config }) => (config.knowledgeDir ? undefined : "needs `knowledgeDir` in the config"),
15
- async contribute({ config, spec, runDir, interactive }) {
16
- const knowledgeDir = config.knowledgeDir;
17
- const store = spec.knowledgeStore?.(config) ?? createFileKnowledgeStore(knowledgeDir, { pageTypes: spec.knowledgePageTypes });
18
- // The person's preferences, by title, for its prompt section (#33).
19
- const preferences = (await store.list()).filter((page) => page.type === "preference" && page.status === "active").map(({ id, title }) => ({ id, title }));
20
- return {
21
- // knowledge_retire asks a person, so it exists only where there is one.
22
- mcpServers: { knowledge: createKnowledgeServer(store, { runDir, interactive }) },
23
- promptSection: knowledgePromptSection({ withSources: Boolean(config.sourcesDir), pageTypes: store.types(), preferences }),
24
- // Never the file tools on the knowledge folder (ADR-024): the denial points to the tools.
25
- toolOnlyDirs: [{ dir: knowledgeDir, instead: "the knowledge_* tools" }],
26
- readOnlyTools: ["mcp__knowledge__knowledge_index", "mcp__knowledge__knowledge_search", "mcp__knowledge__knowledge_read", "mcp__knowledge__knowledge_check"],
27
- selfAskingTools: ["mcp__knowledge__knowledge_retire"],
28
- toolLabels: knowledgeToolLabels(),
29
- helpLines: [
30
- "You keep a knowledge base (your memory across sessions). Its commands: `/knowledge:ingest` (learn the new or changed files of the sources folder), `/knowledge:query <question>` (answer from it), `/knowledge:lint` (check it and fix what's mechanical).",
31
- ],
32
- api: { knowledgeStore: store },
33
- };
34
- },
35
- };
11
+ export function knowledge(options) {
12
+ return {
13
+ name: "knowledge",
14
+ plugin: knowledgePluginRoot(),
15
+ missing: ({ config }) => (folderOf(options.dir, config) ? undefined : "needs a folder (`dir`)"),
16
+ async contribute({ config, runDir, interactive, capabilities }) {
17
+ const knowledgeDir = folderOf(options.dir, config);
18
+ const store = options.store?.(config, knowledgeDir) ?? createFileKnowledgeStore(knowledgeDir, { pageTypes: options.pageTypes });
19
+ // The person's preferences, by title, for its prompt section (#33).
20
+ const preferences = (await store.list()).filter((page) => page.type === "preference" && page.status === "active").map(({ id, title }) => ({ id, title }));
21
+ return {
22
+ // knowledge_retire asks a person, so it exists only where there is one.
23
+ mcpServers: { knowledge: createKnowledgeServer(store, { runDir, interactive }) },
24
+ // Beside the sources, its summaries say which original they're about.
25
+ promptSection: knowledgePromptSection({ withSources: capabilities.has("sources"), pageTypes: store.types(), preferences }),
26
+ // Never the file tools on the knowledge folder (ADR-024): the denial points to the tools.
27
+ toolOnlyDirs: [{ dir: knowledgeDir, instead: "the knowledge_* tools" }],
28
+ readOnlyTools: ["mcp__knowledge__knowledge_index", "mcp__knowledge__knowledge_search", "mcp__knowledge__knowledge_read", "mcp__knowledge__knowledge_check"],
29
+ selfAskingTools: ["mcp__knowledge__knowledge_retire"],
30
+ toolLabels: knowledgeToolLabels(),
31
+ helpLines: [
32
+ "You keep a knowledge base (your memory across sessions). Its commands: `/knowledge:ingest` (learn the new or changed files of the sources folder), `/knowledge:query <question>` (answer from it), `/knowledge:lint` (check it and fix what's mechanical).",
33
+ ],
34
+ api: { knowledgeStore: store },
35
+ };
36
+ },
37
+ };
38
+ }
@@ -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: one of the kit's, or one an agent declares (`AgentSpec.knowledgePageTypes`). */
8
+ /** A kind of page: one of the kit's, or one an agent declares (`knowledge({ pageTypes })`). */
9
9
  export interface PageType {
10
10
  /** Its name, lowercase: `concept`, `topic`. Page ids start with it. */
11
11
  type: string;
@@ -1,11 +1,11 @@
1
1
  import type { PageType } from "./knowledgeStore.js";
2
2
  /**
3
- * The built-in knowledge base (an "LLM wiki"): `sourcesDir` holds the originals, `knowledgeDir`
4
- * is the wiki the agent maintains through the `knowledge_*` tools (ADR-024), and the rules below
3
+ * The built-in knowledge base (an "LLM wiki"): the sources folder holds the originals, its own
4
+ * folder is the wiki the agent maintains through the `knowledge_*` tools (ADR-024), and the rules below
5
5
  * plus the plugin's skills (knowledge-ingest, knowledge-query, knowledge-lint) and commands
6
6
  * (/knowledge:ingest, /knowledge:query, /knowledge:lint) are the schema that tells it how.
7
- * Domain-agnostic on purpose: an agent adds its own page types (`AgentSpec.knowledgePageTypes`),
8
- * or leaves it out of `AgentSpec.extensions` and writes its own rules for `knowledgeDir`.
7
+ * Domain-agnostic on purpose: an agent adds its own page types (`knowledge({ pageTypes })`), or
8
+ * leaves it out of `AgentSpec.extensions` and keeps notes of its own in `extraWritableDirs`.
9
9
  */
10
10
  /** Absolute path of the knowledge base's plugin shipped with the kit, next to `dist/` (or `src/` under tsx). */
11
11
  export declare function knowledgePluginRoot(): string;
@@ -1,12 +1,12 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  /**
4
- * The built-in knowledge base (an "LLM wiki"): `sourcesDir` holds the originals, `knowledgeDir`
5
- * is the wiki the agent maintains through the `knowledge_*` tools (ADR-024), and the rules below
4
+ * The built-in knowledge base (an "LLM wiki"): the sources folder holds the originals, its own
5
+ * folder is the wiki the agent maintains through the `knowledge_*` tools (ADR-024), and the rules below
6
6
  * plus the plugin's skills (knowledge-ingest, knowledge-query, knowledge-lint) and commands
7
7
  * (/knowledge:ingest, /knowledge:query, /knowledge:lint) are the schema that tells it how.
8
- * Domain-agnostic on purpose: an agent adds its own page types (`AgentSpec.knowledgePageTypes`),
9
- * or leaves it out of `AgentSpec.extensions` and writes its own rules for `knowledgeDir`.
8
+ * Domain-agnostic on purpose: an agent adds its own page types (`knowledge({ pageTypes })`), or
9
+ * leaves it out of `AgentSpec.extensions` and keeps notes of its own in `extraWritableDirs`.
10
10
  */
11
11
  /** Absolute path of the knowledge base's plugin shipped with the kit, next to `dist/` (or `src/` under tsx). */
12
12
  export function knowledgePluginRoot() {
@@ -1,12 +1,21 @@
1
- import type { Extension } from "../../core/extensions.js";
1
+ import type { BaseSessionConfig } from "../../core/agentSpec.js";
2
+ import { type Extension, type FolderOption } from "../../core/extensions.js";
2
3
  /** Absolute path of the memory extension's plugin, next to `dist/` (or `src/` under tsx). */
3
4
  export declare function memoryPluginRoot(): string;
4
5
  /** The memory's section of the system prompt: what it is, what it holds now, and its rules. */
5
6
  export declare function memoryPromptSection(index: string): string;
7
+ /** The memory's options. */
8
+ export interface MemoryOptions<TConfig extends BaseSessionConfig = BaseSessionConfig> {
9
+ /**
10
+ * Its folder, the agent's own outside any project (e.g. `~/.miyagi/memory`), kept across all
11
+ * the person's projects and never shared with another agent. Without a folder the extension is off.
12
+ */
13
+ dir: FolderOption<TConfig>;
14
+ }
6
15
  /**
7
- * The memory of the person (`config.memoryDir`, #34): what the agent learns about the person it
8
- * works for, kept across all their projects in a folder of its own, reached only through its
9
- * tools (`recall`, `remember`, `forget`). What it remembers must quote the person's own messages, which it hears through
10
- * a `UserPromptSubmit` hook (and, for a resumed run, from the run's conversation).
16
+ * The memory of the person (#34): what the agent learns about the person it works for, kept
17
+ * across all their projects in a folder of its own, reached only through its tools (`recall`,
18
+ * `remember`, `forget`). What it remembers must quote the person's own messages, which it hears
19
+ * through a `UserPromptSubmit` hook (and, for a resumed run, from the run's conversation).
11
20
  */
12
- export declare const memoryExtension: Extension;
21
+ export declare function memory<TConfig extends BaseSessionConfig = BaseSessionConfig>(options: MemoryOptions<TConfig>): Extension;
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
+ import { folderOf } from "../../core/extensions.js";
3
4
  import { readConversation } from "../../core/runs.js";
4
5
  import { memoryToolLabels } from "./labels.js";
5
6
  import { createMemoryStore } from "./memoryStore.js";
@@ -22,35 +23,37 @@ ${index}
22
23
  - Write entries in the language you reply in.`;
23
24
  }
24
25
  /**
25
- * The memory of the person (`config.memoryDir`, #34): what the agent learns about the person it
26
- * works for, kept across all their projects in a folder of its own, reached only through its
27
- * tools (`recall`, `remember`, `forget`). What it remembers must quote the person's own messages, which it hears through
28
- * a `UserPromptSubmit` hook (and, for a resumed run, from the run's conversation).
26
+ * The memory of the person (#34): what the agent learns about the person it works for, kept
27
+ * across all their projects in a folder of its own, reached only through its tools (`recall`,
28
+ * `remember`, `forget`). What it remembers must quote the person's own messages, which it hears
29
+ * through a `UserPromptSubmit` hook (and, for a resumed run, from the run's conversation).
29
30
  */
30
- export const memoryExtension = {
31
- name: "memory",
32
- plugin: memoryPluginRoot(),
33
- missing: ({ config }) => (config.memoryDir ? undefined : "needs `memoryDir` in the config"),
34
- async contribute({ config, runDir }) {
35
- const memoryDir = config.memoryDir;
36
- const store = createMemoryStore(memoryDir);
37
- // What the person wrote: earlier in a resumed run, then each message as it's sent.
38
- const said = (await readConversation(runDir)).filter((message) => message.role === "user").map((message) => message.text);
39
- const hear = async (input) => {
40
- if (input.hook_event_name === "UserPromptSubmit")
41
- said.push(input.prompt);
42
- return {};
43
- };
44
- return {
45
- mcpServers: { memory: createMemoryServer(store, said) },
46
- promptSection: memoryPromptSection(memoryIndex(await store.list())),
47
- toolOnlyDirs: [{ dir: memoryDir, instead: "its tools: recall, remember, forget" }],
48
- readOnlyTools: ["mcp__memory__recall"],
49
- hooks: { UserPromptSubmit: [{ hooks: [hear] }] },
50
- toolLabels: memoryToolLabels(),
51
- helpLines: [
52
- "You keep a memory of the person across all their projects (how they like things done, who they are): `/memory:list` shows it, `/memory:forget <entry>` forgets an entry. Only what they tell you goes in it.",
53
- ],
54
- };
55
- },
56
- };
31
+ export function memory(options) {
32
+ return {
33
+ name: "memory",
34
+ plugin: memoryPluginRoot(),
35
+ missing: ({ config }) => (folderOf(options.dir, config) ? undefined : "needs a folder (`dir`)"),
36
+ async contribute({ config, runDir }) {
37
+ const memoryDir = folderOf(options.dir, config);
38
+ const store = createMemoryStore(memoryDir);
39
+ // What the person wrote: earlier in a resumed run, then each message as it's sent.
40
+ const said = (await readConversation(runDir)).filter((message) => message.role === "user").map((message) => message.text);
41
+ const hear = async (input) => {
42
+ if (input.hook_event_name === "UserPromptSubmit")
43
+ said.push(input.prompt);
44
+ return {};
45
+ };
46
+ return {
47
+ mcpServers: { memory: createMemoryServer(store, said) },
48
+ promptSection: memoryPromptSection(memoryIndex(await store.list())),
49
+ toolOnlyDirs: [{ dir: memoryDir, instead: "its tools: recall, remember, forget" }],
50
+ readOnlyTools: ["mcp__memory__recall"],
51
+ hooks: { UserPromptSubmit: [{ hooks: [hear] }] },
52
+ toolLabels: memoryToolLabels(),
53
+ helpLines: [
54
+ "You keep a memory of the person across all their projects (how they like things done, who they are): `/memory:list` shows it, `/memory:forget <entry>` forgets an entry. Only what they tell you goes in it.",
55
+ ],
56
+ };
57
+ },
58
+ };
59
+ }
@@ -1,9 +1,21 @@
1
- import type { Extension } from "../../core/extensions.js";
1
+ import type { BaseSessionConfig } from "../../core/agentSpec.js";
2
+ import { type Extension, type FolderOption } from "../../core/extensions.js";
2
3
  /** Absolute path of the sources extension's plugin, next to `dist/` (or `src/` under tsx). */
3
4
  export declare function sourcesPluginRoot(): string;
5
+ /** The sources extension's options. */
6
+ export interface SourcesOptions<TConfig extends BaseSessionConfig = BaseSessionConfig> {
7
+ /**
8
+ * The sources folder: original files kept as obtained, apart from any notes. Material the
9
+ * person drops in, plus whatever the agent saves with `save_to_sources` (downloaded documents,
10
+ * transcripts…). The agent reads and searches it, never edits it, and `save_to_sources` never
11
+ * overwrites. Without a folder the extension is off.
12
+ */
13
+ dir: FolderOption<TConfig>;
14
+ /** The `save_to_sources` tool's description in the agent's own words, instead of the kit's generic one. */
15
+ saveDescription?: string;
16
+ }
4
17
  /**
5
- * The sources folder (`config.sourcesDir`): originals kept as obtained, read with `Read` and
6
- * `extract_text`, added and retired only through its tools (`sourceFiles`). It knows nothing of
7
- * a knowledge base (#30).
18
+ * The sources folder: originals kept as obtained, read with `Read` and `extract_text`, added and
19
+ * retired only through its tools (`sourceFiles`). It knows nothing of a knowledge base (#30).
8
20
  */
9
- export declare const sourcesExtension: Extension;
21
+ export declare function sources<TConfig extends BaseSessionConfig = BaseSessionConfig>(options: SourcesOptions<TConfig>): Extension;