@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.
@@ -1,7 +1,8 @@
1
- import type { Extension } from "./extensions.js";
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
- /** Programs it needs on this computer, found on the `PATH` (`"docker"`, `"python3"`): without one it's off, saying which. */
63
- needs: string[];
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
- needs: Array.isArray(kit.needs) ? kit.needs.filter((program) => typeof program === "string") : [],
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
- const dirs = (env.PATH ?? env.Path ?? "").split(path.delimiter).filter(Boolean);
406
- const extensions = process.platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").map((extension) => extension.toLowerCase())] : [""];
407
- for (const dir of dirs) {
408
- for (const extension of extensions) {
409
- const found = await stat(path.join(dir, `${program}${extension}`)).then((entry) => entry.isFile(), () => false);
410
- if (found)
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 missing = [];
433
- for (const program of manifest.needs)
434
- if (!(await onPath(program)))
435
- missing.push(program);
436
- if (missing.length)
437
- return `needs ${missing.join(", ")}, which ${missing.length === 1 ? "isn't" : "aren't"} on this computer (on the PATH)`;
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({ name: extension.name, description: extension.description, provides: extension.provides, servers: extension.servers, help: extension.help });
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 {
@@ -119,4 +119,5 @@ export declare function installFromMarketplace(dir: string, spec: string, scopeD
119
119
  replaced: boolean;
120
120
  commit?: string;
121
121
  marketplace: string;
122
+ missing: string[];
122
123
  }>;
@@ -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;
@@ -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"})`,
@@ -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)",
@@ -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)",
@@ -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
@@ -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
- const missingCapabilities = await skillsMissingCapabilities(pluginRoots, resolved.capabilities);
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, { description: manifest.description, provides: manifest.provides }])),
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
- return `- **${name}**: ${description}${toolList}${help.map((line) => `\n ${line}`).join("")}`;
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
- const readme = path.join(extension.dir, "README.md");
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
- ...field("Needs", manifest.needs.join(", ") || undefined),
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)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falkenslab/agent-kit",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Library for building an agent on top of the Claude Agent SDK",
5
5
  "author": "Francisco Vargas Ruiz",
6
6
  "license": "MIT",