@falkenslab/agent-kit 0.18.0 → 0.20.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 (102) hide show
  1. package/assets/extension-launcher.mjs +24 -0
  2. package/dist/chat/chatController.d.ts +267 -0
  3. package/dist/chat/chatController.js +536 -0
  4. package/dist/chat/extensions.d.ts +18 -0
  5. package/dist/chat/extensions.js +44 -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 +11 -1
  9. package/dist/{tui → chat}/runs.js +1 -1
  10. package/dist/core/agentSpec.d.ts +28 -50
  11. package/dist/core/extensions.d.ts +128 -0
  12. package/dist/core/extensions.js +87 -0
  13. package/dist/core/externalExtensions.d.ts +143 -0
  14. package/dist/core/externalExtensions.js +368 -0
  15. package/dist/core/hooks/planGate.d.ts +2 -0
  16. package/dist/core/hooks/planGate.js +1 -10
  17. package/dist/core/hooks/stepGate.d.ts +1 -1
  18. package/dist/core/hooks/stepGate.js +3 -3
  19. package/dist/core/liveExtensions.d.ts +51 -0
  20. package/dist/core/liveExtensions.js +70 -0
  21. package/dist/core/marketplaces.d.ts +118 -0
  22. package/dist/core/marketplaces.js +242 -0
  23. package/dist/core/messages/de.js +12 -32
  24. package/dist/core/messages/en.d.ts +15 -16
  25. package/dist/core/messages/en.js +12 -32
  26. package/dist/core/messages/es.js +12 -32
  27. package/dist/core/messages/fr.js +12 -32
  28. package/dist/core/messages/index.d.ts +6 -0
  29. package/dist/core/messages/index.js +12 -1
  30. package/dist/core/packaged.d.ts +10 -0
  31. package/dist/core/packaged.js +38 -0
  32. package/dist/core/pluginAgents.d.ts +20 -0
  33. package/dist/core/pluginAgents.js +76 -0
  34. package/dist/core/pluginDownloads.d.ts +14 -0
  35. package/dist/core/pluginDownloads.js +141 -0
  36. package/dist/core/runner.js +21 -9
  37. package/dist/core/session.d.ts +38 -6
  38. package/dist/core/session.js +200 -99
  39. package/dist/core/sessionFacts.d.ts +85 -0
  40. package/dist/core/sessionFacts.js +49 -0
  41. package/dist/core/toolLabels.d.ts +24 -6
  42. package/dist/core/toolLabels.js +24 -39
  43. package/dist/extensions/awareness/index.d.ts +22 -0
  44. package/dist/extensions/awareness/index.js +42 -0
  45. package/dist/extensions/awareness/labels.d.ts +3 -0
  46. package/dist/extensions/awareness/labels.js +12 -0
  47. package/dist/extensions/awareness/tools.d.ts +5 -0
  48. package/dist/extensions/awareness/tools.js +54 -0
  49. package/dist/extensions/knowledge/index.d.ts +22 -0
  50. package/dist/extensions/knowledge/index.js +38 -0
  51. package/dist/{core → extensions/knowledge}/knowledgeStore.d.ts +2 -1
  52. package/dist/extensions/knowledge/labels.d.ts +3 -0
  53. package/dist/extensions/knowledge/labels.js +119 -0
  54. package/dist/{core/knowledge.d.ts → extensions/knowledge/prompt.d.ts} +4 -4
  55. package/dist/{core/knowledge.js → extensions/knowledge/prompt.js} +10 -7
  56. package/dist/{core/tools/knowledgeTools.d.ts → extensions/knowledge/tools.d.ts} +1 -1
  57. package/dist/{core/tools/knowledgeTools.js → extensions/knowledge/tools.js} +2 -2
  58. package/dist/extensions/memory/index.d.ts +21 -0
  59. package/dist/extensions/memory/index.js +59 -0
  60. package/dist/extensions/memory/labels.d.ts +3 -0
  61. package/dist/extensions/memory/labels.js +58 -0
  62. package/dist/extensions/memory/memoryStore.d.ts +53 -0
  63. package/dist/extensions/memory/memoryStore.js +88 -0
  64. package/dist/extensions/memory/tools.d.ts +9 -0
  65. package/dist/extensions/memory/tools.js +82 -0
  66. package/dist/extensions/sources/index.d.ts +21 -0
  67. package/dist/extensions/sources/index.js +42 -0
  68. package/dist/extensions/sources/labels.d.ts +3 -0
  69. package/dist/extensions/sources/labels.js +82 -0
  70. package/dist/{core/tools/saveToSources.d.ts → extensions/sources/tools.d.ts} +1 -1
  71. package/dist/{core/tools/saveToSources.js → extensions/sources/tools.js} +5 -5
  72. package/dist/index.d.ts +19 -9
  73. package/dist/index.js +16 -7
  74. package/dist/tui/chatTui.d.ts +11 -20
  75. package/dist/tui/chatTui.js +75 -230
  76. package/dist/tui/extensionCommand.d.ts +30 -0
  77. package/dist/tui/extensionCommand.js +257 -0
  78. package/dist/tui/ink/runChatInk.d.ts +4 -3
  79. package/dist/tui/ink/runChatInk.js +101 -217
  80. package/extensions/awareness/.claude-plugin/plugin.json +10 -0
  81. package/{assets/agent-help → extensions/awareness/skills/help}/SKILL.md +4 -3
  82. package/{assets/knowledge-plugin → extensions/knowledge}/.claude-plugin/plugin.json +7 -1
  83. package/extensions/memory/.claude-plugin/plugin.json +10 -0
  84. package/extensions/memory/commands/forget.md +8 -0
  85. package/extensions/memory/commands/list.md +5 -0
  86. package/extensions/sources/.claude-plugin/plugin.json +10 -0
  87. package/package.json +3 -2
  88. package/dist/core/agentHelp.d.ts +0 -30
  89. package/dist/core/agentHelp.js +0 -55
  90. /package/dist/{core → extensions/knowledge}/fileKnowledgeStore.d.ts +0 -0
  91. /package/dist/{core → extensions/knowledge}/fileKnowledgeStore.js +0 -0
  92. /package/dist/{core → extensions/knowledge}/knowledgeStore.js +0 -0
  93. /package/dist/{core → extensions/sources}/extractText.d.ts +0 -0
  94. /package/dist/{core → extensions/sources}/extractText.js +0 -0
  95. /package/dist/{core → extensions/sources}/sources.d.ts +0 -0
  96. /package/dist/{core → extensions/sources}/sources.js +0 -0
  97. /package/{assets/knowledge-plugin → extensions/knowledge}/commands/ingest.md +0 -0
  98. /package/{assets/knowledge-plugin → extensions/knowledge}/commands/lint.md +0 -0
  99. /package/{assets/knowledge-plugin → extensions/knowledge}/commands/query.md +0 -0
  100. /package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-ingest/SKILL.md +0 -0
  101. /package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-lint/SKILL.md +0 -0
  102. /package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-query/SKILL.md +0 -0
@@ -1,5 +1,7 @@
1
1
  import type { Options } from "@anthropic-ai/claude-agent-sdk";
2
- import type { ModeControl } from "../core/session.js";
2
+ import type { ExtensionsStatus, ModeControl } from "../core/session.js";
3
+ import type { ToolLabels } from "../core/toolLabels.js";
4
+ import type { ExtensionSwitch } from "../core/liveExtensions.js";
3
5
  import { type RunFolder, type RunSummary } from "../core/runs.js";
4
6
  /**
5
7
  * Builds the session's options for a run folder, e.g.
@@ -10,11 +12,19 @@ import { type RunFolder, type RunSummary } from "../core/runs.js";
10
12
  export type SessionOpener = (run: RunFolder) => Promise<Options | {
11
13
  options: Options;
12
14
  modeControl?: ModeControl;
15
+ toolLabels?: ToolLabels;
16
+ extensions?: ExtensionsStatus;
17
+ apis?: Record<string, Record<string, unknown>>;
18
+ switchExtension?: ExtensionSwitch;
13
19
  }>;
14
20
  export interface OpenedSession {
15
21
  run: RunFolder;
16
22
  options: Options;
17
23
  modeControl?: ModeControl;
24
+ toolLabels?: ToolLabels;
25
+ extensions?: ExtensionsStatus;
26
+ apis?: Record<string, Record<string, unknown>>;
27
+ switchExtension?: ExtensionSwitch;
18
28
  }
19
29
  /** The run a chat starts with: the latest one with `--continue` (if any), a new one otherwise. */
20
30
  export declare function firstRun(runsDir: string, argv?: readonly string[]): Promise<RunFolder>;
@@ -13,7 +13,7 @@ export async function firstRun(runsDir, argv = process.argv.slice(2)) {
13
13
  export async function openSession(opener, run) {
14
14
  const opened = await opener(run);
15
15
  // The SDK's Options has no "options" field: this is buildSessionOptions()'s result.
16
- return "options" in opened ? { run, options: opened.options, modeControl: opened.modeControl } : { run, options: opened };
16
+ return "options" in opened ? { run, options: opened.options, modeControl: opened.modeControl, toolLabels: opened.toolLabels, extensions: opened.extensions, apis: opened.apis, switchExtension: opened.switchExtension } : { run, options: opened };
17
17
  }
18
18
  /** A run's date for a person, in the kit's language. */
19
19
  export function runDate(date) {
@@ -1,5 +1,6 @@
1
+ import type { ExtensionDirs } from "./externalExtensions.js";
1
2
  import type { AgentDefinition as SdkSubagentDefinition, McpServerConfig, SettingSource } from "@anthropic-ai/claude-agent-sdk";
2
- import type { KnowledgeStore, PageType } from "./knowledgeStore.js";
3
+ import type { Extension } from "./extensions.js";
3
4
  /**
4
5
  * The human-supervision spectrum every agent built on this kit shares, independent of
5
6
  * domain: "interactive" pauses before every single action, "guided" only pauses before a
@@ -23,25 +24,24 @@ export type Mode = "interactive" | "guided" | "autonomous" | "plan";
23
24
  */
24
25
  export interface BaseSessionConfig {
25
26
  mode: Mode;
26
- /** Project root (the session's cwd): the folder under which the agent's notes, sources and runs live. */
27
+ /** Project root (the session's cwd): the folder under which the agent's folders and runs live. */
27
28
  projectDir: string;
28
29
  /**
29
- * Where the agent writes its own notes (the "wiki"). Setting this or `sourcesDir` is what
30
- * gives the session the file tools (Read/Write/Edit/Glob/Grep, scoped — see
31
- * hooks/fileScopeGate.ts); without either, the agent has no file access at all.
30
+ * Where extensions are installed for this agent (#37): `agent` for all its projects (e.g.
31
+ * `~/.miyagi/extensions`), `project` for this one (e.g. `<projectDir>/extensions`), which wins.
32
+ * The enabled ones whose files match their lock run with the session; `runExtensionCommand()`
33
+ * installs them.
32
34
  */
33
- knowledgeDir?: string;
35
+ extensionDirs?: ExtensionDirs;
34
36
  /**
35
- * Original files, kept as obtained and apart from the notes in `knowledgeDir`: material
36
- * the user drops in, plus whatever the agent saves with `save_to_sources` (downloaded
37
- * documents, transcripts...). The agent can read and search it but never edit it — Write/
38
- * Edit are scoped to `knowledgeDir` — and `save_to_sources` never overwrites.
37
+ * Directories where the agent reads, searches and writes with the file tools (Write/Edit,
38
+ * scoped — see hooks/fileScopeGate.ts), e.g. a folder of its own notes. Setting one gives the
39
+ * session all five file tools; an extension's folders (the sources, the knowledge base) come
40
+ * with the extension.
39
41
  */
40
- sourcesDir?: string;
41
- /** Directories besides `knowledgeDir`/`sourcesDir` where Write/Edit are allowed (see hooks/fileScopeGate.ts). */
42
42
  extraWritableDirs?: string[];
43
43
  /**
44
- * Directories besides `sourcesDir`/`extraWritableDirs` the agent may read and search (Read,
44
+ * Directories besides `extraWritableDirs` and the extensions' folders the agent may read and search (Read,
45
45
  * Glob, Grep), never write. Read and Glob reach nothing else on the disk but what the kit
46
46
  * knows the agent needs (see hooks/fileScopeGate.ts).
47
47
  */
@@ -77,29 +77,21 @@ export interface AgentSpec<TConfig extends BaseSessionConfig> {
77
77
  /** The system prompt for this run — everything role/mode/domain-specific lives behind this call. */
78
78
  buildSystemPrompt(config: TConfig): string;
79
79
  /**
80
- * Who the agent is. With it, the kit tells the model its name, version and what it is, plus
81
- * agent-kit's version, and offers the `agent-help` skill, which answers how to use the
82
- * agent: the kit's chat (modes, keys, slash commands, resuming) and `helpGuide`. Without
83
- * it, neither.
80
+ * Who the agent is: its name, version and what it is. The `awareness` extension tells the
81
+ * model (with agent-kit's version), and needs it; the session's facts carry it too.
84
82
  */
85
83
  identity?: AgentIdentity;
86
- /**
87
- * Absolute path of a markdown guide to the agent's own domain (its commands, configuration,
88
- * folders), which the `agent-help` skill includes. Only with `identity`.
89
- */
90
- helpGuide?: string;
91
84
  /**
92
85
  * MCP servers this agent always registers (e.g. Playwright for a browser-driving
93
86
  * agent), on top of the generic ones `buildSessionOptions()` wires up itself when
94
- * applicable (human approval, manual intervention, save-to-knowledge) — those aren't
95
- * part of the spec because they're driven by `mode` plus this spec's own
96
- * `manualInterventionTexts`/`knowledgeDir`/`sourcesDir`, not by anything else
97
- * domain-specific.
87
+ * applicable (human approval, manual intervention) and the extensions' — those aren't
88
+ * part of this method because they're driven by `mode`, this spec's own
89
+ * `manualInterventionTexts` and its `extensions`.
98
90
  */
99
91
  buildMcpServers(config: TConfig, runDir: string): Record<string, McpServerConfig>;
100
92
  /**
101
93
  * Local plugin roots (skills/commands, SDK "local" plugin type) to load, in the order
102
- * given, when the session has file tools (`config.knowledgeDir`/`config.sourcesDir`). Absolute paths; `buildSessionOptions()`
94
+ * given. Absolute paths; `buildSessionOptions()`
103
95
  * doesn't know or care where they live on disk.
104
96
  */
105
97
  pluginRoots(config: TConfig): string[];
@@ -123,28 +115,14 @@ export interface AgentSpec<TConfig extends BaseSessionConfig> {
123
115
  * over a permissive `canUseTool` — a disallowed tool never even reaches that callback.
124
116
  */
125
117
  disallowedTools?: string[];
126
- /** Overrides this kit's generic `save_to_sources` tool description (registered whenever `config.sourcesDir` is set) with domain-specific wording. */
127
- saveToSourcesDescription?: string;
128
- /**
129
- * The built-in knowledge base (see knowledge.ts): when `config.knowledgeDir` is set, the kit
130
- * appends its "Knowledge base" rules to the system prompt, loads its plugin (skills
131
- * knowledge-ingest/knowledge-query/knowledge-lint, commands /knowledge:ingest, /knowledge:query,
132
- * /knowledge:lint) and its `knowledge_*` tools, the only way into the knowledge base (ADR-024).
133
- * On by default; `false` makes `knowledgeDir` a folder of the agent's own notes, kept with the
134
- * file tools under rules it writes itself.
135
- */
136
- knowledgeBase?: boolean;
137
- /**
138
- * The agent's own page types for the built-in knowledge base, besides the kit's
139
- * (summary, concept, entity, synthesis): each with its folder (`""` for the root), index
140
- * section, description (told to the model) and template.
141
- */
142
- knowledgePageTypes?: PageType[];
143
118
  /**
144
- * The knowledge base's store, instead of the kit's over markdown files in `knowledgeDir`
145
- * (e.g. a database or a vector store implementing `KnowledgeStore`).
119
+ * The extensions the agent runs with (ADR-025), in order, each made with its own options: the
120
+ * kit's through their factories (`awareness()`, `sources({ dir })`, `knowledge({ dir })`,
121
+ * `memory({ dir })`), the agent's own as `Extension` objects (its plugin and what it brings).
122
+ * An extension the session lacks something for (its folder), or whose required capabilities
123
+ * nothing enabled provides, is left out, and the prompt says why. None by default.
146
124
  */
147
- knowledgeStore?(config: TConfig): KnowledgeStore;
125
+ extensions?: Extension[];
148
126
  /**
149
127
  * Whether the kit tells the agent (and each subagent) to reply in the resolved language
150
128
  * (`config.language`, `--language`, the system's), following the human if they write in
@@ -166,7 +144,7 @@ export interface AgentSpec<TConfig extends BaseSessionConfig> {
166
144
  * The skills the agent offers (the SDK's `skills`): names, or `plugin:skill` for a
167
145
  * plugin's. Default `"all"`, every skill found: the SDK's own (about twenty, whatever
168
146
  * `settingSources` says), the project's, the plugins'. `"plugins"` offers only those of the
169
- * plugins the session loads (`pluginRoots`, the knowledge base's, agent-help's), without
147
+ * plugins the session loads (`pluginRoots`, the extensions'), without
170
148
  * naming them. A list keeps the rest out of each turn's context; the knowledge base's own
171
149
  * skills are added to it when the knowledge base is on. A context filter, not a sandbox.
172
150
  */
@@ -217,8 +195,8 @@ export interface AgentIdentity {
217
195
  export interface PlanModeSpec<TConfig extends BaseSessionConfig = BaseSessionConfig> {
218
196
  /**
219
197
  * Whether `filePath` (absolute) is a plan file Write/Edit may touch in plan mode, e.g.
220
- * `drafts/<slug>/plan.md`. It must also be writable under the file scope (`knowledgeDir`
221
- * or `extraWritableDirs`), which still applies.
198
+ * `drafts/<slug>/plan.md`. It must also be writable under the file scope (`extraWritableDirs`),
199
+ * which still applies.
222
200
  */
223
201
  isPlanFile?(filePath: string, config: TConfig): boolean;
224
202
  /**
@@ -0,0 +1,128 @@
1
+ import type { HookCallbackMatcher, HookEvent, McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
2
+ import type { AgentSpec, BaseSessionConfig, Mode } from "./agentSpec.js";
3
+ import type { ToolLabels } from "./toolLabels.js";
4
+ import type { SessionFacts } from "./sessionFacts.js";
5
+ /**
6
+ * Extensions (ADR-025): what an agent is besides the kit's core. An extension is a Claude Code
7
+ * plugin with the kit's data in its manifest (`.claude-plugin/plugin.json`, key `"agent-kit"`:
8
+ * the capabilities it `provides` and `requires`), plus, for an internal one, the code that
9
+ * says what it brings to a session. An agent enables the ones it wants (`AgentSpec.extensions`),
10
+ * each made with its own options (the kit's through their factories, `knowledge({ dir })`…);
11
+ * `buildSessionOptions()` puts the contributions of the active ones together, so the core names
12
+ * none of them. The kit's own (awareness, knowledge, sources, memory) live in
13
+ * `src/extensions/<name>/`, their plugin in `extensions/<name>/`; the core never imports them
14
+ * (an ESLint rule keeps it so), and no extension imports another: each owns its data, and the
15
+ * model connects them through their tools (#30).
16
+ */
17
+ /** A folder an extension's options name: a path, or one worked out from the session's config (its project). */
18
+ export type FolderOption<TConfig extends BaseSessionConfig = BaseSessionConfig> = string | ((config: TConfig) => string | undefined);
19
+ /** The folder `option` names for this session's `config`, if any. */
20
+ export declare function folderOf<TConfig extends BaseSessionConfig>(option: FolderOption<TConfig> | undefined, config: BaseSessionConfig): string | undefined;
21
+ /** What an extension sees of the session it's contributing to. */
22
+ export interface ExtensionContext<TConfig extends BaseSessionConfig = BaseSessionConfig> {
23
+ config: TConfig;
24
+ spec: AgentSpec<TConfig>;
25
+ /** This run's folder. */
26
+ runDir: string;
27
+ mode: Mode;
28
+ /** A person can be asked (not autonomous): tools that ask them may exist. */
29
+ interactive: boolean;
30
+ /**
31
+ * What the session's active extensions provide (their manifests' `provides`), for one that
32
+ * works differently beside another (the knowledge base beside the sources). Known once the
33
+ * extensions are resolved: empty in `missing()`.
34
+ */
35
+ capabilities: ReadonlySet<string>;
36
+ /**
37
+ * What the session is at the moment of the call (#43): its mode now, the extensions running
38
+ * and off, their tools, the subagents, skills, commands and context. The core's facts, read
39
+ * anew on every call; for a tool, not for `contribute()`, which runs before the session exists.
40
+ */
41
+ session(): Promise<SessionFacts>;
42
+ }
43
+ /** What an extension brings to a session. Every part is optional. */
44
+ export interface ExtensionContribution {
45
+ /** MCP servers, by name (`mcp__<name>__<tool>`). */
46
+ mcpServers?: Record<string, McpServerConfig>;
47
+ /** A section of the system prompt, after the agent's own and the list of extensions. */
48
+ promptSection?: string;
49
+ /** Built-in file tools it needs the session to have (`Read`, `Glob`, `Grep`…). */
50
+ fileTools?: string[];
51
+ /** Folders the file tools may read and search, never write (a sources folder). */
52
+ readOnlyDirs?: string[];
53
+ /** Folders reached only through its own tools, never the file tools, and what to use instead. */
54
+ toolOnlyDirs?: {
55
+ dir: string;
56
+ instead: string;
57
+ }[];
58
+ /** Its MCP tools that only read: plan mode lets them through. */
59
+ readOnlyTools?: string[];
60
+ /** Its MCP tools that ask the person themselves: interactive mode's step gate doesn't ask first. */
61
+ selfAskingTools?: string[];
62
+ /** How the chat shows its tools, by full name, in the kit's language: its line and how it counts in a folded group. */
63
+ toolLabels?: ToolLabels;
64
+ /**
65
+ * Its hooks, by event, run after the kit's own: an internal extension's only (the memory
66
+ * hears the person's messages with `UserPromptSubmit`).
67
+ */
68
+ hooks?: Partial<Record<HookEvent, HookCallbackMatcher[]>>;
69
+ /** What it says about itself in this session (its commands, its folder), for the agent to tell the person (`SessionFacts`). */
70
+ helpLines?: string[];
71
+ /** What the session hands back to the host (e.g. the knowledge base's store), by name. */
72
+ api?: Record<string, unknown>;
73
+ }
74
+ /**
75
+ * An internal extension: its plugin (whose manifest gives its name, description and
76
+ * capabilities), what it needs from the session, and what it brings.
77
+ */
78
+ export interface Extension {
79
+ /** Its name, the same as its plugin's (`plugin.json`'s `name`). */
80
+ name: string;
81
+ /** Its plugin's root: `.claude-plugin/plugin.json`, and its skills and commands, if any. */
82
+ plugin: string;
83
+ /** Installed rather than shipped in a package (#37): its subagents get no `Bash`, and no plugin hook runs. */
84
+ external?: boolean;
85
+ /** Why it can't run in this session (e.g. "needs a folder (`dir`)"), or `undefined` when it can. */
86
+ missing?(context: ExtensionContext): string | undefined;
87
+ /** What it brings to the session, once it's active. */
88
+ contribute(context: ExtensionContext): Promise<ExtensionContribution>;
89
+ }
90
+ /** An extension's manifest, as the kit reads it. */
91
+ export interface ExtensionManifest {
92
+ name: string;
93
+ description: string;
94
+ /** Capabilities it lets the agent use. */
95
+ provides: string[];
96
+ /** Capabilities it needs another enabled extension to provide. */
97
+ requires: string[];
98
+ }
99
+ /** Reads an extension's manifest from its plugin. */
100
+ export declare function readExtensionManifest(pluginRoot: string): ExtensionManifest;
101
+ /** The extensions a session runs with, and the ones it can't, with why. */
102
+ export interface ResolvedExtensions {
103
+ active: {
104
+ extension: Extension;
105
+ manifest: ExtensionManifest;
106
+ }[];
107
+ inactive: {
108
+ name: string;
109
+ reason: string;
110
+ }[];
111
+ /** What the active ones provide. */
112
+ capabilities: Set<string>;
113
+ }
114
+ /**
115
+ * Which of the `extensions` run in this session, in the order given: those the session lacks
116
+ * something for are left out, and so are those requiring a capability no active one provides,
117
+ * until nothing more drops out.
118
+ */
119
+ export declare function resolveExtensions(extensions: readonly Extension[], context: ExtensionContext): ResolvedExtensions;
120
+ /** The prompt's "Extensions" section: what's on, and what isn't and why (ADR-025). */
121
+ export declare function extensionsPromptSection(resolved: ResolvedExtensions): string;
122
+ /**
123
+ * The skills of the plugins in `roots` whose frontmatter `requires` a capability not in
124
+ * `capabilities`, as the SDK names them (`<plugin>:<folder>`): the session leaves them out.
125
+ */
126
+ export declare function skillsMissingCapabilities(roots: readonly string[], capabilities: ReadonlySet<string>): Promise<string[]>;
127
+ /** A SKILL.md's `requires:` from its frontmatter: `requires: a`, `requires: [a, b]`, `a, b` or a YAML list. */
128
+ export declare function requiredCapabilities(skill: string): string[];
@@ -0,0 +1,87 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { readdir, readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { frontmatter } from "./pluginAgents.js";
5
+ /** The folder `option` names for this session's `config`, if any. */
6
+ export function folderOf(option, config) {
7
+ return typeof option === "function" ? option(config) : option;
8
+ }
9
+ /** Reads an extension's manifest from its plugin. */
10
+ export function readExtensionManifest(pluginRoot) {
11
+ const file = path.join(pluginRoot, ".claude-plugin", "plugin.json");
12
+ const raw = JSON.parse(readFileSync(file, "utf8"));
13
+ if (!raw.name)
14
+ throw new Error(`${file} has no "name".`);
15
+ return { name: raw.name, description: raw.description ?? "", provides: raw["agent-kit"]?.provides ?? [], requires: raw["agent-kit"]?.requires ?? [] };
16
+ }
17
+ /**
18
+ * Which of the `extensions` run in this session, in the order given: those the session lacks
19
+ * something for are left out, and so are those requiring a capability no active one provides,
20
+ * until nothing more drops out.
21
+ */
22
+ export function resolveExtensions(extensions, context) {
23
+ const inactive = [];
24
+ let active = [];
25
+ for (const extension of extensions) {
26
+ const manifest = readExtensionManifest(extension.plugin);
27
+ if (manifest.name !== extension.name)
28
+ throw new Error(`The extension "${extension.name}" has the plugin "${manifest.name}": they must have the same name.`);
29
+ const reason = extension.missing?.(context);
30
+ if (reason)
31
+ inactive.push({ name: extension.name, reason });
32
+ else
33
+ active.push({ extension, manifest });
34
+ }
35
+ // Requirements: drop what can't be met, again and again, since dropping one may starve another.
36
+ for (let changed = true; changed;) {
37
+ changed = false;
38
+ const provided = new Set(active.flatMap(({ manifest }) => manifest.provides));
39
+ for (const entry of active) {
40
+ const unmet = entry.manifest.requires.filter((capability) => !provided.has(capability));
41
+ if (!unmet.length)
42
+ continue;
43
+ inactive.push({ name: entry.extension.name, reason: `requires ${unmet.join(", ")}, which no enabled extension provides` });
44
+ active = active.filter((other) => other !== entry);
45
+ changed = true;
46
+ break;
47
+ }
48
+ }
49
+ return { active, inactive, capabilities: new Set(active.flatMap(({ manifest }) => manifest.provides)) };
50
+ }
51
+ /** The prompt's "Extensions" section: what's on, and what isn't and why (ADR-025). */
52
+ export function extensionsPromptSection(resolved) {
53
+ const lines = resolved.active.map(({ manifest }) => `- **${manifest.name}**: ${manifest.description}${manifest.provides.length ? ` Provides: ${manifest.provides.join(", ")}.` : ""}`);
54
+ const off = resolved.inactive.map(({ name, reason }) => `- **${name}** isn't available: it ${reason}. If the person asks for what it does, tell them why.`);
55
+ return `## Extensions\nWhat you can do besides your own tools comes from these extensions; their sections below say how to use them.\n${[...lines, ...off].join("\n")}`;
56
+ }
57
+ /**
58
+ * The skills of the plugins in `roots` whose frontmatter `requires` a capability not in
59
+ * `capabilities`, as the SDK names them (`<plugin>:<folder>`): the session leaves them out.
60
+ */
61
+ export async function skillsMissingCapabilities(roots, capabilities) {
62
+ const missing = [];
63
+ for (const root of roots) {
64
+ let plugin;
65
+ try {
66
+ plugin = JSON.parse(await readFile(path.join(root, ".claude-plugin", "plugin.json"), "utf8")).name;
67
+ }
68
+ catch {
69
+ continue;
70
+ }
71
+ const folders = await readdir(path.join(root, "skills"), { withFileTypes: true }).catch(() => []);
72
+ for (const folder of folders.filter((entry) => entry.isDirectory())) {
73
+ const text = await readFile(path.join(root, "skills", folder.name, "SKILL.md"), "utf8").catch(() => "");
74
+ const required = requiredCapabilities(text);
75
+ if (required.some((capability) => !capabilities.has(capability)))
76
+ missing.push(`${plugin}:${folder.name}`);
77
+ }
78
+ }
79
+ return missing;
80
+ }
81
+ /** A SKILL.md's `requires:` from its frontmatter: `requires: a`, `requires: [a, b]`, `a, b` or a YAML list. */
82
+ export function requiredCapabilities(skill) {
83
+ const value = frontmatter(skill).fields.requires;
84
+ if (value === undefined)
85
+ return [];
86
+ return (Array.isArray(value) ? value : value.split(",")).map((item) => item.trim()).filter(Boolean);
87
+ }
@@ -0,0 +1,143 @@
1
+ import type { Extension } from "./extensions.js";
2
+ import type { Language } from "./language.js";
3
+ import { type ToolPhrase } from "./messages/index.js";
4
+ /**
5
+ * External extensions (ADR-025, #37): installed into an agent, for all its projects (the
6
+ * agent's scope) or for one (the project's, which wins), from a folder or a git repository.
7
+ * Each scope is a folder of extensions, one per subfolder, with a lock (`extensions.lock.json`)
8
+ * that records where each came from, a SHA-256 over its files and whether it's enabled. An
9
+ * extension is a Claude Code plugin as it is (#38): its MCP servers come from its `.mcp.json`,
10
+ * and the kit starts each with Node through a launcher that leaves it only the system's
11
+ * variables and its own `env`. The `"agent-kit"` key of its `plugin.json` is optional: it adds
12
+ * what the kit's gates and chat can use (capabilities, read-only tools, labels, help).
13
+ */
14
+ /** The two folders an agent installs extensions into; either may be left out. */
15
+ export interface ExtensionDirs {
16
+ /** The agent's, for all its projects, e.g. `~/.miyagi/extensions`. */
17
+ agent?: string;
18
+ /** The project's, for this one only, e.g. `<projectDir>/extensions`: it wins over the agent's. */
19
+ project?: string;
20
+ }
21
+ /** One of the two scopes an extension can be installed in: the agent's or the project's. */
22
+ export type ExtensionScope = keyof ExtensionDirs;
23
+ /** One of a plugin's MCP servers, as its `.mcp.json` (or `plugin.json`'s `mcpServers`) declares it. */
24
+ export interface PluginServer {
25
+ command: string;
26
+ args?: string[];
27
+ /** Its variables: a value may take the agent's with `${VAR}`. */
28
+ env?: Record<string, string>;
29
+ }
30
+ /** How the chat shows one of its tools in one language: `{field}` takes the call's input. */
31
+ export interface ExternalToolLabel {
32
+ label: string;
33
+ phrase?: ToolPhrase;
34
+ }
35
+ /** Who made an extension, as Claude Code's `plugin.json` has it. */
36
+ export interface ExtensionAuthor {
37
+ name: string;
38
+ email?: string;
39
+ url?: string;
40
+ }
41
+ /**
42
+ * An external extension's manifest: its `plugin.json`, the plugin's own metadata (Claude Code's
43
+ * fields, which its validator checks) and the kit's key (`"agent-kit"`, which it ignores).
44
+ */
45
+ export interface ExternalManifest {
46
+ name: string;
47
+ description: string;
48
+ version?: string;
49
+ author?: ExtensionAuthor;
50
+ homepage?: string;
51
+ repository?: string;
52
+ license?: string;
53
+ keywords?: string[];
54
+ provides: string[];
55
+ requires: string[];
56
+ /** The agent-kit versions it works with, e.g. `">=0.19.0 <0.21.0"`. */
57
+ kit?: string;
58
+ /** Programs it needs on this computer, found on the `PATH` (`"docker"`, `"python3"`): without one it's off, saying which. */
59
+ needs: string[];
60
+ /** Its MCP servers, by name (`mcp__<name>__<tool>`), from the plugin's `.mcp.json` or `plugin.json`. */
61
+ servers: Record<string, PluginServer>;
62
+ /** Its tools that only read (short names, of any of its servers): plan mode lets them through. */
63
+ readOnlyTools: string[];
64
+ /** Its tools' chat labels, by short name (of any of its servers) and language (English when the kit's isn't there). */
65
+ labels: Record<string, Partial<Record<Language, ExternalToolLabel>>>;
66
+ /** What it says about itself in a session, for the agent to tell the person (`SessionFacts`). */
67
+ help?: string;
68
+ }
69
+ /** One extension in a scope's lock. */
70
+ export interface LockEntry {
71
+ /** Where it came from: a folder, or a git URL (with `#ref` when given). */
72
+ source: string;
73
+ /** The commit it was cloned at, for a git source. */
74
+ commit?: string;
75
+ /** The marketplace it was installed from (`<name>@<marketplace>`), if any. */
76
+ marketplace?: string;
77
+ /** SHA-256 over its files, checked when it loads. */
78
+ sha256: string;
79
+ enabled: boolean;
80
+ /** When it was installed, ISO 8601 in UTC. */
81
+ installed: string;
82
+ }
83
+ export declare const LOCK_FILE = "extensions.lock.json";
84
+ /** A scope's lock: its extensions by name. */
85
+ export declare function readLock(scopeDir: string): Promise<Record<string, LockEntry>>;
86
+ /** Reads an extension's manifest, checking what the kit needs from it. */
87
+ export declare function readExternalManifest(dir: string): Promise<ExternalManifest>;
88
+ /** SHA-256 over an extension's files (their paths and contents, in order), `.git` aside. */
89
+ export declare function hashExtension(dir: string): Promise<string>;
90
+ /**
91
+ * Whether `version` (x.y.z) is in `range`: comparators separated by spaces, all of which must
92
+ * hold (`>=0.19.0 <0.21.0`, `=0.19.2`, `0.19.2`).
93
+ */
94
+ export declare function inKitRange(version: string, range: string): boolean;
95
+ /** One installed extension, as the scopes and their locks list it. */
96
+ export interface InstalledExtension {
97
+ name: string;
98
+ scope: ExtensionScope;
99
+ dir: string;
100
+ lock: LockEntry;
101
+ /** Also installed in the project's scope, which wins: this one isn't used. */
102
+ shadowed: boolean;
103
+ /** Its manifest, when it can be read. */
104
+ manifest?: ExternalManifest;
105
+ }
106
+ /** Every extension installed in the scopes, the project's first. */
107
+ export declare function listInstalled(dirs: ExtensionDirs): Promise<InstalledExtension[]>;
108
+ /** The launcher that starts an external extension's server with a clean environment. */
109
+ export declare function extensionLauncherPath(): string;
110
+ /** The installed extensions a session runs with, and those it can't (with why). Disabled ones are neither. */
111
+ export declare function loadExternalExtensions(dirs: ExtensionDirs): Promise<{
112
+ extensions: Extension[];
113
+ off: {
114
+ name: string;
115
+ reason: string;
116
+ }[];
117
+ }>;
118
+ /** Why an installed extension can't load, or its manifest when it can. */
119
+ /** Whether a program is on the `PATH`, as a shell would find it (with `PATHEXT`'s extensions on Windows). */
120
+ export declare function onPath(program: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
121
+ /** Whether `source` names a git repository rather than a folder. */
122
+ export declare function isGitSource(source: string): boolean;
123
+ /**
124
+ * Installs an extension into a scope from a folder or a git repository (`url#ref`, `sha` to pin
125
+ * a commit, and `subdir` for one inside it): copies its files (never runs a script), checks its
126
+ * manifest, and locks it, enabled, with the marketplace it came from when it did (`origin`, for a
127
+ * temporary folder, is what the lock says it came from). An extension with the same name in that
128
+ * scope is replaced.
129
+ */
130
+ export declare function addExtension(source: string, scopeDir: string, options?: {
131
+ subdir?: string;
132
+ sha?: string;
133
+ marketplace?: string;
134
+ origin?: string;
135
+ }): Promise<{
136
+ name: string;
137
+ replaced: boolean;
138
+ commit?: string;
139
+ }>;
140
+ /** Removes an extension from a scope (its files and its lock entry); false when it wasn't there. */
141
+ export declare function removeExtension(scopeDir: string, name: string): Promise<boolean>;
142
+ /** Enables or disables an installed extension in a scope; false when it isn't there. */
143
+ export declare function setExtensionEnabled(scopeDir: string, name: string, enabled: boolean): Promise<boolean>;