@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
@@ -0,0 +1,242 @@
1
+ import { execFile } from "node:child_process";
2
+ import { cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { addExtension, isGitSource } from "./externalExtensions.js";
7
+ import { fetchArchivePlugin, fetchNpmPlugin } from "./pluginDownloads.js";
8
+ /**
9
+ * Marketplaces (ADR-025, #29): a repository of extensions is a Claude Code marketplace as it is,
10
+ * a folder or git repository with `.claude-plugin/marketplace.json` listing plugins and where
11
+ * each comes from. An agent knows the marketplaces added to it, kept in its extensions folder
12
+ * (`marketplaces.json`, and a copy of each in `.marketplaces/<name>/`), and installs from them
13
+ * by `<plugin>@<marketplace>` into either scope, through `addExtension()`: the same copy, hash and
14
+ * lock as any extension. Its official marketplace is known without asking; any other is added
15
+ * only by hand, after a warning (the command asks to type its name).
16
+ */
17
+ const exec = promisify(execFile);
18
+ const REGISTRY = "marketplaces.json";
19
+ const COPIES = ".marketplaces";
20
+ const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
21
+ /** Reads and checks a marketplace's `.claude-plugin/marketplace.json`. */
22
+ export async function readMarketplaceManifest(root) {
23
+ const file = path.join(root, ".claude-plugin", "marketplace.json");
24
+ const raw = JSON.parse(await readFile(file, "utf8").catch(() => {
25
+ throw new Error(`${root} isn't a marketplace: it has no .claude-plugin/marketplace.json.`);
26
+ }));
27
+ const name = raw.name;
28
+ if (typeof name !== "string" || !NAME.test(name) || name.includes(".."))
29
+ throw new Error(`${file}: "name" must be letters, digits, ".", "_" or "-".`);
30
+ const owner = raw.owner;
31
+ if (!owner || typeof owner.name !== "string")
32
+ throw new Error(`${file}: "owner" needs a "name".`);
33
+ if (!Array.isArray(raw.plugins))
34
+ throw new Error(`${file}: "plugins" must be a list.`);
35
+ const metadata = (raw.metadata ?? {});
36
+ const plugins = raw.plugins.map((plugin, index) => {
37
+ if (typeof plugin?.name !== "string" || !NAME.test(plugin.name))
38
+ throw new Error(`${file}: plugin ${index + 1} has no valid "name".`);
39
+ if (plugin.source === undefined)
40
+ throw new Error(`${file}: ${plugin.name} has no "source".`);
41
+ return plugin;
42
+ });
43
+ const description = raw.description ?? metadata.description;
44
+ return { name, owner, ...(description ? { description } : {}), ...(metadata.pluginRoot ? { pluginRoot: metadata.pluginRoot } : {}), plugins };
45
+ }
46
+ async function readRegistry(dir) {
47
+ try {
48
+ return JSON.parse(await readFile(path.join(dir, REGISTRY), "utf8"));
49
+ }
50
+ catch (error) {
51
+ if (error.code === "ENOENT")
52
+ return {};
53
+ throw error;
54
+ }
55
+ }
56
+ async function writeRegistry(dir, registry) {
57
+ await mkdir(dir, { recursive: true });
58
+ const sorted = Object.fromEntries(Object.keys(registry).sort().map((name) => [name, registry[name]]));
59
+ await writeFile(path.join(dir, REGISTRY), `${JSON.stringify(sorted, null, 2)}\n`, "utf8");
60
+ }
61
+ /** A git URL for a marketplace or plugin source: `owner/repo` is GitHub's. */
62
+ function gitUrl(source) {
63
+ return /^[\w.-]+\/[\w.-]+$/.test(source) ? `https://github.com/${source}.git` : source.replace(/^github:/, "https://github.com/");
64
+ }
65
+ /** Whether a marketplace source is a git repository rather than a folder (`owner/repo` included, unless such a folder exists). */
66
+ async function isGitMarketplace(source) {
67
+ const [location] = source.split("#");
68
+ if (isGitSource(location))
69
+ return true;
70
+ if (!/^[\w.-]+\/[\w.-]+$/.test(location))
71
+ return false;
72
+ return !(await stat(path.resolve(location)).then((found) => found.isDirectory(), () => false));
73
+ }
74
+ /** Takes a copy of a marketplace (a folder, or a git clone at its ref) into a temporary folder. */
75
+ async function fetchMarketplace(source) {
76
+ const copy = await mkdtemp(path.join(os.tmpdir(), "agent-kit-marketplace-"));
77
+ try {
78
+ if (await isGitMarketplace(source)) {
79
+ const [location, ref] = source.split("#");
80
+ await exec("git", ["clone", "--depth", "1", ...(ref ? ["--branch", ref] : []), gitUrl(location), copy]);
81
+ const commit = (await exec("git", ["-C", copy, "rev-parse", "HEAD"])).stdout.trim();
82
+ await rm(path.join(copy, ".git"), { recursive: true, force: true });
83
+ return { copy, commit };
84
+ }
85
+ await cp(path.resolve(source), copy, { recursive: true, filter: (file) => path.basename(file) !== ".git" });
86
+ return { copy };
87
+ }
88
+ catch (error) {
89
+ await rm(copy, { recursive: true, force: true });
90
+ throw error;
91
+ }
92
+ }
93
+ /** Reads a marketplace from a folder, a git URL (`#ref`) or `owner/repo` without adding it: what to show before asking. */
94
+ export async function inspectMarketplace(source) {
95
+ const { copy } = await fetchMarketplace((await isGitMarketplace(source)) ? source : path.resolve(source));
96
+ try {
97
+ return await readMarketplaceManifest(copy);
98
+ }
99
+ finally {
100
+ await rm(copy, { recursive: true, force: true });
101
+ }
102
+ }
103
+ /**
104
+ * Adds a marketplace to the agent (its extensions folder, `dir`) from a folder, a git URL
105
+ * (`#ref`) or `owner/repo`, taking a copy; one with the same name is replaced. Asking the person
106
+ * first is the caller's: the kit's command asks them to type its name, unless it's `official`.
107
+ */
108
+ export async function addMarketplace(source, dir, options = {}) {
109
+ const location = (await isGitMarketplace(source)) ? source : path.resolve(source);
110
+ const { copy, commit } = await fetchMarketplace(location);
111
+ try {
112
+ const manifest = await readMarketplaceManifest(copy);
113
+ const registry = await readRegistry(dir);
114
+ const replaced = Boolean(registry[manifest.name]);
115
+ const target = path.join(dir, COPIES, manifest.name);
116
+ await rm(target, { recursive: true, force: true });
117
+ await mkdir(path.dirname(target), { recursive: true });
118
+ await cp(copy, target, { recursive: true });
119
+ registry[manifest.name] = { source: location, ...(commit ? { commit } : {}), official: Boolean(options.official || registry[manifest.name]?.official), updated: new Date().toISOString() };
120
+ await writeRegistry(dir, registry);
121
+ return { name: manifest.name, replaced, manifest, ...(commit ? { commit } : {}) };
122
+ }
123
+ finally {
124
+ await rm(copy, { recursive: true, force: true });
125
+ }
126
+ }
127
+ /** The marketplaces the agent knows, with their manifests when they can be read. */
128
+ export async function listMarketplaces(dir) {
129
+ const registry = await readRegistry(dir);
130
+ return Promise.all(Object.entries(registry).map(async ([name, entry]) => {
131
+ const copy = path.join(dir, COPIES, name);
132
+ const manifest = await readMarketplaceManifest(copy).catch(() => undefined);
133
+ return { name, ...entry, dir: copy, ...(manifest ? { manifest } : {}) };
134
+ }));
135
+ }
136
+ /** Takes a fresh copy of a known marketplace from where it came from. */
137
+ export async function updateMarketplace(dir, name) {
138
+ const known = (await readRegistry(dir))[name];
139
+ if (!known)
140
+ throw new Error(`No marketplace ${name}.`);
141
+ const result = await addMarketplace(known.source, dir, { official: known.official });
142
+ if (result.name !== name)
143
+ throw new Error(`${known.source} is now the marketplace ${result.name}, not ${name}.`);
144
+ return result.commit ? { commit: result.commit } : {};
145
+ }
146
+ /** Forgets a marketplace (its copy and its entry); the extensions installed from it stay. False when it wasn't known. */
147
+ export async function removeMarketplace(dir, name) {
148
+ const registry = await readRegistry(dir);
149
+ if (!registry[name])
150
+ return false;
151
+ delete registry[name];
152
+ await rm(path.join(dir, COPIES, name), { recursive: true, force: true });
153
+ await writeRegistry(dir, registry);
154
+ return true;
155
+ }
156
+ /** A plugin found in the known marketplaces: `<name>@<marketplace>`, or a name only one of them offers. */
157
+ export async function findPlugin(dir, spec) {
158
+ const [name, wanted] = spec.split("@");
159
+ const marketplaces = (await listMarketplaces(dir)).filter((marketplace) => !wanted || marketplace.name === wanted);
160
+ if (wanted && !marketplaces.length)
161
+ throw new Error(`No marketplace ${wanted}.`);
162
+ const found = marketplaces.flatMap((marketplace) => (marketplace.manifest?.plugins ?? []).filter((plugin) => plugin.name === name).map((plugin) => ({ marketplace, plugin })));
163
+ if (!found.length)
164
+ throw new Error(`No marketplace offers ${name}${wanted ? ` (${wanted} doesn't)` : ""}.`);
165
+ if (found.length > 1)
166
+ throw new Error(`${name} is in several marketplaces (${found.map(({ marketplace }) => marketplace.name).join(", ")}): say which, ${name}@<marketplace>.`);
167
+ return found[0];
168
+ }
169
+ /** Where a plugin's source points: a folder of the marketplace's copy, a git repository, an npm package or a zip. */
170
+ export function resolvePluginSource(marketplace, plugin) {
171
+ const { source } = plugin;
172
+ if (typeof source === "string") {
173
+ // From the marketplace's root: `./plugins/x`, or a bare name under `pluginRoot`; never "..", as Claude Code.
174
+ if (source.split(/[\\/]/).includes(".."))
175
+ throw new Error(`${plugin.name}'s source leaves its marketplace: ${source}.`);
176
+ const relative = source.startsWith("./") || source === "." ? source : path.join(marketplace.manifest?.pluginRoot ?? ".", source);
177
+ const root = path.resolve(marketplace.dir);
178
+ const folder = path.resolve(root, relative);
179
+ if (folder !== root && !folder.startsWith(`${root}${path.sep}`))
180
+ throw new Error(`${plugin.name}'s source leaves its marketplace: ${source}.`);
181
+ return { source: folder };
182
+ }
183
+ // Its fields, as written: the union above only names the forms the kit knows.
184
+ const entry = source;
185
+ const ref = typeof entry.ref === "string" ? `#${entry.ref}` : "";
186
+ const sha = typeof entry.sha === "string" ? { sha: entry.sha } : {};
187
+ switch (source.source) {
188
+ case "github":
189
+ return { source: `https://github.com/${String(entry.repo)}.git${ref}`, ...sha };
190
+ case "url":
191
+ return { source: `${String(entry.url)}${ref}`, ...sha };
192
+ case "git-subdir":
193
+ return { source: `${gitUrl(String(entry.url))}${ref}`, subdir: String(entry.path), ...sha };
194
+ case "npm": {
195
+ // "package" may carry its version ("@scope/name@^2.0.0"); "version" wins.
196
+ const spec = String(entry.package);
197
+ const at = spec.lastIndexOf("@");
198
+ const [name, inline] = at > 0 ? [spec.slice(0, at), spec.slice(at + 1)] : [spec, undefined];
199
+ const version = typeof entry.version === "string" ? entry.version : inline;
200
+ return { npm: { name, ...(version ? { version } : {}), ...(typeof entry.registry === "string" ? { registry: entry.registry } : {}) } };
201
+ }
202
+ case "archive":
203
+ return { archive: { url: String(entry.url), ...(typeof entry.sha256 === "string" ? { sha256: entry.sha256 } : {}) } };
204
+ default:
205
+ // command runs a program of the marketplace's on this machine: never.
206
+ throw new Error(`${plugin.name} comes from a "${source.source}" source, which agent-kit doesn't install: only folders of the marketplace, github, url, git-subdir, npm and archive.`);
207
+ }
208
+ }
209
+ /**
210
+ * Installs `<plugin>@<marketplace>` (or a plugin only one known marketplace offers) into a scope,
211
+ * locked with where it came from. Its name in the agent is its `plugin.json`'s, as for any extension.
212
+ */
213
+ export async function installFromMarketplace(dir, spec, scopeDir) {
214
+ const { marketplace, plugin } = await findPlugin(dir, spec);
215
+ const resolved = resolvePluginSource(marketplace, plugin);
216
+ const from = `${plugin.name}@${marketplace.name}`;
217
+ if ("npm" in resolved) {
218
+ const { name, version, registry } = resolved.npm;
219
+ const fetched = await fetchNpmPlugin(name, version, registry);
220
+ try {
221
+ return { ...(await addExtension(fetched.dir, scopeDir, { marketplace: from, origin: `npm:${name}@${fetched.version}` })), marketplace: marketplace.name };
222
+ }
223
+ finally {
224
+ await rm(fetched.dir, { recursive: true, force: true });
225
+ }
226
+ }
227
+ if ("archive" in resolved) {
228
+ const fetched = await fetchArchivePlugin(resolved.archive.url, resolved.archive.sha256);
229
+ try {
230
+ return { ...(await addExtension(fetched.root, scopeDir, { marketplace: from, origin: resolved.archive.url })), marketplace: marketplace.name };
231
+ }
232
+ finally {
233
+ await rm(fetched.dir, { recursive: true, force: true });
234
+ }
235
+ }
236
+ const result = await addExtension(resolved.source, scopeDir, {
237
+ ...(resolved.subdir ? { subdir: resolved.subdir } : {}),
238
+ ...(resolved.sha ? { sha: resolved.sha } : {}),
239
+ marketplace: from,
240
+ });
241
+ return { ...result, marketplace: marketplace.name };
242
+ }
@@ -29,6 +29,7 @@ export const de = {
29
29
  extensionsNone: "(keine Erweiterungen)",
30
30
  extensionOff: (name, reason) => `${name} ist aus: ${reason}`,
31
31
  extensionsInstalled: "Installiert:",
32
+ extensionSwitched: (name, enabled) => `(${name} ${enabled ? "aktiviert" : "deaktiviert"}, in derselben Sitzung)`,
32
33
  extensionToggled: (name, enabled) => `(${name} ${enabled ? "aktiviert" : "deaktiviert"}: die Sitzung wird mit derselben Unterhaltung neu geöffnet)`,
33
34
  extensionNotInstalled: (name) => `(${name} ist nicht installiert: /extensions listet sie auf)`,
34
35
  extensionsUsage: "(Verwendung: /extensions, /extensions enable <Name>, /extensions disable <Name>)",
@@ -38,6 +38,8 @@ export interface Messages {
38
38
  extensionsNone: string;
39
39
  extensionOff(name: string, reason: string): string;
40
40
  extensionsInstalled: string;
41
+ /** An installed extension turned off or on in the running session (#49). */
42
+ extensionSwitched(name: string, enabled: boolean): string;
41
43
  extensionToggled(name: string, enabled: boolean): string;
42
44
  extensionNotInstalled(name: string): string;
43
45
  extensionsUsage: string;
@@ -28,6 +28,7 @@ export const en = {
28
28
  extensionsNone: "(no extensions)",
29
29
  extensionOff: (name, reason) => `${name} is off: it ${reason}`,
30
30
  extensionsInstalled: "Installed:",
31
+ extensionSwitched: (name, enabled) => `(${name} ${enabled ? "enabled" : "disabled"}, in this same session)`,
31
32
  extensionToggled: (name, enabled) => `(${name} ${enabled ? "enabled" : "disabled"}: the session reopens, with the same conversation)`,
32
33
  extensionNotInstalled: (name) => `(${name} isn't installed: /extensions lists them)`,
33
34
  extensionsUsage: "(usage: /extensions, /extensions enable <name>, /extensions disable <name>)",
@@ -29,6 +29,7 @@ export const es = {
29
29
  extensionsNone: "(ninguna extensión)",
30
30
  extensionOff: (name, reason) => `${name} está desactivada: ${reason}`,
31
31
  extensionsInstalled: "Instaladas:",
32
+ extensionSwitched: (name, enabled) => `(${name} ${enabled ? "activada" : "desactivada"}, en esta misma sesión)`,
32
33
  extensionToggled: (name, enabled) => `(${name} ${enabled ? "activada" : "desactivada"}: la sesión se reabre, con la misma conversación)`,
33
34
  extensionNotInstalled: (name) => `(${name} no está instalada: /extensions las lista)`,
34
35
  extensionsUsage: "(uso: /extensions, /extensions enable <nombre>, /extensions disable <nombre>)",
@@ -29,6 +29,7 @@ export const fr = {
29
29
  extensionsNone: "(aucune extension)",
30
30
  extensionOff: (name, reason) => `${name} est désactivée : ${reason}`,
31
31
  extensionsInstalled: "Installées :",
32
+ extensionSwitched: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"}, dans cette même session)`,
32
33
  extensionToggled: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"} : la session se rouvre, avec la même conversation)`,
33
34
  extensionNotInstalled: (name) => `(${name} n'est pas installée : /extensions les liste)`,
34
35
  extensionsUsage: "(usage : /extensions, /extensions enable <nom>, /extensions disable <nom>)",
@@ -0,0 +1,14 @@
1
+ /** The regular files of a tar archive, by path (ustar, with pax and GNU long names). */
2
+ export declare function untar(tar: Uint8Array): Map<string, Uint8Array>;
3
+ /** The highest of `versions` that `wanted` asks for: an exact version, or a range (`^`, `~`, comparators, `*`). */
4
+ export declare function pickVersion(versions: readonly string[], wanted: string): string | undefined;
5
+ /** An npm package into a new temporary folder: `{ dir, version }`. */
6
+ export declare function fetchNpmPlugin(name: string, wanted?: string, registry?: string): Promise<{
7
+ dir: string;
8
+ version: string;
9
+ }>;
10
+ /** A zip archive into a new temporary folder; the plugin is its root, or its only folder. */
11
+ export declare function fetchArchivePlugin(url: string, sha256?: string): Promise<{
12
+ dir: string;
13
+ root: string;
14
+ }>;
@@ -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
+ }
@@ -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({ supportedCommands: () => result.supportedCommands(), contextUsage: () => contextUsage() });
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() {
@@ -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
- /** The knowledge base's store, when the knowledge extension is on (for a host that reads the knowledge base). */
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/`,