@falkenslab/agent-kit 0.21.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.
Files changed (57) hide show
  1. package/README.md +1 -4
  2. package/assets/extension-launcher.mjs +28 -5
  3. package/dist/chat/chatController.d.ts +60 -0
  4. package/dist/chat/chatController.js +212 -2
  5. package/dist/chat/extensions.d.ts +36 -2
  6. package/dist/chat/extensions.js +87 -3
  7. package/dist/chat/runs.d.ts +3 -1
  8. package/dist/chat/runs.js +1 -1
  9. package/dist/core/agentSpec.d.ts +11 -0
  10. package/dist/core/elicitation.d.ts +54 -0
  11. package/dist/core/elicitation.js +120 -0
  12. package/dist/core/extensionCatalog.d.ts +103 -0
  13. package/dist/core/extensionCatalog.js +235 -0
  14. package/dist/core/extensionOptions.d.ts +56 -0
  15. package/dist/core/extensionOptions.js +139 -0
  16. package/dist/core/extensionRequirements.d.ts +66 -0
  17. package/dist/core/extensionRequirements.js +112 -0
  18. package/dist/core/extensions.d.ts +75 -2
  19. package/dist/core/extensions.js +78 -6
  20. package/dist/core/externalExtensions.d.ts +101 -11
  21. package/dist/core/externalExtensions.js +263 -51
  22. package/dist/core/hooks/planGate.d.ts +2 -0
  23. package/dist/core/hooks/planGate.js +3 -0
  24. package/dist/core/interaction.d.ts +7 -0
  25. package/dist/core/liveExtensions.d.ts +15 -0
  26. package/dist/core/liveExtensions.js +33 -2
  27. package/dist/core/marketplaces.d.ts +7 -2
  28. package/dist/core/marketplaces.js +6 -4
  29. package/dist/core/messages/de.js +37 -1
  30. package/dist/core/messages/en.d.ts +44 -0
  31. package/dist/core/messages/en.js +37 -1
  32. package/dist/core/messages/es.js +37 -1
  33. package/dist/core/messages/fr.js +37 -1
  34. package/dist/core/npxPackages.d.ts +48 -0
  35. package/dist/core/npxPackages.js +121 -0
  36. package/dist/core/pluginDownloads.d.ts +2 -1
  37. package/dist/core/pluginDownloads.js +2 -2
  38. package/dist/core/readOnlyHints.d.ts +13 -0
  39. package/dist/core/readOnlyHints.js +26 -0
  40. package/dist/core/runner.js +2 -0
  41. package/dist/core/session.d.ts +17 -5
  42. package/dist/core/session.js +44 -11
  43. package/dist/core/sessionFacts.d.ts +11 -1
  44. package/dist/core/workspaceTrust.d.ts +12 -0
  45. package/dist/core/workspaceTrust.js +60 -0
  46. package/dist/extensions/awareness/tools.js +6 -2
  47. package/dist/index.d.ts +9 -2
  48. package/dist/index.js +7 -2
  49. package/dist/tui/extensionCommand.d.ts +10 -2
  50. package/dist/tui/extensionCommand.js +199 -34
  51. package/extensions/awareness/.claude-plugin/plugin.json +13 -5
  52. package/extensions/knowledge/.claude-plugin/plugin.json +13 -5
  53. package/extensions/memory/.claude-plugin/plugin.json +13 -5
  54. package/extensions/sources/.claude-plugin/plugin.json +13 -5
  55. package/package.json +7 -5
  56. package/schemas/agent-kit-metadata.schema.json +120 -0
  57. package/schemas/marketplace-catalog.schema.json +74 -0
@@ -1,9 +1,13 @@
1
1
  import { stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
- import { addMarketplace, findPlugin, inspectMarketplace, installFromMarketplace, listMarketplaces, removeMarketplace, updateMarketplace, } from "../core/marketplaces.js";
5
- import { addExtension, folderSize, isGitSource, listInstalled, removeExtension, setExtensionEnabled, } from "../core/externalExtensions.js";
6
- import { describe } from "../chat/extensions.js";
4
+ import { addMarketplace, findPlugin, inspectMarketplace, installFromMarketplace, removeMarketplace, updateMarketplace, } from "../core/marketplaces.js";
5
+ import { addExtension, extensionOptionStates, installTarget, folderSize, isGitSource, listInstalled, removeExtension, setExtensionEnabled, setExtensionOptions, } from "../core/externalExtensions.js";
6
+ import { describe, describeOptions } from "../chat/extensions.js";
7
+ import { extensionCatalog, knownMarketplaces as catalogMarketplaces, restoreExtensions, updateExtension } from "../core/extensionCatalog.js";
8
+ import { trustWorkspace } from "../core/workspaceTrust.js";
9
+ import { describeCapability } from "../core/extensions.js";
10
+ import { getLanguage } from "../core/messages/index.js";
7
11
  export { chatExtensionsCommand, describe } from "../chat/extensions.js";
8
12
  /**
9
13
  * The `extension` command an agent exposes in its own binary (#37), e.g. `captain extension add
@@ -13,10 +17,14 @@ export { chatExtensionsCommand, describe } from "../chat/extensions.js";
13
17
  *
14
18
  * - `extension list`
15
19
  * - `extension info <name>`: its metadata, what it offers and where its README is
16
- * - `extension add <folder | git URL[#ref] | plugin[@marketplace]> [--project] [--path <subfolder>] [--yes]`
20
+ * - `extension add <folder | git URL[#ref] | plugin[@marketplace]> [--project] [--path <subfolder>] [--set key=value…] [--yes]`: asks for its options (#55) unless `--set` gives them
17
21
  * - `extension add <folder> --link [--project]`: registered where it is, to develop it (not copied, its files not checked)
18
22
  * - `extension remove <name> [--project | --agent] [--yes | --keep-data]`: asks before deleting its data folder
19
23
  * - `extension enable|disable <name> [--project | --agent]`
24
+ * - `extension config <name> [key=value… | key… | --unset key…] [--project | --agent]`: its options; `key` alone asks for it (without echo when it's a secret)
25
+ * - `extension update [name] [--yes]`: one installed from a marketplace (or all that have a newer version there) to the version it offers now, its data and options kept
26
+ * - `extension restore [--trust]`: gets the code a workspace's lock lists and this computer lacks (#57); `--trust` trusts it too
27
+ * - `extension trust [name…]`: lets a workspace's extensions run, as their code is now
20
28
  * - `extension search [words]`: what the known marketplaces offer
21
29
  * - `extension marketplace add <folder | git URL[#ref] | owner/repo> [--yes]`, `list`, `update [name]`, `remove <name>`
22
30
  *
@@ -37,7 +45,8 @@ export async function runExtensionCommand(argv, options) {
37
45
  const index = rest.indexOf(`--${name}`);
38
46
  return index >= 0 ? rest[index + 1] : undefined;
39
47
  };
40
- const positional = rest.filter((arg, index) => !arg.startsWith("--") && rest[index - 1] !== "--path");
48
+ const values = (name) => rest.flatMap((arg, index) => (rest[index - 1] === `--${name}` ? [arg] : []));
49
+ const positional = rest.filter((arg, index) => !arg.startsWith("--") && !["--path", "--set", "--unset"].includes(rest[index - 1]));
41
50
  const askedScope = flag("project") ? "project" : flag("agent") ? "agent" : undefined;
42
51
  const fail = (message) => {
43
52
  write(message);
@@ -48,19 +57,41 @@ export async function runExtensionCommand(argv, options) {
48
57
  // The marketplaces live with the agent's extensions (the project's when it has no scope of its own).
49
58
  const marketsDir = options.dirs.agent ?? options.dirs.project;
50
59
  const ask = options.ask ?? askOnTerminal;
51
- const confirmed = async (question, accepts) => flag("yes") || accepts((await ask(question)).trim());
52
- // The agent's own marketplace, known without asking; a folder is copied afresh each time (it's the agent's own code).
53
- const knownMarketplaces = async () => {
54
- if (!marketsDir)
55
- throw new Error("This agent has no folder for extensions.");
56
- const known = await listMarketplaces(marketsDir);
57
- const official = options.official;
58
- if (official && !(isGitSource(official) && known.some((marketplace) => marketplace.official))) {
59
- await addMarketplace(official, marketsDir, { official: true });
60
- return listMarketplaces(marketsDir);
60
+ const askSecret = options.askSecret ?? (options.ask ? options.ask : askSecretOnTerminal);
61
+ /**
62
+ * After installing `name` into `dir`: its options from `--set`, then asked for each still
63
+ * without a value (on a terminal; none without one), saying which required ones are left.
64
+ */
65
+ const configure = async (name, dir) => {
66
+ const installed = (await listInstalled(options.dirs)).find((extension) => extension.name === name && extension.scopeDir === dir);
67
+ const declared = installed?.manifest?.userConfig ?? {};
68
+ if (!installed || !Object.keys(declared).length)
69
+ return;
70
+ const typed = {};
71
+ for (const pair of values("set")) {
72
+ const at = pair.indexOf("=");
73
+ if (at <= 0)
74
+ throw new Error(`--set takes key=value: "${pair}"`);
75
+ typed[pair.slice(0, at)] = pair.slice(at + 1);
76
+ }
77
+ const states = await describeOptionStates(installed);
78
+ for (const [key, option] of Object.entries(declared)) {
79
+ if (key in typed || states.get(key))
80
+ continue;
81
+ const question = `${option.title}${option.required ? "" : " (optional)"}${option.default !== undefined ? ` [${String(option.default)}]` : ""}: ${option.description}\n ${key} = `;
82
+ const answer = (await (option.sensitive ? askSecret : ask)(question)).trim();
83
+ if (answer)
84
+ typed[key] = answer;
61
85
  }
62
- return known;
86
+ if (Object.keys(typed).length)
87
+ await setExtensionOptions(installed, name, typed);
88
+ const missing = Object.entries(declared).filter(([key, option]) => option.required && !(key in typed) && !states.get(key) && option.default === undefined).map(([key]) => key);
89
+ if (missing.length)
90
+ write(`It stays off until ${missing.join(", ")} ${missing.length === 1 ? "is" : "are"} set: \`${command} extension config ${name} ${missing[0]}\`.`);
63
91
  };
92
+ const confirmed = async (question, accepts) => flag("yes") || accepts((await ask(question)).trim());
93
+ // The agent's own marketplace, known without asking; a folder is copied afresh each time (it's the agent's own code).
94
+ const knownMarketplaces = () => catalogMarketplaces(options.dirs, options.official);
64
95
  try {
65
96
  switch (action) {
66
97
  case "list": {
@@ -92,13 +123,26 @@ export async function runExtensionCommand(argv, options) {
92
123
  const dir = scopeDir(scope);
93
124
  if (!dir)
94
125
  return fail(`This agent has no ${scope} scope for extensions.`);
126
+ // The packages its npx servers run, in the agent's cache, shared by both scopes (#54); a
127
+ // project's code in the agent's store, its lock all the workspace keeps (#57).
128
+ const packages = installTarget(options.dirs, scope);
129
+ // Installed into the workspace by the person: trusted there as it is.
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
+ };
95
136
  // A plugin of a marketplace, unless it's a folder here or a git URL.
96
137
  const isFolder = await stat(path.resolve(source)).then((found) => found.isDirectory(), () => false);
97
138
  if (flag("link")) {
98
139
  if (!isFolder)
99
140
  return fail(`Only a folder can be linked: ${source} isn't one here.`);
100
- const result = await addExtension(source, dir, { link: true });
141
+ const result = await addExtension(source, dir, { link: true, ...(packages.packages ? { packages: packages.packages } : {}) });
142
+ await trusted(result.name);
143
+ await configure(result.name, dir);
101
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);
102
146
  return true;
103
147
  }
104
148
  if (!isFolder && !isGitSource(source) && /^[A-Za-z0-9][\w.-]*(@[A-Za-z0-9][\w.-]*)?$/.test(source)) {
@@ -110,13 +154,19 @@ export async function runExtensionCommand(argv, options) {
110
154
  if (!(await confirmed("Install it? (y/N) ", (typed) => /^(y|yes|s|si|sí)$/i.test(typed))))
111
155
  return fail("Not installed.");
112
156
  }
113
- const result = await installFromMarketplace(marketsDir, source, dir);
157
+ const result = await installFromMarketplace(marketsDir, source, dir, packages);
158
+ await trusted(result.name);
159
+ await configure(result.name, dir);
114
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);
115
162
  return true;
116
163
  }
117
164
  const subdir = value("path");
118
- const result = await addExtension(source, dir, subdir ? { subdir } : {});
165
+ const result = await addExtension(source, dir, { ...(subdir ? { subdir } : {}), ...packages });
166
+ await trusted(result.name);
167
+ await configure(result.name, dir);
119
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);
120
170
  return true;
121
171
  }
122
172
  case "remove":
@@ -130,7 +180,7 @@ export async function runExtensionCommand(argv, options) {
130
180
  return fail(`${name} isn't installed${askedScope ? ` in the ${askedScope} scope` : ""}. \`${command} extension list\` lists them.`);
131
181
  if (found.length > 1 && !askedScope)
132
182
  return fail(`${name} is installed in both scopes: say which, with --project or --agent.`);
133
- const { scope, dataDir } = found[0];
183
+ const { scope, dataDir, privateDir } = found[0];
134
184
  const dir = scopeDir(scope);
135
185
  if (action !== "remove") {
136
186
  await setExtensionEnabled(dir, name, action === "enable");
@@ -140,24 +190,95 @@ export async function runExtensionCommand(argv, options) {
140
190
  // Its data (a browser profile, a signed-in session) may be worth keeping for a reinstall: asked, unless said.
141
191
  const size = await folderSize(dataDir);
142
192
  const deleteData = size > 0 && !flag("keep-data") && (await confirmed(`Delete its data too (${dataDir}, ${formatSize(size)})? (y/N) `, (typed) => /^(y|yes|s|si|sí)$/i.test(typed)));
143
- await removeExtension(dir, name, { deleteData });
193
+ await removeExtension(dir, name, { deleteData, privateDir });
144
194
  write(`${name} removed in the ${scope} scope${size > 0 ? (deleteData ? ", with its data" : `; its data stays in ${dataDir}`) : ""}. It applies from the next session (in the chat, \`/extensions\` reopens it).`);
145
195
  return true;
146
196
  }
197
+ case "config": {
198
+ const name = positional[0];
199
+ if (!name)
200
+ return fail(`Usage: ${command} extension config <name> [key=value… | key… | --unset key…] [--project | --agent]`);
201
+ const found = (await listInstalled(options.dirs)).filter((extension) => extension.name === name && !extension.shadowed && (!askedScope || extension.scope === askedScope));
202
+ if (!found.length)
203
+ return fail(`${name} isn't installed${askedScope ? ` in the ${askedScope} scope` : ""}. \`${command} extension list\` lists them.`);
204
+ const installed = found[0];
205
+ const declared = installed.manifest?.userConfig ?? {};
206
+ const typed = {};
207
+ for (const key of values("unset"))
208
+ typed[key] = undefined;
209
+ for (const pair of positional.slice(1)) {
210
+ const at = pair.indexOf("=");
211
+ if (at > 0)
212
+ typed[pair.slice(0, at)] = pair.slice(at + 1);
213
+ else {
214
+ const option = declared[pair];
215
+ if (!option)
216
+ return fail(`${name} has no option "${pair}"${Object.keys(declared).length ? `: it has ${Object.keys(declared).join(", ")}` : ""}.`);
217
+ typed[pair] = (await (option.sensitive ? askSecret : ask)(`${option.title}: ${option.description}\n ${pair} = `)).trim() || undefined;
218
+ }
219
+ }
220
+ if (Object.keys(typed).length) {
221
+ await setExtensionOptions(installed, name, typed);
222
+ write(`${name}'s options saved in the ${installed.scope} scope. They apply when its server starts again: the next session, or in a chat \`/extensions disable ${name}\` and \`enable\`.`);
223
+ }
224
+ const lines = await describeOptions((await listInstalled(options.dirs)).find((extension) => extension.name === name && extension.scopeDir === installed.scopeDir));
225
+ if (!lines.length)
226
+ write(`${name} has no options.`);
227
+ for (const line of lines)
228
+ write(line);
229
+ return true;
230
+ }
231
+ case "restore": {
232
+ const result = await restoreExtensions(options.dirs, options.official ? { official: options.official } : {});
233
+ for (const name of result.restored)
234
+ write(`Restored ${name}.`);
235
+ for (const { name, reason } of result.failed)
236
+ write(`${name} can't be restored: ${reason}.`);
237
+ if (!result.restored.length && !result.failed.length)
238
+ write("This workspace's extensions are all on this computer.");
239
+ if (result.failed.length)
240
+ process.exitCode = 1;
241
+ if (flag("trust"))
242
+ for (const name of await trustWorkspace(options.dirs))
243
+ write(`Trusted ${name} in this workspace.`);
244
+ else if (options.dirs.project && options.dirs.agent)
245
+ write(`They run once you trust them: \`${command} extension trust\`, or the chat asks.`);
246
+ return true;
247
+ }
248
+ case "trust": {
249
+ if (!options.dirs.project || !options.dirs.agent)
250
+ return fail("This agent has no workspace scope to trust: only its own, which never asks.");
251
+ const names = await trustWorkspace(options.dirs, positional.length ? positional : undefined);
252
+ if (!names.length)
253
+ write("This workspace has no extensions.");
254
+ for (const name of names)
255
+ write(`Trusted ${name} in this workspace, as it is now: another version asks again.`);
256
+ return true;
257
+ }
147
258
  case "search": {
148
- const words = positional.map((word) => word.toLowerCase());
149
- const installed = new Set((await listInstalled(options.dirs)).map((extension) => extension.name));
150
- const offered = (await knownMarketplaces()).flatMap((marketplace) => (marketplace.manifest?.plugins ?? []).map((plugin) => ({ marketplace, plugin })));
151
- const matching = offered.filter(({ plugin }) => {
152
- const text = [plugin.name, plugin.description, plugin.category, ...(plugin.tags ?? []), ...(plugin.keywords ?? [])].join(" ").toLowerCase();
153
- return words.every((word) => text.includes(word));
154
- });
259
+ const matching = await extensionCatalog(options.dirs, { ...(options.official ? { official: options.official } : {}), ...(options.catalogTopics?.length ? { topics: options.catalogTopics } : {}), words: positional });
155
260
  if (!matching.length) {
156
- write(offered.length ? "No extension matches." : `No marketplace offers extensions yet. \`${command} extension marketplace add <folder | git URL | owner/repo>\` adds one.`);
261
+ const offered = (await knownMarketplaces()).some((marketplace) => marketplace.manifest?.plugins.length);
262
+ write(offered ? "No extension matches." : `No marketplace offers extensions yet. \`${command} extension marketplace add <folder | git URL | owner/repo>\` adds one.`);
157
263
  return true;
158
264
  }
159
- for (const { marketplace, plugin } of matching) {
160
- write(`${plugin.name}@${marketplace.name}${plugin.version ? ` ${plugin.version}` : ""}${installed.has(plugin.name) ? " (installed)" : ""}${plugin.description ? ` ${plugin.description}` : ""}`);
265
+ for (const entry of matching) {
266
+ const state = entry.installed ? (entry.update ? ` (installed: ${entry.installed.manifest?.version ?? "?"}, update available)` : " (installed)") : "";
267
+ write(`${entry.spec}${entry.version ? ` ${entry.version}` : ""}${state}${entry.plugin.description ? ` ${entry.plugin.description}` : ""}`);
268
+ }
269
+ return true;
270
+ }
271
+ case "update": {
272
+ const name = positional[0];
273
+ const official = options.official ? { official: options.official } : {};
274
+ const names = name ? [name] : (await extensionCatalog(options.dirs, official)).filter((entry) => entry.update).map((entry) => entry.plugin.name);
275
+ if (!names.length) {
276
+ write("Every extension from a marketplace is up to date.");
277
+ return true;
278
+ }
279
+ for (const each of names) {
280
+ const result = await updateExtension(options.dirs, each, official);
281
+ write(`Updated ${each}${result.from || result.to ? ` (${result.from ?? "?"} → ${result.to ?? "?"})` : ""} in the ${result.scope} scope: its data and options kept. It runs from the next session.`);
161
282
  }
162
283
  return true;
163
284
  }
@@ -168,10 +289,14 @@ export async function runExtensionCommand(argv, options) {
168
289
  `Usage: ${command} extension <action>`,
169
290
  " list what's installed, in which scope, on or off",
170
291
  " info <name> its metadata, what it offers, its README",
171
- " add <folder | git URL[#ref] | plugin[@marketplace]> [--project] [--path <subfolder>] [--yes]",
292
+ " add <folder | git URL[#ref] | plugin[@marketplace]> [--project] [--path <subfolder>] [--set key=value…] [--yes]",
172
293
  " add <folder> --link [--project] to develop one: runs from its folder, not copied nor checked",
173
294
  " remove <name> [--project | --agent] [--yes | --keep-data]",
174
295
  " enable|disable <name> [--project | --agent]",
296
+ " config <name> [key=value… | key… | --unset key…] its options (key alone asks for it)",
297
+ " update [name] to the version its marketplace offers now (all that have one, without a name)",
298
+ " restore [--trust] gets what this workspace's lock lists and this computer lacks",
299
+ " trust [name…] lets this workspace's extensions run, as they are now",
175
300
  " search [words] what the known marketplaces offer",
176
301
  " marketplace add <folder | git URL[#ref] | owner/repo> [--yes]",
177
302
  " marketplace list | update [name] | remove <name>",
@@ -234,6 +359,32 @@ export async function runExtensionCommand(argv, options) {
234
359
  }
235
360
  }
236
361
  /** A question on the terminal, or no answer without one. */
362
+ /** Which of an installed extension's options have a value of their own (not a default). */
363
+ async function describeOptionStates(installed) {
364
+ return new Map((await extensionOptionStates(installed)).map((state) => [state.key, state.set && !state.isDefault]));
365
+ }
366
+ /** A secret from the terminal, not echoed; "" without one. */
367
+ async function askSecretOnTerminal(question) {
368
+ if (!process.stdin.isTTY)
369
+ return "";
370
+ const terminal = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
371
+ // What's typed isn't written back: only the question, readline's first write.
372
+ const output = terminal;
373
+ let prompted = false;
374
+ output._writeToOutput = (text) => {
375
+ if (prompted)
376
+ return;
377
+ prompted = true;
378
+ output.output.write(text);
379
+ };
380
+ try {
381
+ return await terminal.question(question);
382
+ }
383
+ finally {
384
+ terminal.close();
385
+ process.stdout.write("\n");
386
+ }
387
+ }
237
388
  async function askOnTerminal(question) {
238
389
  if (!process.stdin.isTTY)
239
390
  return "";
@@ -261,11 +412,25 @@ async function info(extension) {
261
412
  ...field("Repository", manifest.repository),
262
413
  ...field("Keywords", manifest.keywords?.join(", ")),
263
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
+ }),
264
420
  ...field("Provides", manifest.provides.join(", ") || undefined),
265
421
  ...field("Requires", manifest.requires.join(", ") || undefined),
266
- ...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(", ")}` : ""}`)),
267
424
  ...field("Servers", Object.keys(manifest.servers).join(", ") || undefined),
268
- ...field("Only read", manifest.readOnlyTools.join(", ") || undefined),
425
+ ...field("Only read", manifest.readOnlyTools
426
+ ? `${manifest.readOnlyTools.join(", ") || "none"} (its manifest)`
427
+ : Object.keys(manifest.servers).length
428
+ ? "what its servers mark readOnlyHint"
429
+ : undefined),
430
+ ...field("Packages", Object.keys(extension.lock.packages ?? {}).join(", ") || undefined),
431
+ ...field("Options", Object.entries(manifest.userConfig)
432
+ .map(([key, option]) => `${key} (${[option.title, option.required && "required", option.sensitive && "secret"].filter(Boolean).join(", ")})`)
433
+ .join(", ") || undefined),
269
434
  ...field("Variables", Object.values(manifest.servers).flatMap((server) => Object.keys(server.env ?? {})).join(", ") || undefined),
270
435
  ...field("Installed", `${extension.scope} scope, ${extension.lock.enabled ? "enabled" : "disabled"}, on ${extension.lock.installed}, from ${describe(extension).split(" from ")[1]}`),
271
436
  ...field("Data", (await stat(extension.dataDir).catch(() => null))?.isDirectory() ? `${extension.dataDir} (${formatSize(await folderSize(extension.dataDir))})` : undefined),
@@ -1,10 +1,18 @@
1
1
  {
2
2
  "name": "awareness",
3
+ "version": "1.0.0",
3
4
  "description": "You know who you are and what you are right now (your mode, extensions, tools, subagents, commands, context) through about_me, and how your chat is used through the help skill.",
4
- "agent-kit": {
5
- "provides": [
6
- "self-awareness"
7
- ],
8
- "requires": []
5
+ "author": {
6
+ "name": "Falkenslab",
7
+ "url": "https://github.com/falkenslab"
8
+ },
9
+ "license": "MIT",
10
+ "metadata": {
11
+ "agent-kit": {
12
+ "provides": [
13
+ "self-awareness"
14
+ ],
15
+ "requires": []
16
+ }
9
17
  }
10
18
  }
@@ -1,10 +1,18 @@
1
1
  {
2
2
  "name": "knowledge",
3
+ "version": "1.0.0",
3
4
  "description": "Built-in knowledge base workflows (an LLM wiki) over the kit's knowledge_* tools: ingesting sources, querying the knowledge base and health-checking it.",
4
- "agent-kit": {
5
- "provides": [
6
- "knowledge-base"
7
- ],
8
- "requires": []
5
+ "author": {
6
+ "name": "Falkenslab",
7
+ "url": "https://github.com/falkenslab"
8
+ },
9
+ "license": "MIT",
10
+ "metadata": {
11
+ "agent-kit": {
12
+ "provides": [
13
+ "knowledge-base"
14
+ ],
15
+ "requires": []
16
+ }
9
17
  }
10
18
  }
@@ -1,10 +1,18 @@
1
1
  {
2
2
  "name": "memory",
3
+ "version": "1.0.0",
3
4
  "description": "Your memory of the person you work for, across all their projects: who they are and how they want things done, kept only from what they tell you, through its tools: recall, remember and forget.",
4
- "agent-kit": {
5
- "provides": [
6
- "person-memory"
7
- ],
8
- "requires": []
5
+ "author": {
6
+ "name": "Falkenslab",
7
+ "url": "https://github.com/falkenslab"
8
+ },
9
+ "license": "MIT",
10
+ "metadata": {
11
+ "agent-kit": {
12
+ "provides": [
13
+ "person-memory"
14
+ ],
15
+ "requires": []
16
+ }
9
17
  }
10
18
  }
@@ -1,10 +1,18 @@
1
1
  {
2
2
  "name": "sources",
3
+ "version": "1.0.0",
3
4
  "description": "The sources folder: originals kept as obtained, listed, downloaded, asked for and retired through the kit's sources tools.",
4
- "agent-kit": {
5
- "provides": [
6
- "sources"
7
- ],
8
- "requires": []
5
+ "author": {
6
+ "name": "Falkenslab",
7
+ "url": "https://github.com/falkenslab"
8
+ },
9
+ "license": "MIT",
10
+ "metadata": {
11
+ "agent-kit": {
12
+ "provides": [
13
+ "sources"
14
+ ],
15
+ "requires": []
16
+ }
9
17
  }
10
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falkenslab/agent-kit",
3
- "version": "0.21.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",
@@ -33,7 +33,8 @@
33
33
  "files": [
34
34
  "dist",
35
35
  "assets",
36
- "extensions"
36
+ "extensions",
37
+ "schemas"
37
38
  ],
38
39
  "scripts": {
39
40
  "build": "tsc",
@@ -48,7 +49,7 @@
48
49
  "prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build"
49
50
  },
50
51
  "dependencies": {
51
- "@anthropic-ai/claude-agent-sdk": "^0.3.234",
52
+ "@anthropic-ai/claude-agent-sdk": "^0.3.300",
52
53
  "@inkjs/ui": "^2.0.0",
53
54
  "@inquirer/prompts": "^8.5.2",
54
55
  "@types/react": "^19.3.0",
@@ -66,6 +67,7 @@
66
67
  "@mozilla/readability": "^0.6.0",
67
68
  "@types/node": "^24.0.0",
68
69
  "@types/turndown": "^5.0.6",
70
+ "ajv": "^8.20.0",
69
71
  "eslint": "^10.9.1",
70
72
  "fflate": "^0.8.3",
71
73
  "globals": "^17.11.0",
@@ -85,10 +87,10 @@
85
87
  },
86
88
  "peerDependencies": {
87
89
  "@mozilla/readability": ">=0.5.0",
90
+ "fflate": ">=0.8.0",
88
91
  "linkedom": ">=0.16.0",
89
- "turndown": ">=7.0.0",
90
92
  "mammoth": ">=1.6.0",
91
- "fflate": ">=0.8.0"
93
+ "turndown": ">=7.0.0"
92
94
  },
93
95
  "peerDependenciesMeta": {
94
96
  "@mozilla/readability": {
@@ -0,0 +1,120 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://falkenslab.github.io/agent-kit/schemas/agent-kit-metadata.schema.json",
4
+ "title": "agent-kit's data in an extension's plugin.json",
5
+ "description": "What goes under metadata[\"agent-kit\"] in a Claude Code plugin's .claude-plugin/plugin.json, for agent-kit's gates, chats and catalog. Claude Code doesn't read metadata. Every field is optional.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "kit": {
10
+ "description": "The agent-kit versions it works with: comparators separated by spaces, all of which must hold (\">=0.22.0\", \">=0.22.0 <0.24.0\").",
11
+ "type": "string",
12
+ "pattern": "^\\s*(?:(?:>=|<=|>|<|=)?\\s*v?\\d+(?:\\.\\d+){0,2})(?:\\s+(?:>=|<=|>|<|=)?\\s*v?\\d+(?:\\.\\d+){0,2})*\\s*$"
13
+ },
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
+ },
20
+ "requires": { "description": "Capabilities another enabled extension must provide.", "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
+ },
26
+ "readOnlyTools": {
27
+ "description": "Its tools that only read (short names): plan mode lets them through. Without it, plan mode asks its servers (readOnlyHint).",
28
+ "type": "array",
29
+ "items": { "type": "string", "minLength": 1 }
30
+ },
31
+ "labels": {
32
+ "description": "How the chat shows each tool (short name), per language (en, es, fr, de): {field} takes the call's input.",
33
+ "type": "object",
34
+ "additionalProperties": {
35
+ "type": "object",
36
+ "propertyNames": { "enum": ["en", "es", "fr", "de"] },
37
+ "additionalProperties": {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "required": ["label"],
41
+ "properties": {
42
+ "label": { "type": "string", "minLength": 1 },
43
+ "phrase": {
44
+ "description": "How it counts in a folded summary: once, and {n} times.",
45
+ "type": "array",
46
+ "prefixItems": [{ "type": "string" }, { "type": "string" }],
47
+ "minItems": 2,
48
+ "maxItems": 2
49
+ }
50
+ }
51
+ }
52
+ }
53
+ },
54
+ "help": { "description": "What it says about itself in a session, for the agent to tell the person.", "type": "string" },
55
+ "catalog": {
56
+ "description": "How a catalog shows it: images inside its folder (SVG, PNG, JPEG or WebP).",
57
+ "type": "object",
58
+ "additionalProperties": false,
59
+ "properties": {
60
+ "logo": { "description": "Square.", "$ref": "#/$defs/image" },
61
+ "banner": { "description": "Wide, about 3:1.", "$ref": "#/$defs/image" }
62
+ }
63
+ }
64
+ },
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
+ },
115
+ "image": {
116
+ "type": "string",
117
+ "pattern": "^(?!/)(?![A-Za-z]:)(?!.*(?:^|[\\\\/])\\.\\.(?:[\\\\/]|$)).+\\.(?:svg|png|jpe?g|webp)$"
118
+ }
119
+ }
120
+ }