@falkenslab/agent-kit 0.22.0 → 0.24.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 +26 -1
- package/dist/core/extensionCatalog.d.ts +11 -0
- package/dist/core/extensionCatalog.js +12 -3
- package/dist/core/extensionRequirements.d.ts +66 -0
- package/dist/core/extensionRequirements.js +112 -0
- package/dist/core/extensions.d.ts +75 -1
- package/dist/core/extensions.js +90 -6
- package/dist/core/externalExtensions.d.ts +21 -3
- package/dist/core/externalExtensions.js +35 -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 +22 -3
- package/package.json +1 -1
- package/schemas/agent-kit-metadata.schema.json +66 -2
- package/schemas/marketplace-catalog.schema.json +25 -1
|
@@ -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
|
|
@@ -48,19 +49,28 @@ export interface ExtensionAuthor {
|
|
|
48
49
|
*/
|
|
49
50
|
export interface ExternalManifest {
|
|
50
51
|
name: string;
|
|
52
|
+
/** Its description in English: `plugin.json`'s, which Claude Code reads. */
|
|
51
53
|
description: string;
|
|
54
|
+
/** Its description in other languages (#67), from `metadata["agent-kit"].description`. */
|
|
55
|
+
descriptions: Partial<Record<Language, string>>;
|
|
52
56
|
version?: string;
|
|
53
57
|
author?: ExtensionAuthor;
|
|
54
58
|
homepage?: string;
|
|
55
59
|
repository?: string;
|
|
56
60
|
license?: string;
|
|
57
61
|
keywords?: string[];
|
|
62
|
+
/** What it lets the agent use: its `provides` and its capabilities' ids. */
|
|
58
63
|
provides: string[];
|
|
59
64
|
requires: string[];
|
|
65
|
+
/** What it lets the agent do, described for the person (#62): skills and tools together. */
|
|
66
|
+
capabilities: ExtensionCapability[];
|
|
60
67
|
/** The agent-kit versions it works with, e.g. `">=0.19.0 <0.21.0"`. */
|
|
61
68
|
kit?: string;
|
|
62
|
-
/**
|
|
63
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Programs it needs on this computer (#63): checked when it loads, never installed. Without one
|
|
71
|
+
* the whole extension needs, it's off; without one only some capabilities need, those are.
|
|
72
|
+
*/
|
|
73
|
+
requirements: ExtensionRequirement[];
|
|
64
74
|
/** Its MCP servers, by name (`mcp__<name>__<tool>`), from the plugin's `.mcp.json` or `plugin.json`. */
|
|
65
75
|
servers: Record<string, PluginServer>;
|
|
66
76
|
/**
|
|
@@ -188,6 +198,13 @@ export declare function loadExternalExtensions(dirs: ExtensionDirs): Promise<{
|
|
|
188
198
|
/** Why an installed extension can't load, or its manifest when it can. */
|
|
189
199
|
/** Whether a program is on the `PATH`, as a shell would find it (with `PATHEXT`'s extensions on Windows). */
|
|
190
200
|
export declare function onPath(program: string, env?: NodeJS.ProcessEnv): Promise<boolean>;
|
|
201
|
+
/** A requirement this computer lacks, as the person reads it: what it is, what for, and how to install it. */
|
|
202
|
+
export declare function missingRequirement(check: Awaited<ReturnType<typeof checkRequirements>>[number]): string;
|
|
203
|
+
/** What an installed extension can't do on this computer, and why: its capabilities whose requirements are missing (#63). */
|
|
204
|
+
export interface UnavailableCapabilities {
|
|
205
|
+
capabilities: string[];
|
|
206
|
+
reason: string;
|
|
207
|
+
}
|
|
191
208
|
/** Whether `source` names a git repository rather than a folder. */
|
|
192
209
|
export declare function isGitSource(source: string): boolean;
|
|
193
210
|
/**
|
|
@@ -212,6 +229,7 @@ export declare function addExtension(source: string, scopeDir: string, options?:
|
|
|
212
229
|
name: string;
|
|
213
230
|
replaced: boolean;
|
|
214
231
|
commit?: string;
|
|
232
|
+
missing: string[];
|
|
215
233
|
}>;
|
|
216
234
|
/**
|
|
217
235
|
* Removes an extension from a scope (its files and its lock entry; a linked one's folder is left
|
|
@@ -5,12 +5,14 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
+
import { capabilitiesProblem, parseCapabilities, parseDescriptions, providedBy } from "./extensions.js";
|
|
8
9
|
import { getLanguage } from "./messages/index.js";
|
|
9
10
|
import { truncate } from "./toolLabels.js";
|
|
10
11
|
import { agentKitVersion } from "./version.js";
|
|
11
12
|
import { outsideArchive } from "./packaged.js";
|
|
12
13
|
import { extensionSecretsPath, missingOptions, optionsNamed, parseOptionValue, parseUserConfig, readOptionValues, removeSecrets, storeOptionValues } from "./extensionOptions.js";
|
|
13
14
|
import { isTrusted } from "./workspaceTrust.js";
|
|
15
|
+
import { checkRequirements, describeMissing, findOnPath, parseRequirements, requirementsProblem } from "./extensionRequirements.js";
|
|
14
16
|
import { cachedPackageDir, cachedPackageProblem, cachePackage, npxServer, packageCacheDir, packageSpec } from "./npxPackages.js";
|
|
15
17
|
export const LOCK_FILE = "extensions.lock.json";
|
|
16
18
|
/**
|
|
@@ -91,9 +93,11 @@ export async function readExternalManifest(dir) {
|
|
|
91
93
|
const author = raw.author;
|
|
92
94
|
// `repository` may be a URL or `{ url }`, as in package.json.
|
|
93
95
|
const repository = typeof raw.repository === "string" ? raw.repository : raw.repository?.url;
|
|
96
|
+
const capabilities = parseCapabilities(kit.capabilities);
|
|
94
97
|
return {
|
|
95
98
|
name,
|
|
96
99
|
description: typeof raw.description === "string" ? raw.description : "",
|
|
100
|
+
descriptions: parseDescriptions(kit.description),
|
|
97
101
|
...text("version"),
|
|
98
102
|
...(author && typeof author === "object" && typeof author.name === "string"
|
|
99
103
|
? { author: { name: author.name, ...(author.email ? { email: author.email } : {}), ...(author.url ? { url: author.url } : {}) } }
|
|
@@ -102,10 +106,11 @@ export async function readExternalManifest(dir) {
|
|
|
102
106
|
...(typeof repository === "string" ? { repository } : {}),
|
|
103
107
|
...text("license"),
|
|
104
108
|
...(Array.isArray(raw.keywords) ? { keywords: raw.keywords.filter((keyword) => typeof keyword === "string") } : {}),
|
|
105
|
-
provides: kit.provides
|
|
109
|
+
provides: providedBy(kit.provides, capabilities),
|
|
106
110
|
requires: kit.requires ?? [],
|
|
111
|
+
capabilities,
|
|
107
112
|
...(kit.kit ? { kit: kit.kit } : {}),
|
|
108
|
-
|
|
113
|
+
requirements: parseRequirements(kit.requirements),
|
|
109
114
|
servers: await pluginServers(dir, raw),
|
|
110
115
|
...(Array.isArray(kit.readOnlyTools) ? { readOnlyTools: kit.readOnlyTools.filter((tool) => typeof tool === "string") } : {}),
|
|
111
116
|
labels: kit.labels ?? {},
|
|
@@ -320,12 +325,13 @@ function manifestToolLabels(manifest) {
|
|
|
320
325
|
}));
|
|
321
326
|
}
|
|
322
327
|
/** The extension an installed one makes: its servers started through the launcher, its rules from its manifest. */
|
|
323
|
-
function externalExtension(installed, manifest, cache) {
|
|
328
|
+
function externalExtension(installed, manifest, cache, unavailable = []) {
|
|
324
329
|
return {
|
|
325
330
|
name: manifest.name,
|
|
326
331
|
plugin: installed.dir,
|
|
327
332
|
external: true,
|
|
328
333
|
installedIn: installed.scopeDir,
|
|
334
|
+
...(unavailable.length ? { unavailable } : {}),
|
|
329
335
|
async contribute() {
|
|
330
336
|
const servers = {};
|
|
331
337
|
if (Object.keys(manifest.servers).length)
|
|
@@ -395,23 +401,19 @@ export async function loadExternalExtensions(dirs) {
|
|
|
395
401
|
if (typeof reason === "string")
|
|
396
402
|
off.push({ name: installed.name, reason });
|
|
397
403
|
else
|
|
398
|
-
extensions.push(externalExtension(installed, reason, cache));
|
|
404
|
+
extensions.push(externalExtension(installed, reason.manifest, cache, reason.unavailable));
|
|
399
405
|
}
|
|
400
406
|
return { extensions, off, untrusted };
|
|
401
407
|
}
|
|
402
408
|
/** Why an installed extension can't load, or its manifest when it can. */
|
|
403
409
|
/** Whether a program is on the `PATH`, as a shell would find it (with `PATHEXT`'s extensions on Windows). */
|
|
404
410
|
export async function onPath(program, env = process.env) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
return true;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
return false;
|
|
411
|
+
return (await findOnPath(program, env)) !== undefined;
|
|
412
|
+
}
|
|
413
|
+
/** A requirement this computer lacks, as the person reads it: what it is, what for, and how to install it. */
|
|
414
|
+
export function missingRequirement(check) {
|
|
415
|
+
const needed = check.requirement.for.length ? ` for ${check.requirement.for.join(", ")}` : "";
|
|
416
|
+
return `needs ${describeMissing(check)}${needed}`;
|
|
415
417
|
}
|
|
416
418
|
async function problem(installed, cache) {
|
|
417
419
|
let manifest;
|
|
@@ -429,16 +431,22 @@ async function problem(installed, cache) {
|
|
|
429
431
|
return `is installed as "${installed.name}" but its manifest says "${manifest.name}"`;
|
|
430
432
|
if (manifest.kit && !inKitRange(agentKitVersion(), manifest.kit))
|
|
431
433
|
return `works with agent-kit ${manifest.kit}, and this agent runs ${agentKitVersion()}`;
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
434
|
+
const requirements = requirementsProblem(manifest.requirements, manifest.provides);
|
|
435
|
+
if (requirements)
|
|
436
|
+
return requirements;
|
|
437
|
+
// A program the whole extension needs keeps it off; one only some capabilities need, those (#63).
|
|
438
|
+
const checks = (await checkRequirements(manifest.requirements)).filter((check) => !check.ok);
|
|
439
|
+
const whole = checks.filter((check) => !check.requirement.for.length);
|
|
440
|
+
if (whole.length)
|
|
441
|
+
return whole.map((check) => missingRequirement(check)).join("; ");
|
|
442
|
+
const unavailable = checks.map((check) => ({ capabilities: check.requirement.for, reason: `needs ${describeMissing(check)}` }));
|
|
443
|
+
const capabilities = await capabilitiesProblem(installed.dir, manifest.capabilities);
|
|
444
|
+
if (capabilities)
|
|
445
|
+
return capabilities;
|
|
438
446
|
const options = optionsProblem(manifest, await readOptionValues(installed.privateDir, installed.name, manifest.userConfig, installed.lock.options));
|
|
439
447
|
if (options)
|
|
440
448
|
return options;
|
|
441
|
-
return (await serversProblem(manifest, installed.dir, { cache, locked: installed.lock.packages })) ?? manifest;
|
|
449
|
+
return (await serversProblem(manifest, installed.dir, { cache, locked: installed.lock.packages })) ?? { manifest, unavailable };
|
|
442
450
|
}
|
|
443
451
|
const exec = promisify(execFile);
|
|
444
452
|
/** Whether `source` names a git repository rather than a folder. */
|
|
@@ -477,9 +485,11 @@ export async function addExtension(source, scopeDir, options = {}) {
|
|
|
477
485
|
if (options.subdir)
|
|
478
486
|
from = path.join(from, options.subdir);
|
|
479
487
|
const manifest = await readExternalManifest(from);
|
|
480
|
-
const servers = await serversProblem(manifest, from);
|
|
488
|
+
const servers = (await serversProblem(manifest, from)) ?? (await capabilitiesProblem(from, manifest.capabilities)) ?? requirementsProblem(manifest.requirements, manifest.provides);
|
|
481
489
|
if (servers)
|
|
482
490
|
throw new Error(`${manifest.name} can't be installed: it ${servers}.`);
|
|
491
|
+
// What it needs and this computer lacks: said, never installed, and it installs anyway (#63).
|
|
492
|
+
const missing = (await checkRequirements(manifest.requirements)).filter((check) => !check.ok).map((check) => missingRequirement(check));
|
|
483
493
|
const target = path.join(scopeDir, manifest.name);
|
|
484
494
|
// Replacing what's installed there would delete the source itself.
|
|
485
495
|
if (path.resolve(from) === path.resolve(target))
|
|
@@ -506,12 +516,12 @@ export async function addExtension(source, scopeDir, options = {}) {
|
|
|
506
516
|
}
|
|
507
517
|
lock[manifest.name] = { source: origin, ...provenance, sha256, stored: true, ...locked, enabled: true, installed: new Date().toISOString() };
|
|
508
518
|
await writeLock(scopeDir, lock);
|
|
509
|
-
return { name: manifest.name, replaced, ...(commit ? { commit } : {}) };
|
|
519
|
+
return { name: manifest.name, replaced, missing, ...(commit ? { commit } : {}) };
|
|
510
520
|
}
|
|
511
521
|
if (options.link) {
|
|
512
522
|
lock[manifest.name] = { source: from, linked: true, sha256: "", ...locked, enabled: true, installed: new Date().toISOString() };
|
|
513
523
|
await writeLock(scopeDir, lock);
|
|
514
|
-
return { name: manifest.name, replaced };
|
|
524
|
+
return { name: manifest.name, replaced, missing };
|
|
515
525
|
}
|
|
516
526
|
await cp(from, target, { recursive: true, filter: (file) => path.basename(file) !== ".git" });
|
|
517
527
|
lock[manifest.name] = {
|
|
@@ -523,7 +533,7 @@ export async function addExtension(source, scopeDir, options = {}) {
|
|
|
523
533
|
installed: new Date().toISOString(),
|
|
524
534
|
};
|
|
525
535
|
await writeLock(scopeDir, lock);
|
|
526
|
-
return { name: manifest.name, replaced, ...(commit ? { commit } : {}) };
|
|
536
|
+
return { name: manifest.name, replaced, missing, ...(commit ? { commit } : {}) };
|
|
527
537
|
}
|
|
528
538
|
finally {
|
|
529
539
|
if (clone)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionsStatus } from "./session.js";
|
|
2
|
+
import { type ExtensionCapability, type UnavailableCapability } from "./extensions.js";
|
|
2
3
|
import type { SessionLive, StaticSessionFacts } from "./sessionFacts.js";
|
|
3
4
|
import type { ToolLabels } from "./toolLabels.js";
|
|
4
5
|
/**
|
|
@@ -43,6 +44,9 @@ export interface LiveExtension {
|
|
|
43
44
|
agents: string[];
|
|
44
45
|
description: string;
|
|
45
46
|
provides: string[];
|
|
47
|
+
capabilities: ExtensionCapability[];
|
|
48
|
+
/** What it can't do on this computer (#63). */
|
|
49
|
+
unavailable: UnavailableCapability[];
|
|
46
50
|
help: string[];
|
|
47
51
|
}
|
|
48
52
|
/** The session's own lists the switch keeps in step: the same objects the gates, the chat and the view read. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cp, lstat, mkdir, readdir, rm, symlink, unlink } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { aboutExtension, describeCapability } from "./extensions.js";
|
|
3
4
|
import { hashExtension, listInstalled, readLock } from "./externalExtensions.js";
|
|
4
5
|
import { cachedPackagesIntact, packageCacheDir } from "./npxPackages.js";
|
|
5
6
|
/**
|
|
@@ -122,9 +123,17 @@ function follow(extension, enabled, targets) {
|
|
|
122
123
|
targets.allowedSubagentTypes.push(...extension.agents);
|
|
123
124
|
Object.assign(targets.toolLabels, extension.toolLabels);
|
|
124
125
|
status.active.push(extension.name);
|
|
125
|
-
status.about[extension.name] = { description: extension.description, provides: extension.provides };
|
|
126
|
+
status.about[extension.name] = aboutExtension({ name: extension.name, description: extension.description, provides: extension.provides, requires: [], capabilities: extension.capabilities }, extension.servers, facts.language, extension.unavailable);
|
|
126
127
|
status.inactive = status.inactive.filter((entry) => entry.name !== extension.name);
|
|
127
|
-
facts.extensions.active.push({
|
|
128
|
+
facts.extensions.active.push({
|
|
129
|
+
name: extension.name,
|
|
130
|
+
description: extension.description,
|
|
131
|
+
provides: extension.provides,
|
|
132
|
+
capabilities: extension.capabilities.map((capability) => describeCapability(capability, "en")),
|
|
133
|
+
unavailable: extension.unavailable.map(({ capability, reason }) => ({ ...describeCapability(capability, "en"), reason })),
|
|
134
|
+
servers: extension.servers,
|
|
135
|
+
help: extension.help,
|
|
136
|
+
});
|
|
128
137
|
facts.extensions.inactive = facts.extensions.inactive.filter((entry) => entry.name !== extension.name);
|
|
129
138
|
}
|
|
130
139
|
else {
|
package/dist/core/messages/de.js
CHANGED
|
@@ -28,6 +28,9 @@ export const de = {
|
|
|
28
28
|
extensionsRunning: (names) => `Aktiv: ${names}`,
|
|
29
29
|
extensionsNone: "(keine Erweiterungen)",
|
|
30
30
|
extensionOff: (name, reason) => `${name} ist aus: ${reason}`,
|
|
31
|
+
extensionCan: (name) => `Was dein Agent mit ${name} kann:`,
|
|
32
|
+
capabilityToolsMissing: (tools) => `(sein Server bietet ${tools} nicht an)`,
|
|
33
|
+
capabilityUnavailable: (label, reason) => `${label}: auf diesem Computer nicht verfügbar: ${reason}`,
|
|
31
34
|
extensionsInstalled: "Installiert:",
|
|
32
35
|
extensionSwitched: (name, enabled) => `(${name} ${enabled ? "aktiviert" : "deaktiviert"}, in derselben Sitzung)`,
|
|
33
36
|
extensionToggled: (name, enabled) => `(${name} ${enabled ? "aktiviert" : "deaktiviert"}: die Sitzung wird mit derselben Unterhaltung neu geöffnet)`,
|
|
@@ -47,6 +50,7 @@ export const de = {
|
|
|
47
50
|
extensionsNoMatch: "(keine Erweiterung passt: /extensions search mit anderen Wörtern, oder ohne, um alle zu sehen)",
|
|
48
51
|
extensionIsInstalled: "(installiert)",
|
|
49
52
|
extensionUpdateAvailable: (installed) => `(installiert: ${installed}, Update verfügbar)`,
|
|
53
|
+
extensionLacks: (name, what) => `(${name}: ${what})`,
|
|
50
54
|
extensionInstalled: (name) => `(${name} installiert: in einem neuen Gespräch da, oder sofort mit /extensions reload, ohne dieses zu verlieren)`,
|
|
51
55
|
extensionUpdated: (name, from, to) => `(${name} aktualisiert, ${from} → ${to}: in einem neuen Gespräch da, oder sofort mit /extensions reload)`,
|
|
52
56
|
extensionsUpToDate: "(alle Erweiterungen aus einem Marketplace sind aktuell)",
|
|
@@ -37,6 +37,12 @@ export interface Messages {
|
|
|
37
37
|
extensionsRunning(names: string): string;
|
|
38
38
|
extensionsNone: string;
|
|
39
39
|
extensionOff(name: string, reason: string): string;
|
|
40
|
+
/** Before an active extension's capabilities (#62), one per line after it. */
|
|
41
|
+
extensionCan(name: string): string;
|
|
42
|
+
/** A capability's tools its server doesn't offer in this session. */
|
|
43
|
+
capabilityToolsMissing(tools: string): string;
|
|
44
|
+
/** A capability that isn't available on this computer (#63), with why (in English, as every reason). */
|
|
45
|
+
capabilityUnavailable(label: string, reason: string): string;
|
|
40
46
|
extensionsInstalled: string;
|
|
41
47
|
/** An installed extension turned off or on in the running session (#49). */
|
|
42
48
|
extensionSwitched(name: string, enabled: boolean): string;
|
|
@@ -60,6 +66,8 @@ export interface Messages {
|
|
|
60
66
|
extensionIsInstalled: string;
|
|
61
67
|
extensionUpdateAvailable(installed: string): string;
|
|
62
68
|
extensionInstalled(name: string): string;
|
|
69
|
+
/** After installing: a program it needs that this computer lacks (#63); `what` is the reason, in English. */
|
|
70
|
+
extensionLacks(name: string, what: string): string;
|
|
63
71
|
extensionUpdated(name: string, from: string, to: string): string;
|
|
64
72
|
extensionsUpToDate: string;
|
|
65
73
|
extensionRemoved(name: string, withData: boolean): string;
|
package/dist/core/messages/en.js
CHANGED
|
@@ -27,6 +27,9 @@ export const en = {
|
|
|
27
27
|
extensionsRunning: (names) => `Running with: ${names}`,
|
|
28
28
|
extensionsNone: "(no extensions)",
|
|
29
29
|
extensionOff: (name, reason) => `${name} is off: it ${reason}`,
|
|
30
|
+
extensionCan: (name) => `What ${name} lets your agent do:`,
|
|
31
|
+
capabilityToolsMissing: (tools) => `(its server doesn't offer ${tools})`,
|
|
32
|
+
capabilityUnavailable: (label, reason) => `${label}: not on this computer, it ${reason}`,
|
|
30
33
|
extensionsInstalled: "Installed:",
|
|
31
34
|
extensionSwitched: (name, enabled) => `(${name} ${enabled ? "enabled" : "disabled"}, in this same session)`,
|
|
32
35
|
extensionToggled: (name, enabled) => `(${name} ${enabled ? "enabled" : "disabled"}: the session reopens, with the same conversation)`,
|
|
@@ -47,6 +50,7 @@ export const en = {
|
|
|
47
50
|
extensionIsInstalled: "(installed)",
|
|
48
51
|
extensionUpdateAvailable: (installed) => `(installed: ${installed}, update available)`,
|
|
49
52
|
extensionInstalled: (name) => `(${name} installed: it's there in a new conversation, or now with /extensions reload, keeping this one)`,
|
|
53
|
+
extensionLacks: (name, what) => `(${name} ${what})`,
|
|
50
54
|
extensionUpdated: (name, from, to) => `(${name} updated, ${from} → ${to}: it's there in a new conversation, or now with /extensions reload)`,
|
|
51
55
|
extensionsUpToDate: "(every extension from a marketplace is up to date)",
|
|
52
56
|
extensionRemoved: (name, withData) => `(${name} removed${withData ? ", with its data" : "; its data is kept for a reinstall"})`,
|
package/dist/core/messages/es.js
CHANGED
|
@@ -28,6 +28,9 @@ export const es = {
|
|
|
28
28
|
extensionsRunning: (names) => `Funcionando con: ${names}`,
|
|
29
29
|
extensionsNone: "(ninguna extensión)",
|
|
30
30
|
extensionOff: (name, reason) => `${name} está desactivada: ${reason}`,
|
|
31
|
+
extensionCan: (name) => `Lo que tu agente puede hacer con ${name}:`,
|
|
32
|
+
capabilityToolsMissing: (tools) => `(su servidor no ofrece ${tools})`,
|
|
33
|
+
capabilityUnavailable: (label, reason) => `${label}: no disponible en este ordenador: ${reason}`,
|
|
31
34
|
extensionsInstalled: "Instaladas:",
|
|
32
35
|
extensionSwitched: (name, enabled) => `(${name} ${enabled ? "activada" : "desactivada"}, en esta misma sesión)`,
|
|
33
36
|
extensionToggled: (name, enabled) => `(${name} ${enabled ? "activada" : "desactivada"}: la sesión se reabre, con la misma conversación)`,
|
|
@@ -47,6 +50,7 @@ export const es = {
|
|
|
47
50
|
extensionsNoMatch: "(ninguna extensión coincide: /extensions search con otras palabras, o sin ninguna para verlas todas)",
|
|
48
51
|
extensionIsInstalled: "(instalada)",
|
|
49
52
|
extensionUpdateAvailable: (installed) => `(instalada: ${installed}, hay actualización)`,
|
|
53
|
+
extensionLacks: (name, what) => `(${name}: ${what})`,
|
|
50
54
|
extensionInstalled: (name) => `(${name} instalada: está en una conversación nueva, o ya con /extensions reload, sin perder esta)`,
|
|
51
55
|
extensionUpdated: (name, from, to) => `(${name} actualizada, ${from} → ${to}: está en una conversación nueva, o ya con /extensions reload)`,
|
|
52
56
|
extensionsUpToDate: "(todas las extensiones de un marketplace están al día)",
|
package/dist/core/messages/fr.js
CHANGED
|
@@ -28,6 +28,9 @@ export const fr = {
|
|
|
28
28
|
extensionsRunning: (names) => `En service : ${names}`,
|
|
29
29
|
extensionsNone: "(aucune extension)",
|
|
30
30
|
extensionOff: (name, reason) => `${name} est désactivée : ${reason}`,
|
|
31
|
+
extensionCan: (name) => `Ce que votre agent peut faire avec ${name} :`,
|
|
32
|
+
capabilityToolsMissing: (tools) => `(son serveur ne propose pas ${tools})`,
|
|
33
|
+
capabilityUnavailable: (label, reason) => `${label} : indisponible sur cet ordinateur : ${reason}`,
|
|
31
34
|
extensionsInstalled: "Installées :",
|
|
32
35
|
extensionSwitched: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"}, dans cette même session)`,
|
|
33
36
|
extensionToggled: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"} : la session se rouvre, avec la même conversation)`,
|
|
@@ -47,6 +50,7 @@ export const fr = {
|
|
|
47
50
|
extensionsNoMatch: "(aucune extension ne correspond : /extensions search avec d'autres mots, ou sans pour toutes les voir)",
|
|
48
51
|
extensionIsInstalled: "(installée)",
|
|
49
52
|
extensionUpdateAvailable: (installed) => `(installée : ${installed}, mise à jour disponible)`,
|
|
53
|
+
extensionLacks: (name, what) => `(${name} : ${what})`,
|
|
50
54
|
extensionInstalled: (name) => `(${name} installée : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload, en gardant celle-ci)`,
|
|
51
55
|
extensionUpdated: (name, from, to) => `(${name} mise à jour, ${from} → ${to} : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload)`,
|
|
52
56
|
extensionsUpToDate: "(toutes les extensions d'un marketplace sont à jour)",
|
package/dist/core/session.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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 AboutExtension } from "./extensions.js";
|
|
4
5
|
import { type ExtensionDirs, type InstalledExtension } from "./externalExtensions.js";
|
|
5
6
|
import type { ToolLabels } from "./toolLabels.js";
|
|
6
7
|
import type { AgentSpec, BaseSessionConfig } from "./agentSpec.js";
|
|
@@ -65,14 +66,13 @@ export interface ExtensionsStatus {
|
|
|
65
66
|
/** The names of the extensions this session runs with, the kit's and the agent's too. */
|
|
66
67
|
active: string[];
|
|
67
68
|
/** What each active one is, from its manifest: for a view that lists them. */
|
|
68
|
-
about: Record<string,
|
|
69
|
-
description: string;
|
|
70
|
-
provides: string[];
|
|
71
|
-
}>;
|
|
69
|
+
about: Record<string, AboutExtension>;
|
|
72
70
|
inactive: {
|
|
73
71
|
name: string;
|
|
74
72
|
reason: string;
|
|
75
73
|
}[];
|
|
74
|
+
/** The running session's tools, as the model names them (empty until it has started): what a capability's tools are checked against. */
|
|
75
|
+
sessionTools?: () => readonly string[];
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
78
|
* The CLI's folder for a project, where it keeps each session's transcript and large tool
|
package/dist/core/session.js
CHANGED
|
@@ -13,7 +13,7 @@ import { createManualLoginServer } from "./tools/manualLogin.js";
|
|
|
13
13
|
import { createTimeServer } from "./tools/time.js";
|
|
14
14
|
import { TODO_TOOL } from "./todos.js";
|
|
15
15
|
import { createModeControl } from "./modeControl.js";
|
|
16
|
-
import { extensionsPromptSection, resolveExtensions, skillsMissingCapabilities } from "./extensions.js";
|
|
16
|
+
import { aboutExtension, describeCapability, extensionsPromptSection, resolveExtensions, skillsMissingCapabilities } from "./extensions.js";
|
|
17
17
|
import { allowAnyMcpTool } from "./mcpPermissions.js";
|
|
18
18
|
import { pluginAgents } from "./pluginAgents.js";
|
|
19
19
|
import { outsideArchive, unpackedClaudeExecutable } from "./packaged.js";
|
|
@@ -130,7 +130,11 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
130
130
|
// `requires` a capability no active extension provides is left out; since the SDK's
|
|
131
131
|
// `skillOverrides` doesn't reach plugin skills (confirmed empirically), leaving one out takes
|
|
132
132
|
// a list: the plugins' skills and the project's, without it.
|
|
133
|
-
|
|
133
|
+
// And the skills of a capability that isn't available on this computer (#63).
|
|
134
|
+
const missingCapabilities = [
|
|
135
|
+
...(await skillsMissingCapabilities(pluginRoots, resolved.capabilities)),
|
|
136
|
+
...resolved.active.flatMap(({ manifest, unavailable }) => unavailable.flatMap(({ capability }) => capability.skills.map((skill) => `${manifest.name}:${skill}`))),
|
|
137
|
+
];
|
|
134
138
|
const requested = spec.skills === "plugins" ? await pluginSkills(pluginRoots) : spec.skills;
|
|
135
139
|
const offeredSkills = missingCapabilities.length && (requested === undefined || requested === "all")
|
|
136
140
|
? [...(await pluginSkills(pluginRoots)), ...((spec.settingSources ?? ["project"]).includes("project") ? await projectSkills(config.projectDir) : [])]
|
|
@@ -352,7 +356,8 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
352
356
|
untrusted: external.untrusted.map((installed) => ({ name: installed.name, ...(installed.manifest?.version ? { version: installed.manifest.version } : {}), source: installed.lock.marketplace ?? installed.lock.source })),
|
|
353
357
|
installed: await listInstalled(extensionDirs),
|
|
354
358
|
active: resolved.active.map(({ extension }) => extension.name),
|
|
355
|
-
about: Object.fromEntries(resolved.active.map(({ extension, manifest }) => [extension.name,
|
|
359
|
+
about: Object.fromEntries(resolved.active.map(({ extension, manifest, unavailable }, index) => [extension.name, aboutExtension(manifest, Object.keys(added[index]?.mcpServers ?? {}), language, unavailable)])),
|
|
360
|
+
sessionTools: () => late.view?.controls().tools ?? [],
|
|
356
361
|
inactive: resolved.inactive,
|
|
357
362
|
};
|
|
358
363
|
// What the session is, for the extensions that ask (sessionFacts.ts, #43).
|
|
@@ -361,10 +366,12 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
361
366
|
kitVersion: agentKitVersion(),
|
|
362
367
|
language,
|
|
363
368
|
extensions: {
|
|
364
|
-
active: resolved.active.map(({ extension, manifest }, index) => ({
|
|
369
|
+
active: resolved.active.map(({ extension, manifest, unavailable }, index) => ({
|
|
365
370
|
name: extension.name,
|
|
366
371
|
description: manifest.description,
|
|
367
372
|
provides: manifest.provides,
|
|
373
|
+
capabilities: manifest.capabilities.map((capability) => describeCapability(capability, "en")),
|
|
374
|
+
unavailable: unavailable.map(({ capability, reason }) => ({ ...describeCapability(capability, "en"), reason })),
|
|
368
375
|
servers: Object.keys(added[index]?.mcpServers ?? {}),
|
|
369
376
|
help: added[index]?.helpLines ?? [],
|
|
370
377
|
})),
|
|
@@ -379,7 +386,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
379
386
|
registerSessionView(sdkOptions, late.view);
|
|
380
387
|
// Turning an installed extension off and on while the session runs (#49), and restarting its
|
|
381
388
|
// servers when its options change (#55).
|
|
382
|
-
const liveExtensions = resolved.active.flatMap(({ extension, manifest }, index) => {
|
|
389
|
+
const liveExtensions = resolved.active.flatMap(({ extension, manifest, unavailable }, index) => {
|
|
383
390
|
const installed = installedDirs.get(extension.name);
|
|
384
391
|
if (!installed)
|
|
385
392
|
return [];
|
|
@@ -396,6 +403,8 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
396
403
|
agents: Object.keys(fromPlugins).filter((agent) => agent.startsWith(`${extension.name}:`)),
|
|
397
404
|
description: manifest.description,
|
|
398
405
|
provides: manifest.provides,
|
|
406
|
+
capabilities: manifest.capabilities,
|
|
407
|
+
unavailable,
|
|
399
408
|
help: contribution?.helpLines ?? [],
|
|
400
409
|
},
|
|
401
410
|
];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { McpServerStatus, Options, SlashCommand } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import type { AgentIdentity, Mode } from "./agentSpec.js";
|
|
3
|
+
import type { DescribedCapability } from "./extensions.js";
|
|
3
4
|
import type { Language } from "./language.js";
|
|
4
5
|
import type { ModeControl } from "./modeControl.js";
|
|
5
6
|
import type { ContextUsage } from "./runner.js";
|
|
@@ -20,10 +21,15 @@ export interface SessionFacts {
|
|
|
20
21
|
switchableModes: Mode[];
|
|
21
22
|
/** The extensions running (with their tools and what they say about themselves here), and the ones off, with why. */
|
|
22
23
|
extensions: {
|
|
24
|
+
/** Each with its capabilities (#62), in English: what the model reads. */
|
|
23
25
|
active: {
|
|
24
26
|
name: string;
|
|
25
27
|
description: string;
|
|
26
28
|
provides: string[];
|
|
29
|
+
capabilities: DescribedCapability[];
|
|
30
|
+
unavailable: (DescribedCapability & {
|
|
31
|
+
reason: string;
|
|
32
|
+
})[];
|
|
27
33
|
tools: string[];
|
|
28
34
|
help: string[];
|
|
29
35
|
}[];
|
|
@@ -17,9 +17,13 @@ export function describeSession(facts) {
|
|
|
17
17
|
const sections = [`# You, right now\n\nYou are ${who}, on agent-kit ${facts.kitVersion}.`];
|
|
18
18
|
const others = facts.switchableModes.filter((mode) => mode !== facts.mode);
|
|
19
19
|
sections.push(`## Mode\n\nYou are in **${facts.mode}** mode now. ${facts.switchableModes.length > 1 ? `The person can switch it with Shift+Tab to ${others.join(" or ")}.` : "It can't be switched in this session."}`);
|
|
20
|
-
const active = facts.extensions.active.map(({ name, description, tools, help }) => {
|
|
20
|
+
const active = facts.extensions.active.map(({ name, description, capabilities, unavailable, tools, help }) => {
|
|
21
21
|
const toolList = tools.length ? ` Tools: ${tools.map(short).join(", ")}.` : "";
|
|
22
|
-
|
|
22
|
+
// What it lets you do, as the person reads it in /extensions (#62).
|
|
23
|
+
const can = capabilities.map(({ id, label, description: what }) => `\n - Can: ${label} (${id})${what ? `: ${what}` : ""}`).join("");
|
|
24
|
+
// What it can't do on this computer, and what to install (#63).
|
|
25
|
+
const cant = unavailable.map(({ id, label, reason }) => `\n - Can't here: ${label} (${id}): it ${reason}.`).join("");
|
|
26
|
+
return `- **${name}**: ${description}${toolList}${can}${cant}${help.map((line) => `\n ${line}`).join("")}`;
|
|
23
27
|
});
|
|
24
28
|
const inactive = facts.extensions.inactive.map(({ name, reason }) => `- **${name}** is off: it ${reason}.`);
|
|
25
29
|
sections.push(`## Extensions\n\n${[...active, ...inactive].join("\n") || "None."}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,8 @@ export { awareness, type AwarenessOptions } from "./extensions/awareness/index.j
|
|
|
15
15
|
export { sources, type SourcesOptions } from "./extensions/sources/index.js";
|
|
16
16
|
export { knowledge, type KnowledgeOptions } from "./extensions/knowledge/index.js";
|
|
17
17
|
export { memory, type MemoryOptions } from "./extensions/memory/index.js";
|
|
18
|
-
export { readExtensionManifest, type Extension, type ExtensionContext, type ExtensionContribution, type ExtensionManifest, type FolderOption, } from "./core/extensions.js";
|
|
18
|
+
export { describeCapability, readExtensionManifest, type AboutExtension, type CapabilityText, type DescribedCapability, type Extension, type ExtensionCapability, type ExtensionContext, type ExtensionContribution, type ExtensionManifest, type FolderOption, type UnavailableCapability, } from "./core/extensions.js";
|
|
19
|
+
export { checkRequirements, type ExtensionRequirement, type RequirementCheck } from "./core/extensionRequirements.js";
|
|
19
20
|
export { addExtension, codeStoreDir, installTarget, scopePrivateDir, extensionDataDir, extensionOptionStates, setExtensionOptions, removeExtension, setExtensionEnabled, listInstalled, readExternalManifest, type ExtensionAuthor, type ExtensionDirs, type ExtensionScope, type ExternalManifest, type ExternalToolLabel, type PluginServer, type InstalledExtension, type LockEntry, type ExtensionOptionState, } from "./core/externalExtensions.js";
|
|
20
21
|
export type { ExtensionOption, OptionValue } from "./core/extensionOptions.js";
|
|
21
22
|
export type { ExtensionRestart, ExtensionSwitch } from "./core/liveExtensions.js";
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,8 @@ export { awareness } from "./extensions/awareness/index.js";
|
|
|
15
15
|
export { sources } from "./extensions/sources/index.js";
|
|
16
16
|
export { knowledge } from "./extensions/knowledge/index.js";
|
|
17
17
|
export { memory } from "./extensions/memory/index.js";
|
|
18
|
-
export { readExtensionManifest, } from "./core/extensions.js";
|
|
18
|
+
export { describeCapability, readExtensionManifest, } from "./core/extensions.js";
|
|
19
|
+
export { checkRequirements } from "./core/extensionRequirements.js";
|
|
19
20
|
export { addExtension, codeStoreDir, installTarget, scopePrivateDir, extensionDataDir, extensionOptionStates, setExtensionOptions, removeExtension, setExtensionEnabled, listInstalled, readExternalManifest, } from "./core/externalExtensions.js";
|
|
20
21
|
export { packageCacheDir } from "./core/npxPackages.js";
|
|
21
22
|
export { addMarketplace, findPlugin, inspectMarketplace, installFromMarketplace, listMarketplaces, readMarketplaceManifest, removeMarketplace, resolvePluginSource, updateMarketplace, } from "./core/marketplaces.js";
|
|
@@ -6,6 +6,8 @@ import { addExtension, extensionOptionStates, installTarget, folderSize, isGitSo
|
|
|
6
6
|
import { describe, describeOptions } from "../chat/extensions.js";
|
|
7
7
|
import { extensionCatalog, knownMarketplaces as catalogMarketplaces, restoreExtensions, updateExtension } from "../core/extensionCatalog.js";
|
|
8
8
|
import { trustWorkspace } from "../core/workspaceTrust.js";
|
|
9
|
+
import { describeCapability, describeInLanguage } from "../core/extensions.js";
|
|
10
|
+
import { getLanguage } from "../core/messages/index.js";
|
|
9
11
|
export { chatExtensionsCommand, describe } from "../chat/extensions.js";
|
|
10
12
|
/**
|
|
11
13
|
* The `extension` command an agent exposes in its own binary (#37), e.g. `captain extension add
|
|
@@ -126,6 +128,11 @@ export async function runExtensionCommand(argv, options) {
|
|
|
126
128
|
const packages = installTarget(options.dirs, scope);
|
|
127
129
|
// Installed into the workspace by the person: trusted there as it is.
|
|
128
130
|
const trusted = async (name) => void (scope === "project" && (await trustWorkspace(options.dirs, [name])));
|
|
131
|
+
// What it needs and this computer lacks (#63): said, never installed.
|
|
132
|
+
const lacking = (missing) => {
|
|
133
|
+
for (const line of missing)
|
|
134
|
+
write(`It ${line}.`);
|
|
135
|
+
};
|
|
129
136
|
// A plugin of a marketplace, unless it's a folder here or a git URL.
|
|
130
137
|
const isFolder = await stat(path.resolve(source)).then((found) => found.isDirectory(), () => false);
|
|
131
138
|
if (flag("link")) {
|
|
@@ -135,6 +142,7 @@ export async function runExtensionCommand(argv, options) {
|
|
|
135
142
|
await trusted(result.name);
|
|
136
143
|
await configure(result.name, dir);
|
|
137
144
|
write(`Linked ${result.name} in the ${scope} scope: it runs from ${path.resolve(source)}, whose files aren't checked. A rebuild takes effect when its server starts again (a new session, or \`/extensions disable\` and \`enable\` in the chat).`);
|
|
145
|
+
lacking(result.missing);
|
|
138
146
|
return true;
|
|
139
147
|
}
|
|
140
148
|
if (!isFolder && !isGitSource(source) && /^[A-Za-z0-9][\w.-]*(@[A-Za-z0-9][\w.-]*)?$/.test(source)) {
|
|
@@ -150,6 +158,7 @@ export async function runExtensionCommand(argv, options) {
|
|
|
150
158
|
await trusted(result.name);
|
|
151
159
|
await configure(result.name, dir);
|
|
152
160
|
write(`${result.replaced ? "Reinstalled" : "Installed"} ${result.name} from ${result.marketplace} in the ${scope} scope (${dir})${result.commit ? `, at ${result.commit.slice(0, 12)}` : ""}. It's enabled: it runs from the next session.`);
|
|
161
|
+
lacking(result.missing);
|
|
153
162
|
return true;
|
|
154
163
|
}
|
|
155
164
|
const subdir = value("path");
|
|
@@ -157,6 +166,7 @@ export async function runExtensionCommand(argv, options) {
|
|
|
157
166
|
await trusted(result.name);
|
|
158
167
|
await configure(result.name, dir);
|
|
159
168
|
write(`${result.replaced ? "Reinstalled" : "Installed"} ${result.name} in the ${scope} scope (${dir})${result.commit ? `, at ${result.commit.slice(0, 12)}` : ""}. It's enabled: it runs from the next session.`);
|
|
169
|
+
lacking(result.missing);
|
|
160
170
|
return true;
|
|
161
171
|
}
|
|
162
172
|
case "remove":
|
|
@@ -392,19 +402,28 @@ async function info(extension) {
|
|
|
392
402
|
if (!manifest)
|
|
393
403
|
return [describe(extension), "Its manifest can't be read."];
|
|
394
404
|
const author = manifest.author ? [manifest.author.name, manifest.author.email && `<${manifest.author.email}>`, manifest.author.url && `(${manifest.author.url})`].filter(Boolean).join(" ") : undefined;
|
|
395
|
-
|
|
405
|
+
// Its README in the kit's language when it has one (README.es.md), else the English (#67).
|
|
406
|
+
const language = getLanguage();
|
|
407
|
+
const translated = path.join(extension.dir, `README.${language}.md`);
|
|
408
|
+
const readme = language !== "en" && (await stat(translated).catch(() => null))?.isFile() ? translated : path.join(extension.dir, "README.md");
|
|
396
409
|
const field = (label, value) => (value ? [`${label.padEnd(13)}${value}`] : []);
|
|
397
410
|
return [
|
|
398
|
-
`${manifest.name}${manifest.version ? ` ${manifest.version}` : ""}: ${manifest.description}`,
|
|
411
|
+
`${manifest.name}${manifest.version ? ` ${manifest.version}` : ""}: ${describeInLanguage(manifest.description, manifest.descriptions, language)}`,
|
|
399
412
|
...field("Author", author),
|
|
400
413
|
...field("License", manifest.license),
|
|
401
414
|
...field("Homepage", manifest.homepage),
|
|
402
415
|
...field("Repository", manifest.repository),
|
|
403
416
|
...field("Keywords", manifest.keywords?.join(", ")),
|
|
404
417
|
...field("Works with", manifest.kit && `agent-kit ${manifest.kit}`),
|
|
418
|
+
// What it lets the agent do (#62), in the kit's language: one per line.
|
|
419
|
+
...manifest.capabilities.flatMap((capability, index) => {
|
|
420
|
+
const { id, label, description } = describeCapability(capability, getLanguage());
|
|
421
|
+
return field(index ? "" : "Can do", `${label} (${id})${description ? `: ${description}` : ""}`);
|
|
422
|
+
}),
|
|
405
423
|
...field("Provides", manifest.provides.join(", ") || undefined),
|
|
406
424
|
...field("Requires", manifest.requires.join(", ") || undefined),
|
|
407
|
-
|
|
425
|
+
// The programs it needs on this computer (#63), one per line, with what for.
|
|
426
|
+
...manifest.requirements.flatMap((requirement, index) => field(index ? "" : "Needs", `${requirement.name} (${requirement.program}${requirement.version ? ` ${requirement.version.min} or later` : ""})${requirement.for.length ? ` for ${requirement.for.join(", ")}` : ""}`)),
|
|
408
427
|
...field("Servers", Object.keys(manifest.servers).join(", ") || undefined),
|
|
409
428
|
...field("Only read", manifest.readOnlyTools
|
|
410
429
|
? `${manifest.readOnlyTools.join(", ") || "none"} (its manifest)`
|