@falkenslab/agent-kit 0.19.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.
- package/dist/chat/chatController.d.ts +4 -1
- package/dist/chat/chatController.js +43 -17
- package/dist/chat/extensions.d.ts +7 -3
- package/dist/chat/extensions.js +11 -8
- package/dist/chat/runs.d.ts +3 -0
- package/dist/chat/runs.js +1 -1
- package/dist/core/agentSpec.d.ts +20 -56
- package/dist/core/extensions.d.ts +21 -12
- package/dist/core/extensions.js +8 -18
- package/dist/core/externalExtensions.d.ts +15 -3
- package/dist/core/externalExtensions.js +33 -5
- package/dist/core/liveExtensions.d.ts +51 -0
- package/dist/core/liveExtensions.js +70 -0
- package/dist/core/marketplaces.d.ts +118 -0
- package/dist/core/marketplaces.js +242 -0
- package/dist/core/messages/de.js +1 -0
- package/dist/core/messages/en.d.ts +2 -0
- package/dist/core/messages/en.js +1 -0
- package/dist/core/messages/es.js +1 -0
- package/dist/core/messages/fr.js +1 -0
- package/dist/core/pluginDownloads.d.ts +14 -0
- package/dist/core/pluginDownloads.js +141 -0
- package/dist/core/runner.js +6 -1
- package/dist/core/session.d.ts +11 -4
- package/dist/core/session.js +74 -22
- package/dist/core/sessionFacts.d.ts +5 -0
- package/dist/core/sessionFacts.js +1 -0
- package/dist/extensions/awareness/index.d.ts +12 -3
- package/dist/extensions/awareness/index.js +18 -15
- package/dist/extensions/awareness/tools.d.ts +1 -1
- package/dist/extensions/awareness/tools.js +3 -3
- package/dist/extensions/knowledge/index.d.ts +20 -6
- package/dist/extensions/knowledge/index.js +32 -29
- package/dist/extensions/knowledge/knowledgeStore.d.ts +1 -1
- package/dist/extensions/knowledge/prompt.d.ts +4 -4
- package/dist/extensions/knowledge/prompt.js +4 -4
- package/dist/extensions/memory/index.d.ts +15 -6
- package/dist/extensions/memory/index.js +34 -31
- package/dist/extensions/sources/index.d.ts +17 -5
- package/dist/extensions/sources/index.js +31 -29
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -0
- package/dist/tui/extensionCommand.d.ts +11 -2
- package/dist/tui/extensionCommand.js +129 -5
- package/package.json +1 -1
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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.
|
|
368
|
-
await
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
*
|
|
9
|
-
* `line` isn't this
|
|
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
|
-
|
|
14
|
+
toggled?: {
|
|
15
|
+
name: string;
|
|
16
|
+
enabled: boolean;
|
|
17
|
+
};
|
|
14
18
|
} | null>;
|
package/dist/chat/extensions.js
CHANGED
|
@@ -4,7 +4,9 @@ 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
6
|
const state = extension.shadowed ? "not used (the project's wins)" : extension.lock.enabled ? "enabled" : "disabled";
|
|
7
|
-
|
|
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
|
-
*
|
|
16
|
-
* `line` isn't this
|
|
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
|
|
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]
|
|
35
|
+
return { lines: [t().extensionsUsage] };
|
|
33
36
|
if (!canReopen)
|
|
34
|
-
return { lines: [t().extensionsCantReopen]
|
|
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)]
|
|
41
|
+
return { lines: [t().extensionNotInstalled(name)] };
|
|
39
42
|
await setExtensionEnabled(dir, name, enable);
|
|
40
|
-
return { lines: [
|
|
43
|
+
return { lines: [], toggled: { name, enabled: enable } };
|
|
41
44
|
}
|
package/dist/chat/runs.d.ts
CHANGED
|
@@ -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) {
|
package/dist/core/agentSpec.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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 `
|
|
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
|
|
108
|
-
* part of
|
|
109
|
-
* `manualInterventionTexts
|
|
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
|
|
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
|
|
159
|
-
*
|
|
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
|
-
|
|
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 (`
|
|
235
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* in the core
|
|
14
|
-
*
|
|
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,7 @@ 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 `
|
|
85
|
+
/** Why it can't run in this session (e.g. "needs a folder (`dir`)"), or `undefined` when it can. */
|
|
75
86
|
missing?(context: ExtensionContext): string | undefined;
|
|
76
87
|
/** What it brings to the session, once it's active. */
|
|
77
88
|
contribute(context: ExtensionContext): Promise<ExtensionContribution>;
|
|
@@ -87,8 +98,6 @@ export interface ExtensionManifest {
|
|
|
87
98
|
}
|
|
88
99
|
/** Reads an extension's manifest from its plugin. */
|
|
89
100
|
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
101
|
/** The extensions a session runs with, and the ones it can't, with why. */
|
|
93
102
|
export interface ResolvedExtensions {
|
|
94
103
|
active: {
|
|
@@ -103,11 +112,11 @@ export interface ResolvedExtensions {
|
|
|
103
112
|
capabilities: Set<string>;
|
|
104
113
|
}
|
|
105
114
|
/**
|
|
106
|
-
* Which of the `
|
|
107
|
-
*
|
|
108
|
-
*
|
|
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.
|
|
109
118
|
*/
|
|
110
|
-
export declare function resolveExtensions(
|
|
119
|
+
export declare function resolveExtensions(extensions: readonly Extension[], context: ExtensionContext): ResolvedExtensions;
|
|
111
120
|
/** The prompt's "Extensions" section: what's on, and what isn't and why (ADR-025). */
|
|
112
121
|
export declare function extensionsPromptSection(resolved: ResolvedExtensions): string;
|
|
113
122
|
/**
|
package/dist/core/extensions.js
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 `
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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(
|
|
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) {
|
|
@@ -55,6 +55,8 @@ export interface ExternalManifest {
|
|
|
55
55
|
requires: string[];
|
|
56
56
|
/** The agent-kit versions it works with, e.g. `">=0.19.0 <0.21.0"`. */
|
|
57
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[];
|
|
58
60
|
/** Its MCP servers, by name (`mcp__<name>__<tool>`), from the plugin's `.mcp.json` or `plugin.json`. */
|
|
59
61
|
servers: Record<string, PluginServer>;
|
|
60
62
|
/** Its tools that only read (short names, of any of its servers): plan mode lets them through. */
|
|
@@ -70,6 +72,8 @@ export interface LockEntry {
|
|
|
70
72
|
source: string;
|
|
71
73
|
/** The commit it was cloned at, for a git source. */
|
|
72
74
|
commit?: string;
|
|
75
|
+
/** The marketplace it was installed from (`<name>@<marketplace>`), if any. */
|
|
76
|
+
marketplace?: string;
|
|
73
77
|
/** SHA-256 over its files, checked when it loads. */
|
|
74
78
|
sha256: string;
|
|
75
79
|
enabled: boolean;
|
|
@@ -111,15 +115,23 @@ export declare function loadExternalExtensions(dirs: ExtensionDirs): Promise<{
|
|
|
111
115
|
reason: string;
|
|
112
116
|
}[];
|
|
113
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>;
|
|
114
121
|
/** Whether `source` names a git repository rather than a folder. */
|
|
115
122
|
export declare function isGitSource(source: string): boolean;
|
|
116
123
|
/**
|
|
117
|
-
* Installs an extension into a scope from a folder or a git repository (`url#ref`,
|
|
118
|
-
* for one inside it): copies its files (never runs a script), checks its
|
|
119
|
-
*
|
|
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.
|
|
120
129
|
*/
|
|
121
130
|
export declare function addExtension(source: string, scopeDir: string, options?: {
|
|
122
131
|
subdir?: string;
|
|
132
|
+
sha?: string;
|
|
133
|
+
marketplace?: string;
|
|
134
|
+
origin?: string;
|
|
123
135
|
}): Promise<{
|
|
124
136
|
name: string;
|
|
125
137
|
replaced: boolean;
|
|
@@ -58,6 +58,7 @@ export async function readExternalManifest(dir) {
|
|
|
58
58
|
provides: kit.provides ?? [],
|
|
59
59
|
requires: kit.requires ?? [],
|
|
60
60
|
...(kit.kit ? { kit: kit.kit } : {}),
|
|
61
|
+
needs: Array.isArray(kit.needs) ? kit.needs.filter((program) => typeof program === "string") : [],
|
|
61
62
|
servers: await pluginServers(dir, raw),
|
|
62
63
|
readOnlyTools: kit.readOnlyTools ?? [],
|
|
63
64
|
labels: kit.labels ?? {},
|
|
@@ -254,6 +255,19 @@ export async function loadExternalExtensions(dirs) {
|
|
|
254
255
|
return { extensions, off };
|
|
255
256
|
}
|
|
256
257
|
/** Why an installed extension can't load, or its manifest when it can. */
|
|
258
|
+
/** Whether a program is on the `PATH`, as a shell would find it (with `PATHEXT`'s extensions on Windows). */
|
|
259
|
+
export async function onPath(program, env = process.env) {
|
|
260
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path.delimiter).filter(Boolean);
|
|
261
|
+
const extensions = process.platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").map((extension) => extension.toLowerCase())] : [""];
|
|
262
|
+
for (const dir of dirs) {
|
|
263
|
+
for (const extension of extensions) {
|
|
264
|
+
const found = await stat(path.join(dir, `${program}${extension}`)).then((entry) => entry.isFile(), () => false);
|
|
265
|
+
if (found)
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
257
271
|
async function problem(installed) {
|
|
258
272
|
let manifest;
|
|
259
273
|
try {
|
|
@@ -268,6 +282,12 @@ async function problem(installed) {
|
|
|
268
282
|
return `is installed as "${installed.name}" but its manifest says "${manifest.name}"`;
|
|
269
283
|
if (manifest.kit && !inKitRange(agentKitVersion(), manifest.kit))
|
|
270
284
|
return `works with agent-kit ${manifest.kit}, and this agent runs ${agentKitVersion()}`;
|
|
285
|
+
const missing = [];
|
|
286
|
+
for (const program of manifest.needs)
|
|
287
|
+
if (!(await onPath(program)))
|
|
288
|
+
missing.push(program);
|
|
289
|
+
if (missing.length)
|
|
290
|
+
return `needs ${missing.join(", ")}, which ${missing.length === 1 ? "isn't" : "aren't"} on this computer (on the PATH)`;
|
|
271
291
|
return (await serversProblem(manifest, installed.dir)) ?? manifest;
|
|
272
292
|
}
|
|
273
293
|
const exec = promisify(execFile);
|
|
@@ -276,9 +296,11 @@ export function isGitSource(source) {
|
|
|
276
296
|
return /^(https?:\/\/|git@|ssh:\/\/|git:\/\/|file:\/\/|github:)/.test(source) || /\.git(#.*)?$/.test(source);
|
|
277
297
|
}
|
|
278
298
|
/**
|
|
279
|
-
* Installs an extension into a scope from a folder or a git repository (`url#ref`,
|
|
280
|
-
* for one inside it): copies its files (never runs a script), checks its
|
|
281
|
-
*
|
|
299
|
+
* Installs an extension into a scope from a folder or a git repository (`url#ref`, `sha` to pin
|
|
300
|
+
* a commit, and `subdir` for one inside it): copies its files (never runs a script), checks its
|
|
301
|
+
* manifest, and locks it, enabled, with the marketplace it came from when it did (`origin`, for a
|
|
302
|
+
* temporary folder, is what the lock says it came from). An extension with the same name in that
|
|
303
|
+
* scope is replaced.
|
|
282
304
|
*/
|
|
283
305
|
export async function addExtension(source, scopeDir, options = {}) {
|
|
284
306
|
let from = path.resolve(source);
|
|
@@ -287,8 +309,13 @@ export async function addExtension(source, scopeDir, options = {}) {
|
|
|
287
309
|
if (isGitSource(source)) {
|
|
288
310
|
const [url, ref] = source.replace(/^github:/, "https://github.com/").split("#");
|
|
289
311
|
clone = await mkdtemp(path.join(os.tmpdir(), "agent-kit-extension-"));
|
|
290
|
-
|
|
312
|
+
// A pinned commit needs the history to check it out; otherwise the tip is enough.
|
|
313
|
+
await exec("git", ["clone", ...(options.sha ? [] : ["--depth", "1"]), ...(ref ? ["--branch", ref] : []), url, clone]);
|
|
314
|
+
if (options.sha)
|
|
315
|
+
await exec("git", ["-C", clone, "checkout", "--quiet", options.sha]);
|
|
291
316
|
commit = (await exec("git", ["-C", clone, "rev-parse", "HEAD"])).stdout.trim();
|
|
317
|
+
if (options.sha && commit !== options.sha.toLowerCase())
|
|
318
|
+
throw new Error(`${url} has no commit ${options.sha}.`);
|
|
292
319
|
from = clone;
|
|
293
320
|
}
|
|
294
321
|
try {
|
|
@@ -305,8 +332,9 @@ export async function addExtension(source, scopeDir, options = {}) {
|
|
|
305
332
|
await mkdir(scopeDir, { recursive: true });
|
|
306
333
|
await cp(from, target, { recursive: true, filter: (file) => path.basename(file) !== ".git" });
|
|
307
334
|
lock[manifest.name] = {
|
|
308
|
-
source: isGitSource(source) ? `${source}${options.subdir ? ` (${options.subdir})` : ""}` : from,
|
|
335
|
+
source: options.origin ?? (isGitSource(source) ? `${source}${options.subdir ? ` (${options.subdir})` : ""}` : from),
|
|
309
336
|
...(commit ? { commit } : {}),
|
|
337
|
+
...(options.marketplace ? { marketplace: options.marketplace } : {}),
|
|
310
338
|
sha256: await hashExtension(target),
|
|
311
339
|
enabled: true,
|
|
312
340
|
installed: new Date().toISOString(),
|