@falkenslab/agent-kit 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/extension-launcher.mjs +28 -5
- package/dist/chat/chatController.d.ts +59 -0
- package/dist/chat/chatController.js +209 -2
- package/dist/chat/extensions.d.ts +30 -2
- package/dist/chat/extensions.js +62 -3
- package/dist/chat/runs.d.ts +3 -1
- package/dist/chat/runs.js +1 -1
- package/dist/core/agentSpec.d.ts +11 -0
- package/dist/core/elicitation.d.ts +54 -0
- package/dist/core/elicitation.js +120 -0
- package/dist/core/extensionCatalog.d.ts +94 -0
- package/dist/core/extensionCatalog.js +228 -0
- package/dist/core/extensionOptions.d.ts +56 -0
- package/dist/core/extensionOptions.js +139 -0
- package/dist/core/extensions.d.ts +7 -1
- package/dist/core/extensions.js +2 -1
- package/dist/core/externalExtensions.d.ts +83 -8
- package/dist/core/externalExtensions.js +233 -30
- package/dist/core/hooks/planGate.d.ts +2 -0
- package/dist/core/hooks/planGate.js +3 -0
- package/dist/core/interaction.d.ts +7 -0
- package/dist/core/liveExtensions.d.ts +11 -0
- package/dist/core/liveExtensions.js +22 -0
- package/dist/core/marketplaces.d.ts +6 -2
- package/dist/core/marketplaces.js +6 -4
- package/dist/core/messages/de.js +33 -1
- package/dist/core/messages/en.d.ts +36 -0
- package/dist/core/messages/en.js +33 -1
- package/dist/core/messages/es.js +33 -1
- package/dist/core/messages/fr.js +33 -1
- package/dist/core/npxPackages.d.ts +48 -0
- package/dist/core/npxPackages.js +121 -0
- package/dist/core/pluginDownloads.d.ts +2 -1
- package/dist/core/pluginDownloads.js +2 -2
- package/dist/core/readOnlyHints.d.ts +13 -0
- package/dist/core/readOnlyHints.js +26 -0
- package/dist/core/runner.js +2 -0
- package/dist/core/session.d.ts +13 -1
- package/dist/core/session.js +31 -7
- package/dist/core/sessionFacts.d.ts +5 -1
- package/dist/core/workspaceTrust.d.ts +12 -0
- package/dist/core/workspaceTrust.js +60 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -1
- package/dist/tui/extensionCommand.d.ts +10 -2
- package/dist/tui/extensionCommand.js +182 -33
- package/extensions/awareness/.claude-plugin/plugin.json +13 -5
- package/extensions/knowledge/.claude-plugin/plugin.json +13 -5
- package/extensions/memory/.claude-plugin/plugin.json +13 -5
- package/extensions/sources/.claude-plugin/plugin.json +13 -5
- package/package.json +7 -5
- package/schemas/agent-kit-metadata.schema.json +62 -0
- package/schemas/marketplace-catalog.schema.json +64 -0
|
@@ -44,6 +44,42 @@ export interface Messages {
|
|
|
44
44
|
extensionNotInstalled(name: string): string;
|
|
45
45
|
extensionsUsage: string;
|
|
46
46
|
extensionsCantReopen: string;
|
|
47
|
+
/** An installed extension's options in the chat (#55). */
|
|
48
|
+
extensionOptions(name: string): string;
|
|
49
|
+
extensionNoOptions(name: string): string;
|
|
50
|
+
optionNotSet: string;
|
|
51
|
+
optionSecretSet: string;
|
|
52
|
+
optionDefault: string;
|
|
53
|
+
optionRequired: string;
|
|
54
|
+
optionSensitiveInChat(key: string, name: string): string;
|
|
55
|
+
extensionOptionsSaved(name: string): string;
|
|
56
|
+
extensionRestarted(name: string): string;
|
|
57
|
+
extensionReconfiguredReopen(name: string): string;
|
|
58
|
+
/** The catalog in the chat (#56). */
|
|
59
|
+
extensionsNoMatch: string;
|
|
60
|
+
extensionIsInstalled: string;
|
|
61
|
+
extensionUpdateAvailable(installed: string): string;
|
|
62
|
+
extensionInstalled(name: string): string;
|
|
63
|
+
extensionUpdated(name: string, from: string, to: string): string;
|
|
64
|
+
extensionsUpToDate: string;
|
|
65
|
+
extensionRemoved(name: string, withData: boolean): string;
|
|
66
|
+
extensionConfirm(spec: string, marketplace: string): string;
|
|
67
|
+
extensionsBusy: string;
|
|
68
|
+
extensionsReloaded: string;
|
|
69
|
+
/** MCP elicitation (#58): a server asking the person for something. */
|
|
70
|
+
elicitTitle(server: string): string;
|
|
71
|
+
elicitQuestion: string;
|
|
72
|
+
elicitAgain: string;
|
|
73
|
+
elicitNotANumber(text: string): string;
|
|
74
|
+
elicitOutOfRange(minimum?: number, maximum?: number): string;
|
|
75
|
+
elicitUrlTitle(server: string): string;
|
|
76
|
+
elicitUrlQuestion: string;
|
|
77
|
+
/** A workspace's extensions to trust (#57). */
|
|
78
|
+
trustTitle: string;
|
|
79
|
+
trustBody: string;
|
|
80
|
+
trustQuestion: string;
|
|
81
|
+
trustDeclined: string;
|
|
82
|
+
trustGiven(names: string): string;
|
|
47
83
|
currentRun: string;
|
|
48
84
|
resumed(date: string): string;
|
|
49
85
|
suggestionKey: string;
|
package/dist/core/messages/en.js
CHANGED
|
@@ -31,8 +31,40 @@ export const en = {
|
|
|
31
31
|
extensionSwitched: (name, enabled) => `(${name} ${enabled ? "enabled" : "disabled"}, in this same session)`,
|
|
32
32
|
extensionToggled: (name, enabled) => `(${name} ${enabled ? "enabled" : "disabled"}: the session reopens, with the same conversation)`,
|
|
33
33
|
extensionNotInstalled: (name) => `(${name} isn't installed: /extensions lists them)`,
|
|
34
|
-
extensionsUsage: "(usage: /extensions,
|
|
34
|
+
extensionsUsage: "(usage: /extensions, enable|disable <name>, config <name> [key=value…], search [words], install <name>, update [name], remove <name> [--delete-data], reload, trust)",
|
|
35
35
|
extensionsCantReopen: "(this chat can't reopen its session: enable or disable it from the terminal, and start again)",
|
|
36
|
+
extensionOptions: (name) => `${name}'s options:`,
|
|
37
|
+
extensionNoOptions: (name) => `(${name} has no options)`,
|
|
38
|
+
optionNotSet: "(not set)",
|
|
39
|
+
optionSecretSet: "(set, hidden)",
|
|
40
|
+
optionDefault: "(default)",
|
|
41
|
+
optionRequired: "required",
|
|
42
|
+
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})`,
|
|
43
|
+
extensionOptionsSaved: (name) => `(${name}'s options saved: they apply when its server starts again)`,
|
|
44
|
+
extensionRestarted: (name) => `(${name}'s options saved, and its server restarted with them, in this same session)`,
|
|
45
|
+
extensionReconfiguredReopen: (name) => `(${name}'s options saved: the session reopens, with the same conversation)`,
|
|
46
|
+
extensionsNoMatch: "(no extension matches: /extensions search with other words, or none to list them all)",
|
|
47
|
+
extensionIsInstalled: "(installed)",
|
|
48
|
+
extensionUpdateAvailable: (installed) => `(installed: ${installed}, update available)`,
|
|
49
|
+
extensionInstalled: (name) => `(${name} installed: it's there in a new conversation, or now with /extensions reload, keeping this one)`,
|
|
50
|
+
extensionUpdated: (name, from, to) => `(${name} updated, ${from} → ${to}: it's there in a new conversation, or now with /extensions reload)`,
|
|
51
|
+
extensionsUpToDate: "(every extension from a marketplace is up to date)",
|
|
52
|
+
extensionRemoved: (name, withData) => `(${name} removed${withData ? ", with its data" : "; its data is kept for a reinstall"})`,
|
|
53
|
+
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)`,
|
|
54
|
+
extensionsBusy: "(after this turn: the agent is working)",
|
|
55
|
+
extensionsReloaded: "(the session reopens with the extensions installed now, keeping the conversation)",
|
|
56
|
+
elicitTitle: (server) => `${server} asks you`,
|
|
57
|
+
elicitQuestion: "Answer it? (Enter/y: answer, n: decline, q: cancel) ",
|
|
58
|
+
elicitAgain: "That isn't a valid value: try again, or leave it empty.",
|
|
59
|
+
elicitNotANumber: (text) => `"${text}" isn't a number`,
|
|
60
|
+
elicitOutOfRange: (minimum, maximum) => `between ${minimum ?? "…"} and ${maximum ?? "…"}`,
|
|
61
|
+
elicitUrlTitle: (server) => `${server} asks you to open a page`,
|
|
62
|
+
elicitUrlQuestion: "Open it yourself, then Enter when you're done (n: decline) ",
|
|
63
|
+
trustTitle: "This workspace brings extensions",
|
|
64
|
+
trustBody: "They run code on this computer with your permissions. Trust them only if you trust where this workspace came from:",
|
|
65
|
+
trustQuestion: "Trust them and run them? (y/N) ",
|
|
66
|
+
trustDeclined: "(they stay off: /extensions trust trusts them later)",
|
|
67
|
+
trustGiven: (names) => `(${names}: trusted in this workspace, as they are now; another version asks again)`,
|
|
36
68
|
currentRun: "(this one)",
|
|
37
69
|
resumed: (date) => `(resumed the conversation of ${date})`,
|
|
38
70
|
suggestionKey: "tab",
|
package/dist/core/messages/es.js
CHANGED
|
@@ -32,8 +32,40 @@ export const es = {
|
|
|
32
32
|
extensionSwitched: (name, enabled) => `(${name} ${enabled ? "activada" : "desactivada"}, en esta misma sesión)`,
|
|
33
33
|
extensionToggled: (name, enabled) => `(${name} ${enabled ? "activada" : "desactivada"}: la sesión se reabre, con la misma conversación)`,
|
|
34
34
|
extensionNotInstalled: (name) => `(${name} no está instalada: /extensions las lista)`,
|
|
35
|
-
extensionsUsage: "(uso: /extensions,
|
|
35
|
+
extensionsUsage: "(uso: /extensions, enable|disable <nombre>, config <nombre> [clave=valor…], search [palabras], install <nombre>, update [nombre], remove <nombre> [--delete-data], reload, trust)",
|
|
36
36
|
extensionsCantReopen: "(este chat no puede reabrir su sesión: actívala o desactívala desde la terminal y vuelve a empezar)",
|
|
37
|
+
extensionOptions: (name) => `Opciones de ${name}:`,
|
|
38
|
+
extensionNoOptions: (name) => `(${name} no tiene opciones)`,
|
|
39
|
+
optionNotSet: "(sin valor)",
|
|
40
|
+
optionSecretSet: "(con valor, oculto)",
|
|
41
|
+
optionDefault: "(por defecto)",
|
|
42
|
+
optionRequired: "obligatoria",
|
|
43
|
+
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})`,
|
|
44
|
+
extensionOptionsSaved: (name) => `(opciones de ${name} guardadas: se aplican cuando su servidor vuelva a arrancar)`,
|
|
45
|
+
extensionRestarted: (name) => `(opciones de ${name} guardadas, y su servidor reiniciado con ellas, en esta misma sesión)`,
|
|
46
|
+
extensionReconfiguredReopen: (name) => `(opciones de ${name} guardadas: la sesión se reabre, con la misma conversación)`,
|
|
47
|
+
extensionsNoMatch: "(ninguna extensión coincide: /extensions search con otras palabras, o sin ninguna para verlas todas)",
|
|
48
|
+
extensionIsInstalled: "(instalada)",
|
|
49
|
+
extensionUpdateAvailable: (installed) => `(instalada: ${installed}, hay actualización)`,
|
|
50
|
+
extensionInstalled: (name) => `(${name} instalada: está en una conversación nueva, o ya con /extensions reload, sin perder esta)`,
|
|
51
|
+
extensionUpdated: (name, from, to) => `(${name} actualizada, ${from} → ${to}: está en una conversación nueva, o ya con /extensions reload)`,
|
|
52
|
+
extensionsUpToDate: "(todas las extensiones de un marketplace están al día)",
|
|
53
|
+
extensionRemoved: (name, withData) => `(${name} desinstalada${withData ? ", con sus datos" : "; sus datos se guardan por si vuelve"})`,
|
|
54
|
+
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)`,
|
|
55
|
+
extensionsBusy: "(después de este turno: el agente está trabajando)",
|
|
56
|
+
extensionsReloaded: "(la sesión se reabre con las extensiones instaladas ahora, con la misma conversación)",
|
|
57
|
+
elicitTitle: (server) => `${server} te pregunta`,
|
|
58
|
+
elicitQuestion: "¿Responder? (Enter/y: responder, n: rechazar, q: cancelar) ",
|
|
59
|
+
elicitAgain: "Ese valor no vale: prueba otra vez, o déjalo vacío.",
|
|
60
|
+
elicitNotANumber: (text) => `"${text}" no es un número`,
|
|
61
|
+
elicitOutOfRange: (minimum, maximum) => `entre ${minimum ?? "…"} y ${maximum ?? "…"}`,
|
|
62
|
+
elicitUrlTitle: (server) => `${server} te pide abrir una página`,
|
|
63
|
+
elicitUrlQuestion: "Ábrela tú, y Enter cuando termines (n: rechazar) ",
|
|
64
|
+
trustTitle: "Este workspace trae extensiones",
|
|
65
|
+
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:",
|
|
66
|
+
trustQuestion: "¿Confiar en ellas y ejecutarlas? (y/N) ",
|
|
67
|
+
trustDeclined: "(siguen apagadas: /extensions trust confía en ellas más tarde)",
|
|
68
|
+
trustGiven: (names) => `(${names}: de confianza en este workspace, tal como están; otra versión vuelve a preguntar)`,
|
|
37
69
|
currentRun: "(esta)",
|
|
38
70
|
resumed: (date) => `(retomada la conversación del ${date})`,
|
|
39
71
|
suggestionKey: "tab",
|
package/dist/core/messages/fr.js
CHANGED
|
@@ -32,8 +32,40 @@ export const fr = {
|
|
|
32
32
|
extensionSwitched: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"}, dans cette même session)`,
|
|
33
33
|
extensionToggled: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"} : la session se rouvre, avec la même conversation)`,
|
|
34
34
|
extensionNotInstalled: (name) => `(${name} n'est pas installée : /extensions les liste)`,
|
|
35
|
-
extensionsUsage: "(usage : /extensions,
|
|
35
|
+
extensionsUsage: "(usage : /extensions, enable|disable <nom>, config <nom> [clé=valeur…], search [mots], install <nom>, update [nom], remove <nom> [--delete-data], reload, trust)",
|
|
36
36
|
extensionsCantReopen: "(ce chat ne peut pas rouvrir sa session : activez-la ou désactivez-la depuis le terminal, puis recommencez)",
|
|
37
|
+
extensionOptions: (name) => `Options de ${name} :`,
|
|
38
|
+
extensionNoOptions: (name) => `(${name} n'a pas d'options)`,
|
|
39
|
+
optionNotSet: "(sans valeur)",
|
|
40
|
+
optionSecretSet: "(définie, cachée)",
|
|
41
|
+
optionDefault: "(par défaut)",
|
|
42
|
+
optionRequired: "obligatoire",
|
|
43
|
+
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})`,
|
|
44
|
+
extensionOptionsSaved: (name) => `(options de ${name} enregistrées : elles s'appliquent quand son serveur redémarre)`,
|
|
45
|
+
extensionRestarted: (name) => `(options de ${name} enregistrées, et son serveur redémarré avec elles, dans cette même session)`,
|
|
46
|
+
extensionReconfiguredReopen: (name) => `(options de ${name} enregistrées : la session se rouvre, avec la même conversation)`,
|
|
47
|
+
extensionsNoMatch: "(aucune extension ne correspond : /extensions search avec d'autres mots, ou sans pour toutes les voir)",
|
|
48
|
+
extensionIsInstalled: "(installée)",
|
|
49
|
+
extensionUpdateAvailable: (installed) => `(installée : ${installed}, mise à jour disponible)`,
|
|
50
|
+
extensionInstalled: (name) => `(${name} installée : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload, en gardant celle-ci)`,
|
|
51
|
+
extensionUpdated: (name, from, to) => `(${name} mise à jour, ${from} → ${to} : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload)`,
|
|
52
|
+
extensionsUpToDate: "(toutes les extensions d'un marketplace sont à jour)",
|
|
53
|
+
extensionRemoved: (name, withData) => `(${name} désinstallée${withData ? ", avec ses données" : " ; ses données sont gardées pour une réinstallation"})`,
|
|
54
|
+
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)`,
|
|
55
|
+
extensionsBusy: "(après ce tour : l'agent travaille)",
|
|
56
|
+
extensionsReloaded: "(la session se rouvre avec les extensions installées maintenant, en gardant la conversation)",
|
|
57
|
+
elicitTitle: (server) => `${server} vous demande`,
|
|
58
|
+
elicitQuestion: "Répondre ? (Entrée/y : répondre, n : refuser, q : annuler) ",
|
|
59
|
+
elicitAgain: "Cette valeur n'est pas valide : réessayez, ou laissez vide.",
|
|
60
|
+
elicitNotANumber: (text) => `« ${text} » n'est pas un nombre`,
|
|
61
|
+
elicitOutOfRange: (minimum, maximum) => `entre ${minimum ?? "…"} et ${maximum ?? "…"}`,
|
|
62
|
+
elicitUrlTitle: (server) => `${server} vous demande d'ouvrir une page`,
|
|
63
|
+
elicitUrlQuestion: "Ouvrez-la vous-même, puis Entrée quand c'est fait (n : refuser) ",
|
|
64
|
+
trustTitle: "Cet espace de travail apporte des extensions",
|
|
65
|
+
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 :",
|
|
66
|
+
trustQuestion: "Leur faire confiance et les lancer ? (y/N) ",
|
|
67
|
+
trustDeclined: "(elles restent éteintes : /extensions trust leur fait confiance plus tard)",
|
|
68
|
+
trustGiven: (names) => `(${names} : de confiance dans cet espace de travail, telles qu'elles sont ; une autre version redemande)`,
|
|
37
69
|
currentRun: "(celle-ci)",
|
|
38
70
|
resumed: (date) => `(conversation du ${date} reprise)`,
|
|
39
71
|
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())]);
|
package/dist/core/runner.js
CHANGED
|
@@ -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();
|
package/dist/core/session.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { ToolLabels } from "./toolLabels.js";
|
|
|
6
6
|
import type { AgentSpec, BaseSessionConfig } from "./agentSpec.js";
|
|
7
7
|
import { type Language } from "./language.js";
|
|
8
8
|
import { type RunFolder } from "./runs.js";
|
|
9
|
-
import { type ExtensionSwitch } from "./liveExtensions.js";
|
|
9
|
+
import { type ExtensionRestart, type ExtensionSwitch } from "./liveExtensions.js";
|
|
10
10
|
/**
|
|
11
11
|
* Builds the `options` object passed to the Agent SDK's `query()` — everything about
|
|
12
12
|
* *how* the agent runs a session (system prompt, tools, MCP servers, hooks), independent
|
|
@@ -44,11 +44,23 @@ export declare function buildSessionOptions<TConfig extends BaseSessionConfig>(c
|
|
|
44
44
|
apis: Record<string, Record<string, unknown>>;
|
|
45
45
|
/** Turns an installed extension off or on in the running session; false when that takes opening it again (#49). */
|
|
46
46
|
switchExtension: ExtensionSwitch;
|
|
47
|
+
/** Restarts an installed extension's servers in the running session, with its options as they are now (#55). */
|
|
48
|
+
restartExtension: ExtensionRestart;
|
|
47
49
|
}>;
|
|
48
50
|
export { createModeControl, togglePlanMode, type ModeControl } from "./modeControl.js";
|
|
49
51
|
/** The extensions of a session: where they're installed, what's installed, what runs and what can't (with why). */
|
|
50
52
|
export interface ExtensionsStatus {
|
|
51
53
|
dirs: ExtensionDirs;
|
|
54
|
+
/** The agent's own marketplace (`officialMarketplace`), for the chats' catalog. */
|
|
55
|
+
official?: string;
|
|
56
|
+
/** The topics the chats' catalog shows (`catalogTopics`); none, every extension. */
|
|
57
|
+
catalogTopics?: string[];
|
|
58
|
+
/** The workspace's extensions that don't run until the person trusts them there (#57): the chats ask. */
|
|
59
|
+
untrusted: {
|
|
60
|
+
name: string;
|
|
61
|
+
version?: string;
|
|
62
|
+
source: string;
|
|
63
|
+
}[];
|
|
52
64
|
installed: InstalledExtension[];
|
|
53
65
|
/** The names of the extensions this session runs with, the kit's and the agent's too. */
|
|
54
66
|
active: string[];
|
package/dist/core/session.js
CHANGED
|
@@ -17,12 +17,15 @@ import { extensionsPromptSection, resolveExtensions, skillsMissingCapabilities }
|
|
|
17
17
|
import { allowAnyMcpTool } from "./mcpPermissions.js";
|
|
18
18
|
import { pluginAgents } from "./pluginAgents.js";
|
|
19
19
|
import { outsideArchive, unpackedClaudeExecutable } from "./packaged.js";
|
|
20
|
-
import { listInstalled, loadExternalExtensions } from "./externalExtensions.js";
|
|
20
|
+
import { listInstalled, loadExternalExtensions, scopePrivateDir } from "./externalExtensions.js";
|
|
21
21
|
import { replyLanguageInstruction } from "./language.js";
|
|
22
22
|
import { chooseLanguage } from "./messages/index.js";
|
|
23
23
|
import { createRunStore } from "./runs.js";
|
|
24
24
|
import { createSessionView, registerSessionView } from "./sessionFacts.js";
|
|
25
|
-
import { createExtensionSwitch, mountPlugin } from "./liveExtensions.js";
|
|
25
|
+
import { createExtensionRestart, createExtensionSwitch, mountPlugin } from "./liveExtensions.js";
|
|
26
|
+
import { secretsDir } from "./extensionOptions.js";
|
|
27
|
+
import { createElicitationHandler } from "./elicitation.js";
|
|
28
|
+
import { createReadOnlyHints } from "./readOnlyHints.js";
|
|
26
29
|
import { agentKitVersion } from "./version.js";
|
|
27
30
|
/**
|
|
28
31
|
* Builds the `options` object passed to the Agent SDK's `query()` — everything about
|
|
@@ -170,8 +173,18 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
170
173
|
const allowedSubagentTypes = [...new Set([...(subagents?.allowedSubagentTypes ?? []), ...Object.keys(fromPlugins)])];
|
|
171
174
|
// The extensions' read-only tools, for the plan gate: a list the live switch changes (liveExtensions.ts).
|
|
172
175
|
const readOnlyTools = fromExtensions((contribution) => contribution.readOnlyTools);
|
|
176
|
+
// Those whose manifest lists none: their servers say, asked while the session runs (#53).
|
|
177
|
+
const readOnlyHintServers = fromExtensions((contribution) => contribution.readOnlyHintServers);
|
|
173
178
|
// What no file tool may touch: the agent's denied paths and the SDK's own credentials.
|
|
174
|
-
|
|
179
|
+
// And the installed extensions' sensitive option values (#55), in each scope's .secrets/.
|
|
180
|
+
const deniedPaths = [
|
|
181
|
+
...(config.deniedPaths ?? []),
|
|
182
|
+
path.join(claudeConfigDir(), ".credentials.json"),
|
|
183
|
+
...["agent", "project"].flatMap((scope) => {
|
|
184
|
+
const privateDir = config.extensionDirs ? scopePrivateDir(config.extensionDirs, scope) : undefined;
|
|
185
|
+
return privateDir ? [secretsDir(privateDir)] : [];
|
|
186
|
+
}),
|
|
187
|
+
];
|
|
175
188
|
const transcriptPath = path.join(runDir, "transcript.jsonl");
|
|
176
189
|
const transcriptLogger = createTranscriptLogger(transcriptPath, config.secrets ?? []);
|
|
177
190
|
const promptSections = [
|
|
@@ -269,6 +282,9 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
269
282
|
// The date and date arithmetic, in every session and mode: only reads (tools/time.ts).
|
|
270
283
|
time: createTimeServer(config.timeZone),
|
|
271
284
|
},
|
|
285
|
+
// A server asking the person for something mid-tool (MCP elicitation, #58): through the
|
|
286
|
+
// interaction port, never the model; declined when no one can be asked (autonomous).
|
|
287
|
+
onElicitation: createElicitationHandler(runDir, () => modeControl.mode !== "autonomous"),
|
|
272
288
|
hooks: {
|
|
273
289
|
PreToolUse: [
|
|
274
290
|
{ hooks: [transcriptLogger.preToolUse] },
|
|
@@ -303,6 +319,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
303
319
|
createPlanGate({
|
|
304
320
|
projectDir: config.projectDir,
|
|
305
321
|
readOnlyTools,
|
|
322
|
+
...(readOnlyHintServers.length ? { isReadOnlyHinted: createReadOnlyHints(readOnlyHintServers, () => late.view?.controls().mcpServerStatus) } : {}),
|
|
306
323
|
...(spec.planMode?.isPlanFile ? { isPlanFile: (filePath) => spec.planMode.isPlanFile(filePath, config) } : {}),
|
|
307
324
|
...(spec.planMode?.isReadOnlyTool ? { isReadOnlyTool: spec.planMode.isReadOnlyTool } : {}),
|
|
308
325
|
}, () => modeControl.mode === "plan"),
|
|
@@ -330,6 +347,9 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
330
347
|
const toolLabels = Object.assign({}, ...added.map((contribution) => contribution.toolLabels ?? {}));
|
|
331
348
|
const extensions = {
|
|
332
349
|
dirs: extensionDirs,
|
|
350
|
+
...(config.officialMarketplace ? { official: config.officialMarketplace } : {}),
|
|
351
|
+
...(config.catalogTopics?.length ? { catalogTopics: config.catalogTopics } : {}),
|
|
352
|
+
untrusted: external.untrusted.map((installed) => ({ name: installed.name, ...(installed.manifest?.version ? { version: installed.manifest.version } : {}), source: installed.lock.marketplace ?? installed.lock.source })),
|
|
333
353
|
installed: await listInstalled(extensionDirs),
|
|
334
354
|
active: resolved.active.map(({ extension }) => extension.name),
|
|
335
355
|
about: Object.fromEntries(resolved.active.map(({ extension, manifest }) => [extension.name, { description: manifest.description, provides: manifest.provides }])),
|
|
@@ -357,8 +377,9 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
357
377
|
};
|
|
358
378
|
late.view = createSessionView(viewBase, modeControl);
|
|
359
379
|
registerSessionView(sdkOptions, late.view);
|
|
360
|
-
// Turning an installed extension off and on while the session runs (#49)
|
|
361
|
-
|
|
380
|
+
// Turning an installed extension off and on while the session runs (#49), and restarting its
|
|
381
|
+
// servers when its options change (#55).
|
|
382
|
+
const liveExtensions = resolved.active.flatMap(({ extension, manifest }, index) => {
|
|
362
383
|
const installed = installedDirs.get(extension.name);
|
|
363
384
|
if (!installed)
|
|
364
385
|
return [];
|
|
@@ -378,9 +399,12 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
|
|
|
378
399
|
help: contribution?.helpLines ?? [],
|
|
379
400
|
},
|
|
380
401
|
];
|
|
381
|
-
})
|
|
402
|
+
});
|
|
403
|
+
const liveTargets = { readOnlyTools, allowedSubagentTypes, toolLabels, status: extensions, facts: viewBase, controls: () => late.view?.controls() ?? {} };
|
|
404
|
+
const switchExtension = createExtensionSwitch(liveExtensions, liveTargets);
|
|
405
|
+
const restartExtension = createExtensionRestart(liveExtensions, liveTargets);
|
|
382
406
|
const apis = Object.fromEntries(resolved.active.flatMap(({ extension }, index) => (added[index]?.api ? [[extension.name, added[index].api]] : [])));
|
|
383
|
-
return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language, toolLabels, extensions, apis, switchExtension };
|
|
407
|
+
return { options: sdkOptions, transcriptLogger, transcriptPath, modeControl, language, toolLabels, extensions, apis, switchExtension, restartExtension };
|
|
384
408
|
}
|
|
385
409
|
export { createModeControl, togglePlanMode } from "./modeControl.js";
|
|
386
410
|
/** The Claude Code configuration folder the SDK's CLI uses: `CLAUDE_CONFIG_DIR`, or `~/.claude`. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Options, SlashCommand } from "@anthropic-ai/claude-agent-sdk";
|
|
1
|
+
import type { McpServerStatus, Options, SlashCommand } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import type { AgentIdentity, Mode } from "./agentSpec.js";
|
|
3
3
|
import type { Language } from "./language.js";
|
|
4
4
|
import type { ModeControl } from "./modeControl.js";
|
|
@@ -61,6 +61,10 @@ export interface SessionLive {
|
|
|
61
61
|
/** The running session's controls an installed extension is turned off and on with (liveExtensions.ts). */
|
|
62
62
|
toggleMcpServer?: (server: string, enabled: boolean) => Promise<void>;
|
|
63
63
|
reloadPlugins?: () => Promise<unknown>;
|
|
64
|
+
/** Restarts one of its MCP servers: an installed extension's, when its options change (#55). */
|
|
65
|
+
reconnectMcpServer?: (server: string) => Promise<void>;
|
|
66
|
+
/** Its MCP servers as they are now: their tools and what each says of itself (plan mode's read-only hints). */
|
|
67
|
+
mcpServerStatus?: () => Promise<McpServerStatus[]>;
|
|
64
68
|
}
|
|
65
69
|
/** The facts known when the options are built: everything but the mode and what only the running session knows. */
|
|
66
70
|
export type StaticSessionFacts = Omit<SessionFacts, "mode" | "switchableModes" | "tools" | "commands" | "context" | "extensions"> & {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ExtensionDirs, type LockEntry } from "./externalExtensions.js";
|
|
2
|
+
/** What's trusted of an extension: its code, its files' hash and its npx packages'. */
|
|
3
|
+
export declare function codeHash(lock: LockEntry): string;
|
|
4
|
+
/** Whether an extension of a project's scope is trusted in that workspace, with the code its lock has now. */
|
|
5
|
+
export declare function isTrusted(agentDir: string, projectScope: string, name: string, lock: LockEntry): Promise<boolean>;
|
|
6
|
+
/** Trusts these extensions of a project's scope, with the code their lock has now. */
|
|
7
|
+
export declare function trust(agentDir: string, projectScope: string, entries: Record<string, LockEntry>): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Trusts a workspace's extensions (its project scope's, all or `names`) with the code its lock
|
|
10
|
+
* has now: what the person does when the chat asks, or with `extension trust`. The names trusted.
|
|
11
|
+
*/
|
|
12
|
+
export declare function trustWorkspace(dirs: ExtensionDirs, names?: readonly string[]): Promise<string[]>;
|