@falkenslab/agent-kit 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/assets/extension-launcher.mjs +9 -2
  2. package/dist/chat/chatController.d.ts +4 -1
  3. package/dist/chat/chatController.js +43 -17
  4. package/dist/chat/extensions.d.ts +7 -3
  5. package/dist/chat/extensions.js +12 -9
  6. package/dist/chat/runs.d.ts +3 -0
  7. package/dist/chat/runs.js +1 -1
  8. package/dist/core/agentSpec.d.ts +20 -56
  9. package/dist/core/extensions.d.ts +23 -12
  10. package/dist/core/extensions.js +8 -18
  11. package/dist/core/externalExtensions.d.ts +49 -9
  12. package/dist/core/externalExtensions.js +89 -18
  13. package/dist/core/liveExtensions.d.ts +65 -0
  14. package/dist/core/liveExtensions.js +121 -0
  15. package/dist/core/marketplaces.d.ts +118 -0
  16. package/dist/core/marketplaces.js +242 -0
  17. package/dist/core/messages/de.js +1 -0
  18. package/dist/core/messages/en.d.ts +2 -0
  19. package/dist/core/messages/en.js +1 -0
  20. package/dist/core/messages/es.js +1 -0
  21. package/dist/core/messages/fr.js +1 -0
  22. package/dist/core/pluginDownloads.d.ts +14 -0
  23. package/dist/core/pluginDownloads.js +141 -0
  24. package/dist/core/runner.js +6 -1
  25. package/dist/core/session.d.ts +11 -4
  26. package/dist/core/session.js +73 -21
  27. package/dist/core/sessionFacts.d.ts +5 -0
  28. package/dist/core/sessionFacts.js +1 -0
  29. package/dist/extensions/awareness/index.d.ts +12 -3
  30. package/dist/extensions/awareness/index.js +18 -15
  31. package/dist/extensions/awareness/tools.d.ts +1 -1
  32. package/dist/extensions/awareness/tools.js +3 -3
  33. package/dist/extensions/knowledge/index.d.ts +20 -6
  34. package/dist/extensions/knowledge/index.js +32 -29
  35. package/dist/extensions/knowledge/knowledgeStore.d.ts +1 -1
  36. package/dist/extensions/knowledge/prompt.d.ts +4 -4
  37. package/dist/extensions/knowledge/prompt.js +4 -4
  38. package/dist/extensions/memory/index.d.ts +15 -6
  39. package/dist/extensions/memory/index.js +34 -31
  40. package/dist/extensions/sources/index.d.ts +17 -5
  41. package/dist/extensions/sources/index.js +31 -29
  42. package/dist/index.d.ts +7 -2
  43. package/dist/index.js +6 -1
  44. package/dist/tui/extensionCommand.d.ts +14 -3
  45. package/dist/tui/extensionCommand.js +164 -12
  46. package/package.json +1 -1
@@ -12,8 +12,15 @@ if (!entry) {
12
12
  process.exit(1);
13
13
  }
14
14
 
15
- // What any process may need to run, on Windows and elsewhere; never a credential.
16
- const SYSTEM = ["PATH", "PATHEXT", "SYSTEMROOT", "SYSTEMDRIVE", "WINDIR", "COMSPEC", "TEMP", "TMP", "TMPDIR", "HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "LANG", "LC_ALL", "TZ"];
15
+ // What any process may need to run, on Windows and elsewhere; never a credential. Where programs
16
+ // are installed and the desktop session too: a browser is found through PROGRAMFILES on Windows
17
+ // (Playwright found no Edge without them, #52) and opens a window through DISPLAY on Linux.
18
+ const SYSTEM = [
19
+ "PATH", "PATHEXT", "SYSTEMROOT", "SYSTEMDRIVE", "WINDIR", "COMSPEC", "TEMP", "TMP", "TMPDIR", "HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "LANG", "LC_ALL", "TZ",
20
+ "PROGRAMFILES", "PROGRAMFILES(X86)", "PROGRAMW6432", "COMMONPROGRAMFILES", "COMMONPROGRAMFILES(X86)", "COMMONPROGRAMW6432", "PROGRAMDATA", "ALLUSERSPROFILE",
21
+ "HOMEDRIVE", "HOMEPATH", "USERNAME", "USER", "LOGNAME", "OS", "PROCESSOR_ARCHITECTURE", "NUMBER_OF_PROCESSORS",
22
+ "DISPLAY", "WAYLAND_DISPLAY", "XDG_RUNTIME_DIR", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME", "DBUS_SESSION_BUS_ADDRESS",
23
+ ];
17
24
  const allowed = new Set([...SYSTEM, ...JSON.parse(declared)].map((name) => name.toUpperCase()));
18
25
  for (const name of Object.keys(process.env)) {
19
26
  if (!allowed.has(name.toUpperCase())) delete process.env[name];
@@ -238,7 +238,10 @@ export declare function createChatController(options: Options | SessionOpener, s
238
238
  * graphical view offers instead of quitting and starting again.
239
239
  */
240
240
  newConversation(): Promise<void>;
241
- /** Enables or disables an installed extension, and opens the session again with the same conversation. */
241
+ /**
242
+ * Enables or disables an installed extension: in the running session when it was running when
243
+ * the session opened (#49), by opening the session again with the same conversation otherwise.
244
+ */
242
245
  setExtension(name: string, enabled: boolean): Promise<void>;
243
246
  /**
244
247
  * Switches the kit's language (`switchLanguage()`) and opens the session again in it, keeping
@@ -160,18 +160,10 @@ export async function createChatController(options, settings = {}) {
160
160
  turnDone = null;
161
161
  done?.();
162
162
  }
163
- function connect() {
164
- const own = ++generation;
165
- queue = createInputQueue({ modeControl: modeControl() });
166
- run = (settings.runQuery ?? runQuery)(queue.iterable, { ...current.options, promptSuggestions: settings.promptSuggestions ?? true });
167
- const events = run.events[Symbol.asyncIterator]();
163
+ /** The commands the session has, and the chat's own, into the state: when it opens, and when an extension is switched in it. */
164
+ function loadCommands() {
165
+ const own = generation;
168
166
  const control = modeControl();
169
- update({ mode: control?.mode ?? null, switchableModes: control?.switchable ?? [], run: current.run, extensions: current.extensions ?? null });
170
- unsubscribeMode?.();
171
- unsubscribeMode = control?.subscribe?.((mode) => {
172
- update({ mode });
173
- emit({ type: "mode", mode });
174
- });
175
167
  const supported = run.supportedCommands().catch(() => null);
176
168
  knownCommands = supported.then((commands) => (commands ? new Set(commands.flatMap((command) => [command.name, ...(command.aliases ?? [])])) : null));
177
169
  void supported.then((commands) => {
@@ -194,6 +186,20 @@ export async function createChatController(options, settings = {}) {
194
186
  const sorted = [...details.values()].sort((a, b) => a.name.localeCompare(b.name));
195
187
  update({ commands: sorted.map((command) => command.name), commandDetails: sorted });
196
188
  });
189
+ }
190
+ function connect() {
191
+ const own = ++generation;
192
+ queue = createInputQueue({ modeControl: modeControl() });
193
+ run = (settings.runQuery ?? runQuery)(queue.iterable, { ...current.options, promptSuggestions: settings.promptSuggestions ?? true });
194
+ const events = run.events[Symbol.asyncIterator]();
195
+ const control = modeControl();
196
+ update({ mode: control?.mode ?? null, switchableModes: control?.switchable ?? [], run: current.run, extensions: current.extensions ?? null });
197
+ unsubscribeMode?.();
198
+ unsubscribeMode = control?.subscribe?.((mode) => {
199
+ update({ mode });
200
+ emit({ type: "mode", mode });
201
+ });
202
+ loadCommands();
197
203
  void (async () => {
198
204
  try {
199
205
  while (true) {
@@ -221,6 +227,9 @@ export async function createChatController(options, settings = {}) {
221
227
  // A language switched to, told to the model with the next message (as a mode change is): the
222
228
  // session's prompt says it already, but a conversation in another language pulls the replies.
223
229
  let languageNotice = null;
230
+ // Extensions turned off or on in the running session, told to the model with the next message
231
+ // too: the prompt's Extensions section still says what the session opened with.
232
+ const extensionNotices = [];
224
233
  async function runTurn(line) {
225
234
  interrupted = false;
226
235
  const from = state.transcript.length;
@@ -229,7 +238,8 @@ export async function createChatController(options, settings = {}) {
229
238
  update({ busy: true, turnStartedAt: startedAt, activity: null, subagentActivity: null, suggestion: null });
230
239
  emit({ type: "turn-start" });
231
240
  const finished = new Promise((resolve) => (turnDone = resolve));
232
- queue.push(languageNotice ? `${languageNotice}\n\n${line}` : line);
241
+ const notices = [languageNotice, ...extensionNotices.splice(0)].filter(Boolean);
242
+ queue.push(notices.length ? `${notices.join("\n")}\n\n${line}` : line);
233
243
  languageNotice = null;
234
244
  await finished;
235
245
  logRenderer.endLine();
@@ -254,6 +264,20 @@ export async function createChatController(options, settings = {}) {
254
264
  emit({ type: "conversation", messages, updatedAt, fromResume });
255
265
  }
256
266
  /** Switches to another session: a run resumed, or the same one opened again. */
267
+ /** An extension whose lock just changed: switched in the running session if it can be, the session opened again if not. */
268
+ async function applyExtension(name, enabled) {
269
+ if (await current.switchExtension?.(name, enabled).catch(() => false)) {
270
+ notice(t().extensionSwitched(name, enabled));
271
+ extensionNotices.push(enabled
272
+ ? `<system-reminder>The user turned the ${name} extension back on: its tools, skills and commands are available again.</system-reminder>`
273
+ : `<system-reminder>The user turned the ${name} extension off: its tools, skills, commands and subagents are gone until it's turned on again, whatever your instructions or earlier messages say.</system-reminder>`);
274
+ update({ extensions: current.extensions ? { ...current.extensions } : null });
275
+ loadCommands();
276
+ return;
277
+ }
278
+ notice(t().extensionToggled(name, enabled));
279
+ await switchTo({ dir: current.run.dir, sessionId: await readRunSession(current.run.dir) }, false);
280
+ }
257
281
  async function switchTo(folder, fromResume) {
258
282
  queue.end();
259
283
  run.close();
@@ -364,8 +388,8 @@ export async function createChatController(options, settings = {}) {
364
388
  if (extensions) {
365
389
  for (const text of extensions.lines)
366
390
  notice(text);
367
- if (extensions.reopen)
368
- await switchTo({ dir: current.run.dir, sessionId: await readRunSession(current.run.dir) }, false);
391
+ if (extensions.toggled)
392
+ await applyExtension(extensions.toggled.name, extensions.toggled.enabled);
369
393
  return;
370
394
  }
371
395
  const token = slashCommandToken(line);
@@ -447,7 +471,10 @@ export async function createChatController(options, settings = {}) {
447
471
  connect();
448
472
  emit({ type: "session" });
449
473
  },
450
- /** Enables or disables an installed extension, and opens the session again with the same conversation. */
474
+ /**
475
+ * Enables or disables an installed extension: in the running session when it was running when
476
+ * the session opened (#49), by opening the session again with the same conversation otherwise.
477
+ */
451
478
  async setExtension(name, enabled) {
452
479
  // An action of its own, not a line the person typed: a graphical view's toggle.
453
480
  if (!opener || !current.run)
@@ -457,8 +484,7 @@ export async function createChatController(options, settings = {}) {
457
484
  if (!installed || !dir)
458
485
  return notice(t().extensionNotInstalled(name));
459
486
  await setExtensionEnabled(dir, name, enabled);
460
- notice(t().extensionToggled(name, enabled));
461
- await switchTo({ dir: current.run.dir, sessionId: await readRunSession(current.run.dir) }, false);
487
+ await applyExtension(name, enabled);
462
488
  },
463
489
  /**
464
490
  * Switches the kit's language (`switchLanguage()`) and opens the session again in it, keeping
@@ -5,10 +5,14 @@ export declare function describe(extension: InstalledExtension): string;
5
5
  /**
6
6
  * `/extensions` in a chat (#37): with nothing after it, what the session runs with, what's off
7
7
  * and why, and what's installed; with `enable <name>` or `disable <name>`, changes its lock and
8
- * asks the chat to reopen the session (`reopen`), which keeps the conversation. `null` when
9
- * `line` isn't this command.
8
+ * says which (`toggled`): the chat turns it on or off in the running session when it can (#49),
9
+ * and opens the session again otherwise, keeping the conversation. `null` when `line` isn't this
10
+ * command.
10
11
  */
11
12
  export declare function chatExtensionsCommand(line: string, status: ExtensionsStatus | undefined, canReopen: boolean): Promise<{
12
13
  lines: string[];
13
- reopen: boolean;
14
+ toggled?: {
15
+ name: string;
16
+ enabled: boolean;
17
+ };
14
18
  } | null>;
@@ -3,8 +3,10 @@ import { setExtensionEnabled } from "../core/externalExtensions.js";
3
3
  import { t } from "../core/messages/index.js";
4
4
  /** One line for an installed extension: name and version, scope, on or off, its author, where it came from. */
5
5
  export function describe(extension) {
6
- const state = extension.shadowed ? "not used (the project's wins)" : extension.lock.enabled ? "enabled" : "disabled";
7
- const source = extension.lock.commit ? `${extension.lock.source} @ ${extension.lock.commit.slice(0, 12)}` : path.normalize(extension.lock.source);
6
+ const state = `${extension.shadowed ? "not used (the project's wins)" : extension.lock.enabled ? "enabled" : "disabled"}${extension.lock.linked ? ", linked" : ""}`;
7
+ // From a marketplace, its name there (`jokebook@shipyard`): the copy it came from is the kit's business.
8
+ const where = extension.lock.commit ? `${extension.lock.source} @ ${extension.lock.commit.slice(0, 12)}` : path.normalize(extension.lock.source);
9
+ const source = extension.lock.marketplace ? `${extension.lock.marketplace}${extension.lock.commit ? ` @ ${extension.lock.commit.slice(0, 12)}` : ""}` : where;
8
10
  const version = extension.manifest?.version ? ` ${extension.manifest.version}` : "";
9
11
  const author = extension.manifest?.author ? ` by ${extension.manifest.author.name}` : "";
10
12
  return `${extension.name}${version} [${extension.scope}] ${state}${author} from ${source}`;
@@ -12,8 +14,9 @@ export function describe(extension) {
12
14
  /**
13
15
  * `/extensions` in a chat (#37): with nothing after it, what the session runs with, what's off
14
16
  * and why, and what's installed; with `enable <name>` or `disable <name>`, changes its lock and
15
- * asks the chat to reopen the session (`reopen`), which keeps the conversation. `null` when
16
- * `line` isn't this command.
17
+ * says which (`toggled`): the chat turns it on or off in the running session when it can (#49),
18
+ * and opens the session again otherwise, keeping the conversation. `null` when `line` isn't this
19
+ * command.
17
20
  */
18
21
  export async function chatExtensionsCommand(line, status, canReopen) {
19
22
  const [command, action, name, ...extra] = line.trim().split(/\s+/);
@@ -25,17 +28,17 @@ export async function chatExtensionsCommand(line, status, canReopen) {
25
28
  lines.push(t().extensionOff(off, reason));
26
29
  if (status?.installed.length)
27
30
  lines.push(t().extensionsInstalled, ...status.installed.map((extension) => ` ${describe(extension)}`));
28
- return { lines, reopen: false };
31
+ return { lines };
29
32
  }
30
33
  const enable = action.toLowerCase() === "enable";
31
34
  if ((!enable && action.toLowerCase() !== "disable") || !name || extra.length)
32
- return { lines: [t().extensionsUsage], reopen: false };
35
+ return { lines: [t().extensionsUsage] };
33
36
  if (!canReopen)
34
- return { lines: [t().extensionsCantReopen], reopen: false };
37
+ return { lines: [t().extensionsCantReopen] };
35
38
  const installed = (status?.installed ?? []).find((extension) => extension.name === name && !extension.shadowed);
36
39
  const dir = installed ? status?.dirs[installed.scope] : undefined;
37
40
  if (!installed || !dir)
38
- return { lines: [t().extensionNotInstalled(name)], reopen: false };
41
+ return { lines: [t().extensionNotInstalled(name)] };
39
42
  await setExtensionEnabled(dir, name, enable);
40
- return { lines: [t().extensionToggled(name, enable)], reopen: true };
43
+ return { lines: [], toggled: { name, enabled: enable } };
41
44
  }
@@ -1,6 +1,7 @@
1
1
  import type { Options } from "@anthropic-ai/claude-agent-sdk";
2
2
  import type { ExtensionsStatus, ModeControl } from "../core/session.js";
3
3
  import type { ToolLabels } from "../core/toolLabels.js";
4
+ import type { ExtensionSwitch } from "../core/liveExtensions.js";
4
5
  import { type RunFolder, type RunSummary } from "../core/runs.js";
5
6
  /**
6
7
  * Builds the session's options for a run folder, e.g.
@@ -14,6 +15,7 @@ export type SessionOpener = (run: RunFolder) => Promise<Options | {
14
15
  toolLabels?: ToolLabels;
15
16
  extensions?: ExtensionsStatus;
16
17
  apis?: Record<string, Record<string, unknown>>;
18
+ switchExtension?: ExtensionSwitch;
17
19
  }>;
18
20
  export interface OpenedSession {
19
21
  run: RunFolder;
@@ -22,6 +24,7 @@ export interface OpenedSession {
22
24
  toolLabels?: ToolLabels;
23
25
  extensions?: ExtensionsStatus;
24
26
  apis?: Record<string, Record<string, unknown>>;
27
+ switchExtension?: ExtensionSwitch;
25
28
  }
26
29
  /** The run a chat starts with: the latest one with `--continue` (if any), a new one otherwise. */
27
30
  export declare function firstRun(runsDir: string, argv?: readonly string[]): Promise<RunFolder>;
package/dist/chat/runs.js CHANGED
@@ -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, toolLabels: opened.toolLabels, extensions: opened.extensions, apis: opened.apis } : { 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,6 +1,5 @@
1
1
  import type { ExtensionDirs } from "./externalExtensions.js";
2
2
  import type { AgentDefinition as SdkSubagentDefinition, McpServerConfig, SettingSource } from "@anthropic-ai/claude-agent-sdk";
3
- import type { KnowledgeStore, PageType } from "../extensions/knowledge/knowledgeStore.js";
4
3
  import type { Extension } from "./extensions.js";
5
4
  /**
6
5
  * The human-supervision spectrum every agent built on this kit shares, independent of
@@ -25,27 +24,8 @@ export type Mode = "interactive" | "guided" | "autonomous" | "plan";
25
24
  */
26
25
  export interface BaseSessionConfig {
27
26
  mode: Mode;
28
- /** 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. */
29
28
  projectDir: string;
30
- /**
31
- * Where the agent writes its own notes (the "wiki"). Setting this or `sourcesDir` is what
32
- * gives the session the file tools (Read/Write/Edit/Glob/Grep, scoped — see
33
- * hooks/fileScopeGate.ts); without either, the agent has no file access at all.
34
- */
35
- knowledgeDir?: string;
36
- /**
37
- * Original files, kept as obtained and apart from the notes in `knowledgeDir`: material
38
- * the user drops in, plus whatever the agent saves with `save_to_sources` (downloaded
39
- * documents, transcripts...). The agent can read and search it but never edit it — Write/
40
- * Edit are scoped to `knowledgeDir` — and `save_to_sources` never overwrites.
41
- */
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
29
  /**
50
30
  * Where extensions are installed for this agent (#37): `agent` for all its projects (e.g.
51
31
  * `~/.miyagi/extensions`), `project` for this one (e.g. `<projectDir>/extensions`), which wins.
@@ -53,10 +33,15 @@ export interface BaseSessionConfig {
53
33
  * installs them.
54
34
  */
55
35
  extensionDirs?: ExtensionDirs;
56
- /** Directories besides `knowledgeDir`/`sourcesDir` where Write/Edit are allowed (see hooks/fileScopeGate.ts). */
36
+ /**
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.
41
+ */
57
42
  extraWritableDirs?: string[];
58
43
  /**
59
- * 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,
60
45
  * Glob, Grep), never write. Read and Glob reach nothing else on the disk but what the kit
61
46
  * knows the agent needs (see hooks/fileScopeGate.ts).
62
47
  */
@@ -96,23 +81,17 @@ export interface AgentSpec<TConfig extends BaseSessionConfig> {
96
81
  * model (with agent-kit's version), and needs it; the session's facts carry it too.
97
82
  */
98
83
  identity?: AgentIdentity;
99
- /**
100
- * Absolute path of a markdown guide to the agent's own domain (its commands, configuration,
101
- * folders), which the `awareness` extension's `about_me` gives the agent when the person asks.
102
- */
103
- helpGuide?: string;
104
84
  /**
105
85
  * MCP servers this agent always registers (e.g. Playwright for a browser-driving
106
86
  * agent), on top of the generic ones `buildSessionOptions()` wires up itself when
107
- * applicable (human approval, manual intervention, save-to-knowledge) — those aren't
108
- * part of the spec because they're driven by `mode` plus this spec's own
109
- * `manualInterventionTexts`/`knowledgeDir`/`sourcesDir`, not by anything else
110
- * 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`.
111
90
  */
112
91
  buildMcpServers(config: TConfig, runDir: string): Record<string, McpServerConfig>;
113
92
  /**
114
93
  * Local plugin roots (skills/commands, SDK "local" plugin type) to load, in the order
115
- * given, when the session has file tools (`config.knowledgeDir`/`config.sourcesDir`). Absolute paths; `buildSessionOptions()`
94
+ * given. Absolute paths; `buildSessionOptions()`
116
95
  * doesn't know or care where they live on disk.
117
96
  */
118
97
  pluginRoots(config: TConfig): string[];
@@ -136,29 +115,14 @@ export interface AgentSpec<TConfig extends BaseSessionConfig> {
136
115
  * over a permissive `canUseTool` — a disallowed tool never even reaches that callback.
137
116
  */
138
117
  disallowedTools?: string[];
139
- /** Overrides this kit's generic `save_to_sources` tool description (registered whenever `config.sourcesDir` is set) with domain-specific wording. */
140
- saveToSourcesDescription?: string;
141
- /**
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.
149
- */
150
- extensions?: (string | Extension)[];
151
- /**
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.
155
- */
156
- knowledgePageTypes?: PageType[];
157
118
  /**
158
- * The knowledge base's store, instead of the kit's over markdown files in `knowledgeDir`
159
- * (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.
160
124
  */
161
- knowledgeStore?(config: TConfig): KnowledgeStore;
125
+ extensions?: Extension[];
162
126
  /**
163
127
  * Whether the kit tells the agent (and each subagent) to reply in the resolved language
164
128
  * (`config.language`, `--language`, the system's), following the human if they write in
@@ -231,8 +195,8 @@ export interface AgentIdentity {
231
195
  export interface PlanModeSpec<TConfig extends BaseSessionConfig = BaseSessionConfig> {
232
196
  /**
233
197
  * Whether `filePath` (absolute) is a plan file Write/Edit may touch in plan mode, e.g.
234
- * `drafts/<slug>/plan.md`. It must also be writable under the file scope (`knowledgeDir`
235
- * or `extraWritableDirs`), which still applies.
198
+ * `drafts/<slug>/plan.md`. It must also be writable under the file scope (`extraWritableDirs`),
199
+ * which still applies.
236
200
  */
237
201
  isPlanFile?(filePath: string, config: TConfig): boolean;
238
202
  /**
@@ -7,12 +7,17 @@ import type { SessionFacts } from "./sessionFacts.js";
7
7
  * plugin with the kit's data in its manifest (`.claude-plugin/plugin.json`, key `"agent-kit"`:
8
8
  * the capabilities it `provides` and `requires`), plus, for an internal one, the code that
9
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).
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).
15
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;
16
21
  /** What an extension sees of the session it's contributing to. */
17
22
  export interface ExtensionContext<TConfig extends BaseSessionConfig = BaseSessionConfig> {
18
23
  config: TConfig;
@@ -22,6 +27,12 @@ export interface ExtensionContext<TConfig extends BaseSessionConfig = BaseSessio
22
27
  mode: Mode;
23
28
  /** A person can be asked (not autonomous): tools that ask them may exist. */
24
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>;
25
36
  /**
26
37
  * What the session is at the moment of the call (#43): its mode now, the extensions running
27
38
  * and off, their tools, the subagents, skills, commands and context. The core's facts, read
@@ -71,7 +82,9 @@ export interface Extension {
71
82
  plugin: string;
72
83
  /** Installed rather than shipped in a package (#37): its subagents get no `Bash`, and no plugin hook runs. */
73
84
  external?: boolean;
74
- /** Why it can't run in this session (e.g. "needs `knowledgeDir` in the config"), or `undefined` when it can. */
85
+ /** An installed one's scope folder, where its lock is (its plugin is elsewhere when it's linked). */
86
+ installedIn?: string;
87
+ /** Why it can't run in this session (e.g. "needs a folder (`dir`)"), or `undefined` when it can. */
75
88
  missing?(context: ExtensionContext): string | undefined;
76
89
  /** What it brings to the session, once it's active. */
77
90
  contribute(context: ExtensionContext): Promise<ExtensionContribution>;
@@ -87,8 +100,6 @@ export interface ExtensionManifest {
87
100
  }
88
101
  /** Reads an extension's manifest from its plugin. */
89
102
  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
103
  /** The extensions a session runs with, and the ones it can't, with why. */
93
104
  export interface ResolvedExtensions {
94
105
  active: {
@@ -103,11 +114,11 @@ export interface ResolvedExtensions {
103
114
  capabilities: Set<string>;
104
115
  }
105
116
  /**
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.
117
+ * Which of the `extensions` run in this session, in the order given: those the session lacks
118
+ * something for are left out, and so are those requiring a capability no active one provides,
119
+ * until nothing more drops out.
109
120
  */
110
- export declare function resolveExtensions(enabled: readonly (string | Extension)[], context: ExtensionContext): ResolvedExtensions;
121
+ export declare function resolveExtensions(extensions: readonly Extension[], context: ExtensionContext): ResolvedExtensions;
111
122
  /** The prompt's "Extensions" section: what's on, and what isn't and why (ADR-025). */
112
123
  export declare function extensionsPromptSection(resolved: ResolvedExtensions): string;
113
124
  /**
@@ -2,10 +2,10 @@ import { readFileSync } from "node:fs";
2
2
  import { readdir, readFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
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";
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
9
  /** Reads an extension's manifest from its plugin. */
10
10
  export function readExtensionManifest(pluginRoot) {
11
11
  const file = path.join(pluginRoot, ".claude-plugin", "plugin.json");
@@ -14,22 +14,12 @@ export function readExtensionManifest(pluginRoot) {
14
14
  throw new Error(`${file} has no "name".`);
15
15
  return { name: raw.name, description: raw.description ?? "", provides: raw["agent-kit"]?.provides ?? [], requires: raw["agent-kit"]?.requires ?? [] };
16
16
  }
17
- /** The kit's internal extensions, by name. */
18
- export const BUILT_IN_EXTENSIONS = [awarenessExtension, sourcesExtension, knowledgeExtension, memoryExtension];
19
17
  /**
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.
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.
23
21
  */
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
- });
22
+ export function resolveExtensions(extensions, context) {
33
23
  const inactive = [];
34
24
  let active = [];
35
25
  for (const extension of extensions) {
@@ -9,7 +9,9 @@ import { type ToolPhrase } from "./messages/index.js";
9
9
  * extension is a Claude Code plugin as it is (#38): its MCP servers come from its `.mcp.json`,
10
10
  * and the kit starts each with Node through a launcher that leaves it only the system's
11
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).
12
+ * what the kit's gates and chat can use (capabilities, read-only tools, labels, help). Each has a
13
+ * data folder of its own (`${CLAUDE_PLUGIN_DATA}`, as Claude Code gives a plugin), kept across
14
+ * sessions and updates; one being developed is linked rather than copied (#52).
13
15
  */
14
16
  /** The two folders an agent installs extensions into; either may be left out. */
15
17
  export interface ExtensionDirs {
@@ -24,7 +26,7 @@ export type ExtensionScope = keyof ExtensionDirs;
24
26
  export interface PluginServer {
25
27
  command: string;
26
28
  args?: string[];
27
- /** Its variables: a value may take the agent's with `${VAR}`. */
29
+ /** Its variables: a value may take the agent's with `${VAR}`, and its folders with `${CLAUDE_PLUGIN_ROOT}` and `${CLAUDE_PLUGIN_DATA}`. */
28
30
  env?: Record<string, string>;
29
31
  }
30
32
  /** How the chat shows one of its tools in one language: `{field}` takes the call's input. */
@@ -55,6 +57,8 @@ export interface ExternalManifest {
55
57
  requires: string[];
56
58
  /** The agent-kit versions it works with, e.g. `">=0.19.0 <0.21.0"`. */
57
59
  kit?: string;
60
+ /** Programs it needs on this computer, found on the `PATH` (`"docker"`, `"python3"`): without one it's off, saying which. */
61
+ needs: string[];
58
62
  /** Its MCP servers, by name (`mcp__<name>__<tool>`), from the plugin's `.mcp.json` or `plugin.json`. */
59
63
  servers: Record<string, PluginServer>;
60
64
  /** Its tools that only read (short names, of any of its servers): plan mode lets them through. */
@@ -66,17 +70,31 @@ export interface ExternalManifest {
66
70
  }
67
71
  /** One extension in a scope's lock. */
68
72
  export interface LockEntry {
69
- /** Where it came from: a folder, or a git URL (with `#ref` when given). */
73
+ /** Where it came from: a folder, or a git URL (with `#ref` when given). A linked one runs from there. */
70
74
  source: string;
75
+ /**
76
+ * Linked rather than copied (`extension add --link`), to develop it: it runs from its folder,
77
+ * whose files aren't checked, so a rebuild takes effect when its server starts again.
78
+ */
79
+ linked?: boolean;
71
80
  /** The commit it was cloned at, for a git source. */
72
81
  commit?: string;
73
- /** SHA-256 over its files, checked when it loads. */
82
+ /** The marketplace it was installed from (`<name>@<marketplace>`), if any. */
83
+ marketplace?: string;
84
+ /** SHA-256 over its files, checked when it loads; empty for a linked one. */
74
85
  sha256: string;
75
86
  enabled: boolean;
76
87
  /** When it was installed, ISO 8601 in UTC. */
77
88
  installed: string;
78
89
  }
79
90
  export declare const LOCK_FILE = "extensions.lock.json";
91
+ /**
92
+ * An installed extension's data folder: `${CLAUDE_PLUGIN_DATA}` in its servers' arguments and
93
+ * variables (a browser profile, a cache), in its scope, outside its plugin and its hash, so it's
94
+ * kept across sessions and updates. Created when a session starts it; removing the extension
95
+ * asks before deleting it.
96
+ */
97
+ export declare function extensionDataDir(scopeDir: string, name: string): string;
80
98
  /** A scope's lock: its extensions by name. */
81
99
  export declare function readLock(scopeDir: string): Promise<Record<string, LockEntry>>;
82
100
  /** Reads an extension's manifest, checking what the kit needs from it. */
@@ -92,7 +110,12 @@ export declare function inKitRange(version: string, range: string): boolean;
92
110
  export interface InstalledExtension {
93
111
  name: string;
94
112
  scope: ExtensionScope;
113
+ /** Its scope's folder, where its lock is. */
114
+ scopeDir: string;
115
+ /** Its plugin: in its scope's folder, or where it is when it's linked. */
95
116
  dir: string;
117
+ /** Its data folder (`${CLAUDE_PLUGIN_DATA}`), which may not exist yet. */
118
+ dataDir: string;
96
119
  lock: LockEntry;
97
120
  /** Also installed in the project's scope, which wins: this one isn't used. */
98
121
  shadowed: boolean;
@@ -111,21 +134,38 @@ export declare function loadExternalExtensions(dirs: ExtensionDirs): Promise<{
111
134
  reason: string;
112
135
  }[];
113
136
  }>;
137
+ /** Why an installed extension can't load, or its manifest when it can. */
138
+ /** Whether a program is on the `PATH`, as a shell would find it (with `PATHEXT`'s extensions on Windows). */
139
+ export declare function onPath(program: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
114
140
  /** Whether `source` names a git repository rather than a folder. */
115
141
  export declare function isGitSource(source: string): boolean;
116
142
  /**
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.
143
+ * Installs an extension into a scope from a folder or a git repository (`url#ref`, `sha` to pin
144
+ * a commit, and `subdir` for one inside it): copies its files (never runs a script), checks its
145
+ * manifest, and locks it, enabled, with the marketplace it came from when it did (`origin`, for a
146
+ * temporary folder, is what the lock says it came from). An extension with the same name in that
147
+ * scope is replaced; its data folder stays. With `link`, a folder is registered where it is
148
+ * instead, to develop it: not copied, and its files not checked.
120
149
  */
121
150
  export declare function addExtension(source: string, scopeDir: string, options?: {
122
151
  subdir?: string;
152
+ sha?: string;
153
+ marketplace?: string;
154
+ origin?: string;
155
+ link?: boolean;
123
156
  }): Promise<{
124
157
  name: string;
125
158
  replaced: boolean;
126
159
  commit?: string;
127
160
  }>;
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>;
161
+ /**
162
+ * Removes an extension from a scope (its files and its lock entry; a linked one's folder is left
163
+ * where it is), and its data folder with `deleteData`; false when it wasn't there.
164
+ */
165
+ export declare function removeExtension(scopeDir: string, name: string, options?: {
166
+ deleteData?: boolean;
167
+ }): Promise<boolean>;
168
+ /** The size in bytes of the files under `dir` (0 when it doesn't exist). */
169
+ export declare function folderSize(dir: string): Promise<number>;
130
170
  /** Enables or disables an installed extension in a scope; false when it isn't there. */
131
171
  export declare function setExtensionEnabled(scopeDir: string, name: string, enabled: boolean): Promise<boolean>;