@falkenslab/agent-kit 0.25.0 → 0.27.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 (35) hide show
  1. package/dist/chat/extensions.d.ts +14 -0
  2. package/dist/chat/extensions.js +39 -3
  3. package/dist/core/agentSpec.d.ts +33 -0
  4. package/dist/core/claudeAiConnectors.d.ts +35 -0
  5. package/dist/core/claudeAiConnectors.js +76 -0
  6. package/dist/core/extensions.d.ts +7 -0
  7. package/dist/core/externalExtensions.d.ts +30 -0
  8. package/dist/core/externalExtensions.js +37 -0
  9. package/dist/core/hooks/guidedGate.d.ts +57 -0
  10. package/dist/core/hooks/guidedGate.js +97 -0
  11. package/dist/core/hooks/remoteGate.d.ts +22 -0
  12. package/dist/core/hooks/remoteGate.js +35 -0
  13. package/dist/core/hooks/transcriptLogger.d.ts +2 -0
  14. package/dist/core/hooks/transcriptLogger.js +1 -1
  15. package/dist/core/messages/de.js +10 -1
  16. package/dist/core/messages/en.d.ts +22 -0
  17. package/dist/core/messages/en.js +10 -1
  18. package/dist/core/messages/es.js +10 -1
  19. package/dist/core/messages/fr.js +10 -1
  20. package/dist/core/readOnlyHints.d.ts +12 -6
  21. package/dist/core/readOnlyHints.js +14 -8
  22. package/dist/core/runner.js +24 -0
  23. package/dist/core/session.d.ts +5 -0
  24. package/dist/core/session.js +88 -6
  25. package/dist/core/sessionFacts.d.ts +6 -0
  26. package/dist/core/sessionFacts.js +1 -0
  27. package/dist/core/tools/humanApproval.d.ts +4 -2
  28. package/dist/core/tools/humanApproval.js +3 -1
  29. package/dist/extensions/awareness/tools.js +6 -0
  30. package/dist/index.d.ts +3 -2
  31. package/dist/index.js +2 -1
  32. package/dist/tui/extensionCommand.d.ts +1 -1
  33. package/dist/tui/extensionCommand.js +14 -6
  34. package/package.json +1 -1
  35. package/schemas/agent-kit-metadata.schema.json +5 -0
@@ -35,7 +35,7 @@ export const de = {
35
35
  extensionSwitched: (name, enabled) => `(${name} ${enabled ? "aktiviert" : "deaktiviert"}, in derselben Sitzung)`,
36
36
  extensionToggled: (name, enabled) => `(${name} ${enabled ? "aktiviert" : "deaktiviert"}: die Sitzung wird mit derselben Unterhaltung neu geöffnet)`,
37
37
  extensionNotInstalled: (name) => `(${name} ist nicht installiert: /extensions listet sie auf)`,
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, sign-in <Name> [Rückruf-URL], sign-out <Name>)",
38
+ extensionsUsage: "(Verwendung: /extensions, enable|disable <Name>, config <Name> [Schlüssel=Wert… | confirm=on|off|reset], search [Wörter], install <Name>, update [Name], remove <Name> [--delete-data], reload, trust, sign-in <Name> [Rückruf-URL], sign-out <Name>)",
39
39
  extensionsCantReopen: "(dieser Chat kann seine Sitzung nicht neu öffnen: im Terminal aktivieren oder deaktivieren und neu starten)",
40
40
  extensionOptions: (name) => `Optionen von ${name}:`,
41
41
  extensionNoOptions: (name) => `(${name} hat keine Optionen)`,
@@ -60,6 +60,15 @@ export const de = {
60
60
  extensionAlreadySignedIn: (name) => `(${name} ist bereits angemeldet)`,
61
61
  extensionSignInUnavailable: (name) => `(bei ${name} kann man sich hier nicht anmelden: sie läuft nicht in dieser Sitzung, hat keinen entfernten Server, oder das SDK dieses Agenten kann es nicht)`,
62
62
  extensionSignedOut: (name) => `(bei ${name} abgemeldet: sie bittet dich, dich erneut anzumelden)`,
63
+ remoteCallTitle: (destination) => `Das an ${destination} senden? Es verlässt diesen Computer`,
64
+ guidedTitle: (extension) => `${extension} das tun lassen?`,
65
+ confirmAnswers: { yes: "Ja", session: "Ja, für diese Sitzung", always: "Ja, immer", no: "Nein", never: "Nein, nie" },
66
+ extensionConfirmOn: (name) => `Im geführten Modus fragen die Werkzeuge von ${name}, die etwas ändern, zuerst (confirm=off führt sie ohne Nachfrage aus):`,
67
+ extensionConfirmOff: (name) => `Im geführten Modus laufen die Werkzeuge von ${name} ohne Nachfrage (confirm=on fragt wieder).`,
68
+ confirmKept: (tool, answer) => ` ${tool}: ${answer === "always" ? "immer ja" : "nie"}`,
69
+ confirmNoneKept: " (keine gespeicherten Antworten; confirm=reset vergisst sie)",
70
+ connectorsAllowed: (names) => `claude.ai-Connectoren: ${names}`,
71
+ connectorsMissing: (names) => `Nicht in deinem claude.ai-Konto: ${names} (verbinde ihn auf claude.ai und öffne die Sitzung neu)`,
63
72
  extensionInstalled: (name) => `(${name} installiert: in einem neuen Gespräch da, oder sofort mit /extensions reload, ohne dieses zu verlieren)`,
64
73
  extensionUpdated: (name, from, to) => `(${name} aktualisiert, ${from} → ${to}: in einem neuen Gespräch da, oder sofort mit /extensions reload)`,
65
74
  extensionsUpToDate: "(alle Erweiterungen aus einem Marketplace sind aktuell)",
@@ -82,6 +82,28 @@ export interface Messages {
82
82
  extensionAlreadySignedIn(name: string): string;
83
83
  extensionSignInUnavailable(name: string): string;
84
84
  extensionSignedOut(name: string): string;
85
+ /** Before a call to a remote MCP server, when the agent asks first (#70): where it goes. */
86
+ remoteCallTitle(destination: string): string;
87
+ /** Guided mode's question before an installed extension's tool that changes things (#74): its title. */
88
+ guidedTitle(extension: string): string;
89
+ /** Its answers (#76): this call, this session, always, no, never. */
90
+ confirmAnswers: {
91
+ yes: string;
92
+ session: string;
93
+ always: string;
94
+ no: string;
95
+ never: string;
96
+ };
97
+ /** In /extensions config: whether its tools ask in guided mode, before the answers kept. */
98
+ extensionConfirmOn(name: string): string;
99
+ extensionConfirmOff(name: string): string;
100
+ /** A kept answer about one of its tools. */
101
+ confirmKept(tool: string, answer: "always" | "never"): string;
102
+ confirmNoneKept: string;
103
+ /** In /extensions: the claude.ai connectors let in (#70). */
104
+ connectorsAllowed(names: string): string;
105
+ /** In /extensions: connectors the agent asked for that the account hasn't. */
106
+ connectorsMissing(names: string): string;
85
107
  extensionUpdated(name: string, from: string, to: string): string;
86
108
  extensionsUpToDate: string;
87
109
  extensionRemoved(name: string, withData: boolean): string;
@@ -34,7 +34,7 @@ export const en = {
34
34
  extensionSwitched: (name, enabled) => `(${name} ${enabled ? "enabled" : "disabled"}, in this same session)`,
35
35
  extensionToggled: (name, enabled) => `(${name} ${enabled ? "enabled" : "disabled"}: the session reopens, with the same conversation)`,
36
36
  extensionNotInstalled: (name) => `(${name} isn't installed: /extensions lists them)`,
37
- extensionsUsage: "(usage: /extensions, enable|disable <name>, config <name> [key=value…], search [words], install <name>, update [name], remove <name> [--delete-data], reload, trust, sign-in <name> [callback URL], sign-out <name>)",
37
+ extensionsUsage: "(usage: /extensions, enable|disable <name>, config <name> [key=value… | confirm=on|off|reset], search [words], install <name>, update [name], remove <name> [--delete-data], reload, trust, sign-in <name> [callback URL], sign-out <name>)",
38
38
  extensionsCantReopen: "(this chat can't reopen its session: enable or disable it from the terminal, and start again)",
39
39
  extensionOptions: (name) => `${name}'s options:`,
40
40
  extensionNoOptions: (name) => `(${name} has no options)`,
@@ -60,6 +60,15 @@ export const en = {
60
60
  extensionAlreadySignedIn: (name) => `(${name} is already signed in)`,
61
61
  extensionSignInUnavailable: (name) => `(${name} can't be signed in to here: it isn't running in this session, has no remote server, or this agent's SDK can't sign in)`,
62
62
  extensionSignedOut: (name) => `(signed out of ${name}: it will ask you to sign in again)`,
63
+ remoteCallTitle: (destination) => `Send this to ${destination}? It leaves this computer`,
64
+ guidedTitle: (extension) => `Let ${extension} do this?`,
65
+ confirmAnswers: { yes: "Yes", session: "Yes, for this session", always: "Yes, always", no: "No", never: "No, never" },
66
+ extensionConfirmOn: (name) => `In guided mode, ${name}'s tools that change things ask first (confirm=off runs them without asking):`,
67
+ extensionConfirmOff: (name) => `In guided mode, ${name}'s tools run without asking (confirm=on asks again).`,
68
+ confirmKept: (tool, answer) => ` ${tool}: ${answer === "always" ? "always yes" : "never"}`,
69
+ confirmNoneKept: " (no kept answers; confirm=reset forgets them)",
70
+ connectorsAllowed: (names) => `claude.ai connectors: ${names}`,
71
+ connectorsMissing: (names) => `Not in your claude.ai account: ${names} (connect it on claude.ai, then reopen the session)`,
63
72
  extensionUpdated: (name, from, to) => `(${name} updated, ${from} → ${to}: it's there in a new conversation, or now with /extensions reload)`,
64
73
  extensionsUpToDate: "(every extension from a marketplace is up to date)",
65
74
  extensionRemoved: (name, withData) => `(${name} removed${withData ? ", with its data" : "; its data is kept for a reinstall"})`,
@@ -35,7 +35,7 @@ export const es = {
35
35
  extensionSwitched: (name, enabled) => `(${name} ${enabled ? "activada" : "desactivada"}, en esta misma sesión)`,
36
36
  extensionToggled: (name, enabled) => `(${name} ${enabled ? "activada" : "desactivada"}: la sesión se reabre, con la misma conversación)`,
37
37
  extensionNotInstalled: (name) => `(${name} no está instalada: /extensions las lista)`,
38
- extensionsUsage: "(uso: /extensions, enable|disable <nombre>, config <nombre> [clave=valor…], search [palabras], install <nombre>, update [nombre], remove <nombre> [--delete-data], reload, trust, sign-in <nombre> [URL de vuelta], sign-out <nombre>)",
38
+ extensionsUsage: "(uso: /extensions, enable|disable <nombre>, config <nombre> [clave=valor… | confirm=on|off|reset], search [palabras], install <nombre>, update [nombre], remove <nombre> [--delete-data], reload, trust, sign-in <nombre> [URL de vuelta], sign-out <nombre>)",
39
39
  extensionsCantReopen: "(este chat no puede reabrir su sesión: actívala o desactívala desde la terminal y vuelve a empezar)",
40
40
  extensionOptions: (name) => `Opciones de ${name}:`,
41
41
  extensionNoOptions: (name) => `(${name} no tiene opciones)`,
@@ -60,6 +60,15 @@ export const es = {
60
60
  extensionAlreadySignedIn: (name) => `(${name} ya tiene la sesión iniciada)`,
61
61
  extensionSignInUnavailable: (name) => `(no se puede iniciar sesión en ${name} aquí: no se ejecuta en esta sesión, no tiene servidor remoto, o el SDK de este agente no sabe hacerlo)`,
62
62
  extensionSignedOut: (name) => `(sesión cerrada en ${name}: te pedirá que vuelvas a iniciarla)`,
63
+ remoteCallTitle: (destination) => `¿Enviar esto a ${destination}? Sale de este ordenador`,
64
+ guidedTitle: (extension) => `¿Dejas que ${extension} haga esto?`,
65
+ confirmAnswers: { yes: "Sí", session: "Sí, en esta sesión", always: "Sí, siempre", no: "No", never: "No, nunca" },
66
+ extensionConfirmOn: (name) => `En modo guiado, las herramientas de ${name} que cambian cosas preguntan antes (confirm=off las ejecuta sin preguntar):`,
67
+ extensionConfirmOff: (name) => `En modo guiado, las herramientas de ${name} se ejecutan sin preguntar (confirm=on vuelve a preguntar).`,
68
+ confirmKept: (tool, answer) => ` ${tool}: ${answer === "always" ? "siempre sí" : "nunca"}`,
69
+ confirmNoneKept: " (ninguna respuesta guardada; confirm=reset las olvida)",
70
+ connectorsAllowed: (names) => `Conectores de claude.ai: ${names}`,
71
+ connectorsMissing: (names) => `No están en tu cuenta de claude.ai: ${names} (conéctalo en claude.ai y vuelve a abrir la sesión)`,
63
72
  extensionInstalled: (name) => `(${name} instalada: está en una conversación nueva, o ya con /extensions reload, sin perder esta)`,
64
73
  extensionUpdated: (name, from, to) => `(${name} actualizada, ${from} → ${to}: está en una conversación nueva, o ya con /extensions reload)`,
65
74
  extensionsUpToDate: "(todas las extensiones de un marketplace están al día)",
@@ -35,7 +35,7 @@ export const fr = {
35
35
  extensionSwitched: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"}, dans cette même session)`,
36
36
  extensionToggled: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"} : la session se rouvre, avec la même conversation)`,
37
37
  extensionNotInstalled: (name) => `(${name} n'est pas installée : /extensions les liste)`,
38
- extensionsUsage: "(usage : /extensions, enable|disable <nom>, config <nom> [clé=valeur…], search [mots], install <nom>, update [nom], remove <nom> [--delete-data], reload, trust, sign-in <nom> [URL de retour], sign-out <nom>)",
38
+ extensionsUsage: "(usage : /extensions, enable|disable <nom>, config <nom> [clé=valeur… | confirm=on|off|reset], search [mots], install <nom>, update [nom], remove <nom> [--delete-data], reload, trust, sign-in <nom> [URL de retour], sign-out <nom>)",
39
39
  extensionsCantReopen: "(ce chat ne peut pas rouvrir sa session : activez-la ou désactivez-la depuis le terminal, puis recommencez)",
40
40
  extensionOptions: (name) => `Options de ${name} :`,
41
41
  extensionNoOptions: (name) => `(${name} n'a pas d'options)`,
@@ -60,6 +60,15 @@ export const fr = {
60
60
  extensionAlreadySignedIn: (name) => `(${name} est déjà connectée)`,
61
61
  extensionSignInUnavailable: (name) => `(impossible de se connecter à ${name} ici : elle ne tourne pas dans cette session, n'a pas de serveur distant, ou le SDK de cet agent ne sait pas le faire)`,
62
62
  extensionSignedOut: (name) => `(déconnecté de ${name} : elle vous demandera de vous reconnecter)`,
63
+ remoteCallTitle: (destination) => `Envoyer ceci à ${destination} ? Cela quitte cet ordinateur`,
64
+ guidedTitle: (extension) => `Laisser ${extension} faire ceci ?`,
65
+ confirmAnswers: { yes: "Oui", session: "Oui, pour cette session", always: "Oui, toujours", no: "Non", never: "Non, jamais" },
66
+ extensionConfirmOn: (name) => `En mode guidé, les outils de ${name} qui modifient des choses demandent d'abord (confirm=off les exécute sans demander) :`,
67
+ extensionConfirmOff: (name) => `En mode guidé, les outils de ${name} s'exécutent sans demander (confirm=on demande à nouveau).`,
68
+ confirmKept: (tool, answer) => ` ${tool} : ${answer === "always" ? "toujours oui" : "jamais"}`,
69
+ confirmNoneKept: " (aucune réponse gardée ; confirm=reset les oublie)",
70
+ connectorsAllowed: (names) => `Connecteurs claude.ai : ${names}`,
71
+ connectorsMissing: (names) => `Absents de votre compte claude.ai : ${names} (connectez-le sur claude.ai, puis rouvrez la session)`,
63
72
  extensionInstalled: (name) => `(${name} installée : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload, en gardant celle-ci)`,
64
73
  extensionUpdated: (name, from, to) => `(${name} mise à jour, ${from} → ${to} : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload)`,
65
74
  extensionsUpToDate: "(toutes les extensions d'un marketplace sont à jour)",
@@ -1,13 +1,19 @@
1
1
  import type { McpServerStatus } from "@anthropic-ai/claude-agent-sdk";
2
+ /** What an MCP tool's own server says about it: `readOnlyHint`, `destructiveHint`. */
3
+ export type ToolHint = "readOnly" | "destructive";
2
4
  /**
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
+ * What the MCP servers themselves say about their tools (#53, #76): for an installed extension
6
+ * whose manifest lists no `readOnlyTools` (plan mode, guided mode) or no `confirmTools` (guided
7
+ * mode), a tool its server marks `readOnlyHint` only reads, and one it marks `destructiveHint`
8
+ * always asks.
5
9
  *
6
- * Asked when plan mode meets the call, not once at start: a server is `pending` for seconds after
10
+ * Asked when a gate meets the call, not once at start: a server is `pending` for seconds after
7
11
  * the session starts, and one turned off and on again (liveExtensions.ts) may come back with
8
12
  * 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.
13
+ * `annotations.readOnly` (and `destructive`), set exactly when the server says the hint is `true`
14
+ * (confirmed empirically for `readOnlyHint`: `false` and no hint both leave it out; the names are
15
+ * the short ones). Anything else — not connected, not asked in time, no hint — is no.
12
16
  */
17
+ export declare function createToolHints(servers: readonly string[], status: () => (() => Promise<McpServerStatus[]>) | undefined, hint: ToolHint): (toolName: string) => Promise<boolean>;
18
+ /** Plan mode's read-only tools from the servers themselves: `createToolHints()` for `readOnlyHint`. */
13
19
  export declare function createReadOnlyHints(servers: readonly string[], status: () => (() => Promise<McpServerStatus[]>) | undefined): (toolName: string) => Promise<boolean>;
@@ -1,15 +1,17 @@
1
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.
2
+ * What the MCP servers themselves say about their tools (#53, #76): for an installed extension
3
+ * whose manifest lists no `readOnlyTools` (plan mode, guided mode) or no `confirmTools` (guided
4
+ * mode), a tool its server marks `readOnlyHint` only reads, and one it marks `destructiveHint`
5
+ * always asks.
4
6
  *
5
- * Asked when plan mode meets the call, not once at start: a server is `pending` for seconds after
7
+ * Asked when a gate meets the call, not once at start: a server is `pending` for seconds after
6
8
  * the session starts, and one turned off and on again (liveExtensions.ts) may come back with
7
9
  * 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.
10
+ * `annotations.readOnly` (and `destructive`), set exactly when the server says the hint is `true`
11
+ * (confirmed empirically for `readOnlyHint`: `false` and no hint both leave it out; the names are
12
+ * the short ones). Anything else — not connected, not asked in time, no hint — is no.
11
13
  */
12
- export function createReadOnlyHints(servers, status) {
14
+ export function createToolHints(servers, status, hint) {
13
15
  return async (toolName) => {
14
16
  const server = servers.find((name) => toolName.startsWith(`mcp__${name}__`));
15
17
  const read = status();
@@ -18,9 +20,13 @@ export function createReadOnlyHints(servers, status) {
18
20
  const tool = toolName.slice(`mcp__${server}__`.length);
19
21
  const statuses = await within(read(), []);
20
22
  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;
23
+ return found?.tools?.some((entry) => entry.name === tool && entry.annotations?.[hint] === true) ?? false;
22
24
  };
23
25
  }
26
+ /** Plan mode's read-only tools from the servers themselves: `createToolHints()` for `readOnlyHint`. */
27
+ export function createReadOnlyHints(servers, status) {
28
+ return createToolHints(servers, status, "readOnly");
29
+ }
24
30
  // A call to the session made from a hook, before a tool runs: never let it hold the tool up.
25
31
  const TIMEOUT_MS = 3000;
26
32
  const within = (promise, fallback) => Promise.race([promise.catch(() => fallback), new Promise((resolve) => setTimeout(() => resolve(fallback), TIMEOUT_MS).unref())]);
@@ -35,6 +35,29 @@ function signInControls(result) {
35
35
  ...(clear ? { mcpClearAuth: (server) => clear(server) } : {}),
36
36
  };
37
37
  }
38
+ /**
39
+ * Turns back on the servers the CLI saved as switched off in an earlier session (#75):
40
+ * `toggleMcpServer(name, false)` is saved in the Claude Code config of the session's folder
41
+ * (`~/.claude.json`, `disabledMcpServers`), not only in the session (confirmed empirically, CLI
42
+ * 2.1.300), so a server the kit passes again would start disabled. The kit passes only what it
43
+ * wants running, so one of them that starts disabled is turned on, which also removes the saved
44
+ * entry. Only while the session's servers start: switching one off later in the session is the
45
+ * live switch's (liveExtensions.ts).
46
+ */
47
+ async function restoreSavedSwitches(result, options) {
48
+ const passed = new Set(Object.keys(options.mcpServers ?? {}));
49
+ if (!passed.size)
50
+ return;
51
+ for (let attempt = 0; attempt < 30; attempt++) {
52
+ const status = (await result.mcpServerStatus().catch(() => [])).filter((server) => passed.has(server.name));
53
+ for (const server of status)
54
+ if (server.status === "disabled")
55
+ await result.toggleMcpServer(server.name, true).catch(() => undefined);
56
+ if (status.length && !status.some((server) => server.status === "pending" || server.status === "disabled"))
57
+ return;
58
+ await new Promise((resolve) => setTimeout(resolve, 500));
59
+ }
60
+ }
38
61
  /**
39
62
  * Thin wrapper around the SDK's own `query()`: same two-argument shape (a plain string
40
63
  * for a one-shot run, or an `AsyncIterable<SDKUserMessage>` — see `createInputQueue()` —
@@ -56,6 +79,7 @@ export function runQuery(prompt, options) {
56
79
  mcpServerStatus: () => result.mcpServerStatus(),
57
80
  ...signInControls(result),
58
81
  });
82
+ void restoreSavedSwitches(result, options);
59
83
  // The main agent's tool calls by id, to name the results that come back for them.
60
84
  const toolNames = new Map();
61
85
  async function* generateEvents() {
@@ -77,6 +77,11 @@ export interface ExtensionsStatus {
77
77
  sessionTools?: () => readonly string[];
78
78
  /** Its MCP servers as they are now (empty until it has started): which remote ones need signing in (#61). */
79
79
  serverStatus?: () => Promise<McpServerStatus[]>;
80
+ /** The claude.ai connectors let in (#70), and the ones asked for that the account hasn't; none when the agent asks for none. */
81
+ connectors?: {
82
+ allowed: string[];
83
+ missing: string[];
84
+ };
80
85
  }
81
86
  /**
82
87
  * The CLI's folder for a project, where it keeps each session's transcript and large tool
@@ -8,6 +8,9 @@ import { createSubagentTypeGate } from "./hooks/subagentTypeGate.js";
8
8
  import { createSubagentForegroundGate } from "./hooks/subagentForegroundGate.js";
9
9
  import { createFileScopeGate } from "./hooks/fileScopeGate.js";
10
10
  import { createPlanGate } from "./hooks/planGate.js";
11
+ import { createRemoteGate } from "./hooks/remoteGate.js";
12
+ import { createGuidedGate } from "./hooks/guidedGate.js";
13
+ import { chooseConnectors, discoverConnectors, serverToolPrefix } from "./claudeAiConnectors.js";
11
14
  import { createHumanApprovalServer } from "./tools/humanApproval.js";
12
15
  import { createManualLoginServer } from "./tools/manualLogin.js";
13
16
  import { createTimeServer } from "./tools/time.js";
@@ -17,7 +20,7 @@ import { aboutExtension, describeCapability, extensionsPromptSection, resolveExt
17
20
  import { allowAnyMcpTool } from "./mcpPermissions.js";
18
21
  import { pluginAgents } from "./pluginAgents.js";
19
22
  import { outsideArchive, unpackedClaudeExecutable } from "./packaged.js";
20
- import { listInstalled, loadExternalExtensions, scopePrivateDir } from "./externalExtensions.js";
23
+ import { listInstalled, loadExternalExtensions, readLock, scopePrivateDir } from "./externalExtensions.js";
21
24
  import { replyLanguageInstruction } from "./language.js";
22
25
  import { chooseLanguage } from "./messages/index.js";
23
26
  import { createRunStore } from "./runs.js";
@@ -25,7 +28,7 @@ import { createSessionView, registerSessionView } from "./sessionFacts.js";
25
28
  import { createExtensionAuth, createExtensionRestart, createExtensionSwitch, mountPlugin } from "./liveExtensions.js";
26
29
  import { secretsDir } from "./extensionOptions.js";
27
30
  import { createElicitationHandler } from "./elicitation.js";
28
- import { createReadOnlyHints } from "./readOnlyHints.js";
31
+ import { createReadOnlyHints, createToolHints } from "./readOnlyHints.js";
29
32
  import { agentKitVersion } from "./version.js";
30
33
  /**
31
34
  * Builds the `options` object passed to the Agent SDK's `query()` — everything about
@@ -179,6 +182,28 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
179
182
  const readOnlyTools = fromExtensions((contribution) => contribution.readOnlyTools);
180
183
  // Those whose manifest lists none: their servers say, asked while the session runs (#53).
181
184
  const readOnlyHintServers = fromExtensions((contribution) => contribution.readOnlyHintServers);
185
+ const readOnlyHinted = readOnlyHintServers.length ? createReadOnlyHints(readOnlyHintServers, () => late.view?.controls().mcpServerStatus) : undefined;
186
+ // Guided mode's gate for the installed extensions' tools that change things (#74, #76): whose
187
+ // they are, which always ask (the manifest's list and the agent's, else the servers' hints),
188
+ // and those the person said never to, kept in the lock: out of the session altogether.
189
+ const gateSpec = spec.extensionGate ?? {};
190
+ const guardedExtensions = gateSpec.enabled === false
191
+ ? []
192
+ : resolved.active.flatMap(({ extension }, index) => {
193
+ const installed = installedDirs.get(extension.name);
194
+ if (!installed)
195
+ return [];
196
+ const servers = Object.keys(added[index]?.mcpServers ?? {});
197
+ const own = (gateSpec.confirmTools?.[extension.name] ?? []).flatMap((tool) => servers.map((server) => `mcp__${server}__${tool}`));
198
+ return [{ name: extension.name, scopeDir: installed.scopeDir, servers, confirmTools: [...(added[index]?.confirmTools ?? []), ...own] }];
199
+ });
200
+ const neverTools = (await Promise.all(guardedExtensions.map(async (extension) => {
201
+ const confirm = (await readLock(extension.scopeDir).catch(() => ({})))[extension.name]?.confirm;
202
+ if (confirm?.off)
203
+ return [];
204
+ const never = Object.entries(confirm?.tools ?? {}).filter(([, answer]) => answer === "never");
205
+ return never.flatMap(([tool]) => extension.servers.map((server) => `mcp__${server}__${tool}`));
206
+ }))).flat();
182
207
  // What no file tool may touch: the agent's denied paths and the SDK's own credentials.
183
208
  // And the installed extensions' sensitive option values (#55), in each scope's .secrets/.
184
209
  const deniedPaths = [
@@ -191,6 +216,19 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
191
216
  ];
192
217
  const transcriptPath = path.join(runDir, "transcript.jsonl");
193
218
  const transcriptLogger = createTranscriptLogger(transcriptPath, config.secrets ?? []);
219
+ const guidedGate = guardedExtensions.length
220
+ ? createGuidedGate({
221
+ runDir,
222
+ mode: () => modeControl.mode,
223
+ extensions: guardedExtensions,
224
+ isReadOnly: async (tool) => readOnlyTools.includes(tool) || ((await readOnlyHinted?.(tool)) ?? false),
225
+ isDestructive: createToolHints(fromExtensions((contribution) => contribution.confirmHintServers), () => late.view?.controls().mcpServerStatus, "destructive"),
226
+ ...(gateSpec.autonomous ? { autonomous: gateSpec.autonomous } : {}),
227
+ // The session's labels, built below: read when a call is asked about.
228
+ labels: () => toolLabels,
229
+ record: (decision) => transcriptLogger.note("guided_decision", { ...decision }),
230
+ })
231
+ : undefined;
194
232
  const promptSections = [
195
233
  spec.buildSystemPrompt(config),
196
234
  // What's on and what isn't, then the extensions' own sections.
@@ -200,6 +238,12 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
200
238
  ];
201
239
  // Packaged, the SDK would look for its CLI binary inside the archive, and hang (#42).
202
240
  const claudeExecutable = unpackedClaudeExecutable();
241
+ // The claude.ai connectors the agent lets in (#70): the account asked which it has, the rest
242
+ // denied by name (a session setting, never the person's config). None asked, none looked for.
243
+ const connectors = config.claudeAiConnectors?.length
244
+ ? chooseConnectors(await discoverConnectors(config.projectDir, claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}), config.claudeAiConnectors)
245
+ : undefined;
246
+ const allowedConnectors = connectors?.allowed ?? [];
203
247
  const sdkOptions = {
204
248
  ...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}),
205
249
  systemPrompt: withReplyLine(promptSections.join("\n\n")),
@@ -220,7 +264,8 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
220
264
  autoCompactEnabled: options.autoCompactEnabled ?? true,
221
265
  autoMemoryEnabled: false,
222
266
  includeGitInstructions: false,
223
- disableClaudeAiConnectors: true,
267
+ disableClaudeAiConnectors: !allowedConnectors.length,
268
+ ...(allowedConnectors.length && connectors.denied.length ? { deniedMcpServers: connectors.denied.map((serverName) => ({ serverName })) } : {}),
224
269
  // An installed extension's plugin may carry hooks: none run (ADR-025). The kit's own,
225
270
  // passed in `hooks`, still do (confirmed empirically).
226
271
  ...(externalPlugins.size ? { disableAllHooks: true } : {}),
@@ -246,7 +291,8 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
246
291
  // mcpPermissions.ts for why a fixed allowedTools wildcard can't cover a server whose
247
292
  // name isn't known ahead of time.
248
293
  canUseTool: allowAnyMcpTool,
249
- disallowedTools: spec.disallowedTools ?? [],
294
+ // And the extensions' tools the person said never to (#76).
295
+ disallowedTools: [...(spec.disallowedTools ?? []), ...neverTools],
250
296
  // TodoWrite (todos.ts) only exists with CLAUDE_CODE_ENABLE_TASKS=0: by default the CLI
251
297
  // offers its TaskCreate/TaskUpdate/TaskList/TaskGet tools instead, and drops TodoWrite from
252
298
  // the session even when `tools` names it (confirmed empirically). `env` replaces the
@@ -279,7 +325,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
279
325
  ...(subagentDefinitions ? { agents: subagentDefinitions } : {}),
280
326
  mcpServers: {
281
327
  ...spec.buildMcpServers(config, runDir),
282
- ...(includeApprovalTool ? { approvals: createHumanApprovalServer(runDir, spec.humanApprovalTexts, { modeControl }) } : {}),
328
+ ...(includeApprovalTool ? { approvals: createHumanApprovalServer(runDir, spec.humanApprovalTexts, { modeControl, ...(guidedGate ? { onApproval: guidedGate.approvalAnswered } : {}) }) } : {}),
283
329
  ...(manualInterventionTexts ? { manualLogin: createManualLoginServer(runDir, manualInterventionTexts) } : {}),
284
330
  // The extensions' tools (the knowledge base's, the sources').
285
331
  ...Object.assign({}, ...added.map((contribution) => contribution.mcpServers ?? {})),
@@ -292,6 +338,20 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
292
338
  hooks: {
293
339
  PreToolUse: [
294
340
  { hooks: [transcriptLogger.preToolUse] },
341
+ // Remote servers (#70): connectors not chosen are blocked; the rest as `remoteServers` says.
342
+ {
343
+ hooks: [
344
+ createRemoteGate({
345
+ runDir,
346
+ policy: config.remoteServers ?? "allow",
347
+ destination: (tool) => remoteDestinations.find(([prefix]) => tool.startsWith(prefix))?.[1],
348
+ allowedConnectors: allowedConnectors.map(serverToolPrefix),
349
+ canAsk: () => modeControl.mode !== "autonomous",
350
+ }),
351
+ ],
352
+ },
353
+ // Guided mode asks before an installed extension's tool that changes things (#74, #76).
354
+ ...(guidedGate ? [{ hooks: [guidedGate.preToolUse] }] : []),
295
355
  ...(fileTools.length || toolOnlyDirs.length
296
356
  ? [{ hooks: [createFileScopeGate({
297
357
  projectDir: config.projectDir,
@@ -323,7 +383,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
323
383
  createPlanGate({
324
384
  projectDir: config.projectDir,
325
385
  readOnlyTools,
326
- ...(readOnlyHintServers.length ? { isReadOnlyHinted: createReadOnlyHints(readOnlyHintServers, () => late.view?.controls().mcpServerStatus) } : {}),
386
+ ...(readOnlyHinted ? { isReadOnlyHinted: readOnlyHinted } : {}),
327
387
  ...(spec.planMode?.isPlanFile ? { isPlanFile: (filePath) => spec.planMode.isPlanFile(filePath, config) } : {}),
328
388
  ...(spec.planMode?.isReadOnlyTool ? { isReadOnlyTool: spec.planMode.isReadOnlyTool } : {}),
329
389
  }, () => modeControl.mode === "plan"),
@@ -340,8 +400,28 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
340
400
  : []),
341
401
  ],
342
402
  PostToolUse: [{ hooks: [transcriptLogger.postToolUse] }],
403
+ // The person's message ends a batch they approved (guided mode, #74).
404
+ ...(guidedGate ? { UserPromptSubmit: [{ hooks: [guidedGate.userPromptSubmit] }] } : {}),
343
405
  },
344
406
  };
407
+ // Where each remote server's calls go (#70), for the remote gate: an http/sse server's host
408
+ // (an installed extension's, the agent's own), or the claude.ai connector itself.
409
+ const remoteDestinations = [
410
+ ...Object.entries(sdkOptions.mcpServers ?? {}).flatMap(([name, server]) => {
411
+ const remote = server;
412
+ if ((remote.type !== "http" && remote.type !== "sse") || !remote.url)
413
+ return [];
414
+ let host = remote.url;
415
+ try {
416
+ host = new URL(remote.url).host;
417
+ }
418
+ catch {
419
+ // Not a URL the kit can read: the address as given.
420
+ }
421
+ return [[serverToolPrefix(name), host]];
422
+ }),
423
+ ...allowedConnectors.map((name) => [serverToolPrefix(name), name]),
424
+ ];
345
425
  // The extensions' own hooks (the memory hears the person's messages), after the kit's.
346
426
  for (const contribution of added) {
347
427
  for (const [event, matchers] of Object.entries(contribution.hooks ?? {})) {
@@ -360,6 +440,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
360
440
  sessionTools: () => late.view?.controls().tools ?? [],
361
441
  serverStatus: async () => (await late.view?.controls().mcpServerStatus?.().catch(() => [])) ?? [],
362
442
  inactive: resolved.inactive,
443
+ ...(connectors ? { connectors: { allowed: connectors.allowed, missing: connectors.missing } } : {}),
363
444
  };
364
445
  // What the session is, for the extensions that ask (sessionFacts.ts, #43).
365
446
  const viewBase = {
@@ -378,6 +459,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
378
459
  })),
379
460
  // Its own list: /extensions' status has another, and the live switch changes both.
380
461
  inactive: [...resolved.inactive],
462
+ ...(connectors ? { connectors: { allowed: connectors.allowed, missing: connectors.missing } } : {}),
381
463
  },
382
464
  subagents: Object.entries(allAgents).map(([name, agent]) => ({ name, description: agent.description })),
383
465
  skills: sdkOptions.skills === "all" ? await pluginSkills(pluginRoots) : [...(sdkOptions.skills ?? [])],
@@ -37,6 +37,11 @@ export interface SessionFacts {
37
37
  name: string;
38
38
  reason: string;
39
39
  }[];
40
+ /** The claude.ai connectors let in (#70), and the ones asked for that the account hasn't. */
41
+ connectors?: {
42
+ allowed: string[];
43
+ missing: string[];
44
+ };
40
45
  };
41
46
  /** The subagents it can delegate to. */
42
47
  subagents: {
@@ -93,6 +98,7 @@ export type StaticSessionFacts = Omit<SessionFacts, "mode" | "switchableModes" |
93
98
  servers: string[];
94
99
  })[];
95
100
  inactive: SessionFacts["extensions"]["inactive"];
101
+ connectors?: SessionFacts["extensions"]["connectors"];
96
102
  };
97
103
  };
98
104
  export interface SessionView {
@@ -26,6 +26,7 @@ export function createSessionView(base, modeControl) {
26
26
  tools: tools.length ? tools.filter((tool) => servers.some((server) => tool.startsWith(`mcp__${server}__`))) : servers.map((server) => `mcp__${server}__*`),
27
27
  })),
28
28
  inactive: base.extensions.inactive,
29
+ ...(base.extensions.connectors ? { connectors: base.extensions.connectors } : {}),
29
30
  },
30
31
  tools,
31
32
  skills: live.skills ?? base.skills,
@@ -10,10 +10,12 @@ export declare const DEFAULT_HUMAN_APPROVAL_TEXTS: HumanApprovalTexts;
10
10
  /**
11
11
  * The tools that ask the person, in every mode but autonomous: `request_human_approval`
12
12
  * (right before an action that publishes something visible to others and that's hard to
13
- * undo), `ask_human` (a choice between options, in a panel, within the turn) and, when the
13
+ * undo; approved, guided mode lets the calls that follow through until the person's next
14
+ * message, see hooks/guidedGate.ts), `ask_human` (a choice between options, in a panel, within the turn) and, when the
14
15
  * session can be in plan mode (`modeControl`), `present_plan`, the kit's own way out of plan
15
16
  * mode (ADR-023): the person runs the plan, keeps planning or cancels.
16
17
  */
17
18
  export declare function createHumanApprovalServer(runDir: string, texts?: HumanApprovalTexts, options?: {
18
- modeControl?: ModeControl;
19
+ modeControl?: ModeControl; /** Each approval's answer: guided mode's batch (#74). */
20
+ onApproval?: (approved: boolean) => void;
19
21
  }): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
@@ -16,7 +16,8 @@ const ok = (text) => ({ content: [{ type: "text", text }] });
16
16
  /**
17
17
  * The tools that ask the person, in every mode but autonomous: `request_human_approval`
18
18
  * (right before an action that publishes something visible to others and that's hard to
19
- * undo), `ask_human` (a choice between options, in a panel, within the turn) and, when the
19
+ * undo; approved, guided mode lets the calls that follow through until the person's next
20
+ * message, see hooks/guidedGate.ts), `ask_human` (a choice between options, in a panel, within the turn) and, when the
20
21
  * session can be in plan mode (`modeControl`), `present_plan`, the kit's own way out of plan
21
22
  * mode (ADR-023): the person runs the plan, keeps planning or cancels.
22
23
  */
@@ -29,6 +30,7 @@ export function createHumanApprovalServer(runDir, texts = DEFAULT_HUMAN_APPROVAL
29
30
  lines: [t().summaryLine(args.summary)],
30
31
  });
31
32
  const approved = APPROVED_ANSWERS.has(answer);
33
+ options.onApproval?.(approved);
32
34
  return {
33
35
  content: [{ type: "text", text: approved ? texts.approved : texts.rejected }],
34
36
  };
@@ -27,6 +27,12 @@ export function describeSession(facts) {
27
27
  });
28
28
  const inactive = facts.extensions.inactive.map(({ name, reason }) => `- **${name}** is off: it ${reason}.`);
29
29
  sections.push(`## Extensions\n\n${[...active, ...inactive].join("\n") || "None."}`);
30
+ // The claude.ai connectors let in (#70): the account's integrations, not installed extensions.
31
+ const connectors = facts.extensions.connectors;
32
+ if (connectors && (connectors.allowed.length || connectors.missing.length)) {
33
+ const missing = connectors.missing.length ? ` Asked for but not in the person's claude.ai account: ${connectors.missing.join(", ")} (they connect it on claude.ai, then the session opens again).` : "";
34
+ sections.push(`## claude.ai connectors\n\n${connectors.allowed.length ? `${connectors.allowed.join(", ")}: their tools are the person's claude.ai integrations; what you send them leaves the computer.` : "None of the ones asked for."}${missing}`);
35
+ }
30
36
  if (facts.subagents.length)
31
37
  sections.push(`## Subagents you can delegate to\n\n${facts.subagents.map(({ name, description }) => `- **${name}**: ${description}`).join("\n")}`);
32
38
  const own = facts.tools.filter((tool) => !tool.startsWith("mcp__"));
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { createPromptLoader } from "./core/promptTemplate.js";
6
6
  export { resolveClaudeAuth, type ClaudeAuthConfig } from "./core/claudeAuth.js";
7
7
  export { ensureClaudeAuth } from "./tui/claudeAuth.js";
8
8
  export { createFriendlyToolLabel, truncate, truncatePath, withToolLabels, withToolPhrases, type ToolDescriber, type ToolLabel, type ToolLabels, } from "./core/toolLabels.js";
9
- export type { Mode, BaseSessionConfig, AgentSpec, AgentIdentity, PlanModeSpec } from "./core/agentSpec.js";
9
+ export type { Mode, BaseSessionConfig, AgentSpec, AgentIdentity, PlanModeSpec, ExtensionGateSpec } from "./core/agentSpec.js";
10
10
  export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, togglePlanMode, type ModeControl, type ExtensionsStatus } from "./core/session.js";
11
11
  export { runQuery, type AgentEvent, type AgentRun, type SessionUsage, type ContextUsage } from "./core/runner.js";
12
12
  export type { McpAuthentication, SessionFacts } from "./core/sessionFacts.js";
@@ -17,7 +17,7 @@ export { knowledge, type KnowledgeOptions } from "./extensions/knowledge/index.j
17
17
  export { memory, type MemoryOptions } from "./extensions/memory/index.js";
18
18
  export { describeCapability, readExtensionManifest, type AboutExtension, type CapabilityText, type DescribedCapability, type Extension, type ExtensionCapability, type ExtensionContext, type ExtensionContribution, type ExtensionManifest, type FolderOption, type UnavailableCapability, } from "./core/extensions.js";
19
19
  export { checkRequirements, type ExtensionRequirement, type RequirementCheck } from "./core/extensionRequirements.js";
20
- export { addExtension, codeStoreDir, installTarget, scopePrivateDir, extensionDataDir, extensionOptionStates, setExtensionOptions, removeExtension, setExtensionEnabled, listInstalled, readExternalManifest, type ExtensionAuthor, type ExtensionDirs, type ExtensionScope, type ExternalManifest, type ExternalToolLabel, type PluginServer, type LocalServer, type RemoteServer, isRemoteServer, remoteHosts, remoteServerName, type InstalledExtension, type LockEntry, type ExtensionOptionState, } from "./core/externalExtensions.js";
20
+ export { addExtension, codeStoreDir, installTarget, scopePrivateDir, extensionDataDir, extensionOptionStates, setExtensionOptions, removeExtension, setExtensionEnabled, setExtensionConfirm, listInstalled, readExternalManifest, type ExtensionAuthor, type ExtensionDirs, type ExtensionScope, type ExternalManifest, type ExternalToolLabel, type PluginServer, type LocalServer, type RemoteServer, isRemoteServer, remoteHosts, remoteServerName, type InstalledExtension, type LockEntry, type ExtensionOptionState, type ExtensionConfirm, type ToolConfirmation, } from "./core/externalExtensions.js";
21
21
  export type { ExtensionOption, OptionValue } from "./core/extensionOptions.js";
22
22
  export type { ExtensionAuth, ExtensionRestart, ExtensionSwitch, SignIn } from "./core/liveExtensions.js";
23
23
  export { packageCacheDir, type CachedPackage } from "./core/npxPackages.js";
@@ -41,6 +41,7 @@ export { createSubagentTypeGate } from "./core/hooks/subagentTypeGate.js";
41
41
  export { createSubagentForegroundGate } from "./core/hooks/subagentForegroundGate.js";
42
42
  export { createFileScopeGate, checkFileScope, type FileScope } from "./core/hooks/fileScopeGate.js";
43
43
  export { createPlanGate, checkPlanScope, type PlanScope } from "./core/hooks/planGate.js";
44
+ export { createGuidedGate, type GuardedExtension, type GuidedDecision, type GuidedGate } from "./core/hooks/guidedGate.js";
44
45
  export { askForDecision, askForManualIntervention, askForChoice, askForText, type ChoiceAnswer } from "./core/hooks/humanInput.js";
45
46
  export { setInteractionPort, getInteractionPort, type InteractionPort, type ApprovalPrompt, type ChoiceSettings } from "./core/interaction.js";
46
47
  export { formFields, type FormAnswer, type FormField, type FormRequest } from "./core/elicitation.js";
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ export { knowledge } from "./extensions/knowledge/index.js";
17
17
  export { memory } from "./extensions/memory/index.js";
18
18
  export { describeCapability, readExtensionManifest, } from "./core/extensions.js";
19
19
  export { checkRequirements } from "./core/extensionRequirements.js";
20
- export { addExtension, codeStoreDir, installTarget, scopePrivateDir, extensionDataDir, extensionOptionStates, setExtensionOptions, removeExtension, setExtensionEnabled, listInstalled, readExternalManifest, isRemoteServer, remoteHosts, remoteServerName, } from "./core/externalExtensions.js";
20
+ export { addExtension, codeStoreDir, installTarget, scopePrivateDir, extensionDataDir, extensionOptionStates, setExtensionOptions, removeExtension, setExtensionEnabled, setExtensionConfirm, listInstalled, readExternalManifest, isRemoteServer, remoteHosts, remoteServerName, } from "./core/externalExtensions.js";
21
21
  export { packageCacheDir } from "./core/npxPackages.js";
22
22
  export { addMarketplace, findPlugin, inspectMarketplace, installFromMarketplace, listMarketplaces, readMarketplaceManifest, removeMarketplace, resolvePluginSource, updateMarketplace, } from "./core/marketplaces.js";
23
23
  export { extensionCatalog, installExtension, restoreExtensions, updateExtension } from "./core/extensionCatalog.js";
@@ -40,6 +40,7 @@ export { createSubagentTypeGate } from "./core/hooks/subagentTypeGate.js";
40
40
  export { createSubagentForegroundGate } from "./core/hooks/subagentForegroundGate.js";
41
41
  export { createFileScopeGate, checkFileScope } from "./core/hooks/fileScopeGate.js";
42
42
  export { createPlanGate, checkPlanScope } from "./core/hooks/planGate.js";
43
+ export { createGuidedGate } from "./core/hooks/guidedGate.js";
43
44
  export { askForDecision, askForManualIntervention, askForChoice, askForText } from "./core/hooks/humanInput.js";
44
45
  export { setInteractionPort, getInteractionPort } from "./core/interaction.js";
45
46
  export { formFields } from "./core/elicitation.js";
@@ -12,7 +12,7 @@ export { chatExtensionsCommand, describe } from "../chat/extensions.js";
12
12
  * - `extension add <folder> --link [--project]`: registered where it is, to develop it (not copied, its files not checked)
13
13
  * - `extension remove <name> [--project | --agent] [--yes | --keep-data]`: asks before deleting its data folder
14
14
  * - `extension enable|disable <name> [--project | --agent]`
15
- * - `extension config <name> [key=value… | key… | --unset key…] [--project | --agent]`: its options; `key` alone asks for it (without echo when it's a secret)
15
+ * - `extension config <name> [key=value… | key… | --unset key… | confirm=on|off|reset] [--project | --agent]`: its options; `key` alone asks for it (without echo when it's a secret); `confirm=` what guided mode asks about its tools (#74, #76)
16
16
  * - `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
17
17
  * - `extension restore [--trust]`: gets the code a workspace's lock lists and this computer lacks (#57); `--trust` trusts it too
18
18
  * - `extension trust [name…]`: lets a workspace's extensions run, as their code is now