@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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { inKitRange } from "./externalExtensions.js";
|
|
6
|
+
async function fflate() {
|
|
7
|
+
try {
|
|
8
|
+
return (await import("fflate"));
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
throw new Error('Installing from an npm or archive source needs the optional "fflate" library: npm install fflate', { cause: error });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function download(url) {
|
|
15
|
+
if (!url.startsWith("https://"))
|
|
16
|
+
throw new Error(`Only https downloads: ${url}.`);
|
|
17
|
+
const response = await fetch(url);
|
|
18
|
+
if (!response.ok)
|
|
19
|
+
throw new Error(`${url} answered ${response.status}.`);
|
|
20
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
21
|
+
}
|
|
22
|
+
/** Files into `dir`, each at its relative path; one that would land outside it is refused. */
|
|
23
|
+
async function writeFiles(dir, files) {
|
|
24
|
+
const root = path.resolve(dir);
|
|
25
|
+
for (const [name, data] of files) {
|
|
26
|
+
const target = path.resolve(root, name);
|
|
27
|
+
if (!target.startsWith(`${root}${path.sep}`) || name.split(/[\\/]/).includes(".."))
|
|
28
|
+
throw new Error(`The download has a file outside its folder: ${name}.`);
|
|
29
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
30
|
+
await writeFile(target, data);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** The regular files of a tar archive, by path (ustar, with pax and GNU long names). */
|
|
34
|
+
export function untar(tar) {
|
|
35
|
+
const files = new Map();
|
|
36
|
+
const text = (from, length) => new TextDecoder().decode(tar.subarray(from, from + length)).replace(/\0.*$/s, "");
|
|
37
|
+
let offset = 0;
|
|
38
|
+
let longName;
|
|
39
|
+
while (offset + 512 <= tar.length) {
|
|
40
|
+
const name = text(offset, 100);
|
|
41
|
+
if (!name)
|
|
42
|
+
break; // the two empty blocks at the end
|
|
43
|
+
const size = parseInt(text(offset + 124, 12).trim() || "0", 8);
|
|
44
|
+
const type = text(offset + 156, 1);
|
|
45
|
+
const prefix = text(offset + 345, 155);
|
|
46
|
+
const body = tar.subarray(offset + 512, offset + 512 + size);
|
|
47
|
+
if (type === "x") {
|
|
48
|
+
longName = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(new TextDecoder().decode(body))?.[1] ?? longName;
|
|
49
|
+
}
|
|
50
|
+
else if (type === "L") {
|
|
51
|
+
longName = new TextDecoder().decode(body).replace(/\0.*$/s, "");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
if (type === "0" || type === "" || type === "7")
|
|
55
|
+
files.set(longName ?? (prefix ? `${prefix}/${name}` : name), body);
|
|
56
|
+
longName = undefined;
|
|
57
|
+
}
|
|
58
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
59
|
+
}
|
|
60
|
+
return files;
|
|
61
|
+
}
|
|
62
|
+
/** The highest of `versions` that `wanted` asks for: an exact version, or a range (`^`, `~`, comparators, `*`). */
|
|
63
|
+
export function pickVersion(versions, wanted) {
|
|
64
|
+
const range = wanted.trim();
|
|
65
|
+
if (versions.includes(range))
|
|
66
|
+
return range;
|
|
67
|
+
const comparators = range.split(/\s+/).flatMap((part) => {
|
|
68
|
+
if (part === "*" || part === "x" || part === "")
|
|
69
|
+
return [];
|
|
70
|
+
const caret = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(part);
|
|
71
|
+
if (caret) {
|
|
72
|
+
const [major, minor, patch] = caret.slice(1).map(Number);
|
|
73
|
+
const upper = major > 0 ? `${major + 1}.0.0` : minor > 0 ? `0.${minor + 1}.0` : `0.0.${patch + 1}`;
|
|
74
|
+
return [`>=${major}.${minor}.${patch}`, `<${upper}`];
|
|
75
|
+
}
|
|
76
|
+
const tilde = /^~(\d+)\.(\d+)\.(\d+)$/.exec(part);
|
|
77
|
+
if (tilde)
|
|
78
|
+
return [`>=${tilde[1]}.${tilde[2]}.${tilde[3]}`, `<${tilde[1]}.${Number(tilde[2]) + 1}.0`];
|
|
79
|
+
return [part];
|
|
80
|
+
});
|
|
81
|
+
const stable = versions.filter((version) => /^\d+\.\d+\.\d+$/.test(version));
|
|
82
|
+
const matching = stable.filter((version) => !comparators.length || inKitRange(version, comparators.join(" ")));
|
|
83
|
+
const order = (version) => version.split(".").map(Number);
|
|
84
|
+
return matching.sort((a, b) => {
|
|
85
|
+
const [x, y] = [order(a), order(b)];
|
|
86
|
+
for (let i = 0; i < 3; i++)
|
|
87
|
+
if (x[i] !== y[i])
|
|
88
|
+
return x[i] - y[i];
|
|
89
|
+
return 0;
|
|
90
|
+
}).at(-1);
|
|
91
|
+
}
|
|
92
|
+
/** An npm package into a new temporary folder: `{ dir, version }`. */
|
|
93
|
+
export async function fetchNpmPlugin(name, wanted = "latest", registry = "https://registry.npmjs.org") {
|
|
94
|
+
if (!/^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i.test(name))
|
|
95
|
+
throw new Error(`"${name}" isn't an npm package name.`);
|
|
96
|
+
const response = await fetch(`${registry.replace(/\/+$/, "")}/${name.replace("/", "%2f")}`, { headers: { accept: "application/json" } });
|
|
97
|
+
if (!response.ok)
|
|
98
|
+
throw new Error(`The registry has no package ${name} (${response.status}).`);
|
|
99
|
+
const metadata = (await response.json());
|
|
100
|
+
const version = metadata["dist-tags"]?.[wanted] ?? pickVersion(Object.keys(metadata.versions ?? {}), wanted);
|
|
101
|
+
const dist = version ? metadata.versions?.[version]?.dist : undefined;
|
|
102
|
+
if (!version || !dist?.tarball)
|
|
103
|
+
throw new Error(`${name} has no version ${wanted}.`);
|
|
104
|
+
const tarball = await download(dist.tarball);
|
|
105
|
+
const sri = /^sha512-(.+)$/.exec(dist.integrity ?? "");
|
|
106
|
+
if (!sri)
|
|
107
|
+
throw new Error(`${name}@${version} has no sha512 integrity to check it against.`);
|
|
108
|
+
if (createHash("sha512").update(tarball).digest("base64") !== sri[1])
|
|
109
|
+
throw new Error(`${name}@${version}'s download doesn't match its integrity.`);
|
|
110
|
+
const { gunzipSync } = await fflate();
|
|
111
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "agent-kit-npm-"));
|
|
112
|
+
try {
|
|
113
|
+
// npm puts a package's files under package/.
|
|
114
|
+
const files = [...untar(gunzipSync(tarball))].flatMap(([file, data]) => (file.startsWith("package/") ? [[file.slice("package/".length), data]] : []));
|
|
115
|
+
await writeFiles(dir, files);
|
|
116
|
+
return { dir, version };
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
await rm(dir, { recursive: true, force: true });
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** A zip archive into a new temporary folder; the plugin is its root, or its only folder. */
|
|
124
|
+
export async function fetchArchivePlugin(url, sha256) {
|
|
125
|
+
const zip = await download(url);
|
|
126
|
+
if (sha256 && createHash("sha256").update(zip).digest("hex") !== sha256.toLowerCase())
|
|
127
|
+
throw new Error(`${url} doesn't match its sha256.`);
|
|
128
|
+
const { unzipSync } = await fflate();
|
|
129
|
+
const files = Object.entries(unzipSync(zip)).filter(([name]) => !name.endsWith("/"));
|
|
130
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "agent-kit-archive-"));
|
|
131
|
+
try {
|
|
132
|
+
await writeFiles(dir, files);
|
|
133
|
+
const tops = new Set(files.map(([name]) => name.split("/")[0]));
|
|
134
|
+
const only = tops.size === 1 && files.every(([name]) => name.includes("/")) ? [...tops][0] : "";
|
|
135
|
+
return { dir, root: path.join(dir, only) };
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
await rm(dir, { recursive: true, force: true });
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
package/dist/core/runner.js
CHANGED
|
@@ -31,7 +31,12 @@ export function runQuery(prompt, options) {
|
|
|
31
31
|
// What the session is, for an extension that asks (sessionFacts.ts): the controls now, the
|
|
32
32
|
// tools and skills when the session starts.
|
|
33
33
|
const view = sessionViewOf(options);
|
|
34
|
-
view?.attach({
|
|
34
|
+
view?.attach({
|
|
35
|
+
supportedCommands: () => result.supportedCommands(),
|
|
36
|
+
contextUsage: () => contextUsage(),
|
|
37
|
+
toggleMcpServer: (server, enabled) => result.toggleMcpServer(server, enabled),
|
|
38
|
+
reloadPlugins: () => result.reloadPlugins(),
|
|
39
|
+
});
|
|
35
40
|
// The main agent's tool calls by id, to name the results that come back for them.
|
|
36
41
|
const toolNames = new Map();
|
|
37
42
|
async function* generateEvents() {
|
package/dist/core/session.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Options, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import { type TranscriptLogger } from "./hooks/transcriptLogger.js";
|
|
3
3
|
import { type ModeControl } from "./modeControl.js";
|
|
4
|
-
import type { KnowledgeStore } from "../extensions/knowledge/knowledgeStore.js";
|
|
5
4
|
import { type ExtensionDirs, type InstalledExtension } from "./externalExtensions.js";
|
|
6
5
|
import type { ToolLabels } from "./toolLabels.js";
|
|
7
6
|
import type { AgentSpec, BaseSessionConfig } from "./agentSpec.js";
|
|
8
7
|
import { type Language } from "./language.js";
|
|
9
8
|
import { type RunFolder } from "./runs.js";
|
|
9
|
+
import { type ExtensionSwitch } from "./liveExtensions.js";
|
|
10
10
|
/**
|
|
11
11
|
* Builds the `options` object passed to the Agent SDK's `query()` — everything about
|
|
12
12
|
* *how* the agent runs a session (system prompt, tools, MCP servers, hooks), independent
|
|
@@ -40,10 +40,10 @@ export declare function buildSessionOptions<TConfig extends BaseSessionConfig>(c
|
|
|
40
40
|
toolLabels: ToolLabels;
|
|
41
41
|
/** The installed extensions and what this session runs with, for `/extensions` (#37). */
|
|
42
42
|
extensions: ExtensionsStatus;
|
|
43
|
-
/**
|
|
44
|
-
knowledgeStore?: KnowledgeStore;
|
|
45
|
-
/** What each active extension hands the host (its contribution's `api`), by its name: the sources' `addSource`, the knowledge base's store. */
|
|
43
|
+
/** What each active extension hands the host (its contribution's `api`), by its name: the sources' `addSource`, the knowledge base's `knowledgeStore`. */
|
|
46
44
|
apis: Record<string, Record<string, unknown>>;
|
|
45
|
+
/** Turns an installed extension off or on in the running session; false when that takes opening it again (#49). */
|
|
46
|
+
switchExtension: ExtensionSwitch;
|
|
47
47
|
}>;
|
|
48
48
|
export { createModeControl, togglePlanMode, type ModeControl } from "./modeControl.js";
|
|
49
49
|
/** The extensions of a session: where they're installed, what's installed, what runs and what can't (with why). */
|
|
@@ -68,6 +68,13 @@ export interface ExtensionsStatus {
|
|
|
68
68
|
* and digits as "-" (confirmed empirically).
|
|
69
69
|
*/
|
|
70
70
|
export declare function claudeProjectDir(projectDir: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* The SDK's `permissions.deny` rules for paths no file tool may read or change, each one and
|
|
73
|
+
* whatever is inside it. An absolute path is written `//` and POSIX: `C:\Users\x` is
|
|
74
|
+
* `//c/Users/x` (confirmed empirically on Windows; on POSIX `/home/x` is `//home/x`). `Read`
|
|
75
|
+
* rules reach the tools that read, `Edit` rules those that write.
|
|
76
|
+
*/
|
|
77
|
+
export declare function denyRules(paths: readonly string[]): string[];
|
|
71
78
|
/**
|
|
72
79
|
* The skills of the plugins in `roots`, as the SDK names them: `<plugin>:<folder>`, the
|
|
73
80
|
* plugin's name from its `.claude-plugin/plugin.json` and the skill's folder under `skills/`,
|
package/dist/core/session.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
1
|
+
import { cp, readdir, readFile, rm } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createTranscriptLogger } from "./hooks/transcriptLogger.js";
|
|
@@ -22,6 +22,7 @@ import { replyLanguageInstruction } from "./language.js";
|
|
|
22
22
|
import { chooseLanguage } from "./messages/index.js";
|
|
23
23
|
import { createRunStore } from "./runs.js";
|
|
24
24
|
import { createSessionView, registerSessionView } from "./sessionFacts.js";
|
|
25
|
+
import { createExtensionSwitch } from "./liveExtensions.js";
|
|
25
26
|
import { agentKitVersion } from "./version.js";
|
|
26
27
|
/**
|
|
27
28
|
* Builds the `options` object passed to the Agent SDK's `query()` — everything about
|
|
@@ -69,32 +70,39 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
69
70
|
runDir,
|
|
70
71
|
mode,
|
|
71
72
|
interactive: mode !== "autonomous",
|
|
73
|
+
// What the active ones provide, once they're resolved below.
|
|
74
|
+
capabilities: new Set(),
|
|
72
75
|
session: () => (late.view ? late.view.facts() : Promise.reject(new Error("The session isn't built yet: call session() from a tool, not from contribute()."))),
|
|
73
76
|
};
|
|
74
77
|
// The installed ones (#37), the project's over the agent's: those that can't load are off with
|
|
75
78
|
// why, and so is one named like an extension the spec enables.
|
|
76
79
|
const extensionDirs = config.extensionDirs ?? {};
|
|
77
80
|
const external = await loadExternalExtensions(extensionDirs);
|
|
78
|
-
const enabledNames = new Set((spec.extensions ?? []).map((
|
|
81
|
+
const enabledNames = new Set((spec.extensions ?? []).map((extension) => extension.name));
|
|
79
82
|
const resolved = resolveExtensions([...(spec.extensions ?? []), ...external.extensions.filter((extension) => !enabledNames.has(extension.name))], extensionContext);
|
|
80
83
|
resolved.inactive.push(...external.off, ...external.extensions.filter((extension) => enabledNames.has(extension.name)).map((extension) => ({ name: extension.name, reason: "has the name of one this agent already has" })));
|
|
84
|
+
// Each installed one's plugin, copied into the run's folder: the running session empties and
|
|
85
|
+
// fills the copy to turn the extension off and on (liveExtensions.ts), never what's installed.
|
|
86
|
+
const installedDirs = new Map();
|
|
87
|
+
for (const { extension } of resolved.active.filter(({ extension }) => extension.external)) {
|
|
88
|
+
const copy = path.join(runDir, "extensions", extension.name);
|
|
89
|
+
await rm(copy, { recursive: true, force: true });
|
|
90
|
+
await cp(extension.plugin, copy, { recursive: true });
|
|
91
|
+
installedDirs.set(extension.name, extension.plugin);
|
|
92
|
+
extension.plugin = copy;
|
|
93
|
+
}
|
|
81
94
|
const externalPlugins = new Set(resolved.active.filter(({ extension }) => extension.external).map(({ extension }) => path.resolve(extension.plugin)));
|
|
95
|
+
extensionContext.capabilities = resolved.capabilities;
|
|
82
96
|
const added = await Promise.all(resolved.active.map(({ extension }) => extension.contribute(extensionContext)));
|
|
83
97
|
const extensionPlugins = resolved.active.map(({ extension }) => extension.plugin);
|
|
84
98
|
const fromExtensions = (pick) => added.flatMap((contribution) => pick(contribution) ?? []);
|
|
85
|
-
const knowledgeStore = added.map((contribution) => contribution.api?.knowledgeStore).find(Boolean);
|
|
86
|
-
// An extension may claim `knowledgeDir` as reached only through its tools (the knowledge base,
|
|
87
|
-
// ADR-024); otherwise it's a folder of the agent's own notes, kept with the file tools under
|
|
88
|
-
// its own rules.
|
|
89
|
-
const claimed = (dir) => fromExtensions((contribution) => contribution.toolOnlyDirs).some((only) => path.resolve(only.dir) === path.resolve(dir));
|
|
90
|
-
const notesDir = config.knowledgeDir && !claimed(config.knowledgeDir) ? config.knowledgeDir : undefined;
|
|
91
99
|
const extraDirs = config.extraWritableDirs ?? [];
|
|
92
100
|
const readableExtras = config.extraReadableDirs ?? [];
|
|
93
|
-
// The file tools the session needs: all five for the
|
|
94
|
-
//
|
|
95
|
-
//
|
|
101
|
+
// The file tools the session needs: all five for the extra writable folders, the reading ones
|
|
102
|
+
// for the extra readable folders, and whatever the extensions ask for (the sources' reading
|
|
103
|
+
// ones); without any of them, none.
|
|
96
104
|
const wantedFileTools = new Set([
|
|
97
|
-
...(
|
|
105
|
+
...(extraDirs.length ? FILE_TOOLS : []),
|
|
98
106
|
...(readableExtras.length ? ["Read", "Glob", "Grep"] : []),
|
|
99
107
|
...fromExtensions((contribution) => contribution.fileTools),
|
|
100
108
|
]);
|
|
@@ -105,10 +113,10 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
105
113
|
// searchable but deliberately NOT writable: the only way to add to them is its tools.
|
|
106
114
|
const readOnlyDirs = fromExtensions((contribution) => contribution.readOnlyDirs);
|
|
107
115
|
const toolOnlyDirs = fromExtensions((contribution) => contribution.toolOnlyDirs);
|
|
108
|
-
const writableDirs =
|
|
109
|
-
const searchableDirs = [
|
|
116
|
+
const writableDirs = extraDirs;
|
|
117
|
+
const searchableDirs = [...readOnlyDirs, ...extraDirs, ...readableExtras];
|
|
110
118
|
// Skills/commands/plugins are a separate concern from the file tools: an agent that
|
|
111
|
-
// wants a plugin-provided skill/command but has no
|
|
119
|
+
// wants a plugin-provided skill/command but has no folder of its own shouldn't have
|
|
112
120
|
// to invent one just to get `cwd`/`skills: "all"`/`plugins` wired up. Gated on either
|
|
113
121
|
// signal, not on `pluginRoots` alone, so an agent with file tools keeps getting the SDK's
|
|
114
122
|
// own project-level `.claude/skills`/`.claude/commands` discovery.
|
|
@@ -160,6 +168,10 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
160
168
|
// map, and inherits the full session tools (Bash included) rather than the narrow list
|
|
161
169
|
// each declared subagent actually declares.
|
|
162
170
|
const allowedSubagentTypes = [...new Set([...(subagents?.allowedSubagentTypes ?? []), ...Object.keys(fromPlugins)])];
|
|
171
|
+
// The extensions' read-only tools, for the plan gate: a list the live switch changes (liveExtensions.ts).
|
|
172
|
+
const readOnlyTools = fromExtensions((contribution) => contribution.readOnlyTools);
|
|
173
|
+
// What no file tool may touch: the agent's denied paths and the SDK's own credentials.
|
|
174
|
+
const deniedPaths = [...(config.deniedPaths ?? []), path.join(claudeConfigDir(), ".credentials.json")];
|
|
163
175
|
const transcriptPath = path.join(runDir, "transcript.jsonl");
|
|
164
176
|
const transcriptLogger = createTranscriptLogger(transcriptPath, config.secrets ?? []);
|
|
165
177
|
const promptSections = [
|
|
@@ -195,6 +207,9 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
195
207
|
// An installed extension's plugin may carry hooks: none run (ADR-025). The kit's own,
|
|
196
208
|
// passed in `hooks`, still do (confirmed empirically).
|
|
197
209
|
...(externalPlugins.size ? { disableAllHooks: true } : {}),
|
|
210
|
+
// The denied paths again, as the SDK's own rules: a second layer under the file scope gate,
|
|
211
|
+
// which a tool the gate doesn't know of can't pass either (#49).
|
|
212
|
+
permissions: { deny: denyRules(deniedPaths) },
|
|
198
213
|
},
|
|
199
214
|
// No built-in tools except, if applicable, Read/Write/Edit/Glob/Grep scoped to the
|
|
200
215
|
// project's own folders (fileScopeGate below), and WebFetch/WebSearch
|
|
@@ -230,7 +245,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
230
245
|
// skills, on top of the plugin-provided built-ins from spec.pluginRoots()
|
|
231
246
|
// below. skipMcpDiscovery: true on those plugins is the caller's choice.
|
|
232
247
|
cwd: config.projectDir,
|
|
233
|
-
// Empty when there's no
|
|
248
|
+
// Empty when there's no folder of the agent's or its extensions' (the plugin-only case) — an
|
|
234
249
|
// empty array is a valid, no-op value for this SDK option, not omitted, since
|
|
235
250
|
// this whole block is already conditioned on includeSkillsAndPlugins.
|
|
236
251
|
additionalDirectories: searchableDirs,
|
|
@@ -264,7 +279,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
264
279
|
searchableDirs,
|
|
265
280
|
readOnlyDirs,
|
|
266
281
|
// The SDK's own credentials, whatever the agent denies.
|
|
267
|
-
deniedPaths
|
|
282
|
+
deniedPaths,
|
|
268
283
|
// Read and Glob only in the agent's folders, plus what only the kit knows it
|
|
269
284
|
// needs: the run folder, the plugins, the project's .claude/ (when loaded) and
|
|
270
285
|
// the SDK's large tool results. Never the rest of the disk (~/.ssh, other
|
|
@@ -287,7 +302,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
287
302
|
hooks: [
|
|
288
303
|
createPlanGate({
|
|
289
304
|
projectDir: config.projectDir,
|
|
290
|
-
readOnlyTools
|
|
305
|
+
readOnlyTools,
|
|
291
306
|
...(spec.planMode?.isPlanFile ? { isPlanFile: (filePath) => spec.planMode.isPlanFile(filePath, config) } : {}),
|
|
292
307
|
...(spec.planMode?.isReadOnlyTool ? { isReadOnlyTool: spec.planMode.isReadOnlyTool } : {}),
|
|
293
308
|
}, () => modeControl.mode === "plan"),
|
|
@@ -321,7 +336,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
321
336
|
inactive: resolved.inactive,
|
|
322
337
|
};
|
|
323
338
|
// What the session is, for the extensions that ask (sessionFacts.ts, #43).
|
|
324
|
-
|
|
339
|
+
const viewBase = {
|
|
325
340
|
...(spec.identity ? { identity: spec.identity } : {}),
|
|
326
341
|
kitVersion: agentKitVersion(),
|
|
327
342
|
language,
|
|
@@ -333,15 +348,39 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
333
348
|
servers: Object.keys(added[index]?.mcpServers ?? {}),
|
|
334
349
|
help: added[index]?.helpLines ?? [],
|
|
335
350
|
})),
|
|
336
|
-
|
|
351
|
+
// Its own list: /extensions' status has another, and the live switch changes both.
|
|
352
|
+
inactive: [...resolved.inactive],
|
|
337
353
|
},
|
|
338
354
|
subagents: Object.entries(allAgents).map(([name, agent]) => ({ name, description: agent.description })),
|
|
339
355
|
skills: sdkOptions.skills === "all" ? await pluginSkills(pluginRoots) : [...(sdkOptions.skills ?? [])],
|
|
340
356
|
runDir,
|
|
341
|
-
}
|
|
357
|
+
};
|
|
358
|
+
late.view = createSessionView(viewBase, modeControl);
|
|
342
359
|
registerSessionView(sdkOptions, late.view);
|
|
360
|
+
// Turning an installed extension off and on while the session runs (#49).
|
|
361
|
+
const switchExtension = createExtensionSwitch(resolved.active.flatMap(({ extension, manifest }, index) => {
|
|
362
|
+
const installedDir = installedDirs.get(extension.name);
|
|
363
|
+
if (!installedDir)
|
|
364
|
+
return [];
|
|
365
|
+
const contribution = added[index];
|
|
366
|
+
return [
|
|
367
|
+
{
|
|
368
|
+
name: extension.name,
|
|
369
|
+
installedDir,
|
|
370
|
+
scopeDir: path.dirname(installedDir),
|
|
371
|
+
pluginCopy: extension.plugin,
|
|
372
|
+
servers: Object.keys(contribution?.mcpServers ?? {}),
|
|
373
|
+
readOnlyTools: contribution?.readOnlyTools ?? [],
|
|
374
|
+
toolLabels: contribution?.toolLabels ?? {},
|
|
375
|
+
agents: Object.keys(fromPlugins).filter((agent) => agent.startsWith(`${extension.name}:`)),
|
|
376
|
+
description: manifest.description,
|
|
377
|
+
provides: manifest.provides,
|
|
378
|
+
help: contribution?.helpLines ?? [],
|
|
379
|
+
},
|
|
380
|
+
];
|
|
381
|
+
}), { readOnlyTools, allowedSubagentTypes, toolLabels, status: extensions, facts: viewBase, controls: () => late.view?.controls() ?? {} });
|
|
343
382
|
const apis = Object.fromEntries(resolved.active.flatMap(({ extension }, index) => (added[index]?.api ? [[extension.name, added[index].api]] : [])));
|
|
344
|
-
return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language, toolLabels, extensions, apis,
|
|
383
|
+
return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language, toolLabels, extensions, apis, switchExtension };
|
|
345
384
|
}
|
|
346
385
|
export { createModeControl, togglePlanMode } from "./modeControl.js";
|
|
347
386
|
/** The Claude Code configuration folder the SDK's CLI uses: `CLAUDE_CONFIG_DIR`, or `~/.claude`. */
|
|
@@ -356,6 +395,19 @@ function claudeConfigDir() {
|
|
|
356
395
|
export function claudeProjectDir(projectDir) {
|
|
357
396
|
return path.join(claudeConfigDir(), "projects", path.resolve(projectDir).replace(/[^a-zA-Z0-9]/g, "-"));
|
|
358
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* The SDK's `permissions.deny` rules for paths no file tool may read or change, each one and
|
|
400
|
+
* whatever is inside it. An absolute path is written `//` and POSIX: `C:\Users\x` is
|
|
401
|
+
* `//c/Users/x` (confirmed empirically on Windows; on POSIX `/home/x` is `//home/x`). `Read`
|
|
402
|
+
* rules reach the tools that read, `Edit` rules those that write.
|
|
403
|
+
*/
|
|
404
|
+
export function denyRules(paths) {
|
|
405
|
+
return paths.flatMap((denied) => {
|
|
406
|
+
const posix = path.resolve(denied).replace(/\\/g, "/").replace(/^([A-Za-z]):/, (_, drive) => `/${drive.toLowerCase()}`);
|
|
407
|
+
const rule = `/${posix}`;
|
|
408
|
+
return [`Read(${rule})`, `Read(${rule}/**)`, `Edit(${rule})`, `Edit(${rule}/**)`];
|
|
409
|
+
});
|
|
410
|
+
}
|
|
359
411
|
/** The built-in file tools, in the order the session lists them. */
|
|
360
412
|
const FILE_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep"];
|
|
361
413
|
/**
|
|
@@ -58,6 +58,9 @@ export interface SessionLive {
|
|
|
58
58
|
skills?: string[];
|
|
59
59
|
supportedCommands?: () => Promise<SlashCommand[]>;
|
|
60
60
|
contextUsage?: () => Promise<ContextUsage | null>;
|
|
61
|
+
/** The running session's controls an installed extension is turned off and on with (liveExtensions.ts). */
|
|
62
|
+
toggleMcpServer?: (server: string, enabled: boolean) => Promise<void>;
|
|
63
|
+
reloadPlugins?: () => Promise<unknown>;
|
|
61
64
|
}
|
|
62
65
|
/** The facts known when the options are built: everything but the mode and what only the running session knows. */
|
|
63
66
|
export type StaticSessionFacts = Omit<SessionFacts, "mode" | "switchableModes" | "tools" | "commands" | "context" | "extensions"> & {
|
|
@@ -71,6 +74,8 @@ export type StaticSessionFacts = Omit<SessionFacts, "mode" | "switchableModes" |
|
|
|
71
74
|
export interface SessionView {
|
|
72
75
|
facts(): Promise<SessionFacts>;
|
|
73
76
|
attach(live: SessionLive): void;
|
|
77
|
+
/** What the running session has attached so far. */
|
|
78
|
+
controls(): SessionLive;
|
|
74
79
|
}
|
|
75
80
|
/** The view of a session: its static facts, the mode read from `modeControl` on every call, and what the running session attaches. */
|
|
76
81
|
export declare function createSessionView(base: StaticSessionFacts, modeControl: ModeControl): SessionView;
|
|
@@ -4,10 +4,19 @@ import type { Extension } from "../../core/extensions.js";
|
|
|
4
4
|
export declare function awarenessPluginRoot(): string;
|
|
5
5
|
/** The "Who you are" section of the system prompt: name, version, what it is, the kit's version, and where to look. */
|
|
6
6
|
export declare function identityPromptSection(identity: AgentIdentity): string;
|
|
7
|
+
/** The awareness extension's options. */
|
|
8
|
+
export interface AwarenessOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Absolute path of a markdown guide to the agent's own domain (its commands, configuration,
|
|
11
|
+
* folders), which `about_me` gives the agent when the person asks.
|
|
12
|
+
*/
|
|
13
|
+
guide?: string;
|
|
14
|
+
}
|
|
7
15
|
/**
|
|
8
16
|
* The awareness (#43): the agent knows who it is and what it is at each moment of a session,
|
|
9
17
|
* and the person can ask it. A "Who you are" section, `about_me` (the session's facts, read
|
|
10
|
-
* anew on every call through `ExtensionContext.session()`, and the agent's `
|
|
11
|
-
* the `help` skill, how the kit's chat is used, which doesn't change.
|
|
18
|
+
* anew on every call through `ExtensionContext.session()`, and the agent's `guide`), and
|
|
19
|
+
* the `help` skill, how the kit's chat is used, which doesn't change. It needs the spec's
|
|
20
|
+
* `identity`.
|
|
12
21
|
*/
|
|
13
|
-
export declare
|
|
22
|
+
export declare function awareness(options?: AwarenessOptions): Extension;
|
|
@@ -21,19 +21,22 @@ You are ${who}. You are built on agent-kit ${agentKitVersion()}, which provides
|
|
|
21
21
|
/**
|
|
22
22
|
* The awareness (#43): the agent knows who it is and what it is at each moment of a session,
|
|
23
23
|
* and the person can ask it. A "Who you are" section, `about_me` (the session's facts, read
|
|
24
|
-
* anew on every call through `ExtensionContext.session()`, and the agent's `
|
|
25
|
-
* the `help` skill, how the kit's chat is used, which doesn't change.
|
|
24
|
+
* anew on every call through `ExtensionContext.session()`, and the agent's `guide`), and
|
|
25
|
+
* the `help` skill, how the kit's chat is used, which doesn't change. It needs the spec's
|
|
26
|
+
* `identity`.
|
|
26
27
|
*/
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
28
|
+
export function awareness(options = {}) {
|
|
29
|
+
return {
|
|
30
|
+
name: "awareness",
|
|
31
|
+
plugin: awarenessPluginRoot(),
|
|
32
|
+
missing: ({ spec }) => (spec.identity ? undefined : "needs `identity` in the spec"),
|
|
33
|
+
async contribute({ spec, session }) {
|
|
34
|
+
return {
|
|
35
|
+
mcpServers: { awareness: createAwarenessServer(session, options.guide) },
|
|
36
|
+
promptSection: identityPromptSection(spec.identity),
|
|
37
|
+
readOnlyTools: ["mcp__awareness__about_me"],
|
|
38
|
+
toolLabels: awarenessToolLabels(),
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -2,4 +2,4 @@ import type { SessionFacts } from "../../core/sessionFacts.js";
|
|
|
2
2
|
/** The session's facts as the model reads them: markdown, in English (ADR-019). */
|
|
3
3
|
export declare function describeSession(facts: SessionFacts): string;
|
|
4
4
|
/** The awareness's MCP server: `about_me`, from the session's facts and the agent's guide. */
|
|
5
|
-
export declare function createAwarenessServer(session: () => Promise<SessionFacts>,
|
|
5
|
+
export declare function createAwarenessServer(session: () => Promise<SessionFacts>, guide?: string): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
@@ -41,12 +41,12 @@ export function describeSession(facts) {
|
|
|
41
41
|
return sections.join("\n\n");
|
|
42
42
|
}
|
|
43
43
|
/** The awareness's MCP server: `about_me`, from the session's facts and the agent's guide. */
|
|
44
|
-
export function createAwarenessServer(session,
|
|
44
|
+
export function createAwarenessServer(session, guide) {
|
|
45
45
|
const aboutMe = tool("about_me", "What you are right now: your mode and the ones the person can switch to, your extensions with their tools and the ones off (with why), your subagents, the commands the person can type, how full your context is. With `part: \"guide\"`, your own guide to your domain (your commands, configuration, folders). It changes during a session: call it whenever the person asks about you, rather than answering from memory.", { part: z.enum(["now", "guide"]).optional().describe("`now` (the default): what you are right now. `guide`: your own guide to your domain.") }, async ({ part }) => {
|
|
46
46
|
if (part === "guide") {
|
|
47
|
-
if (!
|
|
47
|
+
if (!guide)
|
|
48
48
|
return ok("You have no guide of your own: for your domain, say only what your instructions tell you, and that the rest isn't documented.");
|
|
49
|
-
return ok(await readFile(
|
|
49
|
+
return ok(await readFile(guide, "utf8"));
|
|
50
50
|
}
|
|
51
51
|
return ok(describeSession(await session()));
|
|
52
52
|
}, { annotations: { readOnlyHint: true } });
|
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { BaseSessionConfig } from "../../core/agentSpec.js";
|
|
2
|
+
import { type Extension, type FolderOption } from "../../core/extensions.js";
|
|
3
|
+
import type { KnowledgeStore, PageType } from "./knowledgeStore.js";
|
|
4
|
+
/** The knowledge base's options. */
|
|
5
|
+
export interface KnowledgeOptions<TConfig extends BaseSessionConfig = BaseSessionConfig> {
|
|
6
|
+
/** The knowledge base's folder, reached only through its tools, never the file tools. Without a folder the extension is off. */
|
|
7
|
+
dir: FolderOption<TConfig>;
|
|
8
|
+
/**
|
|
9
|
+
* The agent's own page types, besides the kit's (summary, concept, entity, synthesis,
|
|
10
|
+
* preference): each with its folder (`""` for the root), index section, description (told to
|
|
11
|
+
* the model) and template.
|
|
12
|
+
*/
|
|
13
|
+
pageTypes?: PageType[];
|
|
14
|
+
/** Its store, instead of the kit's over markdown files in `dir` (e.g. a database or a vector store implementing `KnowledgeStore`). */
|
|
15
|
+
store?(config: TConfig, dir: string): KnowledgeStore;
|
|
16
|
+
}
|
|
2
17
|
/**
|
|
3
|
-
* The built-in knowledge base (
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* folder of the agent's own notes.
|
|
18
|
+
* The built-in knowledge base (ADR-008, ADR-024): an interlinked wiki the agent keeps only through
|
|
19
|
+
* its `knowledge_*` tools, over a `KnowledgeStore` (the kit's over markdown files, or the agent's
|
|
20
|
+
* own). The host reads it through `apis.knowledge.knowledgeStore`.
|
|
7
21
|
*/
|
|
8
|
-
export declare
|
|
22
|
+
export declare function knowledge<TConfig extends BaseSessionConfig = BaseSessionConfig>(options: KnowledgeOptions<TConfig>): Extension;
|
|
@@ -1,35 +1,38 @@
|
|
|
1
|
+
import { folderOf } from "../../core/extensions.js";
|
|
1
2
|
import { createFileKnowledgeStore } from "./fileKnowledgeStore.js";
|
|
2
3
|
import { knowledgeToolLabels } from "./labels.js";
|
|
3
4
|
import { knowledgePluginRoot, knowledgePromptSection } from "./prompt.js";
|
|
4
5
|
import { createKnowledgeServer } from "./tools.js";
|
|
5
6
|
/**
|
|
6
|
-
* The built-in knowledge base (
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* folder of the agent's own notes.
|
|
7
|
+
* The built-in knowledge base (ADR-008, ADR-024): an interlinked wiki the agent keeps only through
|
|
8
|
+
* its `knowledge_*` tools, over a `KnowledgeStore` (the kit's over markdown files, or the agent's
|
|
9
|
+
* own). The host reads it through `apis.knowledge.knowledgeStore`.
|
|
10
10
|
*/
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
};
|
|
11
|
+
export function knowledge(options) {
|
|
12
|
+
return {
|
|
13
|
+
name: "knowledge",
|
|
14
|
+
plugin: knowledgePluginRoot(),
|
|
15
|
+
missing: ({ config }) => (folderOf(options.dir, config) ? undefined : "needs a folder (`dir`)"),
|
|
16
|
+
async contribute({ config, runDir, interactive, capabilities }) {
|
|
17
|
+
const knowledgeDir = folderOf(options.dir, config);
|
|
18
|
+
const store = options.store?.(config, knowledgeDir) ?? createFileKnowledgeStore(knowledgeDir, { pageTypes: options.pageTypes });
|
|
19
|
+
// The person's preferences, by title, for its prompt section (#33).
|
|
20
|
+
const preferences = (await store.list()).filter((page) => page.type === "preference" && page.status === "active").map(({ id, title }) => ({ id, title }));
|
|
21
|
+
return {
|
|
22
|
+
// knowledge_retire asks a person, so it exists only where there is one.
|
|
23
|
+
mcpServers: { knowledge: createKnowledgeServer(store, { runDir, interactive }) },
|
|
24
|
+
// Beside the sources, its summaries say which original they're about.
|
|
25
|
+
promptSection: knowledgePromptSection({ withSources: capabilities.has("sources"), pageTypes: store.types(), preferences }),
|
|
26
|
+
// Never the file tools on the knowledge folder (ADR-024): the denial points to the tools.
|
|
27
|
+
toolOnlyDirs: [{ dir: knowledgeDir, instead: "the knowledge_* tools" }],
|
|
28
|
+
readOnlyTools: ["mcp__knowledge__knowledge_index", "mcp__knowledge__knowledge_search", "mcp__knowledge__knowledge_read", "mcp__knowledge__knowledge_check"],
|
|
29
|
+
selfAskingTools: ["mcp__knowledge__knowledge_retire"],
|
|
30
|
+
toolLabels: knowledgeToolLabels(),
|
|
31
|
+
helpLines: [
|
|
32
|
+
"You keep a knowledge base (your memory across sessions). Its commands: `/knowledge:ingest` (learn the new or changed files of the sources folder), `/knowledge:query <question>` (answer from it), `/knowledge:lint` (check it and fix what's mechanical).",
|
|
33
|
+
],
|
|
34
|
+
api: { knowledgeStore: store },
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* skills. A page is identified by its type and slug, `concept/bowline`, and links between
|
|
6
6
|
* pages use that id too: `[Bowline](concept/bowline)`.
|
|
7
7
|
*/
|
|
8
|
-
/** A kind of page: one of the kit's, or one an agent declares (`
|
|
8
|
+
/** A kind of page: one of the kit's, or one an agent declares (`knowledge({ pageTypes })`). */
|
|
9
9
|
export interface PageType {
|
|
10
10
|
/** Its name, lowercase: `concept`, `topic`. Page ids start with it. */
|
|
11
11
|
type: string;
|