@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.
@@ -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, 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,6 +93,7 @@ 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 : "",
@@ -102,10 +105,11 @@ export async function readExternalManifest(dir) {
102
105
  ...(typeof repository === "string" ? { repository } : {}),
103
106
  ...text("license"),
104
107
  ...(Array.isArray(raw.keywords) ? { keywords: raw.keywords.filter((keyword) => typeof keyword === "string") } : {}),
105
- provides: kit.provides ?? [],
108
+ provides: providedBy(kit.provides, capabilities),
106
109
  requires: kit.requires ?? [],
110
+ capabilities,
107
111
  ...(kit.kit ? { kit: kit.kit } : {}),
108
- needs: Array.isArray(kit.needs) ? kit.needs.filter((program) => typeof program === "string") : [],
112
+ requirements: parseRequirements(kit.requirements),
109
113
  servers: await pluginServers(dir, raw),
110
114
  ...(Array.isArray(kit.readOnlyTools) ? { readOnlyTools: kit.readOnlyTools.filter((tool) => typeof tool === "string") } : {}),
111
115
  labels: kit.labels ?? {},
@@ -320,12 +324,13 @@ function manifestToolLabels(manifest) {
320
324
  }));
321
325
  }
322
326
  /** The extension an installed one makes: its servers started through the launcher, its rules from its manifest. */
323
- function externalExtension(installed, manifest, cache) {
327
+ function externalExtension(installed, manifest, cache, unavailable = []) {
324
328
  return {
325
329
  name: manifest.name,
326
330
  plugin: installed.dir,
327
331
  external: true,
328
332
  installedIn: installed.scopeDir,
333
+ ...(unavailable.length ? { unavailable } : {}),
329
334
  async contribute() {
330
335
  const servers = {};
331
336
  if (Object.keys(manifest.servers).length)
@@ -395,23 +400,19 @@ export async function loadExternalExtensions(dirs) {
395
400
  if (typeof reason === "string")
396
401
  off.push({ name: installed.name, reason });
397
402
  else
398
- extensions.push(externalExtension(installed, reason, cache));
403
+ extensions.push(externalExtension(installed, reason.manifest, cache, reason.unavailable));
399
404
  }
400
405
  return { extensions, off, untrusted };
401
406
  }
402
407
  /** Why an installed extension can't load, or its manifest when it can. */
403
408
  /** Whether a program is on the `PATH`, as a shell would find it (with `PATHEXT`'s extensions on Windows). */
404
409
  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;
410
+ return (await findOnPath(program, env)) !== undefined;
411
+ }
412
+ /** A requirement this computer lacks, as the person reads it: what it is, what for, and how to install it. */
413
+ export function missingRequirement(check) {
414
+ const needed = check.requirement.for.length ? ` for ${check.requirement.for.join(", ")}` : "";
415
+ return `needs ${describeMissing(check)}${needed}`;
415
416
  }
416
417
  async function problem(installed, cache) {
417
418
  let manifest;
@@ -429,16 +430,22 @@ async function problem(installed, cache) {
429
430
  return `is installed as "${installed.name}" but its manifest says "${manifest.name}"`;
430
431
  if (manifest.kit && !inKitRange(agentKitVersion(), manifest.kit))
431
432
  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)`;
433
+ const requirements = requirementsProblem(manifest.requirements, manifest.provides);
434
+ if (requirements)
435
+ return requirements;
436
+ // A program the whole extension needs keeps it off; one only some capabilities need, those (#63).
437
+ const checks = (await checkRequirements(manifest.requirements)).filter((check) => !check.ok);
438
+ const whole = checks.filter((check) => !check.requirement.for.length);
439
+ if (whole.length)
440
+ return whole.map((check) => missingRequirement(check)).join("; ");
441
+ const unavailable = checks.map((check) => ({ capabilities: check.requirement.for, reason: `needs ${describeMissing(check)}` }));
442
+ const capabilities = await capabilitiesProblem(installed.dir, manifest.capabilities);
443
+ if (capabilities)
444
+ return capabilities;
438
445
  const options = optionsProblem(manifest, await readOptionValues(installed.privateDir, installed.name, manifest.userConfig, installed.lock.options));
439
446
  if (options)
440
447
  return options;
441
- return (await serversProblem(manifest, installed.dir, { cache, locked: installed.lock.packages })) ?? manifest;
448
+ return (await serversProblem(manifest, installed.dir, { cache, locked: installed.lock.packages })) ?? { manifest, unavailable };
442
449
  }
443
450
  const exec = promisify(execFile);
444
451
  /** Whether `source` names a git repository rather than a folder. */
@@ -477,9 +484,11 @@ export async function addExtension(source, scopeDir, options = {}) {
477
484
  if (options.subdir)
478
485
  from = path.join(from, options.subdir);
479
486
  const manifest = await readExternalManifest(from);
480
- const servers = await serversProblem(manifest, from);
487
+ const servers = (await serversProblem(manifest, from)) ?? (await capabilitiesProblem(from, manifest.capabilities)) ?? requirementsProblem(manifest.requirements, manifest.provides);
481
488
  if (servers)
482
489
  throw new Error(`${manifest.name} can't be installed: it ${servers}.`);
490
+ // What it needs and this computer lacks: said, never installed, and it installs anyway (#63).
491
+ const missing = (await checkRequirements(manifest.requirements)).filter((check) => !check.ok).map((check) => missingRequirement(check));
483
492
  const target = path.join(scopeDir, manifest.name);
484
493
  // Replacing what's installed there would delete the source itself.
485
494
  if (path.resolve(from) === path.resolve(target))
@@ -506,12 +515,12 @@ export async function addExtension(source, scopeDir, options = {}) {
506
515
  }
507
516
  lock[manifest.name] = { source: origin, ...provenance, sha256, stored: true, ...locked, enabled: true, installed: new Date().toISOString() };
508
517
  await writeLock(scopeDir, lock);
509
- return { name: manifest.name, replaced, ...(commit ? { commit } : {}) };
518
+ return { name: manifest.name, replaced, missing, ...(commit ? { commit } : {}) };
510
519
  }
511
520
  if (options.link) {
512
521
  lock[manifest.name] = { source: from, linked: true, sha256: "", ...locked, enabled: true, installed: new Date().toISOString() };
513
522
  await writeLock(scopeDir, lock);
514
- return { name: manifest.name, replaced };
523
+ return { name: manifest.name, replaced, missing };
515
524
  }
516
525
  await cp(from, target, { recursive: true, filter: (file) => path.basename(file) !== ".git" });
517
526
  lock[manifest.name] = {
@@ -523,7 +532,7 @@ export async function addExtension(source, scopeDir, options = {}) {
523
532
  installed: new Date().toISOString(),
524
533
  };
525
534
  await writeLock(scopeDir, lock);
526
- return { name: manifest.name, replaced, ...(commit ? { commit } : {}) };
535
+ return { name: manifest.name, replaced, missing, ...(commit ? { commit } : {}) };
527
536
  }
528
537
  finally {
529
538
  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 } 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":
@@ -402,9 +412,15 @@ async function info(extension) {
402
412
  ...field("Repository", manifest.repository),
403
413
  ...field("Keywords", manifest.keywords?.join(", ")),
404
414
  ...field("Works with", manifest.kit && `agent-kit ${manifest.kit}`),
415
+ // What it lets the agent do (#62), in the kit's language: one per line.
416
+ ...manifest.capabilities.flatMap((capability, index) => {
417
+ const { id, label, description } = describeCapability(capability, getLanguage());
418
+ return field(index ? "" : "Can do", `${label} (${id})${description ? `: ${description}` : ""}`);
419
+ }),
405
420
  ...field("Provides", manifest.provides.join(", ") || undefined),
406
421
  ...field("Requires", manifest.requires.join(", ") || undefined),
407
- ...field("Needs", manifest.needs.join(", ") || undefined),
422
+ // The programs it needs on this computer (#63), one per line, with what for.
423
+ ...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
424
  ...field("Servers", Object.keys(manifest.servers).join(", ") || undefined),
409
425
  ...field("Only read", manifest.readOnlyTools
410
426
  ? `${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.23.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",
@@ -11,9 +11,18 @@
11
11
  "type": "string",
12
12
  "pattern": "^\\s*(?:(?:>=|<=|>|<|=)?\\s*v?\\d+(?:\\.\\d+){0,2})(?:\\s+(?:>=|<=|>|<|=)?\\s*v?\\d+(?:\\.\\d+){0,2})*\\s*$"
13
13
  },
14
- "provides": { "description": "Capabilities it lets the agent use.", "type": "array", "items": { "type": "string", "minLength": 1 } },
14
+ "provides": { "description": "Capabilities it lets the agent use, by name only. Its capabilities' ids count as provided too.", "type": "array", "items": { "type": "string", "minLength": 1 } },
15
+ "capabilities": {
16
+ "description": "What it lets the agent do, for the person: each one's skills and tools together, described per language. Its id is what requires names; neutral ids (lms-grading) let skills depend on what can be done, not on which extension does it.",
17
+ "type": "array",
18
+ "items": { "$ref": "#/$defs/capability" }
19
+ },
15
20
  "requires": { "description": "Capabilities another enabled extension must provide.", "type": "array", "items": { "type": "string", "minLength": 1 } },
16
- "needs": { "description": "Programs it needs on the computer's PATH.", "type": "array", "items": { "type": "string", "minLength": 1 } },
21
+ "requirements": {
22
+ "description": "Programs it needs on the computer, which the person installs: agent-kit checks them, says what's missing and how to install it, and never installs them. Without one the whole extension needs, it's off; without one only some capabilities need (for), those are.",
23
+ "type": "array",
24
+ "items": { "$ref": "#/$defs/requirement" }
25
+ },
17
26
  "readOnlyTools": {
18
27
  "description": "Its tools that only read (short names): plan mode lets them through. Without it, plan mode asks its servers (readOnlyHint).",
19
28
  "type": "array",
@@ -54,6 +63,55 @@
54
63
  }
55
64
  },
56
65
  "$defs": {
66
+ "requirement": {
67
+ "type": "object",
68
+ "additionalProperties": false,
69
+ "required": ["name", "program", "install"],
70
+ "properties": {
71
+ "name": { "description": "What the person reads: Docker.", "type": "string", "minLength": 1 },
72
+ "program": { "description": "Its bare name, found on the PATH (on Windows with PATHEXT's extensions): never a path.", "type": "string", "pattern": "^[A-Za-z0-9][A-Za-z0-9._+-]*$" },
73
+ "version": {
74
+ "description": "How to check its version: run with these arguments only (options or version, no shell), the first x.y.z it prints compared with min.",
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "required": ["args", "min"],
78
+ "properties": {
79
+ "args": { "type": "array", "items": { "type": "string", "pattern": "^(--?[A-Za-z][A-Za-z0-9-]*|version)$" } },
80
+ "min": { "type": "string", "pattern": "^\\d+(\\.\\d+){0,2}$" }
81
+ }
82
+ },
83
+ "install": {
84
+ "description": "How to install it on each system: a command, or a link (url, the fallback).",
85
+ "type": "object",
86
+ "additionalProperties": false,
87
+ "minProperties": 1,
88
+ "properties": { "windows": { "type": "string", "minLength": 1 }, "macos": { "type": "string", "minLength": 1 }, "linux": { "type": "string", "minLength": 1 }, "url": { "type": "string", "pattern": "^https?://" } }
89
+ },
90
+ "for": { "description": "The capabilities (ids it provides) that need it; without it, the whole extension does.", "type": "array", "items": { "type": "string", "minLength": 1 } }
91
+ }
92
+ },
93
+ "capability": {
94
+ "type": "object",
95
+ "additionalProperties": false,
96
+ "required": ["id", "texts"],
97
+ "properties": {
98
+ "id": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" },
99
+ "skills": { "description": "Its skills, by their folder in the extension's skills/.", "type": "array", "items": { "type": "string", "minLength": 1 } },
100
+ "tools": { "description": "Its tools, by short name (of any of its servers).", "type": "array", "items": { "type": "string", "minLength": 1 } },
101
+ "texts": {
102
+ "description": "Its label and description per language (en, es, fr, de); English is shown where its language is missing.",
103
+ "type": "object",
104
+ "propertyNames": { "enum": ["en", "es", "fr", "de"] },
105
+ "minProperties": 1,
106
+ "additionalProperties": {
107
+ "type": "object",
108
+ "additionalProperties": false,
109
+ "required": ["label"],
110
+ "properties": { "label": { "type": "string", "minLength": 1 }, "description": { "type": "string" } }
111
+ }
112
+ }
113
+ }
114
+ },
57
115
  "image": {
58
116
  "type": "string",
59
117
  "pattern": "^(?!/)(?![A-Za-z]:)(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+\\.(?:svg|png|jpe?g|webp)$"
@@ -56,6 +56,16 @@
56
56
  "agentKit": { "description": "After the agent's own command (<agent> …).", "type": "string" }
57
57
  }
58
58
  },
59
+ "capabilities": {
60
+ "description": "What it lets the agent do: its metadata[\"agent-kit\"].capabilities as it declares them, texts in every language it gives.",
61
+ "type": "array",
62
+ "items": { "$ref": "https://falkenslab.github.io/agent-kit/schemas/agent-kit-metadata.schema.json#/$defs/capability" }
63
+ },
64
+ "requirements": {
65
+ "description": "The programs it needs on the computer: its metadata[\"agent-kit\"].requirements as it declares them.",
66
+ "type": "array",
67
+ "items": { "$ref": "https://falkenslab.github.io/agent-kit/schemas/agent-kit-metadata.schema.json#/$defs/requirement" }
68
+ },
59
69
  "logo": { "description": "A URL anyone can load.", "$ref": "#/$defs/url" },
60
70
  "banner": { "description": "A URL anyone can load.", "$ref": "#/$defs/url" }
61
71
  }