@falkenslab/agent-kit 0.17.0 → 0.19.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 (111) hide show
  1. package/assets/extension-launcher.mjs +24 -0
  2. package/dist/chat/chatController.d.ts +264 -0
  3. package/dist/chat/chatController.js +510 -0
  4. package/dist/chat/extensions.d.ts +14 -0
  5. package/dist/chat/extensions.js +41 -0
  6. package/dist/chat/history.d.ts +17 -0
  7. package/dist/chat/history.js +39 -0
  8. package/dist/{tui → chat}/runs.d.ts +8 -1
  9. package/dist/{tui → chat}/runs.js +1 -1
  10. package/dist/core/agentSpec.d.ts +41 -27
  11. package/dist/core/extensions.d.ts +119 -0
  12. package/dist/core/extensions.js +97 -0
  13. package/dist/core/externalExtensions.d.ts +131 -0
  14. package/dist/core/externalExtensions.js +340 -0
  15. package/dist/core/hooks/fileScopeGate.d.ts +17 -2
  16. package/dist/core/hooks/fileScopeGate.js +45 -10
  17. package/dist/core/hooks/planGate.d.ts +2 -0
  18. package/dist/core/hooks/planGate.js +1 -10
  19. package/dist/core/hooks/stepGate.d.ts +1 -1
  20. package/dist/core/hooks/stepGate.js +3 -3
  21. package/dist/core/language.js +1 -1
  22. package/dist/core/messages/de.js +12 -33
  23. package/dist/core/messages/en.d.ts +13 -16
  24. package/dist/core/messages/en.js +12 -33
  25. package/dist/core/messages/es.js +12 -33
  26. package/dist/core/messages/fr.js +12 -33
  27. package/dist/core/messages/index.d.ts +6 -0
  28. package/dist/core/messages/index.js +12 -1
  29. package/dist/core/packaged.d.ts +10 -0
  30. package/dist/core/packaged.js +38 -0
  31. package/dist/core/pluginAgents.d.ts +20 -0
  32. package/dist/core/pluginAgents.js +76 -0
  33. package/dist/core/runner.js +16 -9
  34. package/dist/core/session.d.ts +42 -5
  35. package/dist/core/session.js +203 -99
  36. package/dist/core/sessionFacts.d.ts +80 -0
  37. package/dist/core/sessionFacts.js +48 -0
  38. package/dist/core/toolLabels.d.ts +24 -6
  39. package/dist/core/toolLabels.js +24 -39
  40. package/dist/core/tools/humanApproval.js +3 -3
  41. package/dist/extensions/awareness/index.d.ts +13 -0
  42. package/dist/extensions/awareness/index.js +39 -0
  43. package/dist/extensions/awareness/labels.d.ts +3 -0
  44. package/dist/extensions/awareness/labels.js +12 -0
  45. package/dist/extensions/awareness/tools.d.ts +5 -0
  46. package/dist/extensions/awareness/tools.js +54 -0
  47. package/dist/{core → extensions/knowledge}/fileKnowledgeStore.d.ts +1 -3
  48. package/dist/{core → extensions/knowledge}/fileKnowledgeStore.js +23 -29
  49. package/dist/extensions/knowledge/index.d.ts +8 -0
  50. package/dist/extensions/knowledge/index.js +35 -0
  51. package/dist/{core → extensions/knowledge}/knowledgeStore.d.ts +5 -9
  52. package/dist/{core → extensions/knowledge}/knowledgeStore.js +16 -2
  53. package/dist/extensions/knowledge/labels.d.ts +3 -0
  54. package/dist/extensions/knowledge/labels.js +119 -0
  55. package/dist/extensions/knowledge/prompt.d.ts +28 -0
  56. package/dist/extensions/knowledge/prompt.js +59 -0
  57. package/dist/{core/tools/knowledgeTools.d.ts → extensions/knowledge/tools.d.ts} +7 -7
  58. package/dist/{core/tools/knowledgeTools.js → extensions/knowledge/tools.js} +20 -55
  59. package/dist/extensions/memory/index.d.ts +12 -0
  60. package/dist/extensions/memory/index.js +56 -0
  61. package/dist/extensions/memory/labels.d.ts +3 -0
  62. package/dist/extensions/memory/labels.js +58 -0
  63. package/dist/extensions/memory/memoryStore.d.ts +53 -0
  64. package/dist/extensions/memory/memoryStore.js +88 -0
  65. package/dist/extensions/memory/tools.d.ts +9 -0
  66. package/dist/extensions/memory/tools.js +82 -0
  67. package/dist/extensions/sources/index.d.ts +9 -0
  68. package/dist/extensions/sources/index.js +40 -0
  69. package/dist/extensions/sources/labels.d.ts +3 -0
  70. package/dist/extensions/sources/labels.js +82 -0
  71. package/dist/{core → extensions/sources}/sources.d.ts +24 -31
  72. package/dist/{core → extensions/sources}/sources.js +45 -108
  73. package/dist/{core/tools/saveToSources.d.ts → extensions/sources/tools.d.ts} +7 -3
  74. package/dist/{core/tools/saveToSources.js → extensions/sources/tools.js} +22 -17
  75. package/dist/index.d.ts +14 -9
  76. package/dist/index.js +11 -7
  77. package/dist/tui/chatTui.d.ts +11 -20
  78. package/dist/tui/chatTui.js +75 -230
  79. package/dist/tui/extensionCommand.d.ts +21 -0
  80. package/dist/tui/extensionCommand.js +133 -0
  81. package/dist/tui/ink/runChatInk.d.ts +4 -3
  82. package/dist/tui/ink/runChatInk.js +101 -217
  83. package/extensions/awareness/.claude-plugin/plugin.json +10 -0
  84. package/{assets/agent-help → extensions/awareness/skills/help}/SKILL.md +4 -3
  85. package/{assets/knowledge-plugin → extensions/knowledge}/.claude-plugin/plugin.json +7 -1
  86. package/extensions/knowledge/commands/ingest.md +7 -0
  87. package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-ingest/SKILL.md +8 -3
  88. package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-lint/SKILL.md +12 -5
  89. package/extensions/memory/.claude-plugin/plugin.json +10 -0
  90. package/extensions/memory/commands/forget.md +8 -0
  91. package/extensions/memory/commands/list.md +5 -0
  92. package/extensions/sources/.claude-plugin/plugin.json +10 -0
  93. package/package.json +3 -2
  94. package/assets/knowledge-plugin/commands/ingest.md +0 -7
  95. package/assets/knowledge-plugin/commands/query.md +0 -5
  96. package/assets/knowledge-plugin-files/.claude-plugin/plugin.json +0 -4
  97. package/assets/knowledge-plugin-files/commands/ingest.md +0 -7
  98. package/assets/knowledge-plugin-files/commands/lint.md +0 -5
  99. package/assets/knowledge-plugin-files/skills/knowledge-ingest/SKILL.md +0 -46
  100. package/assets/knowledge-plugin-files/skills/knowledge-lint/SKILL.md +0 -26
  101. package/assets/knowledge-plugin-files/skills/knowledge-pages/SKILL.md +0 -137
  102. package/assets/knowledge-plugin-files/skills/knowledge-query/SKILL.md +0 -14
  103. package/dist/core/agentHelp.d.ts +0 -30
  104. package/dist/core/agentHelp.js +0 -55
  105. package/dist/core/knowledge.d.ts +0 -25
  106. package/dist/core/knowledge.js +0 -84
  107. /package/dist/{core → extensions/sources}/extractText.d.ts +0 -0
  108. /package/dist/{core → extensions/sources}/extractText.js +0 -0
  109. /package/{assets/knowledge-plugin → extensions/knowledge}/commands/lint.md +0 -0
  110. /package/{assets/knowledge-plugin-files → extensions/knowledge}/commands/query.md +0 -0
  111. /package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-query/SKILL.md +0 -0
@@ -3,11 +3,11 @@ import { t } from "./messages/index.js";
3
3
  * Turns a raw tool name + input into a short, readable console description, instead of
4
4
  * the tool's technical name (e.g. "mcp__playwright__browser_click"). Covers the tools
5
5
  * this kit itself provides (Read/Write/Edit/Glob/Grep/Bash/Agent/WebFetch/WebSearch/Skill,
6
- * plus its own human-approval/manual-intervention/save-to-sources MCP tools)
7
- * generically; a
8
- * concrete agent supplies a `describe` callback for its own domain-specific tools (e.g.
9
- * Playwright's browser_* cases) via `createFriendlyToolLabel()`. The kit's own labels are in
10
- * the current language (see messages/).
6
+ * plus its own human-approval/manual-intervention/clock MCP tools) generically; an extension
7
+ * brings its tools' labels with its contribution (`ToolLabels`, ADR-025), and a concrete agent
8
+ * supplies a `describe` callback for its own domain-specific tools (e.g. Playwright's
9
+ * browser_* cases) via `createFriendlyToolLabel()`. The kit's own labels are in the current
10
+ * language (see messages/).
11
11
  */
12
12
  /** Cuts `text` to `max` characters, with "…" at the end. */
13
13
  export function truncate(text, max = 60) {
@@ -37,38 +37,6 @@ function describeCore(shortName, input) {
37
37
  return labels.checkingTime;
38
38
  case "date_math":
39
39
  return labels.calculatingDates;
40
- case "knowledge_index":
41
- return labels.knowledgeIndex;
42
- case "knowledge_search":
43
- return labels.knowledgeSearch(truncate(text(input.query, ""), 60));
44
- case "knowledge_read":
45
- return labels.knowledgeRead(text(input.page, labels.aPage));
46
- case "knowledge_create":
47
- return labels.knowledgeCreate(`${text(input.type, "")}/${text(input.slug, "")}`);
48
- case "knowledge_edit":
49
- return labels.knowledgeEdit(text(input.page, labels.aPage));
50
- case "knowledge_rewrite":
51
- return labels.knowledgeRewrite(text(input.page, labels.aPage));
52
- case "knowledge_supersede":
53
- return labels.knowledgeSupersede(text(input.page, labels.aPage));
54
- case "knowledge_retire":
55
- return labels.knowledgeRetire(text(input.page, labels.aPage));
56
- case "knowledge_log":
57
- return labels.knowledgeLog;
58
- case "knowledge_check":
59
- return labels.knowledgeCheck;
60
- case "list_sources":
61
- return labels.listingSources;
62
- case "extract_text":
63
- return labels.extractingText(truncatePath(text(input.source, labels.aFile), 70));
64
- case "download_to_sources":
65
- return labels.downloading(truncate(text(input.url, labels.aPage), 80));
66
- case "request_file":
67
- return labels.requestingFile(truncate(text(input.description, ""), 100));
68
- case "retire_source":
69
- return labels.retiringSource(truncatePath(text(input.source, ""), 70));
70
- case "save_to_sources":
71
- return labels.savingToSources(truncatePath(text(input.destination, ""), 70));
72
40
  case "Read":
73
41
  return labels.reading(truncatePath(text(input.file_path, labels.aFile), 80));
74
42
  case "Write":
@@ -103,15 +71,32 @@ function describeCore(shortName, input) {
103
71
  return undefined;
104
72
  }
105
73
  }
74
+ /**
75
+ * A `formatAction` that shows the tools in `labels()` with their label, and any other with
76
+ * `formatAction` (the kit's by default). `labels` is read on every call, so it can follow the
77
+ * session in use (`/resume` opens another).
78
+ */
79
+ export function withToolLabels(labels, formatAction = createFriendlyToolLabel()) {
80
+ return (toolName, toolInput) => {
81
+ const own = labels()?.[toolName];
82
+ if (!own)
83
+ return formatAction(toolName, toolInput);
84
+ return own.label(toolInput && typeof toolInput === "object" ? toolInput : {});
85
+ };
86
+ }
87
+ /** A `toolPhrase` that counts the tools in `labels()` with their phrase, and any other with `toolPhrase`. */
88
+ export function withToolPhrases(labels, toolPhrase) {
89
+ return (toolName) => labels()?.[toolName]?.phrase ?? toolPhrase?.(toolName);
90
+ }
106
91
  /**
107
92
  * Builds a `friendlyToolLabel(toolName, toolInput)` — `describe` lets the host agent
108
93
  * layer its own domain-specific cases (e.g. Playwright's browser_* tools) on top of this
109
94
  * kit's generic ones; `extraLocalServers` names any *additional* MCP server (beyond this
110
- * kit's own "approvals"/"manualLogin"/"sourceFiles"/"time") whose tools should be unwrapped
95
+ * kit's own "approvals"/"manualLogin"/"time") whose tools should be unwrapped
111
96
  * without a "[server] " prefix, e.g. "playwright".
112
97
  */
113
98
  export function createFriendlyToolLabel(options = {}) {
114
- const localServers = new Set(["approvals", "manualLogin", "sourceFiles", "time", "knowledge", ...(options.extraLocalServers ?? [])]);
99
+ const localServers = new Set(["approvals", "manualLogin", "time", ...(options.extraLocalServers ?? [])]);
115
100
  const describe = (shortName, input) => options.describe?.(shortName, input) ?? describeCore(shortName, input) ?? shortName.replace(/_/g, " ");
116
101
  return (toolName, toolInput) => {
117
102
  const input = toolInput && typeof toolInput === "object" ? toolInput : {};
@@ -34,8 +34,8 @@ export function createHumanApprovalServer(runDir, texts = DEFAULT_HUMAN_APPROVAL
34
34
  };
35
35
  });
36
36
  const askHuman = tool("ask_human", "Ask the person to choose between options, in a panel, and get the answer in this same turn, instead of ending your turn with a written question. Use it when you need their decision to go on (which approach, which of several items). They can also type their own answer.", {
37
- question: z.string().describe("The question, in one sentence, in the person's language"),
38
- options: z.array(z.string()).min(2).max(8).describe("The options, short, in the person's language (2 to 8; an \"Other\" for their own answer is added)"),
37
+ question: z.string().describe("The question, in one sentence, in the language you reply in"),
38
+ options: z.array(z.string()).min(2).max(8).describe("The options, short, in the language you reply in (2 to 8; an \"Other\" for their own answer is added)"),
39
39
  multiple: z.boolean().optional().describe("Whether they can pick several (default: one)"),
40
40
  }, async (args) => {
41
41
  const answer = await askForChoice(runDir, { title: t().questionTitle, lines: [args.question] }, args.options, args.multiple ?? false);
@@ -46,7 +46,7 @@ export function createHumanApprovalServer(runDir, texts = DEFAULT_HUMAN_APPROVAL
46
46
  return ok(parts.length ? parts.join("\n") : "The person didn't answer. Go on with what you can, or ask in your reply.");
47
47
  });
48
48
  const control = options.modeControl;
49
- const presentPlan = tool("present_plan", "In plan mode, when the plan is ready: show it to the person, who decides to run it (plan mode ends and you carry it out right away, in this turn), to keep planning (with a comment on what to change) or to cancel. Outside plan mode it's refused.", { plan: z.string().describe("The whole plan, in markdown, in the person's language: steps, what changes, what to check") }, async (args) => {
49
+ const presentPlan = tool("present_plan", "In plan mode, when the plan is ready: show it to the person, who decides to run it (plan mode ends and you carry it out right away, in this turn), to keep planning (with a comment on what to change) or to cancel. Outside plan mode it's refused.", { plan: z.string().describe("The whole plan, in markdown, in the language you reply in: steps, what changes, what to check") }, async (args) => {
50
50
  if (!control || control.mode !== "plan")
51
51
  return { ...ok("present_plan only works in plan mode: you aren't in it, so just go ahead."), isError: true };
52
52
  const choices = [t().runPlan, t().keepPlanning, t().cancelPlan];
@@ -0,0 +1,13 @@
1
+ import type { AgentIdentity } from "../../core/agentSpec.js";
2
+ import type { Extension } from "../../core/extensions.js";
3
+ /** Absolute path of the awareness extension's plugin, next to `dist/` (or `src/` under tsx). */
4
+ export declare function awarenessPluginRoot(): string;
5
+ /** The "Who you are" section of the system prompt: name, version, what it is, the kit's version, and where to look. */
6
+ export declare function identityPromptSection(identity: AgentIdentity): string;
7
+ /**
8
+ * The awareness (#43): the agent knows who it is and what it is at each moment of a session,
9
+ * 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.
12
+ */
13
+ export declare const awarenessExtension: Extension;
@@ -0,0 +1,39 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { agentKitVersion } from "../../core/version.js";
4
+ import { awarenessToolLabels } from "./labels.js";
5
+ import { createAwarenessServer } from "./tools.js";
6
+ /** Absolute path of the awareness extension's plugin, next to `dist/` (or `src/` under tsx). */
7
+ export function awarenessPluginRoot() {
8
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "extensions", "awareness");
9
+ }
10
+ /** The "Who you are" section of the system prompt: name, version, what it is, the kit's version, and where to look. */
11
+ export function identityPromptSection(identity) {
12
+ const who = `${identity.name}${identity.version ? ` ${identity.version}` : ""}${identity.description ? `: ${identity.description}` : ""}`;
13
+ return `## Who you are
14
+ You are ${who}. You are built on agent-kit ${agentKitVersion()}, which provides your chat (its modes, keys and slash commands) and your built-in tools.
15
+ - When the person asks about you as you are now (your mode, what you can do, your extensions and their tools, what's off and why, your subagents, commands, how full your context is), call \`about_me\` and answer from it: it changes during the session, so never from memory or from an earlier answer.
16
+ - Your mode is the kit's (guided, interactive, autonomous or plan), not a permission mode you may see elsewhere: you don't know which one is on until \`about_me\` says.
17
+ - When they ask about your domain (your own commands, configuration, folders), \`about_me\` with \`part: "guide"\`.
18
+ - When they ask how to use you (the chat's modes, keys and commands, approvals, resuming, the command line), load the \`help\` skill.
19
+ - Don't invent what none of them says.`;
20
+ }
21
+ /**
22
+ * The awareness (#43): the agent knows who it is and what it is at each moment of a session,
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.
26
+ */
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
+ };
@@ -0,0 +1,3 @@
1
+ import type { ToolLabels } from "../../core/toolLabels.js";
2
+ /** How the chat shows the awareness's tool (server `awareness`), in the kit's language. */
3
+ export declare function awarenessToolLabels(): ToolLabels;
@@ -0,0 +1,12 @@
1
+ import { getLanguage } from "../../core/messages/index.js";
2
+ const TEXTS = {
3
+ en: { now: "Looking at itself", guide: "Reading its own guide", phrase: ["looked at itself", "looked at itself {n} times"] },
4
+ es: { now: "Mirándose a sí mismo", guide: "Leyendo su propia guía", phrase: ["se miró a sí mismo", "se miró a sí mismo {n} veces"] },
5
+ fr: { now: "Se regarde lui-même", guide: "Lit son propre guide", phrase: ["s'est regardé lui-même", "s'est regardé lui-même {n} fois"] },
6
+ de: { now: "Betrachtet sich selbst", guide: "Liest seine eigene Anleitung", phrase: ["sich selbst betrachtet", "sich selbst {n}-mal betrachtet"] },
7
+ };
8
+ /** How the chat shows the awareness's tool (server `awareness`), in the kit's language. */
9
+ export function awarenessToolLabels() {
10
+ const texts = TEXTS[getLanguage()];
11
+ return { mcp__awareness__about_me: { label: (input) => (input.part === "guide" ? texts.guide : texts.now), phrase: texts.phrase } };
12
+ }
@@ -0,0 +1,5 @@
1
+ import type { SessionFacts } from "../../core/sessionFacts.js";
2
+ /** The session's facts as the model reads them: markdown, in English (ADR-019). */
3
+ export declare function describeSession(facts: SessionFacts): string;
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;
@@ -0,0 +1,54 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
3
+ import { z } from "zod";
4
+ /**
5
+ * `about_me`, the awareness's one tool (#43): what the agent is right now, read from the
6
+ * session on every call, so it answers with the mode after Shift+Tab, the extensions after
7
+ * `/extensions enable`, the context as it fills; or the agent's own guide to its domain.
8
+ */
9
+ const ok = (text) => ({ content: [{ type: "text", text }] });
10
+ /** A tool's name as the agent's extensions give it, without the `mcp__<server>__` prefix. */
11
+ const short = (tool) => tool.replace(/^mcp__.+?__/, "");
12
+ /** The session's facts as the model reads them: markdown, in English (ADR-019). */
13
+ export function describeSession(facts) {
14
+ const who = facts.identity
15
+ ? `${facts.identity.name}${facts.identity.version ? ` ${facts.identity.version}` : ""}${facts.identity.description ? `: ${facts.identity.description}` : ""}`
16
+ : "an agent";
17
+ const sections = [`# You, right now\n\nYou are ${who}, on agent-kit ${facts.kitVersion}.`];
18
+ const others = facts.switchableModes.filter((mode) => mode !== facts.mode);
19
+ sections.push(`## Mode\n\nYou are in **${facts.mode}** mode now. ${facts.switchableModes.length > 1 ? `The person can switch it with Shift+Tab to ${others.join(" or ")}.` : "It can't be switched in this session."}`);
20
+ const active = facts.extensions.active.map(({ name, description, tools, help }) => {
21
+ const toolList = tools.length ? ` Tools: ${tools.map(short).join(", ")}.` : "";
22
+ return `- **${name}**: ${description}${toolList}${help.map((line) => `\n ${line}`).join("")}`;
23
+ });
24
+ const inactive = facts.extensions.inactive.map(({ name, reason }) => `- **${name}** is off: it ${reason}.`);
25
+ sections.push(`## Extensions\n\n${[...active, ...inactive].join("\n") || "None."}`);
26
+ if (facts.subagents.length)
27
+ sections.push(`## Subagents you can delegate to\n\n${facts.subagents.map(({ name, description }) => `- **${name}**: ${description}`).join("\n")}`);
28
+ const own = facts.tools.filter((tool) => !tool.startsWith("mcp__"));
29
+ if (own.length)
30
+ sections.push(`## Built-in tools\n\n${own.join(", ")}.`);
31
+ if (facts.commands.length) {
32
+ sections.push(`## Commands the person can type\n\n${facts.commands.map(({ name, description, argumentHint }) => `- \`/${name}${argumentHint ? ` ${argumentHint}` : ""}\`${description ? `: ${description}` : ""}`).join("\n")}`);
33
+ }
34
+ else if (facts.skills.length) {
35
+ sections.push(`## Skills\n\n${facts.skills.join(", ")}.`);
36
+ }
37
+ if (facts.context) {
38
+ const thousands = (tokens) => `${Math.round(tokens / 1000)}k`;
39
+ sections.push(`## Context\n\n${Math.round(facts.context.percentage)}% of the context window used (${thousands(facts.context.totalTokens)} of ${thousands(facts.context.maxTokens)} tokens).`);
40
+ }
41
+ return sections.join("\n\n");
42
+ }
43
+ /** The awareness's MCP server: `about_me`, from the session's facts and the agent's guide. */
44
+ export function createAwarenessServer(session, helpGuide) {
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
+ if (part === "guide") {
47
+ if (!helpGuide)
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"));
50
+ }
51
+ return ok(describeSession(await session()));
52
+ }, { annotations: { readOnlyHint: true } });
53
+ return createSdkMcpServer({ name: "awareness", version: "1.0.0", tools: [aboutMe] });
54
+ }
@@ -1,10 +1,8 @@
1
1
  import { type KnowledgeStore, type PageType } from "./knowledgeStore.js";
2
2
  /** Options of `createFileKnowledgeStore()`. */
3
3
  export interface FileKnowledgeStoreOptions {
4
- /** The agent's own page types, besides the kit's four. */
4
+ /** The agent's own page types, besides the kit's. */
5
5
  pageTypes?: readonly PageType[];
6
- /** The sources folder: a summary's `file` field is given relative to it. */
7
- sourcesDir?: string;
8
6
  }
9
7
  /** A `KnowledgeStore` over the markdown files of `knowledgeDir`. */
10
8
  export declare function createFileKnowledgeStore(knowledgeDir: string, options?: FileKnowledgeStoreOptions): KnowledgeStore;
@@ -27,6 +27,8 @@ function serialize(fields, body) {
27
27
  const lines = [...fields].map(([key, value]) => `${key}: ${/[:#]/.test(value) && !/^\[.*\]$/.test(value) ? JSON.stringify(value) : value}`);
28
28
  return `---\n${lines.join("\n")}\n---\n\n${body.replace(/^\s+/, "").replace(/\s*$/, "")}\n`;
29
29
  }
30
+ /** When a summary was written from its original (ISO 8601, as `list_sources`' `changedAt`). */
31
+ const stampNow = () => new Date().toISOString();
30
32
  /** A `KnowledgeStore` over the markdown files of `knowledgeDir`. */
31
33
  export function createFileKnowledgeStore(knowledgeDir, options = {}) {
32
34
  const declared = options.pageTypes ?? [];
@@ -134,9 +136,9 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
134
136
  for (const [i, page] of ready.entries()) {
135
137
  const fields = newPages[i].fields ?? {};
136
138
  page.body = toFile(page.body, page.file);
137
- if (page.type.type === "summary" && !("ingested" in fields))
138
- page.fields.set("ingested", today());
139
- await applyFields(page, fields); // may refuse a summary's missing original, before anything is written
139
+ await applyFields(page, fields);
140
+ if (page.type.type === "summary")
141
+ page.fields.set("ingested", stampNow());
140
142
  }
141
143
  for (const page of ready) {
142
144
  await mkdir(path.dirname(page.file), { recursive: true });
@@ -159,28 +161,20 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
159
161
  if (missing.length)
160
162
  throw new Error(`These links point to pages that don't exist: ${missing.join(", ")}. Create them first, or link existing ones (knowledge_search).`);
161
163
  }
162
- /** A field given by the model, made ready to store: `file` (relative to sources/) becomes a link from the page. */
163
- async function storedField(key, value, file) {
164
- if (key !== "file" || !options.sourcesDir || /^[a-z]+:\/\//i.test(value))
165
- return value;
166
- // Relative to the sources folder; a leading "sources/" or the folder's own name is dropped.
167
- let relative = value.replace(/\\/g, "/").replace(/^\.\//, "");
168
- for (const prefix of ["sources/", `${path.basename(options.sourcesDir)}/`])
169
- if (relative.startsWith(prefix))
170
- relative = relative.slice(prefix.length);
171
- const original = path.resolve(options.sourcesDir, relative);
172
- if (!(await stat(original).catch(() => null))?.isFile())
173
- throw new Error(`"${value}" isn't an original in sources/ (list_sources lists them).`);
174
- return posix(path.relative(path.dirname(file), original));
175
- }
164
+ /**
165
+ * Sets or removes the fields given. `type` is the page's, and a summary's `ingested` is the
166
+ * store's (when it was written from its original), so neither is taken from the caller.
167
+ * `file` is kept as given: the original's path relative to the sources folder, which the
168
+ * store doesn't check (the sources are another extension's data, #30).
169
+ */
176
170
  async function applyFields(page, changes = {}) {
177
171
  for (const [key, value] of Object.entries(changes)) {
178
- if (key === "type")
172
+ if (key === "type" || key === "ingested")
179
173
  continue;
180
174
  if (value === null)
181
175
  page.fields.delete(key);
182
176
  else
183
- page.fields.set(key, await storedField(key, value, page.file));
177
+ page.fields.set(key, value);
184
178
  }
185
179
  page.fields.set("updated", today());
186
180
  }
@@ -191,7 +185,8 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
191
185
  }
192
186
  /** The notice under the frontmatter of a superseded or retired page. */
193
187
  const withNotice = (body, notice) => `> ${notice}\n\n${body.replace(/^> (Superseded|Retired) on [^\n]*\n\n/, "")}`;
194
- async function indexText() {
188
+ /** The catalog: with links as ids (`summary/kitchen`) for the tools, or as files for `index.md`. */
189
+ async function indexText(links = "ids") {
195
190
  const pages = (await allPages()).filter((p) => statusOf(p) !== "retired");
196
191
  const lines = ["# Index", ""];
197
192
  for (const type of types) {
@@ -202,7 +197,7 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
202
197
  for (const page of own) {
203
198
  const extras = (type.indexFields ?? []).filter((f) => page.fields.get(f)).map((f) => `${f}: ${page.fields.get(f)}`);
204
199
  const status = statusOf(page) === "superseded" ? " (superseded)" : "";
205
- lines.push(`- [${titleOf(page)}](${posix(path.relative(knowledgeDir, page.file))}) — ${descriptionOf(page)}${extras.length ? ` (${extras.join(", ")})` : ""}${status}`);
200
+ lines.push(`- [${titleOf(page)}](${links === "ids" ? page.id : posix(path.relative(knowledgeDir, page.file))}) — ${descriptionOf(page)}${extras.length ? ` (${extras.join(", ")})` : ""}${status}`);
206
201
  }
207
202
  lines.push("");
208
203
  }
@@ -210,7 +205,7 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
210
205
  }
211
206
  async function writeIndex() {
212
207
  await mkdir(knowledgeDir, { recursive: true });
213
- await writeFile(path.join(knowledgeDir, "index.md"), await indexText());
208
+ await writeFile(path.join(knowledgeDir, "index.md"), await indexText("files"));
214
209
  }
215
210
  async function pagesByFile() {
216
211
  const pages = await allPages();
@@ -265,6 +260,9 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
265
260
  await linksResolve(content, id);
266
261
  page.body = toFile(/^#\s/.test(content.trimStart()) ? content : `# ${titleOf(page)}\n\n${content}`, page.file);
267
262
  await applyFields(page, fields);
263
+ // A summary redone whole is redone from its original: written now.
264
+ if (page.type.type === "summary")
265
+ page.fields.set("ingested", stampNow());
268
266
  await save(page);
269
267
  },
270
268
  async supersede(id, by, reason) {
@@ -325,7 +323,7 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
325
323
  .slice(0, limit)
326
324
  .map(({ id, title, snippet }) => ({ id, title, snippet }));
327
325
  },
328
- index: indexText,
326
+ index: () => indexText("ids"),
329
327
  async recentLog(limit = 10) {
330
328
  const text = await readFile(path.join(knowledgeDir, "log.md"), "utf8").catch(() => "");
331
329
  // Entries start with "## [date] operation | what"; the newest are at the bottom.
@@ -345,7 +343,7 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
345
343
  const reservedFiles = new Set([...RESERVED].map((name) => path.normalize(path.join(knowledgeDir, `${name}.md`))));
346
344
  const known = new Map(pages.map((p) => [p.id, p]));
347
345
  const inbound = new Map();
348
- const report = { brokenLinks: [], orphans: [], linksToRetired: [], missingOriginals: [] };
346
+ const report = { brokenLinks: [], orphans: [], linksToRetired: [] };
349
347
  for (const page of pages) {
350
348
  if (statusOf(page) === "retired")
351
349
  continue;
@@ -370,13 +368,9 @@ export function createFileKnowledgeStore(knowledgeDir, options = {}) {
370
368
  if (id !== page.id)
371
369
  inbound.set(id, (inbound.get(id) ?? 0) + 1);
372
370
  }
373
- const file = page.fields.get("file");
374
- if (file && !/^[a-z]+:\/\//i.test(file) && !(await stat(path.resolve(path.dirname(page.file), file)).catch(() => null))) {
375
- report.missingOriginals.push({ page: page.id, file });
376
- }
377
371
  }
378
372
  report.orphans = pages
379
- .filter((p) => statusOf(p) !== "retired" && p.type.type !== "summary" && p.type.type !== "synthesis" && !inbound.get(p.id))
373
+ .filter((p) => statusOf(p) !== "retired" && !["summary", "synthesis", "preference"].includes(p.type.type) && !inbound.get(p.id))
380
374
  .map((p) => p.id);
381
375
  return report;
382
376
  },
@@ -0,0 +1,8 @@
1
+ import type { Extension } from "../../core/extensions.js";
2
+ /**
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.
7
+ */
8
+ export declare const knowledgeExtension: Extension;
@@ -0,0 +1,35 @@
1
+ import { createFileKnowledgeStore } from "./fileKnowledgeStore.js";
2
+ import { knowledgeToolLabels } from "./labels.js";
3
+ import { knowledgePluginRoot, knowledgePromptSection } from "./prompt.js";
4
+ import { createKnowledgeServer } from "./tools.js";
5
+ /**
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.
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
+ };
@@ -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;
@@ -44,6 +44,7 @@ export interface PageInfo {
44
44
  status: string;
45
45
  fields: Record<string, string>;
46
46
  }
47
+ /** A page that `search()` found, with where it matched. */
47
48
  export interface SearchHit {
48
49
  id: string;
49
50
  title: string;
@@ -57,18 +58,13 @@ export interface CheckReport {
57
58
  page: string;
58
59
  target: string;
59
60
  }[];
60
- /** Pages nothing links to (summaries and syntheses aside, which the index reaches). */
61
+ /** Pages nothing links to (summaries, syntheses and preferences aside, which the index reaches). */
61
62
  orphans: string[];
62
63
  /** Links to retired pages. */
63
64
  linksToRetired: {
64
65
  page: string;
65
66
  target: string;
66
67
  }[];
67
- /** Summary pages whose original is gone from the sources folder. */
68
- missingOriginals: {
69
- page: string;
70
- file: string;
71
- }[];
72
68
  }
73
69
  /** A page to create. */
74
70
  export interface NewPage {
@@ -110,7 +106,7 @@ export interface KnowledgeStore {
110
106
  retire(id: string, reason: string): Promise<void>;
111
107
  /** Pages matching words of `query` in their title, aliases or body, best first. */
112
108
  search(query: string, limit?: number): Promise<SearchHit[]>;
113
- /** The catalog: every active or superseded page, one line each, by section. */
109
+ /** The catalog: every active or superseded page, one line each, by section, linked by id (`concept/bowline`), as everything the model reads. */
114
110
  index(): Promise<string>;
115
111
  /** Adds an entry to the operation log, dated today. */
116
112
  log(operation: string, what: string, pages?: readonly string[]): Promise<void>;
@@ -126,7 +122,7 @@ export interface KnowledgeStore {
126
122
  /** Replaces the overview. */
127
123
  writeOverview(content: string): Promise<void>;
128
124
  }
129
- /** The kit's page types (ADR-008). */
125
+ /** The kit's page types (ADR-008), and the person's preferences (#33). */
130
126
  export declare const BUILT_IN_PAGE_TYPES: readonly PageType[];
131
127
  /** Whether `slug` is lowercase ASCII words joined by hyphens. */
132
128
  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. */
@@ -0,0 +1,3 @@
1
+ import { type ToolLabels } from "../../core/toolLabels.js";
2
+ /** How the chat shows the knowledge_* tools (server `knowledge`), in the kit's language. */
3
+ export declare function knowledgeToolLabels(): ToolLabels;