@falkenslab/agent-kit 0.22.0 → 0.23.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/README.md +1 -4
- package/dist/chat/chatController.d.ts +1 -0
- package/dist/chat/chatController.js +4 -1
- package/dist/chat/extensions.d.ts +6 -0
- package/dist/chat/extensions.js +25 -0
- package/dist/core/extensionCatalog.d.ts +9 -0
- package/dist/core/extensionCatalog.js +10 -3
- package/dist/core/extensionRequirements.d.ts +66 -0
- package/dist/core/extensionRequirements.js +112 -0
- package/dist/core/extensions.d.ts +68 -1
- package/dist/core/extensions.js +77 -6
- package/dist/core/externalExtensions.d.ts +18 -3
- package/dist/core/externalExtensions.js +34 -25
- package/dist/core/liveExtensions.d.ts +4 -0
- package/dist/core/liveExtensions.js +11 -2
- package/dist/core/marketplaces.d.ts +1 -0
- package/dist/core/messages/de.js +4 -0
- package/dist/core/messages/en.d.ts +8 -0
- package/dist/core/messages/en.js +4 -0
- package/dist/core/messages/es.js +4 -0
- package/dist/core/messages/fr.js +4 -0
- package/dist/core/session.d.ts +4 -4
- package/dist/core/session.js +14 -5
- package/dist/core/sessionFacts.d.ts +6 -0
- package/dist/extensions/awareness/tools.js +6 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/tui/extensionCommand.js +17 -1
- package/package.json +1 -1
- package/schemas/agent-kit-metadata.schema.json +60 -2
- package/schemas/marketplace-catalog.schema.json +10 -0
package/README.md
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
<h1>
|
|
2
|
-
<
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-readme-dark.svg">
|
|
4
|
-
<img src="docs/assets/logo-readme.svg" alt="agent-kit" height="64">
|
|
5
|
-
</picture>
|
|
2
|
+
<img src="https://raw.githubusercontent.com/falkenslab/agent-kit/main/docs/assets/logo-readme-any.svg" alt="agent-kit" height="64">
|
|
6
3
|
</h1>
|
|
7
4
|
|
|
8
5
|
[](https://www.npmjs.com/package/@falkenslab/agent-kit)
|
|
@@ -605,7 +605,10 @@ export async function createChatController(options, settings = {}) {
|
|
|
605
605
|
const result = await installFromCatalog(current.extensions.dirs, spec, { ...official, ...(options.scope ? { scope: options.scope } : {}) });
|
|
606
606
|
await refreshInstalled();
|
|
607
607
|
notice(t().extensionInstalled(result.name));
|
|
608
|
-
|
|
608
|
+
// What it needs and this computer lacks (#63): the person installs it, the kit never does.
|
|
609
|
+
for (const line of result.missing)
|
|
610
|
+
notice(t().extensionLacks(result.name, line));
|
|
611
|
+
return { name: result.name, missing: result.missing };
|
|
609
612
|
},
|
|
610
613
|
/** Updates one installed extension from its marketplace, or every one with a newer version there; the names updated. */
|
|
611
614
|
async updateExtension(name) {
|
|
@@ -3,6 +3,12 @@ import type { ExtensionsStatus } from "../core/session.js";
|
|
|
3
3
|
import { type CatalogEntry } from "../core/extensionCatalog.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 declare function describe(extension: InstalledExtension): string;
|
|
6
|
+
/**
|
|
7
|
+
* What the active extensions let the agent do (#62), in the session's language: a heading per
|
|
8
|
+
* extension with capabilities, a line per capability, and the tools a capability names that its
|
|
9
|
+
* server doesn't offer, once the session has started and knows them.
|
|
10
|
+
*/
|
|
11
|
+
export declare function describeCapabilities(status: ExtensionsStatus): string[];
|
|
6
12
|
/** What the chat does for `/extensions install|update|remove|reload` (#56). */
|
|
7
13
|
export type ExtensionAction = {
|
|
8
14
|
type: "install";
|
package/dist/chat/extensions.js
CHANGED
|
@@ -12,6 +12,29 @@ export function describe(extension) {
|
|
|
12
12
|
const author = extension.manifest?.author ? ` by ${extension.manifest.author.name}` : "";
|
|
13
13
|
return `${extension.name}${version} [${extension.scope}] ${state}${author} from ${source}`;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* What the active extensions let the agent do (#62), in the session's language: a heading per
|
|
17
|
+
* extension with capabilities, a line per capability, and the tools a capability names that its
|
|
18
|
+
* server doesn't offer, once the session has started and knows them.
|
|
19
|
+
*/
|
|
20
|
+
export function describeCapabilities(status) {
|
|
21
|
+
const offered = new Set(status.sessionTools?.() ?? []);
|
|
22
|
+
return status.active.flatMap((name) => {
|
|
23
|
+
const capabilities = status.about[name]?.capabilities ?? [];
|
|
24
|
+
const unavailable = status.about[name]?.unavailable ?? [];
|
|
25
|
+
if (!capabilities.length && !unavailable.length)
|
|
26
|
+
return [];
|
|
27
|
+
return [
|
|
28
|
+
t().extensionCan(name),
|
|
29
|
+
...capabilities.map(({ label, description, tools }) => {
|
|
30
|
+
const missing = offered.size ? [...new Set(tools.filter((tool) => !offered.has(tool)).map((tool) => tool.replace(/^mcp__.+?__/, "")))] : [];
|
|
31
|
+
return ` - ${label}${description ? `: ${description}` : ""}${missing.length ? ` ${t().capabilityToolsMissing(missing.join(", "))}` : ""}`;
|
|
32
|
+
}),
|
|
33
|
+
// What it can't do on this computer, with what to install (#63).
|
|
34
|
+
...unavailable.map(({ label, reason }) => ` - ${t().capabilityUnavailable(label, reason)}`),
|
|
35
|
+
];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
15
38
|
/** One line for a catalog entry: what installs it, its version, whether it's installed or has an update, what it is. */
|
|
16
39
|
export function describeEntry(entry) {
|
|
17
40
|
const state = entry.installed ? (entry.update ? ` ${t().extensionUpdateAvailable(entry.installed.manifest?.version ?? "?")}` : ` ${t().extensionIsInstalled}`) : "";
|
|
@@ -83,6 +106,8 @@ export async function chatExtensionsCommand(line, status, canReopen) {
|
|
|
83
106
|
}
|
|
84
107
|
if (!action) {
|
|
85
108
|
const lines = [status?.active.length ? t().extensionsRunning(status.active.join(", ")) : t().extensionsNone];
|
|
109
|
+
if (status)
|
|
110
|
+
lines.push(...describeCapabilities(status));
|
|
86
111
|
for (const { name: off, reason } of status?.inactive ?? [])
|
|
87
112
|
lines.push(t().extensionOff(off, reason));
|
|
88
113
|
if (status?.installed.length)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ExtensionDirs, type ExtensionScope, type InstalledExtension } from "./externalExtensions.js";
|
|
2
|
+
import { type DescribedCapability } from "./extensions.js";
|
|
2
3
|
import { type KnownMarketplace, type MarketplacePlugin } from "./marketplaces.js";
|
|
3
4
|
/**
|
|
4
5
|
* The agent's catalog of extensions (#56): what its known marketplaces offer, what's installed
|
|
@@ -32,6 +33,13 @@ export interface CatalogEntry {
|
|
|
32
33
|
/** Its logo and banner (#59), as files of the marketplace's copy, when it has them. */
|
|
33
34
|
logo?: string;
|
|
34
35
|
banner?: string;
|
|
36
|
+
/** What it lets the agent do (#62), in the kit's language: from its `plugin.json` when the marketplace has its folder. */
|
|
37
|
+
capabilities: DescribedCapability[];
|
|
38
|
+
/** The programs it needs on the computer (#63), by name ("Docker"), with the capabilities that need them (none: all of it). */
|
|
39
|
+
requirements: {
|
|
40
|
+
name: string;
|
|
41
|
+
for: string[];
|
|
42
|
+
}[];
|
|
35
43
|
}
|
|
36
44
|
/** Whether `offered` is newer than `installed` (x.y.z); a version that can't be compared isn't. */
|
|
37
45
|
export declare function isNewer(offered: string | undefined, installed: string | undefined): boolean;
|
|
@@ -61,6 +69,7 @@ export declare function installExtension(dirs: ExtensionDirs, spec: string, opti
|
|
|
61
69
|
replaced: boolean;
|
|
62
70
|
marketplace: string;
|
|
63
71
|
commit?: string;
|
|
72
|
+
missing: string[];
|
|
64
73
|
}>;
|
|
65
74
|
/**
|
|
66
75
|
* Updates an installed extension from the marketplace it came from: a fresh copy of the
|
|
@@ -4,6 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import { addExtension, codeStoreDir, hashExtension, inKitRange, installTarget, isGitSource, listInstalled, readExternalManifest, readLock, storedCodeDir } from "./externalExtensions.js";
|
|
5
5
|
import { cachePackage, cachedPackageDir } from "./npxPackages.js";
|
|
6
6
|
import { trustWorkspace } from "./workspaceTrust.js";
|
|
7
|
+
import { describeCapability } from "./extensions.js";
|
|
8
|
+
import { getLanguage } from "./messages/index.js";
|
|
7
9
|
import { addMarketplace, findPlugin, installFromMarketplace, listMarketplaces, resolvePluginSource, updateMarketplace } from "./marketplaces.js";
|
|
8
10
|
/**
|
|
9
11
|
* The agent's catalog of extensions (#56): what its known marketplaces offer, what's installed
|
|
@@ -64,13 +66,15 @@ async function offered(marketplace, plugin) {
|
|
|
64
66
|
};
|
|
65
67
|
const [logo, banner] = [await image(manifest?.catalog?.logo), await image(manifest?.catalog?.banner)];
|
|
66
68
|
const version = manifest?.version ?? plugin.version;
|
|
67
|
-
|
|
69
|
+
const capabilities = (manifest?.capabilities ?? []).map((capability) => describeCapability(capability, getLanguage()));
|
|
70
|
+
const requirements = (manifest?.requirements ?? []).map((requirement) => ({ name: requirement.name, for: requirement.for }));
|
|
71
|
+
return { ...(version ? { version } : {}), keywords: manifest?.keywords ?? [], ...(logo ? { logo } : {}), ...(banner ? { banner } : {}), capabilities, requirements };
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
74
|
catch {
|
|
71
75
|
// A source the kit doesn't install: the entry's own fields.
|
|
72
76
|
}
|
|
73
|
-
return { ...(plugin.version ? { version: plugin.version } : {}), keywords: [] };
|
|
77
|
+
return { ...(plugin.version ? { version: plugin.version } : {}), keywords: [], capabilities: [], requirements: [] };
|
|
74
78
|
}
|
|
75
79
|
/** Whether `offered` is newer than `installed` (x.y.z); a version that can't be compared isn't. */
|
|
76
80
|
export function isNewer(offered, installed) {
|
|
@@ -91,7 +95,8 @@ export async function extensionCatalog(dirs, options = {}) {
|
|
|
91
95
|
const topics = [...new Set([...look.keywords, ...(plugin.tags ?? []), ...(plugin.keywords ?? []), ...(plugin.category ? [plugin.category] : [])].map((topic) => topic.toLowerCase()))];
|
|
92
96
|
if (wanted && !topics.some((topic) => wanted.has(topic)))
|
|
93
97
|
continue;
|
|
94
|
-
|
|
98
|
+
// Its capabilities count too: a search for "grading" finds what grades.
|
|
99
|
+
const text = [plugin.name, plugin.description, ...topics, ...look.capabilities.flatMap(({ id, label, description }) => [id, label, description])].join(" ").toLowerCase();
|
|
95
100
|
if (!words.every((word) => text.includes(word)))
|
|
96
101
|
continue;
|
|
97
102
|
const version = look.version;
|
|
@@ -108,6 +113,8 @@ export async function extensionCatalog(dirs, options = {}) {
|
|
|
108
113
|
topics,
|
|
109
114
|
...(look.logo ? { logo: look.logo } : {}),
|
|
110
115
|
...(look.banner ? { banner: look.banner } : {}),
|
|
116
|
+
capabilities: look.capabilities,
|
|
117
|
+
requirements: look.requirements,
|
|
111
118
|
});
|
|
112
119
|
}
|
|
113
120
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The programs an extension needs on the computer (#63): Docker for a lab, ffmpeg for videos.
|
|
3
|
+
* The kit installs an extension's files and never a program (ADR-025): it checks them, says what's
|
|
4
|
+
* missing, what for and how to install it on this system, and keeps off only what needs them.
|
|
5
|
+
*/
|
|
6
|
+
/** A program an extension needs on the computer, and how the person installs it. */
|
|
7
|
+
export interface ExtensionRequirement {
|
|
8
|
+
/** What the person reads: "Docker". */
|
|
9
|
+
name: string;
|
|
10
|
+
/** The program, by its bare name, found on the `PATH` (`docker`; on Windows with `PATHEXT`'s extensions). */
|
|
11
|
+
program: string;
|
|
12
|
+
/** How to check its version: its arguments (options or `version` only) and the oldest it works with. */
|
|
13
|
+
version?: {
|
|
14
|
+
args: string[];
|
|
15
|
+
min: string;
|
|
16
|
+
};
|
|
17
|
+
/** How to install it on each system: a command, or a link. */
|
|
18
|
+
install: {
|
|
19
|
+
windows?: string;
|
|
20
|
+
macos?: string;
|
|
21
|
+
linux?: string;
|
|
22
|
+
url?: string;
|
|
23
|
+
};
|
|
24
|
+
/** The capabilities that need it (its manifest's ids); without them, the whole extension does. */
|
|
25
|
+
for: string[];
|
|
26
|
+
}
|
|
27
|
+
/** What checking a requirement found. */
|
|
28
|
+
export interface RequirementCheck {
|
|
29
|
+
requirement: ExtensionRequirement;
|
|
30
|
+
/** On the `PATH`. */
|
|
31
|
+
found: boolean;
|
|
32
|
+
/** The version it said, when it was asked and said one. */
|
|
33
|
+
version?: string;
|
|
34
|
+
/** There, and new enough (or its version couldn't be read). */
|
|
35
|
+
ok: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** A manifest's `requirements`: each as given, for `requirementsProblem()` to judge. */
|
|
38
|
+
export declare function parseRequirements(raw: unknown): ExtensionRequirement[];
|
|
39
|
+
/**
|
|
40
|
+
* Why an extension's requirements can't be checked safely or don't hold together (a program given
|
|
41
|
+
* as a path, a version argument that isn't an option, a capability it doesn't provide), or nothing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function requirementsProblem(requirements: readonly ExtensionRequirement[], provides: readonly string[]): string | undefined;
|
|
44
|
+
/** Where a program is on the `PATH`, as a shell would find it (with `PATHEXT`'s extensions on Windows), or nothing. */
|
|
45
|
+
export declare function findOnPath(program: string, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
|
|
46
|
+
/** Whether `version` is `min` or later, comparing x.y.z (missing parts are 0). */
|
|
47
|
+
export declare function atLeast(version: string, min: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Checks one requirement: finds its program on the `PATH` and, when it says how, runs it with its
|
|
50
|
+
* version arguments only: no shell, nothing else on the command line, and stopped after
|
|
51
|
+
* `timeoutMs`. A Windows script (`.cmd`, `.bat`) can't run without a shell, and a program that
|
|
52
|
+
* says no version can't be compared: either counts as there.
|
|
53
|
+
*/
|
|
54
|
+
export declare function checkRequirement(requirement: ExtensionRequirement, options?: {
|
|
55
|
+
timeoutMs?: number;
|
|
56
|
+
env?: NodeJS.ProcessEnv;
|
|
57
|
+
}): Promise<RequirementCheck>;
|
|
58
|
+
/** Checks an extension's requirements, all at once. */
|
|
59
|
+
export declare function checkRequirements(requirements: readonly ExtensionRequirement[], options?: {
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
env?: NodeJS.ProcessEnv;
|
|
62
|
+
}): Promise<RequirementCheck[]>;
|
|
63
|
+
/** How to install a requirement on this system: its command, else its link. */
|
|
64
|
+
export declare function installHint(requirement: ExtensionRequirement, platform?: NodeJS.Platform): string | undefined;
|
|
65
|
+
/** What's missing, as a reason reads ("Docker (docker 24.0.0 or later), which isn't on this computer: install it with …"). */
|
|
66
|
+
export declare function describeMissing(check: RequirementCheck, platform?: NodeJS.Platform): string;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/** A program's name: a bare one, never a path, so the check can only find it on the `PATH`. */
|
|
5
|
+
const PROGRAM = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
6
|
+
/** A version check's arguments: options and `version`, never what would do something (`docker run …`). */
|
|
7
|
+
const VERSION_ARG = /^(--?[A-Za-z][A-Za-z0-9-]*|version)$/;
|
|
8
|
+
const VERSION = /^\d+(\.\d+){0,2}$/;
|
|
9
|
+
/** A manifest's `requirements`: each as given, for `requirementsProblem()` to judge. */
|
|
10
|
+
export function parseRequirements(raw) {
|
|
11
|
+
if (!Array.isArray(raw))
|
|
12
|
+
return [];
|
|
13
|
+
const text = (value) => (typeof value === "string" && value ? value : undefined);
|
|
14
|
+
return raw.flatMap((item) => {
|
|
15
|
+
if (!item || typeof item !== "object")
|
|
16
|
+
return [];
|
|
17
|
+
const { name, program, version, install, for: needed } = item;
|
|
18
|
+
const programName = text(program);
|
|
19
|
+
if (!programName)
|
|
20
|
+
return [];
|
|
21
|
+
const check = version && typeof version === "object" ? version : undefined;
|
|
22
|
+
const how = install && typeof install === "object" ? install : {};
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
name: text(name) ?? programName,
|
|
26
|
+
program: programName,
|
|
27
|
+
...(check ? { version: { args: Array.isArray(check.args) ? check.args.map(String) : [], min: String(check.min ?? "") } } : {}),
|
|
28
|
+
install: Object.fromEntries(["windows", "macos", "linux", "url"].flatMap((key) => (text(how[key]) ? [[key, text(how[key])]] : []))),
|
|
29
|
+
for: Array.isArray(needed) ? needed.filter((id) => typeof id === "string") : [],
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Why an extension's requirements can't be checked safely or don't hold together (a program given
|
|
36
|
+
* as a path, a version argument that isn't an option, a capability it doesn't provide), or nothing.
|
|
37
|
+
*/
|
|
38
|
+
export function requirementsProblem(requirements, provides) {
|
|
39
|
+
for (const requirement of requirements) {
|
|
40
|
+
if (!PROGRAM.test(requirement.program))
|
|
41
|
+
return `needs the program "${requirement.program}", which isn't a bare name found on the PATH`;
|
|
42
|
+
if (requirement.version) {
|
|
43
|
+
const unsafe = requirement.version.args.find((arg) => !VERSION_ARG.test(arg));
|
|
44
|
+
if (unsafe !== undefined)
|
|
45
|
+
return `checks ${requirement.program}'s version with "${unsafe}": only options (--version, -v) or version`;
|
|
46
|
+
if (!VERSION.test(requirement.version.min))
|
|
47
|
+
return `wants ${requirement.program} ${requirement.version.min || "(no version)"}, which isn't a version like 24.0.0`;
|
|
48
|
+
}
|
|
49
|
+
const unknown = requirement.for.find((id) => !provides.includes(id));
|
|
50
|
+
if (unknown)
|
|
51
|
+
return `needs ${requirement.program} for ${unknown}, a capability it doesn't provide`;
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Where a program is on the `PATH`, as a shell would find it (with `PATHEXT`'s extensions on Windows), or nothing. */
|
|
56
|
+
export async function findOnPath(program, env = process.env) {
|
|
57
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path.delimiter).filter(Boolean);
|
|
58
|
+
const extensions = process.platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").map((extension) => extension.toLowerCase())] : [""];
|
|
59
|
+
for (const dir of dirs) {
|
|
60
|
+
for (const extension of extensions) {
|
|
61
|
+
const file = path.join(dir, `${program}${extension}`);
|
|
62
|
+
if (await stat(file).then((entry) => entry.isFile(), () => false))
|
|
63
|
+
return file;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
/** Whether `version` is `min` or later, comparing x.y.z (missing parts are 0). */
|
|
69
|
+
export function atLeast(version, min) {
|
|
70
|
+
const parts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
71
|
+
const [a, b] = [parts(version), parts(min)];
|
|
72
|
+
for (let index = 0; index < 3; index++)
|
|
73
|
+
if ((a[index] ?? 0) !== (b[index] ?? 0))
|
|
74
|
+
return (a[index] ?? 0) > (b[index] ?? 0);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Checks one requirement: finds its program on the `PATH` and, when it says how, runs it with its
|
|
79
|
+
* version arguments only: no shell, nothing else on the command line, and stopped after
|
|
80
|
+
* `timeoutMs`. A Windows script (`.cmd`, `.bat`) can't run without a shell, and a program that
|
|
81
|
+
* says no version can't be compared: either counts as there.
|
|
82
|
+
*/
|
|
83
|
+
export async function checkRequirement(requirement, options = {}) {
|
|
84
|
+
const file = PROGRAM.test(requirement.program) ? await findOnPath(requirement.program, options.env) : undefined;
|
|
85
|
+
if (!file)
|
|
86
|
+
return { requirement, found: false, ok: false };
|
|
87
|
+
if (!requirement.version || /\.(cmd|bat)$/i.test(file))
|
|
88
|
+
return { requirement, found: true, ok: true };
|
|
89
|
+
const said = await new Promise((resolve) => {
|
|
90
|
+
execFile(file, requirement.version.args, { timeout: options.timeoutMs ?? 5000, windowsHide: true, shell: false, ...(options.env ? { env: options.env } : {}) }, (_error, stdout, stderr) => resolve(`${stdout ?? ""}\n${stderr ?? ""}`));
|
|
91
|
+
});
|
|
92
|
+
const version = said.match(/\d+\.\d+(?:\.\d+)?/)?.[0];
|
|
93
|
+
return { requirement, found: true, ...(version ? { version } : {}), ok: !version || atLeast(version, requirement.version.min) };
|
|
94
|
+
}
|
|
95
|
+
/** Checks an extension's requirements, all at once. */
|
|
96
|
+
export function checkRequirements(requirements, options = {}) {
|
|
97
|
+
return Promise.all(requirements.map((requirement) => checkRequirement(requirement, options)));
|
|
98
|
+
}
|
|
99
|
+
/** How to install a requirement on this system: its command, else its link. */
|
|
100
|
+
export function installHint(requirement, platform = process.platform) {
|
|
101
|
+
const system = platform === "win32" ? requirement.install.windows : platform === "darwin" ? requirement.install.macos : requirement.install.linux;
|
|
102
|
+
return system ?? requirement.install.url;
|
|
103
|
+
}
|
|
104
|
+
/** What's missing, as a reason reads ("Docker (docker 24.0.0 or later), which isn't on this computer: install it with …"). */
|
|
105
|
+
export function describeMissing(check, platform = process.platform) {
|
|
106
|
+
const { requirement } = check;
|
|
107
|
+
const wanted = `${requirement.name} (${requirement.program}${requirement.version ? ` ${requirement.version.min} or later` : ""})`;
|
|
108
|
+
const state = check.found ? `, and this computer has ${check.version}` : ", which isn't on this computer";
|
|
109
|
+
const hint = installHint(requirement, platform);
|
|
110
|
+
const how = !hint ? "" : /^https?:\/\//.test(hint) ? `: see ${hint}` : `: install it with \`${hint}\``;
|
|
111
|
+
return `${wanted}${state}${how}`;
|
|
112
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { HookCallbackMatcher, HookEvent, McpServerConfig } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import type { AgentSpec, BaseSessionConfig, Mode } from "./agentSpec.js";
|
|
3
|
+
import { type Language } from "./language.js";
|
|
3
4
|
import type { ToolLabels } from "./toolLabels.js";
|
|
4
5
|
import type { SessionFacts } from "./sessionFacts.js";
|
|
5
6
|
/**
|
|
@@ -90,27 +91,93 @@ export interface Extension {
|
|
|
90
91
|
external?: boolean;
|
|
91
92
|
/** An installed one's scope folder, where its lock is (its plugin is elsewhere when it's linked). */
|
|
92
93
|
installedIn?: string;
|
|
94
|
+
/** What it can't do on this computer (#63): capabilities whose programs are missing, with why. They aren't provided, and their skills are left out. */
|
|
95
|
+
unavailable?: {
|
|
96
|
+
capabilities: string[];
|
|
97
|
+
reason: string;
|
|
98
|
+
}[];
|
|
93
99
|
/** Why it can't run in this session (e.g. "needs a folder (`dir`)"), or `undefined` when it can. */
|
|
94
100
|
missing?(context: ExtensionContext): string | undefined;
|
|
95
101
|
/** What it brings to the session, once it's active. */
|
|
96
102
|
contribute(context: ExtensionContext): Promise<ExtensionContribution>;
|
|
97
103
|
}
|
|
104
|
+
/** How a capability reads to the person, in one language. */
|
|
105
|
+
export interface CapabilityText {
|
|
106
|
+
label: string;
|
|
107
|
+
description: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Something the agent does well with an extension (#62): the skills that say how and the tools
|
|
111
|
+
* that do it, described for the person. Its `id` is what `requires` names, neutral (`lms-grading`,
|
|
112
|
+
* not `moodle-grading`), so an agent's skills depend on what can be done, not on which extension
|
|
113
|
+
* does it.
|
|
114
|
+
*/
|
|
115
|
+
export interface ExtensionCapability {
|
|
116
|
+
id: string;
|
|
117
|
+
/** Its skills, by their folder in the extension's `skills/`. */
|
|
118
|
+
skills: string[];
|
|
119
|
+
/** Its tools, by short name (of any of its servers). */
|
|
120
|
+
tools: string[];
|
|
121
|
+
/** Its label and description, by language. */
|
|
122
|
+
texts: Partial<Record<Language, CapabilityText>>;
|
|
123
|
+
}
|
|
124
|
+
/** A capability as a person reads it: its id, and its texts in their language. */
|
|
125
|
+
export interface DescribedCapability extends CapabilityText {
|
|
126
|
+
id: string;
|
|
127
|
+
}
|
|
128
|
+
/** A manifest's `capabilities`: what has an id, with its lists and texts as far as they're well formed. */
|
|
129
|
+
export declare function parseCapabilities(raw: unknown): ExtensionCapability[];
|
|
130
|
+
/** A capability's texts in `language`: English when it has none there, then any it has, then its id. */
|
|
131
|
+
export declare function describeCapability(capability: ExtensionCapability, language: Language): DescribedCapability;
|
|
132
|
+
/**
|
|
133
|
+
* Why an extension's capabilities don't hold (an id twice, a skill it doesn't have), or nothing.
|
|
134
|
+
* Its tools can't be checked here: only a running server lists them, so `/extensions` says when
|
|
135
|
+
* one is missing.
|
|
136
|
+
*/
|
|
137
|
+
export declare function capabilitiesProblem(pluginRoot: string, capabilities: readonly ExtensionCapability[]): Promise<string | undefined>;
|
|
98
138
|
/** An extension's manifest, as the kit reads it. */
|
|
99
139
|
export interface ExtensionManifest {
|
|
100
140
|
name: string;
|
|
101
141
|
description: string;
|
|
102
|
-
/** Capabilities it lets the agent use. */
|
|
142
|
+
/** Capabilities it lets the agent use: its `provides`, and its `capabilities`' ids. */
|
|
103
143
|
provides: string[];
|
|
104
144
|
/** Capabilities it needs another enabled extension to provide. */
|
|
105
145
|
requires: string[];
|
|
146
|
+
/** What it lets the agent do, described for the person (#62). */
|
|
147
|
+
capabilities: ExtensionCapability[];
|
|
106
148
|
}
|
|
107
149
|
/** Reads an extension's manifest from its plugin. */
|
|
108
150
|
export declare function readExtensionManifest(pluginRoot: string): ExtensionManifest;
|
|
151
|
+
/** An active extension as a view lists it: its capabilities in the session's language, with their tools by full name. */
|
|
152
|
+
export interface AboutExtension {
|
|
153
|
+
description: string;
|
|
154
|
+
provides: string[];
|
|
155
|
+
capabilities: (DescribedCapability & {
|
|
156
|
+
tools: string[];
|
|
157
|
+
})[];
|
|
158
|
+
/** What it can't do on this computer, with why (#63). */
|
|
159
|
+
unavailable: (DescribedCapability & {
|
|
160
|
+
reason: string;
|
|
161
|
+
})[];
|
|
162
|
+
/** Its MCP servers, by name. */
|
|
163
|
+
servers: string[];
|
|
164
|
+
}
|
|
165
|
+
/** What a view says of an active extension: its capabilities' tools under each of its servers' names. */
|
|
166
|
+
export declare function aboutExtension(manifest: ExtensionManifest, servers: readonly string[], language: Language, unavailable?: readonly UnavailableCapability[]): AboutExtension;
|
|
167
|
+
/** What a manifest provides: its `provides` and its capabilities' ids, once each. */
|
|
168
|
+
export declare function providedBy(provides: readonly string[] | undefined, capabilities: readonly ExtensionCapability[]): string[];
|
|
169
|
+
/** A capability an active extension has but can't use on this computer, with why (#63). */
|
|
170
|
+
export interface UnavailableCapability {
|
|
171
|
+
capability: ExtensionCapability;
|
|
172
|
+
reason: string;
|
|
173
|
+
}
|
|
109
174
|
/** The extensions a session runs with, and the ones it can't, with why. */
|
|
110
175
|
export interface ResolvedExtensions {
|
|
176
|
+
/** Each with its manifest as the session sees it (without what's unavailable here) and what's unavailable. */
|
|
111
177
|
active: {
|
|
112
178
|
extension: Extension;
|
|
113
179
|
manifest: ExtensionManifest;
|
|
180
|
+
unavailable: UnavailableCapability[];
|
|
114
181
|
}[];
|
|
115
182
|
inactive: {
|
|
116
183
|
name: string;
|
package/dist/core/extensions.js
CHANGED
|
@@ -2,10 +2,53 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { readdir, readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { frontmatter } from "./pluginAgents.js";
|
|
5
|
+
import { SUPPORTED_LANGUAGES } from "./language.js";
|
|
5
6
|
/** The folder `option` names for this session's `config`, if any. */
|
|
6
7
|
export function folderOf(option, config) {
|
|
7
8
|
return typeof option === "function" ? option(config) : option;
|
|
8
9
|
}
|
|
10
|
+
/** A manifest's `capabilities`: what has an id, with its lists and texts as far as they're well formed. */
|
|
11
|
+
export function parseCapabilities(raw) {
|
|
12
|
+
if (!Array.isArray(raw))
|
|
13
|
+
return [];
|
|
14
|
+
const names = (value) => (Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.length > 0) : []);
|
|
15
|
+
return raw.flatMap((item) => {
|
|
16
|
+
if (!item || typeof item !== "object")
|
|
17
|
+
return [];
|
|
18
|
+
const { id, skills, tools, texts } = item;
|
|
19
|
+
if (typeof id !== "string" || !id)
|
|
20
|
+
return [];
|
|
21
|
+
const byLanguage = Object.fromEntries(Object.entries(texts && typeof texts === "object" ? texts : {}).flatMap(([language, text]) => {
|
|
22
|
+
const { label, description } = (text ?? {});
|
|
23
|
+
return SUPPORTED_LANGUAGES.includes(language) && typeof label === "string" && label ? [[language, { label, description: typeof description === "string" ? description : "" }]] : [];
|
|
24
|
+
}));
|
|
25
|
+
return [{ id, skills: names(skills), tools: names(tools), texts: byLanguage }];
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/** A capability's texts in `language`: English when it has none there, then any it has, then its id. */
|
|
29
|
+
export function describeCapability(capability, language) {
|
|
30
|
+
const text = capability.texts[language] ?? capability.texts.en ?? Object.values(capability.texts)[0];
|
|
31
|
+
return { id: capability.id, label: text?.label ?? capability.id, description: text?.description ?? "" };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Why an extension's capabilities don't hold (an id twice, a skill it doesn't have), or nothing.
|
|
35
|
+
* Its tools can't be checked here: only a running server lists them, so `/extensions` says when
|
|
36
|
+
* one is missing.
|
|
37
|
+
*/
|
|
38
|
+
export async function capabilitiesProblem(pluginRoot, capabilities) {
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
for (const capability of capabilities) {
|
|
41
|
+
if (seen.has(capability.id))
|
|
42
|
+
return `declares the capability ${capability.id} twice`;
|
|
43
|
+
seen.add(capability.id);
|
|
44
|
+
for (const skill of capability.skills) {
|
|
45
|
+
const found = await readFile(path.join(pluginRoot, "skills", skill, "SKILL.md"), "utf8").then(() => true, () => false);
|
|
46
|
+
if (!found)
|
|
47
|
+
return `names the skill ${skill} in its capability ${capability.id}, and has no skills/${skill}/SKILL.md`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
9
52
|
/** Reads an extension's manifest from its plugin. */
|
|
10
53
|
export function readExtensionManifest(pluginRoot) {
|
|
11
54
|
const file = path.join(pluginRoot, ".claude-plugin", "plugin.json");
|
|
@@ -13,7 +56,22 @@ export function readExtensionManifest(pluginRoot) {
|
|
|
13
56
|
if (!raw.name)
|
|
14
57
|
throw new Error(`${file} has no "name".`);
|
|
15
58
|
const kit = raw.metadata?.["agent-kit"];
|
|
16
|
-
|
|
59
|
+
const capabilities = parseCapabilities(kit?.capabilities);
|
|
60
|
+
return { name: raw.name, description: raw.description ?? "", provides: providedBy(kit?.provides, capabilities), requires: kit?.requires ?? [], capabilities };
|
|
61
|
+
}
|
|
62
|
+
/** What a view says of an active extension: its capabilities' tools under each of its servers' names. */
|
|
63
|
+
export function aboutExtension(manifest, servers, language, unavailable = []) {
|
|
64
|
+
return {
|
|
65
|
+
description: manifest.description,
|
|
66
|
+
provides: manifest.provides,
|
|
67
|
+
capabilities: manifest.capabilities.map((capability) => ({ ...describeCapability(capability, language), tools: servers.flatMap((server) => capability.tools.map((tool) => `mcp__${server}__${tool}`)) })),
|
|
68
|
+
unavailable: unavailable.map(({ capability, reason }) => ({ ...describeCapability(capability, language), reason })),
|
|
69
|
+
servers: [...servers],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** What a manifest provides: its `provides` and its capabilities' ids, once each. */
|
|
73
|
+
export function providedBy(provides, capabilities) {
|
|
74
|
+
return [...new Set([...(provides ?? []), ...capabilities.map((capability) => capability.id)])];
|
|
17
75
|
}
|
|
18
76
|
/**
|
|
19
77
|
* Which of the `extensions` run in this session, in the order given: those the session lacks
|
|
@@ -24,14 +82,18 @@ export function resolveExtensions(extensions, context) {
|
|
|
24
82
|
const inactive = [];
|
|
25
83
|
let active = [];
|
|
26
84
|
for (const extension of extensions) {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
throw new Error(`The extension "${extension.name}" has the plugin "${
|
|
85
|
+
const read = readExtensionManifest(extension.plugin);
|
|
86
|
+
if (read.name !== extension.name)
|
|
87
|
+
throw new Error(`The extension "${extension.name}" has the plugin "${read.name}": they must have the same name.`);
|
|
88
|
+
// What it can't do here isn't provided: what requires it is left out, as for a missing extension.
|
|
89
|
+
const unavailable = (extension.unavailable ?? []).flatMap(({ capabilities, reason }) => capabilities.map((id) => ({ capability: read.capabilities.find((capability) => capability.id === id) ?? { id, skills: [], tools: [], texts: {} }, reason })));
|
|
90
|
+
const off = new Set(unavailable.map(({ capability }) => capability.id));
|
|
91
|
+
const manifest = off.size ? { ...read, provides: read.provides.filter((id) => !off.has(id)), capabilities: read.capabilities.filter((capability) => !off.has(capability.id)) } : read;
|
|
30
92
|
const reason = extension.missing?.(context);
|
|
31
93
|
if (reason)
|
|
32
94
|
inactive.push({ name: extension.name, reason });
|
|
33
95
|
else
|
|
34
|
-
active.push({ extension, manifest });
|
|
96
|
+
active.push({ extension, manifest, unavailable });
|
|
35
97
|
}
|
|
36
98
|
// Requirements: drop what can't be met, again and again, since dropping one may starve another.
|
|
37
99
|
for (let changed = true; changed;) {
|
|
@@ -51,7 +113,16 @@ export function resolveExtensions(extensions, context) {
|
|
|
51
113
|
}
|
|
52
114
|
/** The prompt's "Extensions" section: what's on, and what isn't and why (ADR-025). */
|
|
53
115
|
export function extensionsPromptSection(resolved) {
|
|
54
|
-
|
|
116
|
+
// A capability by its id and, when it has one, its English label (the model reads English, ADR-019).
|
|
117
|
+
const provided = (manifest) => manifest.provides.map((id) => {
|
|
118
|
+
const capability = manifest.capabilities.find((candidate) => candidate.id === id);
|
|
119
|
+
const label = capability ? describeCapability(capability, "en").label : id;
|
|
120
|
+
return label === id ? id : `${id} (${label})`;
|
|
121
|
+
});
|
|
122
|
+
const lines = resolved.active.map(({ manifest, unavailable }) => {
|
|
123
|
+
const cant = unavailable.map(({ capability, reason }) => `\n - ${describeCapability(capability, "en").label} (${capability.id}) isn't available on this computer: it ${reason}. If the person asks for it, tell them what to install.`);
|
|
124
|
+
return `- **${manifest.name}**: ${manifest.description}${manifest.provides.length ? ` Provides: ${provided(manifest).join(", ")}.` : ""}${cant.join("")}`;
|
|
125
|
+
});
|
|
55
126
|
const off = resolved.inactive.map(({ name, reason }) => `- **${name}** isn't available: it ${reason}. If the person asks for what it does, tell them why.`);
|
|
56
127
|
return `## Extensions\nWhat you can do besides your own tools comes from these extensions; their sections below say how to use them.\n${[...lines, ...off].join("\n")}`;
|
|
57
128
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Extension, type ExtensionCapability } from "./extensions.js";
|
|
2
2
|
import type { Language } from "./language.js";
|
|
3
3
|
import { type ToolPhrase } from "./messages/index.js";
|
|
4
4
|
import { type ExtensionOption, type OptionValue } from "./extensionOptions.js";
|
|
5
|
+
import { checkRequirements, type ExtensionRequirement } from "./extensionRequirements.js";
|
|
5
6
|
import { type CachedPackage } from "./npxPackages.js";
|
|
6
7
|
/**
|
|
7
8
|
* External extensions (ADR-025, #37): installed into an agent, for all its projects (the
|
|
@@ -55,12 +56,18 @@ export interface ExternalManifest {
|
|
|
55
56
|
repository?: string;
|
|
56
57
|
license?: string;
|
|
57
58
|
keywords?: string[];
|
|
59
|
+
/** What it lets the agent use: its `provides` and its capabilities' ids. */
|
|
58
60
|
provides: string[];
|
|
59
61
|
requires: string[];
|
|
62
|
+
/** What it lets the agent do, described for the person (#62): skills and tools together. */
|
|
63
|
+
capabilities: ExtensionCapability[];
|
|
60
64
|
/** The agent-kit versions it works with, e.g. `">=0.19.0 <0.21.0"`. */
|
|
61
65
|
kit?: string;
|
|
62
|
-
/**
|
|
63
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Programs it needs on this computer (#63): checked when it loads, never installed. Without one
|
|
68
|
+
* the whole extension needs, it's off; without one only some capabilities need, those are.
|
|
69
|
+
*/
|
|
70
|
+
requirements: ExtensionRequirement[];
|
|
64
71
|
/** Its MCP servers, by name (`mcp__<name>__<tool>`), from the plugin's `.mcp.json` or `plugin.json`. */
|
|
65
72
|
servers: Record<string, PluginServer>;
|
|
66
73
|
/**
|
|
@@ -188,6 +195,13 @@ export declare function loadExternalExtensions(dirs: ExtensionDirs): Promise<{
|
|
|
188
195
|
/** Why an installed extension can't load, or its manifest when it can. */
|
|
189
196
|
/** Whether a program is on the `PATH`, as a shell would find it (with `PATHEXT`'s extensions on Windows). */
|
|
190
197
|
export declare function onPath(program: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
|
|
198
|
+
/** A requirement this computer lacks, as the person reads it: what it is, what for, and how to install it. */
|
|
199
|
+
export declare function missingRequirement(check: Awaited<ReturnType<typeof checkRequirements>>[number]): string;
|
|
200
|
+
/** What an installed extension can't do on this computer, and why: its capabilities whose requirements are missing (#63). */
|
|
201
|
+
export interface UnavailableCapabilities {
|
|
202
|
+
capabilities: string[];
|
|
203
|
+
reason: string;
|
|
204
|
+
}
|
|
191
205
|
/** Whether `source` names a git repository rather than a folder. */
|
|
192
206
|
export declare function isGitSource(source: string): boolean;
|
|
193
207
|
/**
|
|
@@ -212,6 +226,7 @@ export declare function addExtension(source: string, scopeDir: string, options?:
|
|
|
212
226
|
name: string;
|
|
213
227
|
replaced: boolean;
|
|
214
228
|
commit?: string;
|
|
229
|
+
missing: string[];
|
|
215
230
|
}>;
|
|
216
231
|
/**
|
|
217
232
|
* Removes an extension from a scope (its files and its lock entry; a linked one's folder is left
|