@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.
- package/assets/extension-launcher.mjs +24 -0
- package/dist/chat/chatController.d.ts +264 -0
- package/dist/chat/chatController.js +510 -0
- package/dist/chat/extensions.d.ts +14 -0
- package/dist/chat/extensions.js +41 -0
- package/dist/chat/history.d.ts +17 -0
- package/dist/chat/history.js +39 -0
- package/dist/{tui → chat}/runs.d.ts +8 -1
- package/dist/{tui → chat}/runs.js +1 -1
- package/dist/core/agentSpec.d.ts +41 -27
- package/dist/core/extensions.d.ts +119 -0
- package/dist/core/extensions.js +97 -0
- package/dist/core/externalExtensions.d.ts +131 -0
- package/dist/core/externalExtensions.js +340 -0
- package/dist/core/hooks/fileScopeGate.d.ts +17 -2
- package/dist/core/hooks/fileScopeGate.js +45 -10
- package/dist/core/hooks/planGate.d.ts +2 -0
- package/dist/core/hooks/planGate.js +1 -10
- package/dist/core/hooks/stepGate.d.ts +1 -1
- package/dist/core/hooks/stepGate.js +3 -3
- package/dist/core/language.js +1 -1
- package/dist/core/messages/de.js +12 -33
- package/dist/core/messages/en.d.ts +13 -16
- package/dist/core/messages/en.js +12 -33
- package/dist/core/messages/es.js +12 -33
- package/dist/core/messages/fr.js +12 -33
- package/dist/core/messages/index.d.ts +6 -0
- package/dist/core/messages/index.js +12 -1
- package/dist/core/packaged.d.ts +10 -0
- package/dist/core/packaged.js +38 -0
- package/dist/core/pluginAgents.d.ts +20 -0
- package/dist/core/pluginAgents.js +76 -0
- package/dist/core/runner.js +16 -9
- package/dist/core/session.d.ts +42 -5
- package/dist/core/session.js +203 -99
- package/dist/core/sessionFacts.d.ts +80 -0
- package/dist/core/sessionFacts.js +48 -0
- package/dist/core/toolLabels.d.ts +24 -6
- package/dist/core/toolLabels.js +24 -39
- package/dist/core/tools/humanApproval.js +3 -3
- package/dist/extensions/awareness/index.d.ts +13 -0
- package/dist/extensions/awareness/index.js +39 -0
- package/dist/extensions/awareness/labels.d.ts +3 -0
- package/dist/extensions/awareness/labels.js +12 -0
- package/dist/extensions/awareness/tools.d.ts +5 -0
- package/dist/extensions/awareness/tools.js +54 -0
- package/dist/{core → extensions/knowledge}/fileKnowledgeStore.d.ts +1 -3
- package/dist/{core → extensions/knowledge}/fileKnowledgeStore.js +23 -29
- package/dist/extensions/knowledge/index.d.ts +8 -0
- package/dist/extensions/knowledge/index.js +35 -0
- package/dist/{core → extensions/knowledge}/knowledgeStore.d.ts +5 -9
- package/dist/{core → extensions/knowledge}/knowledgeStore.js +16 -2
- package/dist/extensions/knowledge/labels.d.ts +3 -0
- package/dist/extensions/knowledge/labels.js +119 -0
- package/dist/extensions/knowledge/prompt.d.ts +28 -0
- package/dist/extensions/knowledge/prompt.js +59 -0
- package/dist/{core/tools/knowledgeTools.d.ts → extensions/knowledge/tools.d.ts} +7 -7
- package/dist/{core/tools/knowledgeTools.js → extensions/knowledge/tools.js} +20 -55
- package/dist/extensions/memory/index.d.ts +12 -0
- package/dist/extensions/memory/index.js +56 -0
- package/dist/extensions/memory/labels.d.ts +3 -0
- package/dist/extensions/memory/labels.js +58 -0
- package/dist/extensions/memory/memoryStore.d.ts +53 -0
- package/dist/extensions/memory/memoryStore.js +88 -0
- package/dist/extensions/memory/tools.d.ts +9 -0
- package/dist/extensions/memory/tools.js +82 -0
- package/dist/extensions/sources/index.d.ts +9 -0
- package/dist/extensions/sources/index.js +40 -0
- package/dist/extensions/sources/labels.d.ts +3 -0
- package/dist/extensions/sources/labels.js +82 -0
- package/dist/{core → extensions/sources}/sources.d.ts +24 -31
- package/dist/{core → extensions/sources}/sources.js +45 -108
- package/dist/{core/tools/saveToSources.d.ts → extensions/sources/tools.d.ts} +7 -3
- package/dist/{core/tools/saveToSources.js → extensions/sources/tools.js} +22 -17
- package/dist/index.d.ts +14 -9
- package/dist/index.js +11 -7
- package/dist/tui/chatTui.d.ts +11 -20
- package/dist/tui/chatTui.js +75 -230
- package/dist/tui/extensionCommand.d.ts +21 -0
- package/dist/tui/extensionCommand.js +133 -0
- package/dist/tui/ink/runChatInk.d.ts +4 -3
- package/dist/tui/ink/runChatInk.js +101 -217
- package/extensions/awareness/.claude-plugin/plugin.json +10 -0
- package/{assets/agent-help → extensions/awareness/skills/help}/SKILL.md +4 -3
- package/{assets/knowledge-plugin → extensions/knowledge}/.claude-plugin/plugin.json +7 -1
- package/extensions/knowledge/commands/ingest.md +7 -0
- package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-ingest/SKILL.md +8 -3
- package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-lint/SKILL.md +12 -5
- package/extensions/memory/.claude-plugin/plugin.json +10 -0
- package/extensions/memory/commands/forget.md +8 -0
- package/extensions/memory/commands/list.md +5 -0
- package/extensions/sources/.claude-plugin/plugin.json +10 -0
- package/package.json +3 -2
- package/assets/knowledge-plugin/commands/ingest.md +0 -7
- package/assets/knowledge-plugin/commands/query.md +0 -5
- package/assets/knowledge-plugin-files/.claude-plugin/plugin.json +0 -4
- package/assets/knowledge-plugin-files/commands/ingest.md +0 -7
- package/assets/knowledge-plugin-files/commands/lint.md +0 -5
- package/assets/knowledge-plugin-files/skills/knowledge-ingest/SKILL.md +0 -46
- package/assets/knowledge-plugin-files/skills/knowledge-lint/SKILL.md +0 -26
- package/assets/knowledge-plugin-files/skills/knowledge-pages/SKILL.md +0 -137
- package/assets/knowledge-plugin-files/skills/knowledge-query/SKILL.md +0 -14
- package/dist/core/agentHelp.d.ts +0 -30
- package/dist/core/agentHelp.js +0 -55
- package/dist/core/knowledge.d.ts +0 -25
- package/dist/core/knowledge.js +0 -84
- /package/dist/{core → extensions/sources}/extractText.d.ts +0 -0
- /package/dist/{core → extensions/sources}/extractText.js +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/commands/lint.md +0 -0
- /package/{assets/knowledge-plugin-files → extensions/knowledge}/commands/query.md +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-query/SKILL.md +0 -0
package/dist/core/agentSpec.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
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 "
|
|
3
|
+
import type { KnowledgeStore, PageType } from "../extensions/knowledge/knowledgeStore.js";
|
|
4
|
+
import type { Extension } from "./extensions.js";
|
|
3
5
|
/**
|
|
4
6
|
* The human-supervision spectrum every agent built on this kit shares, independent of
|
|
5
7
|
* domain: "interactive" pauses before every single action, "guided" only pauses before a
|
|
@@ -38,8 +40,27 @@ export interface BaseSessionConfig {
|
|
|
38
40
|
* Edit are scoped to `knowledgeDir` — and `save_to_sources` never overwrites.
|
|
39
41
|
*/
|
|
40
42
|
sourcesDir?: string;
|
|
43
|
+
/**
|
|
44
|
+
* The memory of the person (the `memory` extension, #34): a folder of the agent's own outside
|
|
45
|
+
* any project, e.g. `~/.miyagi/memory`, kept across all the person's projects. Never share it
|
|
46
|
+
* with another agent. The extension needs it; only its tools (`recall`, `remember`, `forget`) reach it.
|
|
47
|
+
*/
|
|
48
|
+
memoryDir?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Where extensions are installed for this agent (#37): `agent` for all its projects (e.g.
|
|
51
|
+
* `~/.miyagi/extensions`), `project` for this one (e.g. `<projectDir>/extensions`), which wins.
|
|
52
|
+
* The enabled ones whose files match their lock run with the session; `runExtensionCommand()`
|
|
53
|
+
* installs them.
|
|
54
|
+
*/
|
|
55
|
+
extensionDirs?: ExtensionDirs;
|
|
41
56
|
/** Directories besides `knowledgeDir`/`sourcesDir` where Write/Edit are allowed (see hooks/fileScopeGate.ts). */
|
|
42
57
|
extraWritableDirs?: string[];
|
|
58
|
+
/**
|
|
59
|
+
* Directories besides `sourcesDir`/`extraWritableDirs` the agent may read and search (Read,
|
|
60
|
+
* Glob, Grep), never write. Read and Glob reach nothing else on the disk but what the kit
|
|
61
|
+
* knows the agent needs (see hooks/fileScopeGate.ts).
|
|
62
|
+
*/
|
|
63
|
+
extraReadableDirs?: string[];
|
|
43
64
|
/** Files or directories the agent must never read, search or write, e.g. a config file holding a password (see hooks/fileScopeGate.ts). */
|
|
44
65
|
deniedPaths?: string[];
|
|
45
66
|
/** Extra values (e.g. a password) to scrub out of the transcript log — see hooks/transcriptLogger.ts. */
|
|
@@ -71,15 +92,13 @@ export interface AgentSpec<TConfig extends BaseSessionConfig> {
|
|
|
71
92
|
/** The system prompt for this run — everything role/mode/domain-specific lives behind this call. */
|
|
72
93
|
buildSystemPrompt(config: TConfig): string;
|
|
73
94
|
/**
|
|
74
|
-
* Who the agent is
|
|
75
|
-
* agent-kit's version, and
|
|
76
|
-
* agent: the kit's chat (modes, keys, slash commands, resuming) and `helpGuide`. Without
|
|
77
|
-
* it, neither.
|
|
95
|
+
* Who the agent is: its name, version and what it is. The `awareness` extension tells the
|
|
96
|
+
* model (with agent-kit's version), and needs it; the session's facts carry it too.
|
|
78
97
|
*/
|
|
79
98
|
identity?: AgentIdentity;
|
|
80
99
|
/**
|
|
81
100
|
* Absolute path of a markdown guide to the agent's own domain (its commands, configuration,
|
|
82
|
-
* folders), which the `
|
|
101
|
+
* folders), which the `awareness` extension's `about_me` gives the agent when the person asks.
|
|
83
102
|
*/
|
|
84
103
|
helpGuide?: string;
|
|
85
104
|
/**
|
|
@@ -120,26 +139,19 @@ export interface AgentSpec<TConfig extends BaseSessionConfig> {
|
|
|
120
139
|
/** Overrides this kit's generic `save_to_sources` tool description (registered whenever `config.sourcesDir` is set) with domain-specific wording. */
|
|
121
140
|
saveToSourcesDescription?: string;
|
|
122
141
|
/**
|
|
123
|
-
* The
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*/
|
|
131
|
-
knowledgeBase?: boolean;
|
|
132
|
-
/**
|
|
133
|
-
* How the agent reaches the built-in knowledge base: `"store"` (the default), only through
|
|
134
|
-
* the kit's `knowledge_*` tools over a `KnowledgeStore`, never with the file tools, so the
|
|
135
|
-
* storage can change and the wiki's rules are kept by code (ADR-024); `"files"`, with the
|
|
136
|
-
* file tools on `knowledgeDir` and the rules in the prompt, as before.
|
|
142
|
+
* The extensions the agent runs with (ADR-025), in order: the kit's by name (`"knowledge"`, the
|
|
143
|
+
* built-in knowledge base in `config.knowledgeDir`; `"sources"`, the originals in
|
|
144
|
+
* `config.sourcesDir`), and the agent's own as objects (an `Extension`: its plugin and what it
|
|
145
|
+
* brings). An extension the session lacks something for (its folder), or whose required
|
|
146
|
+
* capabilities nothing enabled provides, is left out, and the prompt says why. None by default.
|
|
147
|
+
* Without `"knowledge"`, `knowledgeDir` is a folder of the agent's own notes, kept with the file
|
|
148
|
+
* tools under rules it writes itself.
|
|
137
149
|
*/
|
|
138
|
-
|
|
150
|
+
extensions?: (string | Extension)[];
|
|
139
151
|
/**
|
|
140
|
-
* The agent's own page types for the built-in knowledge base, besides the kit's
|
|
141
|
-
* (summary, concept, entity, synthesis): each with its folder (`""` for the root),
|
|
142
|
-
* section, description (told to the model) and template.
|
|
152
|
+
* The agent's own page types for the built-in knowledge base, besides the kit's
|
|
153
|
+
* (summary, concept, entity, synthesis, preference): each with its folder (`""` for the root),
|
|
154
|
+
* index section, description (told to the model) and template.
|
|
143
155
|
*/
|
|
144
156
|
knowledgePageTypes?: PageType[];
|
|
145
157
|
/**
|
|
@@ -166,11 +178,13 @@ export interface AgentSpec<TConfig extends BaseSessionConfig> {
|
|
|
166
178
|
settingSources?: SettingSource[];
|
|
167
179
|
/**
|
|
168
180
|
* The skills the agent offers (the SDK's `skills`): names, or `plugin:skill` for a
|
|
169
|
-
* plugin's. Default `"all"`, every skill found
|
|
170
|
-
*
|
|
181
|
+
* plugin's. Default `"all"`, every skill found: the SDK's own (about twenty, whatever
|
|
182
|
+
* `settingSources` says), the project's, the plugins'. `"plugins"` offers only those of the
|
|
183
|
+
* plugins the session loads (`pluginRoots`, the extensions'), without
|
|
184
|
+
* naming them. A list keeps the rest out of each turn's context; the knowledge base's own
|
|
171
185
|
* skills are added to it when the knowledge base is on. A context filter, not a sandbox.
|
|
172
186
|
*/
|
|
173
|
-
skills?: string[] | "all";
|
|
187
|
+
skills?: string[] | "all" | "plugins";
|
|
174
188
|
/**
|
|
175
189
|
* Text for the generic human-approval checkpoint (see tools/humanApproval.ts) — what
|
|
176
190
|
* counts as "publishing something visible to others" is entirely domain-specific.
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
* the kit's by name, its own as objects; `buildSessionOptions()` puts the contributions of the
|
|
11
|
+
* active ones together, so the core names none of them. The kit's own (awareness, knowledge, sources, memory) live
|
|
12
|
+
* in `src/extensions/<name>/`, their plugin in `extensions/<name>/`; this file is the only place
|
|
13
|
+
* in the core that imports them (an ESLint rule keeps it so), and no extension imports another:
|
|
14
|
+
* each owns its data, and the model connects them through their tools (#30).
|
|
15
|
+
*/
|
|
16
|
+
/** What an extension sees of the session it's contributing to. */
|
|
17
|
+
export interface ExtensionContext<TConfig extends BaseSessionConfig = BaseSessionConfig> {
|
|
18
|
+
config: TConfig;
|
|
19
|
+
spec: AgentSpec<TConfig>;
|
|
20
|
+
/** This run's folder. */
|
|
21
|
+
runDir: string;
|
|
22
|
+
mode: Mode;
|
|
23
|
+
/** A person can be asked (not autonomous): tools that ask them may exist. */
|
|
24
|
+
interactive: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* What the session is at the moment of the call (#43): its mode now, the extensions running
|
|
27
|
+
* and off, their tools, the subagents, skills, commands and context. The core's facts, read
|
|
28
|
+
* anew on every call; for a tool, not for `contribute()`, which runs before the session exists.
|
|
29
|
+
*/
|
|
30
|
+
session(): Promise<SessionFacts>;
|
|
31
|
+
}
|
|
32
|
+
/** What an extension brings to a session. Every part is optional. */
|
|
33
|
+
export interface ExtensionContribution {
|
|
34
|
+
/** MCP servers, by name (`mcp__<name>__<tool>`). */
|
|
35
|
+
mcpServers?: Record<string, McpServerConfig>;
|
|
36
|
+
/** A section of the system prompt, after the agent's own and the list of extensions. */
|
|
37
|
+
promptSection?: string;
|
|
38
|
+
/** Built-in file tools it needs the session to have (`Read`, `Glob`, `Grep`…). */
|
|
39
|
+
fileTools?: string[];
|
|
40
|
+
/** Folders the file tools may read and search, never write (a sources folder). */
|
|
41
|
+
readOnlyDirs?: string[];
|
|
42
|
+
/** Folders reached only through its own tools, never the file tools, and what to use instead. */
|
|
43
|
+
toolOnlyDirs?: {
|
|
44
|
+
dir: string;
|
|
45
|
+
instead: string;
|
|
46
|
+
}[];
|
|
47
|
+
/** Its MCP tools that only read: plan mode lets them through. */
|
|
48
|
+
readOnlyTools?: string[];
|
|
49
|
+
/** Its MCP tools that ask the person themselves: interactive mode's step gate doesn't ask first. */
|
|
50
|
+
selfAskingTools?: string[];
|
|
51
|
+
/** How the chat shows its tools, by full name, in the kit's language: its line and how it counts in a folded group. */
|
|
52
|
+
toolLabels?: ToolLabels;
|
|
53
|
+
/**
|
|
54
|
+
* Its hooks, by event, run after the kit's own: an internal extension's only (the memory
|
|
55
|
+
* hears the person's messages with `UserPromptSubmit`).
|
|
56
|
+
*/
|
|
57
|
+
hooks?: Partial<Record<HookEvent, HookCallbackMatcher[]>>;
|
|
58
|
+
/** What it says about itself in this session (its commands, its folder), for the agent to tell the person (`SessionFacts`). */
|
|
59
|
+
helpLines?: string[];
|
|
60
|
+
/** What the session hands back to the host (e.g. the knowledge base's store), by name. */
|
|
61
|
+
api?: Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* An internal extension: its plugin (whose manifest gives its name, description and
|
|
65
|
+
* capabilities), what it needs from the session, and what it brings.
|
|
66
|
+
*/
|
|
67
|
+
export interface Extension {
|
|
68
|
+
/** Its name, the same as its plugin's (`plugin.json`'s `name`). */
|
|
69
|
+
name: string;
|
|
70
|
+
/** Its plugin's root: `.claude-plugin/plugin.json`, and its skills and commands, if any. */
|
|
71
|
+
plugin: string;
|
|
72
|
+
/** Installed rather than shipped in a package (#37): its subagents get no `Bash`, and no plugin hook runs. */
|
|
73
|
+
external?: boolean;
|
|
74
|
+
/** Why it can't run in this session (e.g. "needs `knowledgeDir` in the config"), or `undefined` when it can. */
|
|
75
|
+
missing?(context: ExtensionContext): string | undefined;
|
|
76
|
+
/** What it brings to the session, once it's active. */
|
|
77
|
+
contribute(context: ExtensionContext): Promise<ExtensionContribution>;
|
|
78
|
+
}
|
|
79
|
+
/** An extension's manifest, as the kit reads it. */
|
|
80
|
+
export interface ExtensionManifest {
|
|
81
|
+
name: string;
|
|
82
|
+
description: string;
|
|
83
|
+
/** Capabilities it lets the agent use. */
|
|
84
|
+
provides: string[];
|
|
85
|
+
/** Capabilities it needs another enabled extension to provide. */
|
|
86
|
+
requires: string[];
|
|
87
|
+
}
|
|
88
|
+
/** Reads an extension's manifest from its plugin. */
|
|
89
|
+
export declare function readExtensionManifest(pluginRoot: string): ExtensionManifest;
|
|
90
|
+
/** The kit's internal extensions, by name. */
|
|
91
|
+
export declare const BUILT_IN_EXTENSIONS: readonly Extension[];
|
|
92
|
+
/** The extensions a session runs with, and the ones it can't, with why. */
|
|
93
|
+
export interface ResolvedExtensions {
|
|
94
|
+
active: {
|
|
95
|
+
extension: Extension;
|
|
96
|
+
manifest: ExtensionManifest;
|
|
97
|
+
}[];
|
|
98
|
+
inactive: {
|
|
99
|
+
name: string;
|
|
100
|
+
reason: string;
|
|
101
|
+
}[];
|
|
102
|
+
/** What the active ones provide. */
|
|
103
|
+
capabilities: Set<string>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Which of the `enabled` extensions (the kit's by name, an agent's own as objects) run in this
|
|
107
|
+
* session, in the order given: those the session lacks something for are left out, and so are
|
|
108
|
+
* those requiring a capability no active one provides, until nothing more drops out.
|
|
109
|
+
*/
|
|
110
|
+
export declare function resolveExtensions(enabled: readonly (string | Extension)[], context: ExtensionContext): ResolvedExtensions;
|
|
111
|
+
/** The prompt's "Extensions" section: what's on, and what isn't and why (ADR-025). */
|
|
112
|
+
export declare function extensionsPromptSection(resolved: ResolvedExtensions): string;
|
|
113
|
+
/**
|
|
114
|
+
* The skills of the plugins in `roots` whose frontmatter `requires` a capability not in
|
|
115
|
+
* `capabilities`, as the SDK names them (`<plugin>:<folder>`): the session leaves them out.
|
|
116
|
+
*/
|
|
117
|
+
export declare function skillsMissingCapabilities(roots: readonly string[], capabilities: ReadonlySet<string>): Promise<string[]>;
|
|
118
|
+
/** A SKILL.md's `requires:` from its frontmatter: `requires: a`, `requires: [a, b]`, `a, b` or a YAML list. */
|
|
119
|
+
export declare function requiredCapabilities(skill: string): string[];
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
import { awarenessExtension } from "../extensions/awareness/index.js";
|
|
6
|
+
import { sourcesExtension } from "../extensions/sources/index.js";
|
|
7
|
+
import { knowledgeExtension } from "../extensions/knowledge/index.js";
|
|
8
|
+
import { memoryExtension } from "../extensions/memory/index.js";
|
|
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
|
+
/** The kit's internal extensions, by name. */
|
|
18
|
+
export const BUILT_IN_EXTENSIONS = [awarenessExtension, sourcesExtension, knowledgeExtension, memoryExtension];
|
|
19
|
+
/**
|
|
20
|
+
* Which of the `enabled` extensions (the kit's by name, an agent's own as objects) run in this
|
|
21
|
+
* session, in the order given: those the session lacks something for are left out, and so are
|
|
22
|
+
* those requiring a capability no active one provides, until nothing more drops out.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveExtensions(enabled, context) {
|
|
25
|
+
const extensions = enabled.map((entry) => {
|
|
26
|
+
if (typeof entry !== "string")
|
|
27
|
+
return entry;
|
|
28
|
+
const found = BUILT_IN_EXTENSIONS.find((extension) => extension.name === entry);
|
|
29
|
+
if (!found)
|
|
30
|
+
throw new Error(`Unknown extension "${entry}": the kit's are ${BUILT_IN_EXTENSIONS.map((extension) => extension.name).join(", ")}; an agent's own is passed as an object.`);
|
|
31
|
+
return found;
|
|
32
|
+
});
|
|
33
|
+
const inactive = [];
|
|
34
|
+
let active = [];
|
|
35
|
+
for (const extension of extensions) {
|
|
36
|
+
const manifest = readExtensionManifest(extension.plugin);
|
|
37
|
+
if (manifest.name !== extension.name)
|
|
38
|
+
throw new Error(`The extension "${extension.name}" has the plugin "${manifest.name}": they must have the same name.`);
|
|
39
|
+
const reason = extension.missing?.(context);
|
|
40
|
+
if (reason)
|
|
41
|
+
inactive.push({ name: extension.name, reason });
|
|
42
|
+
else
|
|
43
|
+
active.push({ extension, manifest });
|
|
44
|
+
}
|
|
45
|
+
// Requirements: drop what can't be met, again and again, since dropping one may starve another.
|
|
46
|
+
for (let changed = true; changed;) {
|
|
47
|
+
changed = false;
|
|
48
|
+
const provided = new Set(active.flatMap(({ manifest }) => manifest.provides));
|
|
49
|
+
for (const entry of active) {
|
|
50
|
+
const unmet = entry.manifest.requires.filter((capability) => !provided.has(capability));
|
|
51
|
+
if (!unmet.length)
|
|
52
|
+
continue;
|
|
53
|
+
inactive.push({ name: entry.extension.name, reason: `requires ${unmet.join(", ")}, which no enabled extension provides` });
|
|
54
|
+
active = active.filter((other) => other !== entry);
|
|
55
|
+
changed = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { active, inactive, capabilities: new Set(active.flatMap(({ manifest }) => manifest.provides)) };
|
|
60
|
+
}
|
|
61
|
+
/** The prompt's "Extensions" section: what's on, and what isn't and why (ADR-025). */
|
|
62
|
+
export function extensionsPromptSection(resolved) {
|
|
63
|
+
const lines = resolved.active.map(({ manifest }) => `- **${manifest.name}**: ${manifest.description}${manifest.provides.length ? ` Provides: ${manifest.provides.join(", ")}.` : ""}`);
|
|
64
|
+
const off = resolved.inactive.map(({ name, reason }) => `- **${name}** isn't available: it ${reason}. If the person asks for what it does, tell them why.`);
|
|
65
|
+
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")}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The skills of the plugins in `roots` whose frontmatter `requires` a capability not in
|
|
69
|
+
* `capabilities`, as the SDK names them (`<plugin>:<folder>`): the session leaves them out.
|
|
70
|
+
*/
|
|
71
|
+
export async function skillsMissingCapabilities(roots, capabilities) {
|
|
72
|
+
const missing = [];
|
|
73
|
+
for (const root of roots) {
|
|
74
|
+
let plugin;
|
|
75
|
+
try {
|
|
76
|
+
plugin = JSON.parse(await readFile(path.join(root, ".claude-plugin", "plugin.json"), "utf8")).name;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const folders = await readdir(path.join(root, "skills"), { withFileTypes: true }).catch(() => []);
|
|
82
|
+
for (const folder of folders.filter((entry) => entry.isDirectory())) {
|
|
83
|
+
const text = await readFile(path.join(root, "skills", folder.name, "SKILL.md"), "utf8").catch(() => "");
|
|
84
|
+
const required = requiredCapabilities(text);
|
|
85
|
+
if (required.some((capability) => !capabilities.has(capability)))
|
|
86
|
+
missing.push(`${plugin}:${folder.name}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return missing;
|
|
90
|
+
}
|
|
91
|
+
/** A SKILL.md's `requires:` from its frontmatter: `requires: a`, `requires: [a, b]`, `a, b` or a YAML list. */
|
|
92
|
+
export function requiredCapabilities(skill) {
|
|
93
|
+
const value = frontmatter(skill).fields.requires;
|
|
94
|
+
if (value === undefined)
|
|
95
|
+
return [];
|
|
96
|
+
return (Array.isArray(value) ? value : value.split(",")).map((item) => item.trim()).filter(Boolean);
|
|
97
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
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
|
+
/** Its MCP servers, by name (`mcp__<name>__<tool>`), from the plugin's `.mcp.json` or `plugin.json`. */
|
|
59
|
+
servers: Record<string, PluginServer>;
|
|
60
|
+
/** Its tools that only read (short names, of any of its servers): plan mode lets them through. */
|
|
61
|
+
readOnlyTools: string[];
|
|
62
|
+
/** Its tools' chat labels, by short name (of any of its servers) and language (English when the kit's isn't there). */
|
|
63
|
+
labels: Record<string, Partial<Record<Language, ExternalToolLabel>>>;
|
|
64
|
+
/** What it says about itself in a session, for the agent to tell the person (`SessionFacts`). */
|
|
65
|
+
help?: string;
|
|
66
|
+
}
|
|
67
|
+
/** One extension in a scope's lock. */
|
|
68
|
+
export interface LockEntry {
|
|
69
|
+
/** Where it came from: a folder, or a git URL (with `#ref` when given). */
|
|
70
|
+
source: string;
|
|
71
|
+
/** The commit it was cloned at, for a git source. */
|
|
72
|
+
commit?: string;
|
|
73
|
+
/** SHA-256 over its files, checked when it loads. */
|
|
74
|
+
sha256: string;
|
|
75
|
+
enabled: boolean;
|
|
76
|
+
/** When it was installed, ISO 8601 in UTC. */
|
|
77
|
+
installed: string;
|
|
78
|
+
}
|
|
79
|
+
export declare const LOCK_FILE = "extensions.lock.json";
|
|
80
|
+
/** A scope's lock: its extensions by name. */
|
|
81
|
+
export declare function readLock(scopeDir: string): Promise<Record<string, LockEntry>>;
|
|
82
|
+
/** Reads an extension's manifest, checking what the kit needs from it. */
|
|
83
|
+
export declare function readExternalManifest(dir: string): Promise<ExternalManifest>;
|
|
84
|
+
/** SHA-256 over an extension's files (their paths and contents, in order), `.git` aside. */
|
|
85
|
+
export declare function hashExtension(dir: string): Promise<string>;
|
|
86
|
+
/**
|
|
87
|
+
* Whether `version` (x.y.z) is in `range`: comparators separated by spaces, all of which must
|
|
88
|
+
* hold (`>=0.19.0 <0.21.0`, `=0.19.2`, `0.19.2`).
|
|
89
|
+
*/
|
|
90
|
+
export declare function inKitRange(version: string, range: string): boolean;
|
|
91
|
+
/** One installed extension, as the scopes and their locks list it. */
|
|
92
|
+
export interface InstalledExtension {
|
|
93
|
+
name: string;
|
|
94
|
+
scope: ExtensionScope;
|
|
95
|
+
dir: string;
|
|
96
|
+
lock: LockEntry;
|
|
97
|
+
/** Also installed in the project's scope, which wins: this one isn't used. */
|
|
98
|
+
shadowed: boolean;
|
|
99
|
+
/** Its manifest, when it can be read. */
|
|
100
|
+
manifest?: ExternalManifest;
|
|
101
|
+
}
|
|
102
|
+
/** Every extension installed in the scopes, the project's first. */
|
|
103
|
+
export declare function listInstalled(dirs: ExtensionDirs): Promise<InstalledExtension[]>;
|
|
104
|
+
/** The launcher that starts an external extension's server with a clean environment. */
|
|
105
|
+
export declare function extensionLauncherPath(): string;
|
|
106
|
+
/** The installed extensions a session runs with, and those it can't (with why). Disabled ones are neither. */
|
|
107
|
+
export declare function loadExternalExtensions(dirs: ExtensionDirs): Promise<{
|
|
108
|
+
extensions: Extension[];
|
|
109
|
+
off: {
|
|
110
|
+
name: string;
|
|
111
|
+
reason: string;
|
|
112
|
+
}[];
|
|
113
|
+
}>;
|
|
114
|
+
/** Whether `source` names a git repository rather than a folder. */
|
|
115
|
+
export declare function isGitSource(source: string): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Installs an extension into a scope from a folder or a git repository (`url#ref`, and `subdir`
|
|
118
|
+
* for one inside it): copies its files (never runs a script), checks its manifest, and locks it,
|
|
119
|
+
* enabled. An extension with the same name in that scope is replaced.
|
|
120
|
+
*/
|
|
121
|
+
export declare function addExtension(source: string, scopeDir: string, options?: {
|
|
122
|
+
subdir?: string;
|
|
123
|
+
}): Promise<{
|
|
124
|
+
name: string;
|
|
125
|
+
replaced: boolean;
|
|
126
|
+
commit?: string;
|
|
127
|
+
}>;
|
|
128
|
+
/** Removes an extension from a scope (its files and its lock entry); false when it wasn't there. */
|
|
129
|
+
export declare function removeExtension(scopeDir: string, name: string): Promise<boolean>;
|
|
130
|
+
/** Enables or disables an installed extension in a scope; false when it isn't there. */
|
|
131
|
+
export declare function setExtensionEnabled(scopeDir: string, name: string, enabled: boolean): Promise<boolean>;
|