@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
@@ -108,11 +108,16 @@ export type ResolvedSource = {
108
108
  export declare function resolvePluginSource(marketplace: KnownMarketplace, plugin: MarketplacePlugin): ResolvedSource;
109
109
  /**
110
110
  * Installs `<plugin>@<marketplace>` (or a plugin only one known marketplace offers) into a scope,
111
- * locked with where it came from. Its name in the agent is its `plugin.json`'s, as for any extension.
111
+ * locked with where it came from (the packages its npx servers run into `options.packages`, #54).
112
+ * Its name in the agent is its `plugin.json`'s, as for any extension.
112
113
  */
113
- export declare function installFromMarketplace(dir: string, spec: string, scopeDir: string): Promise<{
114
+ export declare function installFromMarketplace(dir: string, spec: string, scopeDir: string, options?: {
115
+ packages?: string;
116
+ store?: string;
117
+ }): Promise<{
114
118
  name: string;
115
119
  replaced: boolean;
116
120
  commit?: string;
117
121
  marketplace: string;
122
+ missing: string[];
118
123
  }>;
@@ -208,9 +208,10 @@ export function resolvePluginSource(marketplace, plugin) {
208
208
  }
209
209
  /**
210
210
  * Installs `<plugin>@<marketplace>` (or a plugin only one known marketplace offers) into a scope,
211
- * locked with where it came from. Its name in the agent is its `plugin.json`'s, as for any extension.
211
+ * locked with where it came from (the packages its npx servers run into `options.packages`, #54).
212
+ * Its name in the agent is its `plugin.json`'s, as for any extension.
212
213
  */
213
- export async function installFromMarketplace(dir, spec, scopeDir) {
214
+ export async function installFromMarketplace(dir, spec, scopeDir, options = {}) {
214
215
  const { marketplace, plugin } = await findPlugin(dir, spec);
215
216
  const resolved = resolvePluginSource(marketplace, plugin);
216
217
  const from = `${plugin.name}@${marketplace.name}`;
@@ -218,7 +219,7 @@ export async function installFromMarketplace(dir, spec, scopeDir) {
218
219
  const { name, version, registry } = resolved.npm;
219
220
  const fetched = await fetchNpmPlugin(name, version, registry);
220
221
  try {
221
- return { ...(await addExtension(fetched.dir, scopeDir, { marketplace: from, origin: `npm:${name}@${fetched.version}` })), marketplace: marketplace.name };
222
+ return { ...(await addExtension(fetched.dir, scopeDir, { ...options, marketplace: from, origin: `npm:${name}@${fetched.version}` })), marketplace: marketplace.name };
222
223
  }
223
224
  finally {
224
225
  await rm(fetched.dir, { recursive: true, force: true });
@@ -227,13 +228,14 @@ export async function installFromMarketplace(dir, spec, scopeDir) {
227
228
  if ("archive" in resolved) {
228
229
  const fetched = await fetchArchivePlugin(resolved.archive.url, resolved.archive.sha256);
229
230
  try {
230
- return { ...(await addExtension(fetched.root, scopeDir, { marketplace: from, origin: resolved.archive.url })), marketplace: marketplace.name };
231
+ return { ...(await addExtension(fetched.root, scopeDir, { ...options, marketplace: from, origin: resolved.archive.url })), marketplace: marketplace.name };
231
232
  }
232
233
  finally {
233
234
  await rm(fetched.dir, { recursive: true, force: true });
234
235
  }
235
236
  }
236
237
  const result = await addExtension(resolved.source, scopeDir, {
238
+ ...options,
237
239
  ...(resolved.subdir ? { subdir: resolved.subdir } : {}),
238
240
  ...(resolved.sha ? { sha: resolved.sha } : {}),
239
241
  marketplace: from,
@@ -28,12 +28,48 @@ 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)`,
34
37
  extensionNotInstalled: (name) => `(${name} ist nicht installiert: /extensions listet sie auf)`,
35
- extensionsUsage: "(Verwendung: /extensions, /extensions enable <Name>, /extensions disable <Name>)",
38
+ extensionsUsage: "(Verwendung: /extensions, enable|disable <Name>, config <Name> [Schlüssel=Wert…], search [Wörter], install <Name>, update [Name], remove <Name> [--delete-data], reload, trust)",
36
39
  extensionsCantReopen: "(dieser Chat kann seine Sitzung nicht neu öffnen: im Terminal aktivieren oder deaktivieren und neu starten)",
40
+ extensionOptions: (name) => `Optionen von ${name}:`,
41
+ extensionNoOptions: (name) => `(${name} hat keine Optionen)`,
42
+ optionNotSet: "(nicht gesetzt)",
43
+ optionSecretSet: "(gesetzt, verborgen)",
44
+ optionDefault: "(Standard)",
45
+ optionRequired: "erforderlich",
46
+ optionSensitiveInChat: (key, name) => `(${key} ist ein Geheimnis: was im Chat getippt wird, bleibt in seinem Verlauf, also im Terminal setzen: extension config ${name} ${key})`,
47
+ extensionOptionsSaved: (name) => `(Optionen von ${name} gespeichert: sie gelten, wenn sein Server neu startet)`,
48
+ extensionRestarted: (name) => `(Optionen von ${name} gespeichert, und sein Server damit neu gestartet, in derselben Sitzung)`,
49
+ extensionReconfiguredReopen: (name) => `(Optionen von ${name} gespeichert: die Sitzung wird neu geöffnet, mit demselben Gespräch)`,
50
+ extensionsNoMatch: "(keine Erweiterung passt: /extensions search mit anderen Wörtern, oder ohne, um alle zu sehen)",
51
+ extensionIsInstalled: "(installiert)",
52
+ extensionUpdateAvailable: (installed) => `(installiert: ${installed}, Update verfügbar)`,
53
+ extensionLacks: (name, what) => `(${name}: ${what})`,
54
+ extensionInstalled: (name) => `(${name} installiert: in einem neuen Gespräch da, oder sofort mit /extensions reload, ohne dieses zu verlieren)`,
55
+ extensionUpdated: (name, from, to) => `(${name} aktualisiert, ${from} → ${to}: in einem neuen Gespräch da, oder sofort mit /extensions reload)`,
56
+ extensionsUpToDate: "(alle Erweiterungen aus einem Marketplace sind aktuell)",
57
+ extensionRemoved: (name, withData) => `(${name} deinstalliert${withData ? ", mit seinen Daten" : "; seine Daten bleiben für eine Neuinstallation"})`,
58
+ extensionConfirm: (spec, marketplace) => `(${spec} kommt aus ${marketplace}, nicht aus dem Marketplace dieses Agenten: es liefe auf diesem Computer mit deinen Rechten. /extensions install ${spec} --yes installiert es)`,
59
+ extensionsBusy: "(nach diesem Zug: der Agent arbeitet)",
60
+ extensionsReloaded: "(die Sitzung wird mit den jetzt installierten Erweiterungen neu geöffnet, mit demselben Gespräch)",
61
+ elicitTitle: (server) => `${server} fragt dich`,
62
+ elicitQuestion: "Antworten? (Enter/y: antworten, n: ablehnen, q: abbrechen) ",
63
+ elicitAgain: "Das ist kein gültiger Wert: versuch es nochmal, oder lass es leer.",
64
+ elicitNotANumber: (text) => `„${text}“ ist keine Zahl`,
65
+ elicitOutOfRange: (minimum, maximum) => `zwischen ${minimum ?? "…"} und ${maximum ?? "…"}`,
66
+ elicitUrlTitle: (server) => `${server} bittet dich, eine Seite zu öffnen`,
67
+ elicitUrlQuestion: "Öffne sie selbst, dann Enter, wenn du fertig bist (n: ablehnen) ",
68
+ trustTitle: "Dieser Arbeitsbereich bringt Erweiterungen mit",
69
+ trustBody: "Sie führen Code auf diesem Computer mit deinen Rechten aus. Vertraue ihnen nur, wenn du der Herkunft dieses Arbeitsbereichs vertraust:",
70
+ trustQuestion: "Ihnen vertrauen und sie ausführen? (y/N) ",
71
+ trustDeclined: "(sie bleiben aus: /extensions trust vertraut ihnen später)",
72
+ trustGiven: (names) => `(${names}: in diesem Arbeitsbereich vertraut, so wie sie sind; eine andere Version fragt erneut)`,
37
73
  currentRun: "(dieses)",
38
74
  resumed: (date) => `(Gespräch vom ${date} fortgesetzt)`,
39
75
  suggestionKey: "Tab",
@@ -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;
@@ -44,6 +50,44 @@ export interface Messages {
44
50
  extensionNotInstalled(name: string): string;
45
51
  extensionsUsage: string;
46
52
  extensionsCantReopen: string;
53
+ /** An installed extension's options in the chat (#55). */
54
+ extensionOptions(name: string): string;
55
+ extensionNoOptions(name: string): string;
56
+ optionNotSet: string;
57
+ optionSecretSet: string;
58
+ optionDefault: string;
59
+ optionRequired: string;
60
+ optionSensitiveInChat(key: string, name: string): string;
61
+ extensionOptionsSaved(name: string): string;
62
+ extensionRestarted(name: string): string;
63
+ extensionReconfiguredReopen(name: string): string;
64
+ /** The catalog in the chat (#56). */
65
+ extensionsNoMatch: string;
66
+ extensionIsInstalled: string;
67
+ extensionUpdateAvailable(installed: string): string;
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;
71
+ extensionUpdated(name: string, from: string, to: string): string;
72
+ extensionsUpToDate: string;
73
+ extensionRemoved(name: string, withData: boolean): string;
74
+ extensionConfirm(spec: string, marketplace: string): string;
75
+ extensionsBusy: string;
76
+ extensionsReloaded: string;
77
+ /** MCP elicitation (#58): a server asking the person for something. */
78
+ elicitTitle(server: string): string;
79
+ elicitQuestion: string;
80
+ elicitAgain: string;
81
+ elicitNotANumber(text: string): string;
82
+ elicitOutOfRange(minimum?: number, maximum?: number): string;
83
+ elicitUrlTitle(server: string): string;
84
+ elicitUrlQuestion: string;
85
+ /** A workspace's extensions to trust (#57). */
86
+ trustTitle: string;
87
+ trustBody: string;
88
+ trustQuestion: string;
89
+ trustDeclined: string;
90
+ trustGiven(names: string): string;
47
91
  currentRun: string;
48
92
  resumed(date: string): string;
49
93
  suggestionKey: string;
@@ -27,12 +27,48 @@ 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)`,
33
36
  extensionNotInstalled: (name) => `(${name} isn't installed: /extensions lists them)`,
34
- extensionsUsage: "(usage: /extensions, /extensions enable <name>, /extensions disable <name>)",
37
+ extensionsUsage: "(usage: /extensions, enable|disable <name>, config <name> [key=value…], search [words], install <name>, update [name], remove <name> [--delete-data], reload, trust)",
35
38
  extensionsCantReopen: "(this chat can't reopen its session: enable or disable it from the terminal, and start again)",
39
+ extensionOptions: (name) => `${name}'s options:`,
40
+ extensionNoOptions: (name) => `(${name} has no options)`,
41
+ optionNotSet: "(not set)",
42
+ optionSecretSet: "(set, hidden)",
43
+ optionDefault: "(default)",
44
+ optionRequired: "required",
45
+ optionSensitiveInChat: (key, name) => `(${key} is a secret: what's typed in the chat is kept in its history, so set it from the terminal: extension config ${name} ${key})`,
46
+ extensionOptionsSaved: (name) => `(${name}'s options saved: they apply when its server starts again)`,
47
+ extensionRestarted: (name) => `(${name}'s options saved, and its server restarted with them, in this same session)`,
48
+ extensionReconfiguredReopen: (name) => `(${name}'s options saved: the session reopens, with the same conversation)`,
49
+ extensionsNoMatch: "(no extension matches: /extensions search with other words, or none to list them all)",
50
+ extensionIsInstalled: "(installed)",
51
+ extensionUpdateAvailable: (installed) => `(installed: ${installed}, update available)`,
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})`,
54
+ extensionUpdated: (name, from, to) => `(${name} updated, ${from} → ${to}: it's there in a new conversation, or now with /extensions reload)`,
55
+ extensionsUpToDate: "(every extension from a marketplace is up to date)",
56
+ extensionRemoved: (name, withData) => `(${name} removed${withData ? ", with its data" : "; its data is kept for a reinstall"})`,
57
+ extensionConfirm: (spec, marketplace) => `(${spec} comes from ${marketplace}, not this agent's own marketplace: it would run on this computer with your permissions. /extensions install ${spec} --yes installs it)`,
58
+ extensionsBusy: "(after this turn: the agent is working)",
59
+ extensionsReloaded: "(the session reopens with the extensions installed now, keeping the conversation)",
60
+ elicitTitle: (server) => `${server} asks you`,
61
+ elicitQuestion: "Answer it? (Enter/y: answer, n: decline, q: cancel) ",
62
+ elicitAgain: "That isn't a valid value: try again, or leave it empty.",
63
+ elicitNotANumber: (text) => `"${text}" isn't a number`,
64
+ elicitOutOfRange: (minimum, maximum) => `between ${minimum ?? "…"} and ${maximum ?? "…"}`,
65
+ elicitUrlTitle: (server) => `${server} asks you to open a page`,
66
+ elicitUrlQuestion: "Open it yourself, then Enter when you're done (n: decline) ",
67
+ trustTitle: "This workspace brings extensions",
68
+ trustBody: "They run code on this computer with your permissions. Trust them only if you trust where this workspace came from:",
69
+ trustQuestion: "Trust them and run them? (y/N) ",
70
+ trustDeclined: "(they stay off: /extensions trust trusts them later)",
71
+ trustGiven: (names) => `(${names}: trusted in this workspace, as they are now; another version asks again)`,
36
72
  currentRun: "(this one)",
37
73
  resumed: (date) => `(resumed the conversation of ${date})`,
38
74
  suggestionKey: "tab",
@@ -28,12 +28,48 @@ 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)`,
34
37
  extensionNotInstalled: (name) => `(${name} no está instalada: /extensions las lista)`,
35
- extensionsUsage: "(uso: /extensions, /extensions enable <nombre>, /extensions disable <nombre>)",
38
+ extensionsUsage: "(uso: /extensions, enable|disable <nombre>, config <nombre> [clave=valor…], search [palabras], install <nombre>, update [nombre], remove <nombre> [--delete-data], reload, trust)",
36
39
  extensionsCantReopen: "(este chat no puede reabrir su sesión: actívala o desactívala desde la terminal y vuelve a empezar)",
40
+ extensionOptions: (name) => `Opciones de ${name}:`,
41
+ extensionNoOptions: (name) => `(${name} no tiene opciones)`,
42
+ optionNotSet: "(sin valor)",
43
+ optionSecretSet: "(con valor, oculto)",
44
+ optionDefault: "(por defecto)",
45
+ optionRequired: "obligatoria",
46
+ optionSensitiveInChat: (key, name) => `(${key} es un secreto: lo que se escribe en el chat queda en su historial, así que ponlo desde la terminal: extension config ${name} ${key})`,
47
+ extensionOptionsSaved: (name) => `(opciones de ${name} guardadas: se aplican cuando su servidor vuelva a arrancar)`,
48
+ extensionRestarted: (name) => `(opciones de ${name} guardadas, y su servidor reiniciado con ellas, en esta misma sesión)`,
49
+ extensionReconfiguredReopen: (name) => `(opciones de ${name} guardadas: la sesión se reabre, con la misma conversación)`,
50
+ extensionsNoMatch: "(ninguna extensión coincide: /extensions search con otras palabras, o sin ninguna para verlas todas)",
51
+ extensionIsInstalled: "(instalada)",
52
+ extensionUpdateAvailable: (installed) => `(instalada: ${installed}, hay actualización)`,
53
+ extensionLacks: (name, what) => `(${name}: ${what})`,
54
+ extensionInstalled: (name) => `(${name} instalada: está en una conversación nueva, o ya con /extensions reload, sin perder esta)`,
55
+ extensionUpdated: (name, from, to) => `(${name} actualizada, ${from} → ${to}: está en una conversación nueva, o ya con /extensions reload)`,
56
+ extensionsUpToDate: "(todas las extensiones de un marketplace están al día)",
57
+ extensionRemoved: (name, withData) => `(${name} desinstalada${withData ? ", con sus datos" : "; sus datos se guardan por si vuelve"})`,
58
+ extensionConfirm: (spec, marketplace) => `(${spec} viene de ${marketplace}, que no es el marketplace de este agente: se ejecutaría en este equipo con tus permisos. /extensions install ${spec} --yes la instala)`,
59
+ extensionsBusy: "(después de este turno: el agente está trabajando)",
60
+ extensionsReloaded: "(la sesión se reabre con las extensiones instaladas ahora, con la misma conversación)",
61
+ elicitTitle: (server) => `${server} te pregunta`,
62
+ elicitQuestion: "¿Responder? (Enter/y: responder, n: rechazar, q: cancelar) ",
63
+ elicitAgain: "Ese valor no vale: prueba otra vez, o déjalo vacío.",
64
+ elicitNotANumber: (text) => `"${text}" no es un número`,
65
+ elicitOutOfRange: (minimum, maximum) => `entre ${minimum ?? "…"} y ${maximum ?? "…"}`,
66
+ elicitUrlTitle: (server) => `${server} te pide abrir una página`,
67
+ elicitUrlQuestion: "Ábrela tú, y Enter cuando termines (n: rechazar) ",
68
+ trustTitle: "Este workspace trae extensiones",
69
+ trustBody: "Ejecutan código en este equipo con tus permisos. Confía en ellas solo si te fías de dónde viene este workspace:",
70
+ trustQuestion: "¿Confiar en ellas y ejecutarlas? (y/N) ",
71
+ trustDeclined: "(siguen apagadas: /extensions trust confía en ellas más tarde)",
72
+ trustGiven: (names) => `(${names}: de confianza en este workspace, tal como están; otra versión vuelve a preguntar)`,
37
73
  currentRun: "(esta)",
38
74
  resumed: (date) => `(retomada la conversación del ${date})`,
39
75
  suggestionKey: "tab",
@@ -28,12 +28,48 @@ 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)`,
34
37
  extensionNotInstalled: (name) => `(${name} n'est pas installée : /extensions les liste)`,
35
- extensionsUsage: "(usage : /extensions, /extensions enable <nom>, /extensions disable <nom>)",
38
+ extensionsUsage: "(usage : /extensions, enable|disable <nom>, config <nom> [clé=valeur…], search [mots], install <nom>, update [nom], remove <nom> [--delete-data], reload, trust)",
36
39
  extensionsCantReopen: "(ce chat ne peut pas rouvrir sa session : activez-la ou désactivez-la depuis le terminal, puis recommencez)",
40
+ extensionOptions: (name) => `Options de ${name} :`,
41
+ extensionNoOptions: (name) => `(${name} n'a pas d'options)`,
42
+ optionNotSet: "(sans valeur)",
43
+ optionSecretSet: "(définie, cachée)",
44
+ optionDefault: "(par défaut)",
45
+ optionRequired: "obligatoire",
46
+ optionSensitiveInChat: (key, name) => `(${key} est un secret : ce qu'on écrit dans le chat reste dans son historique, définissez-la donc depuis le terminal : extension config ${name} ${key})`,
47
+ extensionOptionsSaved: (name) => `(options de ${name} enregistrées : elles s'appliquent quand son serveur redémarre)`,
48
+ extensionRestarted: (name) => `(options de ${name} enregistrées, et son serveur redémarré avec elles, dans cette même session)`,
49
+ extensionReconfiguredReopen: (name) => `(options de ${name} enregistrées : la session se rouvre, avec la même conversation)`,
50
+ extensionsNoMatch: "(aucune extension ne correspond : /extensions search avec d'autres mots, ou sans pour toutes les voir)",
51
+ extensionIsInstalled: "(installée)",
52
+ extensionUpdateAvailable: (installed) => `(installée : ${installed}, mise à jour disponible)`,
53
+ extensionLacks: (name, what) => `(${name} : ${what})`,
54
+ extensionInstalled: (name) => `(${name} installée : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload, en gardant celle-ci)`,
55
+ extensionUpdated: (name, from, to) => `(${name} mise à jour, ${from} → ${to} : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload)`,
56
+ extensionsUpToDate: "(toutes les extensions d'un marketplace sont à jour)",
57
+ extensionRemoved: (name, withData) => `(${name} désinstallée${withData ? ", avec ses données" : " ; ses données sont gardées pour une réinstallation"})`,
58
+ extensionConfirm: (spec, marketplace) => `(${spec} vient de ${marketplace}, qui n'est pas le marketplace de cet agent : elle tournerait sur cet ordinateur avec vos droits. /extensions install ${spec} --yes l'installe)`,
59
+ extensionsBusy: "(après ce tour : l'agent travaille)",
60
+ extensionsReloaded: "(la session se rouvre avec les extensions installées maintenant, en gardant la conversation)",
61
+ elicitTitle: (server) => `${server} vous demande`,
62
+ elicitQuestion: "Répondre ? (Entrée/y : répondre, n : refuser, q : annuler) ",
63
+ elicitAgain: "Cette valeur n'est pas valide : réessayez, ou laissez vide.",
64
+ elicitNotANumber: (text) => `« ${text} » n'est pas un nombre`,
65
+ elicitOutOfRange: (minimum, maximum) => `entre ${minimum ?? "…"} et ${maximum ?? "…"}`,
66
+ elicitUrlTitle: (server) => `${server} vous demande d'ouvrir une page`,
67
+ elicitUrlQuestion: "Ouvrez-la vous-même, puis Entrée quand c'est fait (n : refuser) ",
68
+ trustTitle: "Cet espace de travail apporte des extensions",
69
+ trustBody: "Elles exécutent du code sur cet ordinateur avec vos droits. Faites-leur confiance seulement si vous avez confiance en la provenance de cet espace de travail :",
70
+ trustQuestion: "Leur faire confiance et les lancer ? (y/N) ",
71
+ trustDeclined: "(elles restent éteintes : /extensions trust leur fait confiance plus tard)",
72
+ trustGiven: (names) => `(${names} : de confiance dans cet espace de travail, telles qu'elles sont ; une autre version redemande)`,
37
73
  currentRun: "(celle-ci)",
38
74
  resumed: (date) => `(conversation du ${date} reprise)`,
39
75
  suggestionKey: "tab",
@@ -0,0 +1,48 @@
1
+ import type { ExtensionDirs, PluginServer } from "./externalExtensions.js";
2
+ /**
3
+ * Installed extensions' servers run with `npx` (#54): a `.mcp.json` that says
4
+ * `npx -y <package>@<x.y.z>` runs as it is in Claude Code, and in the kit too, which doesn't run
5
+ * npx (it needs npm, a `node` on the `PATH` for its shims, and the network while the server
6
+ * connects). The kit fetches the package when the extension is installed, its `sha512` integrity
7
+ * checked against the registry, into a cache by version shared by the agent's scopes; a session
8
+ * runs its `bin` with the kit's own Node (Electron's in the desktop app) through the launcher,
9
+ * as any installed server. Only an exact version: a published one never changes, so it's as
10
+ * fixed as the lock's hash. The kit installs no dependencies and runs no script: the package
11
+ * must bring what it needs (bundled, or `bundleDependencies`).
12
+ */
13
+ /** A server run with npx: its package, the exact version, and the arguments after it (its own). */
14
+ export interface NpxServer {
15
+ name: string;
16
+ version: string;
17
+ args: string[];
18
+ }
19
+ /** A cached package, as an extension's lock records it. */
20
+ export interface CachedPackage {
21
+ /** The registry's `sha512-…`, checked when it was fetched. */
22
+ integrity: string;
23
+ /** SHA-256 over its files in the cache, checked when a session loads it. */
24
+ sha256: string;
25
+ /** Its server's script, relative to the package (its `package.json`'s `bin`). */
26
+ bin: string;
27
+ }
28
+ /** `<package>@<version>`, as a lock names a package. */
29
+ export declare const packageSpec: (server: NpxServer) => string;
30
+ /**
31
+ * Whether a server runs with npx: `undefined` when it doesn't, why the kit can't run it when it
32
+ * does but not as the kit can, or its package. Its command is `npx` (`npx.cmd`), its options
33
+ * only `-y`/`--yes`, then `<package>@<x.y.z>` and the server's own arguments.
34
+ */
35
+ export declare function npxServer(server: PluginServer): NpxServer | string | undefined;
36
+ /** Where an agent keeps the packages its extensions' npx servers run: in its scope (the project's when it has none), shared by both. */
37
+ export declare function packageCacheDir(dirs: ExtensionDirs): string | undefined;
38
+ /** A package's folder in the cache. */
39
+ export declare function cachedPackageDir(cache: string, name: string, version: string): string;
40
+ /**
41
+ * A package in the cache, fetched when it isn't there (or its files changed since): its
42
+ * integrity checked, refused when it can't run as the kit installs it. Never runs anything.
43
+ */
44
+ export declare function cachePackage(cache: string, name: string, version: string): Promise<CachedPackage>;
45
+ /** Why a package an extension's lock names can't run from the cache now (missing, or changed), or nothing. */
46
+ export declare function cachedPackageProblem(cache: string | undefined, server: NpxServer, locked: CachedPackage | undefined): Promise<string | undefined>;
47
+ /** Whether every package a lock names is in the cache as it was fetched (`<package>@<version>` keys). */
48
+ export declare function cachedPackagesIntact(cache: string | undefined, locked: Record<string, CachedPackage> | undefined): Promise<boolean>;
@@ -0,0 +1,121 @@
1
+ import { cp, mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { hashExtension } from "./externalExtensions.js";
4
+ import { fetchNpmPlugin } from "./pluginDownloads.js";
5
+ /** `<package>@<version>`, as a lock names a package. */
6
+ export const packageSpec = (server) => `${server.name}@${server.version}`;
7
+ /**
8
+ * Whether a server runs with npx: `undefined` when it doesn't, why the kit can't run it when it
9
+ * does but not as the kit can, or its package. Its command is `npx` (`npx.cmd`), its options
10
+ * only `-y`/`--yes`, then `<package>@<x.y.z>` and the server's own arguments.
11
+ */
12
+ export function npxServer(server) {
13
+ if (!/^npx(\.cmd|\.exe)?$/i.test(path.basename(server.command)))
14
+ return undefined;
15
+ const args = server.args ?? [];
16
+ let index = 0;
17
+ for (; index < args.length && args[index].startsWith("-"); index++) {
18
+ if (args[index] !== "-y" && args[index] !== "--yes")
19
+ return `uses the npx option "${args[index]}", and the kit runs only "npx -y <package>@<version>"`;
20
+ }
21
+ const spec = args[index];
22
+ if (!spec)
23
+ return "names no package for npx";
24
+ const match = /^((?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*)@(\d+\.\d+\.\d+(?:-[\w.]+)?)$/i.exec(spec);
25
+ if (!match)
26
+ return `runs "${spec}" with npx without an exact version (<package>@x.y.z), which the kit needs to run the same code every time`;
27
+ return { name: match[1], version: match[2], args: args.slice(index + 1) };
28
+ }
29
+ /** Where an agent keeps the packages its extensions' npx servers run: in its scope (the project's when it has none), shared by both. */
30
+ export function packageCacheDir(dirs) {
31
+ const scope = dirs.agent ?? dirs.project;
32
+ return scope ? path.join(scope, ".npm") : undefined;
33
+ }
34
+ /** A package's folder in the cache. */
35
+ export function cachedPackageDir(cache, name, version) {
36
+ return path.join(cache, ...name.split("/"), version);
37
+ }
38
+ /** Why a package can't run as the kit installs it (its dependencies, its scripts, its bin), or its server's script. */
39
+ async function packageBin(dir, name) {
40
+ let json;
41
+ try {
42
+ json = JSON.parse(await readFile(path.join(dir, "package.json"), "utf8"));
43
+ }
44
+ catch {
45
+ return { problem: "has no readable package.json" };
46
+ }
47
+ const bundled = json.bundleDependencies ?? json.bundledDependencies;
48
+ const declared = Object.keys({ ...json.dependencies, ...json.optionalDependencies });
49
+ const missing = bundled === true ? [] : declared.filter((dependency) => !(Array.isArray(bundled) && bundled.includes(dependency)));
50
+ if (missing.length)
51
+ return { problem: `has dependencies it doesn't bring (${missing.join(", ")}): the kit installs none, so a package must bundle them, or list them in bundleDependencies` };
52
+ const scripts = ["preinstall", "install", "postinstall"].filter((script) => json.scripts?.[script]);
53
+ if (scripts.length)
54
+ return { problem: `has install scripts (${scripts.join(", ")}), which the kit never runs` };
55
+ if ((await stat(path.join(dir, "binding.gyp")).catch(() => null))?.isFile())
56
+ return { problem: "builds native code when installed (binding.gyp), which the kit never does" };
57
+ const bins = typeof json.bin === "string" ? { [name.split("/").pop()]: json.bin } : (json.bin ?? {});
58
+ const entries = Object.values(bins);
59
+ const bin = entries.length === 1 ? entries[0] : bins[name.split("/").pop()];
60
+ if (!bin)
61
+ return { problem: entries.length ? `has several programs (bin), none named ${name.split("/").pop()}` : "has no program to run (bin in its package.json)" };
62
+ const script = path.resolve(dir, bin);
63
+ if (path.relative(dir, script).startsWith(".."))
64
+ return { problem: `runs a program outside itself (${bin})` };
65
+ if (!(await stat(script).catch(() => null))?.isFile())
66
+ return { problem: `has no file at ${bin}, its program` };
67
+ return { bin: path.relative(dir, script).split(path.sep).join("/") };
68
+ }
69
+ /** What the cache keeps beside each package: what was checked when it was fetched. */
70
+ const recordOf = (dir) => `${dir}.json`;
71
+ /**
72
+ * A package in the cache, fetched when it isn't there (or its files changed since): its
73
+ * integrity checked, refused when it can't run as the kit installs it. Never runs anything.
74
+ */
75
+ export async function cachePackage(cache, name, version) {
76
+ const dir = cachedPackageDir(cache, name, version);
77
+ const record = await readFile(recordOf(dir), "utf8").then((text) => JSON.parse(text), () => undefined);
78
+ if (record && (await stat(dir).catch(() => null))?.isDirectory() && (await hashExtension(dir)) === record.sha256)
79
+ return record;
80
+ const fetched = await fetchNpmPlugin(name, version);
81
+ try {
82
+ const bin = await packageBin(fetched.dir, name);
83
+ if ("problem" in bin)
84
+ throw new Error(`${name}@${version} can't run in the kit: it ${bin.problem}.`);
85
+ // Into its place whole: a session never sees half a package.
86
+ await rm(dir, { recursive: true, force: true });
87
+ await mkdir(path.dirname(dir), { recursive: true });
88
+ // Another disk than the temporary folder's: copied instead.
89
+ await rename(fetched.dir, dir).catch(() => cp(fetched.dir, dir, { recursive: true }));
90
+ const cached = { integrity: fetched.integrity, sha256: await hashExtension(dir), bin: bin.bin };
91
+ await writeFile(recordOf(dir), `${JSON.stringify(cached, null, 2)}\n`, "utf8");
92
+ return cached;
93
+ }
94
+ finally {
95
+ await rm(fetched.dir, { recursive: true, force: true });
96
+ }
97
+ }
98
+ /** Why a package an extension's lock names can't run from the cache now (missing, or changed), or nothing. */
99
+ export async function cachedPackageProblem(cache, server, locked) {
100
+ const spec = packageSpec(server);
101
+ if (!cache || !locked)
102
+ return `runs ${spec}, which wasn't fetched when it was installed: install it again`;
103
+ const dir = cachedPackageDir(cache, server.name, server.version);
104
+ if (!(await stat(dir).catch(() => null))?.isDirectory())
105
+ return `runs ${spec}, which isn't in the agent's package cache any more: install it again`;
106
+ if ((await hashExtension(dir)) !== locked.sha256)
107
+ return `runs ${spec}, whose files in the cache changed since it was installed: install it again to trust them`;
108
+ return undefined;
109
+ }
110
+ /** Whether every package a lock names is in the cache as it was fetched (`<package>@<version>` keys). */
111
+ export async function cachedPackagesIntact(cache, locked) {
112
+ for (const [spec, cached] of Object.entries(locked ?? {})) {
113
+ const at = spec.lastIndexOf("@");
114
+ if (!cache || at <= 0)
115
+ return false;
116
+ const dir = cachedPackageDir(cache, spec.slice(0, at), spec.slice(at + 1));
117
+ if (!(await stat(dir).catch(() => null))?.isDirectory() || (await hashExtension(dir)) !== cached.sha256)
118
+ return false;
119
+ }
120
+ return true;
121
+ }
@@ -2,10 +2,11 @@
2
2
  export declare function untar(tar: Uint8Array): Map<string, Uint8Array>;
3
3
  /** The highest of `versions` that `wanted` asks for: an exact version, or a range (`^`, `~`, comparators, `*`). */
4
4
  export declare function pickVersion(versions: readonly string[], wanted: string): string | undefined;
5
- /** An npm package into a new temporary folder: `{ dir, version }`. */
5
+ /** An npm package into a new temporary folder: `{ dir, version, integrity }` (the registry's `sha512-…`, checked). */
6
6
  export declare function fetchNpmPlugin(name: string, wanted?: string, registry?: string): Promise<{
7
7
  dir: string;
8
8
  version: string;
9
+ integrity: string;
9
10
  }>;
10
11
  /** A zip archive into a new temporary folder; the plugin is its root, or its only folder. */
11
12
  export declare function fetchArchivePlugin(url: string, sha256?: string): Promise<{
@@ -89,7 +89,7 @@ export function pickVersion(versions, wanted) {
89
89
  return 0;
90
90
  }).at(-1);
91
91
  }
92
- /** An npm package into a new temporary folder: `{ dir, version }`. */
92
+ /** An npm package into a new temporary folder: `{ dir, version, integrity }` (the registry's `sha512-…`, checked). */
93
93
  export async function fetchNpmPlugin(name, wanted = "latest", registry = "https://registry.npmjs.org") {
94
94
  if (!/^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i.test(name))
95
95
  throw new Error(`"${name}" isn't an npm package name.`);
@@ -113,7 +113,7 @@ export async function fetchNpmPlugin(name, wanted = "latest", registry = "https:
113
113
  // npm puts a package's files under package/.
114
114
  const files = [...untar(gunzipSync(tarball))].flatMap(([file, data]) => (file.startsWith("package/") ? [[file.slice("package/".length), data]] : []));
115
115
  await writeFiles(dir, files);
116
- return { dir, version };
116
+ return { dir, version, integrity: dist.integrity };
117
117
  }
118
118
  catch (error) {
119
119
  await rm(dir, { recursive: true, force: true });
@@ -0,0 +1,13 @@
1
+ import type { McpServerStatus } from "@anthropic-ai/claude-agent-sdk";
2
+ /**
3
+ * Plan mode's read-only tools from the MCP servers themselves (#53): for an installed extension
4
+ * whose manifest lists no `readOnlyTools`, a tool its server marks `readOnlyHint` only reads.
5
+ *
6
+ * Asked when plan mode meets the call, not once at start: a server is `pending` for seconds after
7
+ * the session starts, and one turned off and on again (liveExtensions.ts) may come back with
8
+ * other tools. The SDK's `mcpServerStatus()` gives each connected server's tools with
9
+ * `annotations.readOnly`, set exactly when the server says `readOnlyHint: true` (confirmed
10
+ * empirically: `readOnlyHint: false` and no hint both leave it out; the names are the short ones).
11
+ * Anything else — not connected, not asked in time, no hint — is no: plan mode denies.
12
+ */
13
+ export declare function createReadOnlyHints(servers: readonly string[], status: () => (() => Promise<McpServerStatus[]>) | undefined): (toolName: string) => Promise<boolean>;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Plan mode's read-only tools from the MCP servers themselves (#53): for an installed extension
3
+ * whose manifest lists no `readOnlyTools`, a tool its server marks `readOnlyHint` only reads.
4
+ *
5
+ * Asked when plan mode meets the call, not once at start: a server is `pending` for seconds after
6
+ * the session starts, and one turned off and on again (liveExtensions.ts) may come back with
7
+ * other tools. The SDK's `mcpServerStatus()` gives each connected server's tools with
8
+ * `annotations.readOnly`, set exactly when the server says `readOnlyHint: true` (confirmed
9
+ * empirically: `readOnlyHint: false` and no hint both leave it out; the names are the short ones).
10
+ * Anything else — not connected, not asked in time, no hint — is no: plan mode denies.
11
+ */
12
+ export function createReadOnlyHints(servers, status) {
13
+ return async (toolName) => {
14
+ const server = servers.find((name) => toolName.startsWith(`mcp__${name}__`));
15
+ const read = status();
16
+ if (!server || !read)
17
+ return false;
18
+ const tool = toolName.slice(`mcp__${server}__`.length);
19
+ const statuses = await within(read(), []);
20
+ const found = statuses.find((entry) => entry.name === server && entry.status === "connected");
21
+ return found?.tools?.some((entry) => entry.name === tool && entry.annotations?.readOnly === true) ?? false;
22
+ };
23
+ }
24
+ // A call to the session made from a hook, before a tool runs: never let it hold the tool up.
25
+ const TIMEOUT_MS = 3000;
26
+ const within = (promise, fallback) => Promise.race([promise.catch(() => fallback), new Promise((resolve) => setTimeout(() => resolve(fallback), TIMEOUT_MS).unref())]);
@@ -36,6 +36,8 @@ export function runQuery(prompt, options) {
36
36
  contextUsage: () => contextUsage(),
37
37
  toggleMcpServer: (server, enabled) => result.toggleMcpServer(server, enabled),
38
38
  reloadPlugins: () => result.reloadPlugins(),
39
+ reconnectMcpServer: (server) => result.reconnectMcpServer(server),
40
+ mcpServerStatus: () => result.mcpServerStatus(),
39
41
  });
40
42
  // The main agent's tool calls by id, to name the results that come back for them.
41
43
  const toolNames = new Map();