@falkenslab/agent-kit 0.28.0 → 0.30.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 (37) hide show
  1. package/dist/chat/chatController.d.ts +5 -2
  2. package/dist/chat/chatController.js +4 -2
  3. package/dist/chat/extensions.d.ts +3 -2
  4. package/dist/chat/extensions.js +14 -5
  5. package/dist/core/agentSpec.d.ts +2 -2
  6. package/dist/core/extensions.d.ts +2 -0
  7. package/dist/core/externalExtensions.d.ts +10 -3
  8. package/dist/core/externalExtensions.js +9 -1
  9. package/dist/core/hooks/guidedGate.d.ts +7 -3
  10. package/dist/core/hooks/guidedGate.js +13 -6
  11. package/dist/core/hooks/signInReminder.d.ts +24 -0
  12. package/dist/core/hooks/signInReminder.js +43 -0
  13. package/dist/core/messages/de.js +1 -0
  14. package/dist/core/messages/en.d.ts +2 -0
  15. package/dist/core/messages/en.js +1 -0
  16. package/dist/core/messages/es.js +1 -0
  17. package/dist/core/messages/fr.js +1 -0
  18. package/dist/core/projectServers.d.ts +30 -0
  19. package/dist/core/projectServers.js +51 -0
  20. package/dist/core/session.d.ts +3 -0
  21. package/dist/core/session.js +31 -4
  22. package/dist/core/sessionFacts.d.ts +16 -0
  23. package/dist/core/sessionFacts.js +7 -1
  24. package/dist/core/toolLabels.d.ts +21 -0
  25. package/dist/core/toolLabels.js +17 -0
  26. package/dist/extensions/awareness/index.js +3 -1
  27. package/dist/extensions/awareness/tools.js +11 -0
  28. package/dist/extensions/knowledge/index.js +3 -1
  29. package/dist/extensions/memory/index.js +3 -1
  30. package/dist/extensions/sources/index.js +3 -1
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/tui/extensionCommand.js +37 -0
  34. package/dist/tui/ink/runChatInk.js +3 -1
  35. package/package.json +1 -1
  36. package/schemas/agent-kit-metadata.schema.json +11 -1
  37. package/schemas/marketplace-catalog.schema.json +133 -94
@@ -7,7 +7,7 @@ import { type AgentEvent, type AgentRun, type SessionUsage } from "../core/runne
7
7
  import { type RunFolder } from "../core/runs.js";
8
8
  import { type ExtensionsStatus, type ModeControl } from "../core/session.js";
9
9
  import { type Todo } from "../core/todos.js";
10
- import { type ToolLabels } from "../core/toolLabels.js";
10
+ import { type ToolLabels, type ToolResult } from "../core/toolLabels.js";
11
11
  import type { ToolPhrase } from "../core/messages/index.js";
12
12
  import type { SignIn } from "../core/liveExtensions.js";
13
13
  import { type ExtensionOptionState, type ExtensionScope } from "../core/externalExtensions.js";
@@ -42,6 +42,8 @@ export interface ChatSettings {
42
42
  /** Tool call labels and folded-group phrases on top of the kit's and the extensions'. */
43
43
  formatAction?: (toolName: string, input: unknown) => string;
44
44
  toolPhrase?: (toolName: string) => ToolPhrase | undefined;
45
+ /** The line under a tool call for its result, before the labels' own (`ToolLabel.result`, #79): a string replaces it, `null` hides it, `undefined` leaves it to them. */
46
+ formatResult?: (toolName: string, result: ToolResult) => string | null | undefined;
45
47
  /** Runs the agent: the SDK's (`runQuery()`) by default; another for tests, or a host of its own. */
46
48
  runQuery?: (prompt: AsyncIterable<SDKUserMessage>, options: Options) => AgentRun;
47
49
  /** The SDK's prompt suggestions (on by default). */
@@ -223,9 +225,10 @@ export declare function createChatController(options: Options | SessionOpener, s
223
225
  subscribe(listener: (state: ChatState) => void): () => void;
224
226
  /** Calls `listener` with every event, in order; returns how to stop. */
225
227
  onEvent(listener: (event: ChatEvent) => void): () => void;
226
- /** Tool call labels and phrases with the extensions' (for a view that renders events itself). */
228
+ /** Tool call labels, phrases and result lines with the extensions' (for a view that renders events itself). */
227
229
  formatAction: (toolName: string, toolInput: unknown) => string;
228
230
  toolPhrase: (toolName: string) => ToolPhrase | undefined;
231
+ formatResult: (toolName: string, result: ToolResult) => string | null | undefined;
229
232
  /** The folder the session works in. */
230
233
  cwd: () => string;
231
234
  /** Draws the run's earlier conversation, then sends the initial prompt (for a new run). */
@@ -7,7 +7,7 @@ import { runQuery } from "../core/runner.js";
7
7
  import { createRunFolder, listRuns, readConversation, readRunSession } from "../core/runs.js";
8
8
  import { createInputQueue, togglePlanMode } from "../core/session.js";
9
9
  import { hasOpenTodos, parseTodos, TODO_TOOL } from "../core/todos.js";
10
- import { withToolLabels, withToolPhrases } from "../core/toolLabels.js";
10
+ import { withToolLabels, withToolPhrases, withToolResults } from "../core/toolLabels.js";
11
11
  import { createConsoleRenderer } from "../tui/consoleRenderer.js";
12
12
  import { chatExtensionsCommand } from "./extensions.js";
13
13
  import { trustWorkspace } from "../core/workspaceTrust.js";
@@ -39,6 +39,7 @@ export async function createChatController(options, settings = {}) {
39
39
  const modeControl = () => current.modeControl ?? settings.modeControl;
40
40
  const formatAction = withToolLabels(() => current.toolLabels ?? settings.toolLabels, settings.formatAction);
41
41
  const toolPhrase = withToolPhrases(() => current.toolLabels ?? settings.toolLabels, settings.toolPhrase);
42
+ const formatResult = withToolResults(() => current.toolLabels ?? settings.toolLabels, settings.formatResult);
42
43
  // State and listeners.
43
44
  const stateListeners = new Set();
44
45
  const eventListeners = new Set();
@@ -425,9 +426,10 @@ export async function createChatController(options, settings = {}) {
425
426
  eventListeners.add(listener);
426
427
  return () => eventListeners.delete(listener);
427
428
  },
428
- /** Tool call labels and phrases with the extensions' (for a view that renders events itself). */
429
+ /** Tool call labels, phrases and result lines with the extensions' (for a view that renders events itself). */
429
430
  formatAction,
430
431
  toolPhrase,
432
+ formatResult,
431
433
  /** The folder the session works in. */
432
434
  cwd: () => current.options.cwd ?? process.cwd(),
433
435
  /** Draws the run's earlier conversation, then sends the initial prompt (for a new run). */
@@ -6,9 +6,10 @@ export declare function describe(extension: InstalledExtension): string;
6
6
  /**
7
7
  * What the active extensions let the agent do (#62), in the session's language: a heading per
8
8
  * extension with capabilities, a line per capability, and the tools a capability names that its
9
- * server doesn't offer, once the session has started and knows them.
9
+ * server doesn't offer, once the session has started and knows them. Not for one waiting for a
10
+ * sign-in (`waiting`): its tools aren't there until the person signs in, which `/extensions` says.
10
11
  */
11
- export declare function describeCapabilities(status: ExtensionsStatus): string[];
12
+ export declare function describeCapabilities(status: ExtensionsStatus, waiting?: readonly string[]): string[];
12
13
  /** What the chat does for `/extensions install|update|remove|reload` (#56). */
13
14
  export type ExtensionAction = {
14
15
  type: "install";
@@ -15,9 +15,10 @@ export function describe(extension) {
15
15
  /**
16
16
  * What the active extensions let the agent do (#62), in the session's language: a heading per
17
17
  * extension with capabilities, a line per capability, and the tools a capability names that its
18
- * server doesn't offer, once the session has started and knows them.
18
+ * server doesn't offer, once the session has started and knows them. Not for one waiting for a
19
+ * sign-in (`waiting`): its tools aren't there until the person signs in, which `/extensions` says.
19
20
  */
20
- export function describeCapabilities(status) {
21
+ export function describeCapabilities(status, waiting = []) {
21
22
  const offered = new Set(status.sessionTools?.() ?? []);
22
23
  return status.active.flatMap((name) => {
23
24
  const capabilities = status.about[name]?.capabilities ?? [];
@@ -27,7 +28,7 @@ export function describeCapabilities(status) {
27
28
  return [
28
29
  t().extensionCan(name),
29
30
  ...capabilities.map(({ label, description, tools }) => {
30
- const missing = offered.size ? [...new Set(tools.filter((tool) => !offered.has(tool)).map((tool) => tool.replace(/^mcp__.+?__/, "")))] : [];
31
+ const missing = offered.size && !waiting.includes(name) ? [...new Set(tools.filter((tool) => !offered.has(tool)).map((tool) => tool.replace(/^mcp__.+?__/, "")))] : [];
31
32
  return ` - ${label}${description ? `: ${description}` : ""}${missing.length ? ` ${t().capabilityToolsMissing(missing.join(", "))}` : ""}`;
32
33
  }),
33
34
  // What it can't do on this computer, with what to install (#63).
@@ -149,13 +150,21 @@ export async function chatExtensionsCommand(line, status, canReopen) {
149
150
  }
150
151
  if (!action) {
151
152
  const lines = [status?.active.length ? t().extensionsRunning(status.active.join(", ")) : t().extensionsNone];
152
- if (status)
153
- lines.push(...describeCapabilities(status), ...(await needingSignIn(status)).map((extension) => t().extensionNeedsSignIn(extension)));
153
+ if (status) {
154
+ const waiting = await needingSignIn(status);
155
+ lines.push(...describeCapabilities(status, waiting), ...waiting.map((extension) => t().extensionNeedsSignIn(extension)));
156
+ }
154
157
  // The claude.ai connectors the agent lets in, apart from installed extensions (#70).
155
158
  if (status?.connectors?.allowed.length)
156
159
  lines.push(t().connectorsAllowed(status.connectors.allowed.map((name) => name.replace(/^claude\.ai /, "")).join(", ")));
157
160
  if (status?.connectors?.missing.length)
158
161
  lines.push(t().connectorsMissing(status.connectors.missing.join(", ")));
162
+ // The project's own .mcp.json servers left out (#83), by its folder's name: "<project>/.mcp.json".
163
+ const project = status?.projectServers;
164
+ if (project) {
165
+ const file = `${path.basename(path.dirname(project.file))}/.mcp.json`;
166
+ lines.push(t().projectServersDuplicated(file, project.duplicates.map(({ server, target }) => `${server} (${target})`).join(", ")));
167
+ }
159
168
  for (const { name: off, reason } of status?.inactive ?? [])
160
169
  lines.push(t().extensionOff(off, reason));
161
170
  if (status?.installed.length)
@@ -219,8 +219,8 @@ export interface ExtensionGateSpec {
219
219
  /** `false` turns the gate off: extensions' tools run without asking, as the model decides. */
220
220
  enabled?: boolean;
221
221
  /**
222
- * Tools that always ask, even inside a batch the person approved, and never offer "always", by
223
- * extension and short tool name (`{ canva: ["create-design"] }`): added to its manifest's `confirmTools`.
222
+ * Tools that always ask, on every call and even inside a batch the person approved (never
223
+ * offered "this session" or "always"), by extension and short tool name (`{ canva: ["create-design"] }`): added to its manifest's `confirmTools`.
224
224
  */
225
225
  confirmTools?: Record<string, string[]>;
226
226
  /** In autonomous mode, where no one can be asked: `"deny"` (the default) blocks those tools, `"allow"` runs them. */
@@ -71,6 +71,8 @@ export interface ExtensionContribution {
71
71
  confirmTools?: string[];
72
72
  /** Its MCP servers whose tools that always ask guided mode learns from their `destructiveHint`: an installed extension whose manifest lists no `confirmTools`. */
73
73
  confirmHintServers?: string[];
74
+ /** Its MCP tools whose server asks the person itself (MCP elicitation), by full name: guided mode doesn't ask too (#77). */
75
+ serverConfirmTools?: string[];
74
76
  /**
75
77
  * Its MCP servers that are remote (#61), by session name: the SDK fixes their URL and headers
76
78
  * when the session opens, so a changed option takes opening it again, and they may need the
@@ -58,6 +58,8 @@ export declare function remoteHosts(manifest: Pick<ExternalManifest, "servers">)
58
58
  export interface ExternalToolLabel {
59
59
  label: string;
60
60
  phrase?: ToolPhrase;
61
+ /** The line under the call when it succeeded, instead of its result's first line, written for the model (#79). */
62
+ result?: string;
61
63
  }
62
64
  /** Who made an extension, as Claude Code's `plugin.json` has it. */
63
65
  export interface ExtensionAuthor {
@@ -102,11 +104,16 @@ export interface ExternalManifest {
102
104
  */
103
105
  readOnlyTools?: string[];
104
106
  /**
105
- * Its tools that always ask the person in guided mode (short names), even inside a batch they
106
- * approved, and never offer "always" (#76): what creates, edits or publishes in their account.
107
- * Without the key, its servers' `destructiveHint` says which.
107
+ * Its tools that always ask the person in guided mode (short names), on every call and even
108
+ * inside a batch they approved, never offering "this session" or "always" (#76, #78): what
109
+ * creates, edits or publishes in their account. Without the key, its servers' `destructiveHint` says which.
108
110
  */
109
111
  confirmTools?: string[];
112
+ /**
113
+ * Its tools whose server asks the person itself before acting, through MCP elicitation (short
114
+ * names, #77): guided mode doesn't ask too while the person can be asked, so it's one question.
115
+ */
116
+ serverConfirmTools?: string[];
110
117
  /** Its tools' chat labels, by short name (of any of its servers) and language (English when the kit's isn't there). */
111
118
  labels: Record<string, Partial<Record<Language, ExternalToolLabel>>>;
112
119
  /** What it says about itself in a session, for the agent to tell the person (`SessionFacts`). */
@@ -141,6 +141,7 @@ export async function readExternalManifest(dir) {
141
141
  servers: await pluginServers(dir, raw),
142
142
  ...(Array.isArray(kit.readOnlyTools) ? { readOnlyTools: kit.readOnlyTools.filter((tool) => typeof tool === "string") } : {}),
143
143
  ...(Array.isArray(kit.confirmTools) ? { confirmTools: kit.confirmTools.filter((tool) => typeof tool === "string") } : {}),
144
+ ...(Array.isArray(kit.serverConfirmTools) ? { serverConfirmTools: kit.serverConfirmTools.filter((tool) => typeof tool === "string") } : {}),
144
145
  labels: kit.labels ?? {},
145
146
  ...(kit.help ? { help: kit.help } : {}),
146
147
  userConfig: parseUserConfig(raw.userConfig),
@@ -367,7 +368,12 @@ function manifestToolLabels(manifest, sessionServers) {
367
368
  const texts = byLanguage[language] ?? byLanguage.en;
368
369
  if (!texts)
369
370
  return [];
370
- const label = { label: (input) => fill(texts.label, input), ...(texts.phrase ? { phrase: texts.phrase } : {}) };
371
+ const label = {
372
+ label: (input) => fill(texts.label, input),
373
+ ...(texts.phrase ? { phrase: texts.phrase } : {}),
374
+ // In the person's language when it succeeded; a failure keeps its own line (#79).
375
+ ...(texts.result ? { result: (result) => (result.isError ? undefined : texts.result) } : {}),
376
+ };
371
377
  return sessionServers.map((server) => [`mcp__${server}__${tool}`, label]);
372
378
  }));
373
379
  }
@@ -429,6 +435,8 @@ function externalExtension(installed, manifest, cache, unavailable = []) {
429
435
  ...(manifest.confirmTools
430
436
  ? { confirmTools: Object.keys(servers).flatMap((server) => manifest.confirmTools.map((tool) => `mcp__${server}__${tool}`)) }
431
437
  : { confirmHintServers: Object.keys(servers) }),
438
+ // And those whose server asks the person itself (#77).
439
+ ...(manifest.serverConfirmTools ? { serverConfirmTools: Object.keys(servers).flatMap((server) => manifest.serverConfirmTools.map((tool) => `mcp__${server}__${tool}`)) } : {}),
432
440
  toolLabels: manifestToolLabels(manifest, Object.keys(servers)),
433
441
  ...(remote.length ? { remoteServers: remote } : {}),
434
442
  ...(manifest.help ? { helpLines: [manifest.help] } : {}),
@@ -10,12 +10,14 @@ export interface GuardedExtension {
10
10
  servers: readonly string[];
11
11
  /** Its tools that always ask (full names): its manifest's `confirmTools` and the agent's own list. */
12
12
  confirmTools: readonly string[];
13
+ /** Its tools whose server asks the person itself (full names, the manifest's `serverConfirmTools`, #77). */
14
+ serverConfirmTools?: readonly string[];
13
15
  }
14
16
  /** One answer the gate gave, for the transcript: what was asked, and what the person (or the mode) said. */
15
17
  export interface GuidedDecision {
16
18
  extension: string;
17
19
  tool: string;
18
- answer: "yes" | "session" | "always" | "no" | "never" | "kept-never" | "autonomous-deny";
20
+ answer: "yes" | "session" | "always" | "no" | "never" | "kept-never" | "autonomous-deny" | "server-asks";
19
21
  }
20
22
  /** The guided gate: its `PreToolUse` hook, and what ends or starts an approved batch. */
21
23
  export interface GuidedGate {
@@ -37,8 +39,10 @@ export interface GuidedGate {
37
39
  * this session, always (kept in the extension's lock), no, never (kept too). Right after
38
40
  * `request_human_approval` is approved, calls go on until the person's next message (a batch),
39
41
  * except those that always ask: the agent's or the manifest's `confirmTools`, else the server's
40
- * `destructiveHint`, which aren't offered "always" either. An extension whose lock says `off`
41
- * runs without asking. Approving gives no decision of its own, so the other gates still judge.
42
+ * `destructiveHint`, asked on every call, so they're offered only yes, no and never (#78). An extension whose lock says `off`
43
+ * runs without asking. A tool whose server asks the person itself (`serverConfirmTools`, #77)
44
+ * isn't asked about too in guided mode, where that question reaches the person: one question,
45
+ * the server's. Approving gives no decision of its own, so the other gates still judge.
42
46
  */
43
47
  export declare function createGuidedGate(options: {
44
48
  runDir: string;
@@ -15,8 +15,10 @@ const APPROVAL_TOOL = "mcp__approvals__request_human_approval";
15
15
  * this session, always (kept in the extension's lock), no, never (kept too). Right after
16
16
  * `request_human_approval` is approved, calls go on until the person's next message (a batch),
17
17
  * except those that always ask: the agent's or the manifest's `confirmTools`, else the server's
18
- * `destructiveHint`, which aren't offered "always" either. An extension whose lock says `off`
19
- * runs without asking. Approving gives no decision of its own, so the other gates still judge.
18
+ * `destructiveHint`, asked on every call, so they're offered only yes, no and never (#78). An extension whose lock says `off`
19
+ * runs without asking. A tool whose server asks the person itself (`serverConfirmTools`, #77)
20
+ * isn't asked about too in guided mode, where that question reaches the person: one question,
21
+ * the server's. Approving gives no decision of its own, so the other gates still judge.
20
22
  */
21
23
  export function createGuidedGate(options) {
22
24
  const thisSession = new Set();
@@ -47,10 +49,14 @@ export function createGuidedGate(options) {
47
49
  await record("kept-never");
48
50
  return deny(pre, `The person never lets ${extension.name} run ${tool} (they said so before). Don't call it again: do the task another way, or tell them it needs it.`);
49
51
  }
50
- const alwaysAsks = extension.confirmTools.includes(full) || (await options.isDestructive(full));
51
- if (kept === "always" && !alwaysAsks)
52
+ // Its server asks the person itself, and in guided mode its question reaches them (#77).
53
+ if (mode === "guided" && extension.serverConfirmTools?.includes(full)) {
54
+ await record("server-asks");
52
55
  return {};
53
- if (thisSession.has(full))
56
+ }
57
+ const alwaysAsks = extension.confirmTools.includes(full) || (await options.isDestructive(full));
58
+ // A tool that always asks is asked on every call (#78): no kept "always", no "this session".
59
+ if (!alwaysAsks && (kept === "always" || thisSession.has(full)))
54
60
  return {};
55
61
  if (mode === "autonomous") {
56
62
  if (options.autonomous === "allow")
@@ -61,7 +67,8 @@ export function createGuidedGate(options) {
61
67
  if (batch && !alwaysAsks)
62
68
  return {};
63
69
  const answers = t().confirmAnswers;
64
- const choices = [answers.yes, answers.session, ...(alwaysAsks ? [] : [answers.always]), answers.no, answers.never];
70
+ // For one that always asks, only what covers this call, or makes it stricter (#78).
71
+ const choices = alwaysAsks ? [answers.yes, answers.no, answers.never] : [answers.yes, answers.session, answers.always, answers.no, answers.never];
65
72
  const label = options.labels?.()[full]?.label((pre.tool_input ?? {}));
66
73
  const answer = await askForChoice(options.runDir, {
67
74
  title: t().guidedTitle(extension.name),
@@ -0,0 +1,24 @@
1
+ import type { HookCallback, McpServerStatus } from "@anthropic-ai/claude-agent-sdk";
2
+ /**
3
+ * What the model is told about signing in to an installed extension (#82): an extension whose
4
+ * remote server waits for the person to sign in has no tools yet, nor the skill that would say
5
+ * how, so the model fell back on Claude Code's `/mcp`, `claude mcp` or claude.ai's connectors,
6
+ * none of which applies in an agent on the kit.
7
+ */
8
+ export declare function signInInstruction(name: string): string;
9
+ /** The system prompt's rule, for a session with installed extensions behind a sign-in. */
10
+ export declare const SIGN_IN_RULE = "An installed extension whose remote server needs the person to sign in has no tools until they do: when one does, ask them to run `/extensions sign-in <extension>` (it opens their browser). Never suggest `/mcp`, `claude mcp` or claude.ai's connectors: they don't apply here.";
11
+ /**
12
+ * A `UserPromptSubmit` hook (#82): with the person's message, tells the model which installed
13
+ * extensions now wait for a sign-in (at the start, after `/extensions sign-out`), once each until
14
+ * it's signed in again.
15
+ */
16
+ export declare function createSignInReminder(options: {
17
+ /** Each installed extension's remote servers, by session name. */
18
+ extensions: () => readonly {
19
+ name: string;
20
+ remoteServers: readonly string[];
21
+ }[];
22
+ /** The running session's servers, as they are now. */
23
+ status: () => (() => Promise<McpServerStatus[]>) | undefined;
24
+ }): HookCallback;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * What the model is told about signing in to an installed extension (#82): an extension whose
3
+ * remote server waits for the person to sign in has no tools yet, nor the skill that would say
4
+ * how, so the model fell back on Claude Code's `/mcp`, `claude mcp` or claude.ai's connectors,
5
+ * none of which applies in an agent on the kit.
6
+ */
7
+ export function signInInstruction(name) {
8
+ return `The ${name} extension needs the person to sign in before its tools are available: ask them to run \`/extensions sign-in ${name}\` (it opens their browser). Never suggest \`/mcp\`, \`claude mcp\` or claude.ai's connectors: they don't apply here.`;
9
+ }
10
+ /** The system prompt's rule, for a session with installed extensions behind a sign-in. */
11
+ export const SIGN_IN_RULE = "An installed extension whose remote server needs the person to sign in has no tools until they do: when one does, ask them to run `/extensions sign-in <extension>` (it opens their browser). Never suggest `/mcp`, `claude mcp` or claude.ai's connectors: they don't apply here.";
12
+ /**
13
+ * A `UserPromptSubmit` hook (#82): with the person's message, tells the model which installed
14
+ * extensions now wait for a sign-in (at the start, after `/extensions sign-out`), once each until
15
+ * it's signed in again.
16
+ */
17
+ export function createSignInReminder(options) {
18
+ const reminded = new Set();
19
+ return async (input) => {
20
+ const read = options.status();
21
+ if (!read)
22
+ return {};
23
+ const statuses = await within(read(), []);
24
+ const waiting = new Set(statuses.filter((server) => server.status === "needs-auth").map((server) => server.name));
25
+ const notes = [];
26
+ for (const { name, remoteServers } of options.extensions()) {
27
+ if (!remoteServers.some((server) => waiting.has(server))) {
28
+ reminded.delete(name); // signed in (or off): a later sign-out reminds again
29
+ continue;
30
+ }
31
+ if (reminded.has(name))
32
+ continue;
33
+ reminded.add(name);
34
+ notes.push(signInInstruction(name));
35
+ }
36
+ if (!notes.length)
37
+ return {};
38
+ return { hookSpecificOutput: { hookEventName: input.hook_event_name, additionalContext: notes.join("\n") } };
39
+ };
40
+ }
41
+ // A call to the session made from a hook: never let it hold the person's message up.
42
+ const TIMEOUT_MS = 3000;
43
+ const within = (promise, fallback) => Promise.race([promise.catch(() => fallback), new Promise((resolve) => setTimeout(() => resolve(fallback), TIMEOUT_MS).unref())]);
@@ -69,6 +69,7 @@ export const de = {
69
69
  confirmKept: (tool, answer) => ` ${tool}: ${answer === "always" ? "immer ja" : "nie"}`,
70
70
  confirmNoneKept: " (keine gespeicherten Antworten; confirm=reset vergisst sie)",
71
71
  connectorsAllowed: (names) => `claude.ai-Connectoren: ${names}`,
72
+ projectServersDuplicated: (file, servers) => `${file} deklariert auch ${servers}: die der Erweiterungen werden verwendet`,
72
73
  connectorsMissing: (names) => `Nicht in deinem claude.ai-Konto: ${names} (verbinde ihn auf claude.ai und öffne die Sitzung neu)`,
73
74
  extensionInstalled: (name) => `(${name} installiert: in einem neuen Gespräch da, oder sofort mit /extensions reload, ohne dieses zu verlieren)`,
74
75
  extensionUpdated: (name, from, to) => `(${name} aktualisiert, ${from} → ${to}: in einem neuen Gespräch da, oder sofort mit /extensions reload)`,
@@ -104,6 +104,8 @@ export interface Messages {
104
104
  confirmNoneKept: string;
105
105
  /** In /extensions: the claude.ai connectors let in (#70). */
106
106
  connectorsAllowed(names: string): string;
107
+ /** In /extensions: the project's .mcp.json servers left out because an extension's duplicates them (#83). */
108
+ projectServersDuplicated(file: string, servers: string): string;
107
109
  /** In /extensions: connectors the agent asked for that the account hasn't. */
108
110
  connectorsMissing(names: string): string;
109
111
  extensionUpdated(name: string, from: string, to: string): string;
@@ -69,6 +69,7 @@ export const en = {
69
69
  confirmKept: (tool, answer) => ` ${tool}: ${answer === "always" ? "always yes" : "never"}`,
70
70
  confirmNoneKept: " (no kept answers; confirm=reset forgets them)",
71
71
  connectorsAllowed: (names) => `claude.ai connectors: ${names}`,
72
+ projectServersDuplicated: (file, servers) => `${file} also declares ${servers}: the extensions' are used`,
72
73
  connectorsMissing: (names) => `Not in your claude.ai account: ${names} (connect it on claude.ai, then reopen the session)`,
73
74
  extensionUpdated: (name, from, to) => `(${name} updated, ${from} → ${to}: it's there in a new conversation, or now with /extensions reload)`,
74
75
  extensionsUpToDate: "(every extension from a marketplace is up to date)",
@@ -69,6 +69,7 @@ export const es = {
69
69
  confirmKept: (tool, answer) => ` ${tool}: ${answer === "always" ? "siempre sí" : "nunca"}`,
70
70
  confirmNoneKept: " (ninguna respuesta guardada; confirm=reset las olvida)",
71
71
  connectorsAllowed: (names) => `Conectores de claude.ai: ${names}`,
72
+ projectServersDuplicated: (file, servers) => `${file} también declara ${servers}: se usan los de las extensiones`,
72
73
  connectorsMissing: (names) => `No están en tu cuenta de claude.ai: ${names} (conéctalo en claude.ai y vuelve a abrir la sesión)`,
73
74
  extensionInstalled: (name) => `(${name} instalada: está en una conversación nueva, o ya con /extensions reload, sin perder esta)`,
74
75
  extensionUpdated: (name, from, to) => `(${name} actualizada, ${from} → ${to}: está en una conversación nueva, o ya con /extensions reload)`,
@@ -69,6 +69,7 @@ export const fr = {
69
69
  confirmKept: (tool, answer) => ` ${tool} : ${answer === "always" ? "toujours oui" : "jamais"}`,
70
70
  confirmNoneKept: " (aucune réponse gardée ; confirm=reset les oublie)",
71
71
  connectorsAllowed: (names) => `Connecteurs claude.ai : ${names}`,
72
+ projectServersDuplicated: (file, servers) => `${file} déclare aussi ${servers} : ceux des extensions sont utilisés`,
72
73
  connectorsMissing: (names) => `Absents de votre compte claude.ai : ${names} (connectez-le sur claude.ai, puis rouvrez la session)`,
73
74
  extensionInstalled: (name) => `(${name} installée : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload, en gardant celle-ci)`,
74
75
  extensionUpdated: (name, from, to) => `(${name} mise à jour, ${from} → ${to} : elle est là dans une nouvelle conversation, ou dès maintenant avec /extensions reload)`,
@@ -0,0 +1,30 @@
1
+ import { type PluginServer } from "./externalExtensions.js";
2
+ /** A project server left out because an installed extension's is the same service (#83). */
3
+ export interface ProjectServerDuplicate {
4
+ /** Its name in the project's `.mcp.json`. */
5
+ server: string;
6
+ /** What it is: the remote server's URL, or the npm package it runs. */
7
+ target: string;
8
+ /** The extension whose server is used instead. */
9
+ extension: string;
10
+ }
11
+ /** What a server is, to compare two: a remote one's URL, or the npm package an `npx` / `bunx` / `uvx` one runs (its version aside). */
12
+ export declare function serverTarget(server: {
13
+ url?: string;
14
+ command?: string;
15
+ args?: string[];
16
+ }): string | undefined;
17
+ /**
18
+ * The servers of the project's `.mcp.json` an installed extension's server duplicates (#83): the
19
+ * same remote URL, or the same npm package, whose own sign-in notice and settings (another
20
+ * version, other courses) would sit beside the extension's and mislead the model. Every other
21
+ * project server stays: the agent can use them as Claude Code would. A name the kit passes itself
22
+ * is never left out: the denylist goes by name, and would take the kit's server too.
23
+ */
24
+ export declare function projectServerDuplicates(projectDir: string, extensions: readonly {
25
+ name: string;
26
+ servers: Record<string, PluginServer>;
27
+ }[], passed: ReadonlySet<string>): Promise<{
28
+ file: string;
29
+ duplicates: ProjectServerDuplicate[];
30
+ } | undefined>;
@@ -0,0 +1,51 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { isRemoteServer } from "./externalExtensions.js";
4
+ /** What a server is, to compare two: a remote one's URL, or the npm package an `npx` / `bunx` / `uvx` one runs (its version aside). */
5
+ export function serverTarget(server) {
6
+ if (typeof server.url === "string" && server.url)
7
+ return server.url.trim().toLowerCase().replace(/\/+$/, "");
8
+ if (typeof server.command !== "string")
9
+ return undefined;
10
+ const runner = path.basename(server.command).toLowerCase().replace(/\.(cmd|exe)$/, "");
11
+ if (!["npx", "bunx", "uvx", "pnpx"].includes(runner))
12
+ return undefined;
13
+ const spec = (server.args ?? []).find((arg) => typeof arg === "string" && !arg.startsWith("-"));
14
+ if (!spec)
15
+ return undefined;
16
+ // @scope/name@1.2.3 → @scope/name; name@1 → name.
17
+ const at = spec.indexOf("@", spec.startsWith("@") ? 1 : 0);
18
+ return (at > 0 ? spec.slice(0, at) : spec).toLowerCase();
19
+ }
20
+ /**
21
+ * The servers of the project's `.mcp.json` an installed extension's server duplicates (#83): the
22
+ * same remote URL, or the same npm package, whose own sign-in notice and settings (another
23
+ * version, other courses) would sit beside the extension's and mislead the model. Every other
24
+ * project server stays: the agent can use them as Claude Code would. A name the kit passes itself
25
+ * is never left out: the denylist goes by name, and would take the kit's server too.
26
+ */
27
+ export async function projectServerDuplicates(projectDir, extensions, passed) {
28
+ const file = path.join(projectDir, ".mcp.json");
29
+ let servers;
30
+ try {
31
+ const raw = JSON.parse(await readFile(file, "utf8"));
32
+ servers = raw.mcpServers && typeof raw.mcpServers === "object" ? raw.mcpServers : {};
33
+ }
34
+ catch {
35
+ return undefined;
36
+ }
37
+ const theirs = extensions.flatMap(({ name, servers: own }) => Object.values(own).flatMap((server) => {
38
+ const target = serverTarget(isRemoteServer(server) ? { url: server.url } : server);
39
+ return target ? [{ extension: name, target }] : [];
40
+ }));
41
+ const duplicates = [];
42
+ for (const [name, server] of Object.entries(servers)) {
43
+ if (passed.has(name) || !server || typeof server !== "object")
44
+ continue;
45
+ const target = serverTarget(server);
46
+ const same = target ? theirs.find((candidate) => candidate.target === target) : undefined;
47
+ if (same)
48
+ duplicates.push({ server: name, target: server.url ?? target, extension: same.extension });
49
+ }
50
+ return duplicates.length ? { file, duplicates } : undefined;
51
+ }
@@ -7,6 +7,7 @@ import type { ToolLabels } from "./toolLabels.js";
7
7
  import type { AgentSpec, BaseSessionConfig } from "./agentSpec.js";
8
8
  import { type Language } from "./language.js";
9
9
  import { type RunFolder } from "./runs.js";
10
+ import { type ProjectServersLeftOut } from "./sessionFacts.js";
10
11
  import { type ExtensionAuth, type ExtensionRestart, type ExtensionSwitch } from "./liveExtensions.js";
11
12
  /**
12
13
  * Builds the `options` object passed to the Agent SDK's `query()` — everything about
@@ -82,6 +83,8 @@ export interface ExtensionsStatus {
82
83
  allowed: string[];
83
84
  missing: string[];
84
85
  };
86
+ /** The project's `.mcp.json` servers left out (#83). */
87
+ projectServers?: ProjectServersLeftOut;
85
88
  }
86
89
  /**
87
90
  * The CLI's folder for a project, where it keeps each session's transcript and large tool
@@ -29,6 +29,8 @@ import { createExtensionAuth, createExtensionRestart, createExtensionSwitch, mou
29
29
  import { secretsDir } from "./extensionOptions.js";
30
30
  import { createElicitationHandler } from "./elicitation.js";
31
31
  import { createReadOnlyHints, createToolHints } from "./readOnlyHints.js";
32
+ import { projectServerDuplicates } from "./projectServers.js";
33
+ import { createSignInReminder, SIGN_IN_RULE } from "./hooks/signInReminder.js";
32
34
  import { agentKitVersion } from "./version.js";
33
35
  /**
34
36
  * Builds the `options` object passed to the Agent SDK's `query()` — everything about
@@ -195,7 +197,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
195
197
  return [];
196
198
  const servers = Object.keys(added[index]?.mcpServers ?? {});
197
199
  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] }];
200
+ return [{ name: extension.name, scopeDir: installed.scopeDir, servers, confirmTools: [...(added[index]?.confirmTools ?? []), ...own], serverConfirmTools: added[index]?.serverConfirmTools ?? [] }];
199
201
  });
200
202
  const neverTools = (await Promise.all(guardedExtensions.map(async (extension) => {
201
203
  const confirm = (await readLock(extension.scopeDir).catch(() => ({})))[extension.name]?.confirm;
@@ -229,10 +231,14 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
229
231
  record: (decision) => transcriptLogger.note("guided_decision", { ...decision }),
230
232
  })
231
233
  : undefined;
234
+ // The installed extensions with remote servers, which may need the person to sign in (#82).
235
+ const signInExtensions = resolved.active.flatMap(({ extension }, index) => (added[index]?.remoteServers?.length ? [{ name: extension.name, remoteServers: added[index].remoteServers }] : []));
232
236
  const promptSections = [
233
237
  spec.buildSystemPrompt(config),
234
238
  // What's on and what isn't, then the extensions' own sections.
235
239
  ...(resolved.active.length || resolved.inactive.length ? [extensionsPromptSection(resolved)] : []),
240
+ // How a sign-in is done here, never Claude Code's way (#82).
241
+ ...(signInExtensions.length ? [SIGN_IN_RULE] : []),
236
242
  // The extensions' own sections, in their order (the sources' before the knowledge base's).
237
243
  ...fromExtensions((contribution) => (contribution.promptSection ? [contribution.promptSection] : [])),
238
244
  ];
@@ -400,10 +406,29 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
400
406
  : []),
401
407
  ],
402
408
  PostToolUse: [{ hooks: [transcriptLogger.postToolUse] }],
403
- // The person's message ends a batch they approved (guided mode, #74).
404
- ...(guidedGate ? { UserPromptSubmit: [{ hooks: [guidedGate.userPromptSubmit] }] } : {}),
409
+ // The person's message: it ends a batch they approved (#74), and carries the sign-in reminders (#82).
410
+ ...(guidedGate || signInExtensions.length
411
+ ? {
412
+ UserPromptSubmit: [
413
+ ...(guidedGate ? [{ hooks: [guidedGate.userPromptSubmit] }] : []),
414
+ // With the person's message, which extensions now wait for a sign-in (#82).
415
+ ...(signInExtensions.length ? [{ hooks: [createSignInReminder({ extensions: () => signInExtensions, status: () => late.view?.controls().mcpServerStatus })] }] : []),
416
+ ],
417
+ }
418
+ : {}),
405
419
  },
406
420
  };
421
+ // A project's own .mcp.json beside the extensions (#83): only a server an extension's duplicates
422
+ // is left out, by name, as the connectors not chosen are (#70), never switched off (the CLI saves
423
+ // that, #75). Every other project server stays. Names the kit passes stay too.
424
+ const installedNow = await listInstalled(extensionDirs);
425
+ const projectServersLeftOut = (spec.settingSources ?? ["project"]).includes("project")
426
+ ? await projectServerDuplicates(config.projectDir, installedNow.filter((installed) => !installed.shadowed && installedDirs.has(installed.name) && installed.manifest).map((installed) => ({ name: installed.name, servers: installed.manifest.servers })), new Set(Object.keys(sdkOptions.mcpServers ?? {})))
427
+ : undefined;
428
+ if (projectServersLeftOut) {
429
+ const settings = sdkOptions.settings;
430
+ settings.deniedMcpServers = [...(settings.deniedMcpServers ?? []), ...projectServersLeftOut.duplicates.map(({ server }) => ({ serverName: server }))];
431
+ }
407
432
  // Where each remote server's calls go (#70), for the remote gate: an http/sse server's host
408
433
  // (an installed extension's, the agent's own), or the claude.ai connector itself.
409
434
  const remoteDestinations = [
@@ -434,13 +459,14 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
434
459
  ...(config.officialMarketplace ? { official: config.officialMarketplace } : {}),
435
460
  ...(config.catalogTopics?.length ? { catalogTopics: config.catalogTopics } : {}),
436
461
  untrusted: external.untrusted.map((installed) => ({ name: installed.name, ...(installed.manifest?.version ? { version: installed.manifest.version } : {}), source: installed.lock.marketplace ?? installed.lock.source })),
437
- installed: await listInstalled(extensionDirs),
462
+ installed: installedNow,
438
463
  active: resolved.active.map(({ extension }) => extension.name),
439
464
  about: Object.fromEntries(resolved.active.map(({ extension, manifest, unavailable }, index) => [extension.name, aboutExtension(manifest, Object.keys(added[index]?.mcpServers ?? {}), language, unavailable)])),
440
465
  sessionTools: () => late.view?.controls().tools ?? [],
441
466
  serverStatus: async () => (await late.view?.controls().mcpServerStatus?.().catch(() => [])) ?? [],
442
467
  inactive: resolved.inactive,
443
468
  ...(connectors ? { connectors: { allowed: connectors.allowed, missing: connectors.missing } } : {}),
469
+ ...(projectServersLeftOut ? { projectServers: projectServersLeftOut } : {}),
444
470
  };
445
471
  // What the session is, for the extensions that ask (sessionFacts.ts, #43).
446
472
  const viewBase = {
@@ -460,6 +486,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
460
486
  // Its own list: /extensions' status has another, and the live switch changes both.
461
487
  inactive: [...resolved.inactive],
462
488
  ...(connectors ? { connectors: { allowed: connectors.allowed, missing: connectors.missing } } : {}),
489
+ ...(projectServersLeftOut ? { projectServers: projectServersLeftOut } : {}),
463
490
  },
464
491
  subagents: Object.entries(allAgents).map(([name, agent]) => ({ name, description: agent.description })),
465
492
  skills: sdkOptions.skills === "all" ? await pluginSkills(pluginRoots) : [...(sdkOptions.skills ?? [])],
@@ -42,6 +42,10 @@ export interface SessionFacts {
42
42
  allowed: string[];
43
43
  missing: string[];
44
44
  };
45
+ /** The project's `.mcp.json` servers left out because an extension's duplicates them (#83). */
46
+ projectServers?: ProjectServersLeftOut;
47
+ /** The running extensions waiting for the person to sign in to a remote server (#82): `/extensions sign-in <name>`. */
48
+ needSignIn?: string[];
45
49
  };
46
50
  /** The subagents it can delegate to. */
47
51
  subagents: {
@@ -99,8 +103,20 @@ export type StaticSessionFacts = Omit<SessionFacts, "mode" | "switchableModes" |
99
103
  })[];
100
104
  inactive: SessionFacts["extensions"]["inactive"];
101
105
  connectors?: SessionFacts["extensions"]["connectors"];
106
+ projectServers?: ProjectServersLeftOut;
102
107
  };
103
108
  };
109
+ /** The project's `.mcp.json` servers a session leaves out because an extension's duplicates them (#83). */
110
+ export interface ProjectServersLeftOut {
111
+ /** The project's `.mcp.json`. */
112
+ file: string;
113
+ /** Those an installed extension's server duplicates: its name there, what it is (a URL, an npm package), and the extension used instead. */
114
+ duplicates: {
115
+ server: string;
116
+ target: string;
117
+ extension: string;
118
+ }[];
119
+ }
104
120
  export interface SessionView {
105
121
  facts(): Promise<SessionFacts>;
106
122
  attach(live: SessionLive): void;
@@ -11,10 +11,14 @@ export function createSessionView(base, modeControl) {
11
11
  controls: () => live,
12
12
  async facts() {
13
13
  const tools = live.tools ?? [];
14
- const [commands, context] = await Promise.all([
14
+ const [commands, context, statuses] = await Promise.all([
15
15
  live.supportedCommands ? within(live.supportedCommands(), []) : Promise.resolve([]),
16
16
  live.contextUsage ? within(live.contextUsage(), null) : Promise.resolve(null),
17
+ live.mcpServerStatus ? within(live.mcpServerStatus(), []) : Promise.resolve([]),
17
18
  ]);
19
+ // Those whose remote server waits for the person to sign in (#82).
20
+ const waiting = new Set(statuses.filter((server) => server.status === "needs-auth").map((server) => server.name));
21
+ const needSignIn = base.extensions.active.filter(({ servers }) => servers.some((server) => waiting.has(server))).map(({ name }) => name);
18
22
  return {
19
23
  ...base,
20
24
  mode: modeControl.mode,
@@ -27,6 +31,8 @@ export function createSessionView(base, modeControl) {
27
31
  })),
28
32
  inactive: base.extensions.inactive,
29
33
  ...(base.extensions.connectors ? { connectors: base.extensions.connectors } : {}),
34
+ ...(base.extensions.projectServers ? { projectServers: base.extensions.projectServers } : {}),
35
+ ...(needSignIn.length ? { needSignIn } : {}),
30
36
  },
31
37
  tools,
32
38
  skills: live.skills ?? base.skills,
@@ -22,7 +22,23 @@ export interface ToolLabel {
22
22
  label(input: Record<string, unknown>): string;
23
23
  /** How it counts in a folded group's summary, `[one, many]` with `{n}`: `["read {n} page", "read {n} pages"]`. */
24
24
  phrase?: ToolPhrase;
25
+ /**
26
+ * The line under it for its result, in the kit's language (#79): a string replaces the result's
27
+ * first line (written for the model, in English), `null` hides it, `undefined` keeps it. The
28
+ * model's result is never changed.
29
+ */
30
+ result?(result: ToolResult): string | null | undefined;
25
31
  }
32
+ /** A tool call's result, as the chat shows it. */
33
+ export interface ToolResult {
34
+ isError: boolean;
35
+ text: string;
36
+ }
37
+ /**
38
+ * Labels whose result line is hidden when the call succeeded (#79): for tools whose label already
39
+ * says what happened, so the person doesn't read the model's English under it. A failure still shows.
40
+ */
41
+ export declare function quietResults(labels: ToolLabels): ToolLabels;
26
42
  /** Tools' labels by full name (`mcp__<server>__<tool>`): what the extensions of a session bring. */
27
43
  export type ToolLabels = Readonly<Record<string, ToolLabel>>;
28
44
  /**
@@ -33,6 +49,11 @@ export type ToolLabels = Readonly<Record<string, ToolLabel>>;
33
49
  export declare function withToolLabels(labels: () => ToolLabels | undefined, formatAction?: (toolName: string, toolInput: unknown) => string): (toolName: string, toolInput: unknown) => string;
34
50
  /** A `toolPhrase` that counts the tools in `labels()` with their phrase, and any other with `toolPhrase`. */
35
51
  export declare function withToolPhrases(labels: () => ToolLabels | undefined, toolPhrase?: (toolName: string) => ToolPhrase | undefined): (toolName: string) => ToolPhrase | undefined;
52
+ /**
53
+ * A result formatter (the Ink chat's `formatResult`) that asks `formatResult` first, then the
54
+ * tools' labels (`ToolLabel.result`, #79): `undefined` from both keeps the result's first line.
55
+ */
56
+ export declare function withToolResults(labels: () => ToolLabels | undefined, formatResult?: (toolName: string, result: ToolResult) => string | null | undefined): (toolName: string, result: ToolResult) => string | null | undefined;
36
57
  /** An agent's labels for its own tools: the tool's short name and input, to a label, or `undefined` for the kit's default. */
37
58
  export type ToolDescriber = (shortName: string, input: Record<string, unknown>) => string | undefined;
38
59
  /**
@@ -71,6 +71,13 @@ function describeCore(shortName, input) {
71
71
  return undefined;
72
72
  }
73
73
  }
74
+ /**
75
+ * Labels whose result line is hidden when the call succeeded (#79): for tools whose label already
76
+ * says what happened, so the person doesn't read the model's English under it. A failure still shows.
77
+ */
78
+ export function quietResults(labels) {
79
+ return Object.fromEntries(Object.entries(labels).map(([tool, label]) => [tool, { ...label, result: (result) => (result.isError ? undefined : null) }]));
80
+ }
74
81
  /**
75
82
  * A `formatAction` that shows the tools in `labels()` with their label, and any other with
76
83
  * `formatAction` (the kit's by default). `labels` is read on every call, so it can follow the
@@ -88,6 +95,16 @@ export function withToolLabels(labels, formatAction = createFriendlyToolLabel())
88
95
  export function withToolPhrases(labels, toolPhrase) {
89
96
  return (toolName) => labels()?.[toolName]?.phrase ?? toolPhrase?.(toolName);
90
97
  }
98
+ /**
99
+ * A result formatter (the Ink chat's `formatResult`) that asks `formatResult` first, then the
100
+ * tools' labels (`ToolLabel.result`, #79): `undefined` from both keeps the result's first line.
101
+ */
102
+ export function withToolResults(labels, formatResult) {
103
+ return (toolName, result) => {
104
+ const own = formatResult?.(toolName, result);
105
+ return own !== undefined ? own : labels()?.[toolName]?.result?.(result);
106
+ };
107
+ }
91
108
  /**
92
109
  * Builds a `friendlyToolLabel(toolName, toolInput)` — `describe` lets the host agent
93
110
  * layer its own domain-specific cases (e.g. Playwright's browser_* tools) on top of this
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
+ import { quietResults } from "../../core/toolLabels.js";
3
4
  import { agentKitVersion } from "../../core/version.js";
4
5
  import { awarenessToolLabels } from "./labels.js";
5
6
  import { createAwarenessServer } from "./tools.js";
@@ -44,7 +45,8 @@ export function awareness(options = {}) {
44
45
  mcpServers: { awareness: createAwarenessServer(session, options.guide, catalog ? [createCatalogTool(config, session)] : []) },
45
46
  promptSection: identityPromptSection(spec.identity, { catalog }),
46
47
  readOnlyTools: ["mcp__awareness__about_me", ...(catalog ? ["mcp__awareness__extensions_catalog"] : [])],
47
- toolLabels: awarenessToolLabels(),
48
+ // Their labels already say what happened: no English result line under them (#79).
49
+ toolLabels: quietResults(awarenessToolLabels()),
48
50
  };
49
51
  },
50
52
  };
@@ -33,6 +33,17 @@ export function describeSession(facts) {
33
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
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
35
  }
36
+ // Extensions waiting for the person to sign in (#82): their tools aren't there until they do.
37
+ const waiting = facts.extensions.needSignIn ?? [];
38
+ if (waiting.length) {
39
+ sections.push(`## Waiting for a sign-in\n\n${waiting.map((name) => `- **${name}**: its tools aren't available until the person signs in: ask them to run \`/extensions sign-in ${name}\` (it opens their browser).`).join("\n")}\nNever suggest \`/mcp\`, \`claude mcp\` or claude.ai's connectors: they don't apply here.`);
40
+ }
41
+ // The project's own .mcp.json servers left out (#83): not this session's, whatever a notice about them says.
42
+ const project = facts.extensions.projectServers;
43
+ if (project) {
44
+ const duplicates = project.duplicates.map(({ server, target, extension }) => `- **${server}** (${target}): left out, the ${extension} extension's server is used instead.`);
45
+ sections.push(`## Servers of the project's .mcp.json left out\n\n${duplicates.join("\n")}\nA notice about one of them (such as one needing authentication) isn't about your extensions.`);
46
+ }
36
47
  if (facts.subagents.length)
37
48
  sections.push(`## Subagents you can delegate to\n\n${facts.subagents.map(({ name, description }) => `- **${name}**: ${description}`).join("\n")}`);
38
49
  const own = facts.tools.filter((tool) => !tool.startsWith("mcp__"));
@@ -1,3 +1,4 @@
1
+ import { quietResults } from "../../core/toolLabels.js";
1
2
  import { folderOf } from "../../core/extensions.js";
2
3
  import { createFileKnowledgeStore } from "./fileKnowledgeStore.js";
3
4
  import { knowledgeToolLabels } from "./labels.js";
@@ -27,7 +28,8 @@ export function knowledge(options) {
27
28
  toolOnlyDirs: [{ dir: knowledgeDir, instead: "the knowledge_* tools" }],
28
29
  readOnlyTools: ["mcp__knowledge__knowledge_index", "mcp__knowledge__knowledge_search", "mcp__knowledge__knowledge_read", "mcp__knowledge__knowledge_check"],
29
30
  selfAskingTools: ["mcp__knowledge__knowledge_retire"],
30
- toolLabels: knowledgeToolLabels(),
31
+ // Their labels already say what happened: no English result line under them (#79).
32
+ toolLabels: quietResults(knowledgeToolLabels()),
31
33
  helpLines: [
32
34
  "You keep a knowledge base (your memory across sessions). Its commands: `/knowledge:ingest` (learn the new or changed files of the sources folder), `/knowledge:query <question>` (answer from it), `/knowledge:lint` (check it and fix what's mechanical).",
33
35
  ],
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
+ import { quietResults } from "../../core/toolLabels.js";
3
4
  import { folderOf } from "../../core/extensions.js";
4
5
  import { readConversation } from "../../core/runs.js";
5
6
  import { memoryToolLabels } from "./labels.js";
@@ -49,7 +50,8 @@ export function memory(options) {
49
50
  toolOnlyDirs: [{ dir: memoryDir, instead: "its tools: recall, remember, forget" }],
50
51
  readOnlyTools: ["mcp__memory__recall"],
51
52
  hooks: { UserPromptSubmit: [{ hooks: [hear] }] },
52
- toolLabels: memoryToolLabels(),
53
+ // Their labels already say what happened: no English result line under them (#79).
54
+ toolLabels: quietResults(memoryToolLabels()),
53
55
  helpLines: [
54
56
  "You keep a memory of the person across all their projects (how they like things done, who they are): `/memory:list` shows it, `/memory:forget <entry>` forgets an entry. Only what they tell you goes in it.",
55
57
  ],
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
+ import { quietResults } from "../../core/toolLabels.js";
3
4
  import { folderOf } from "../../core/extensions.js";
4
5
  import { sourcesToolLabels } from "./labels.js";
5
6
  import { addSource } from "./sources.js";
@@ -29,7 +30,8 @@ export function sources(options) {
29
30
  readOnlyDirs: [sourcesDir],
30
31
  readOnlyTools: ["mcp__sourceFiles__list_sources", "mcp__sourceFiles__extract_text"],
31
32
  selfAskingTools: ["mcp__sourceFiles__request_file", "mcp__sourceFiles__retire_source"],
32
- toolLabels: sourcesToolLabels(),
33
+ // Their labels already say what happened: no English result line under them (#79).
34
+ toolLabels: quietResults(sourcesToolLabels()),
33
35
  // For the host (a page's "add to the chest"): a file the person gave, with the same rules
34
36
  // as the tools (never overwriting, a duplicate recognized) and the person as its origin.
35
37
  api: {
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { allowAnyMcpTool } from "./core/mcpPermissions.js";
5
5
  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
- export { createFriendlyToolLabel, truncate, truncatePath, withToolLabels, withToolPhrases, type ToolDescriber, type ToolLabel, type ToolLabels, } from "./core/toolLabels.js";
8
+ export { createFriendlyToolLabel, truncate, truncatePath, withToolLabels, withToolPhrases, withToolResults, quietResults, type ToolResult, type ToolDescriber, type ToolLabel, type ToolLabels, } from "./core/toolLabels.js";
9
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";
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ export { allowAnyMcpTool } from "./core/mcpPermissions.js";
7
7
  export { createPromptLoader } from "./core/promptTemplate.js";
8
8
  export { resolveClaudeAuth } from "./core/claudeAuth.js";
9
9
  export { ensureClaudeAuth } from "./tui/claudeAuth.js";
10
- export { createFriendlyToolLabel, truncate, truncatePath, withToolLabels, withToolPhrases, } from "./core/toolLabels.js";
10
+ export { createFriendlyToolLabel, truncate, truncatePath, withToolLabels, withToolPhrases, withToolResults, quietResults, } from "./core/toolLabels.js";
11
11
  export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, togglePlanMode } from "./core/session.js";
12
12
  export { runQuery } from "./core/runner.js";
13
13
  export { knowledgePluginRoot, knowledgePromptSection } from "./extensions/knowledge/prompt.js";
@@ -9,6 +9,30 @@ import { trustWorkspace } from "../core/workspaceTrust.js";
9
9
  import { describeCapability, describeInLanguage } from "../core/extensions.js";
10
10
  import { getLanguage } from "../core/messages/index.js";
11
11
  export { chatExtensionsCommand, describe } from "../chat/extensions.js";
12
+ /**
13
+ * Each action's usage and the flags it takes (#81): `--help` prints the usage, and a flag it
14
+ * doesn't take fails instead of being ignored, so a command that changes extensions never runs
15
+ * on a guess (`update --help` updated them all).
16
+ */
17
+ const ACTIONS = {
18
+ list: { usage: ["list"], flags: [] },
19
+ info: { usage: ["info <name>"], flags: [] },
20
+ add: {
21
+ usage: ["add <folder | git URL[#ref] | plugin[@marketplace]> [--project | --agent] [--path <subfolder>] [--set key=value…] [--yes]", "add <folder> --link [--project]"],
22
+ flags: ["project", "agent", "path", "set", "yes", "link"],
23
+ },
24
+ remove: { usage: ["remove <name> [--project | --agent] [--yes | --keep-data]"], flags: ["project", "agent", "yes", "keep-data"] },
25
+ enable: { usage: ["enable <name> [--project | --agent]"], flags: ["project", "agent"] },
26
+ disable: { usage: ["disable <name> [--project | --agent]"], flags: ["project", "agent"] },
27
+ config: { usage: ["config <name> [key=value… | key… | --unset key… | confirm=on|off|reset] [--project | --agent]"], flags: ["project", "agent", "unset"] },
28
+ update: { usage: ["update [name] [--yes]"], flags: ["yes"] },
29
+ restore: { usage: ["restore [--trust]"], flags: ["trust"] },
30
+ trust: { usage: ["trust [name…]"], flags: [] },
31
+ search: { usage: ["search [words]"], flags: [] },
32
+ marketplace: { usage: ["marketplace add <folder | git URL[#ref] | owner/repo> [--yes]", "marketplace list | update [name] | remove <name>"], flags: ["yes"] },
33
+ };
34
+ /** Flags followed by a value of their own, which isn't a flag. */
35
+ const VALUE_FLAGS = ["--path", "--set", "--unset"];
12
36
  /**
13
37
  * The `extension` command an agent exposes in its own binary (#37), e.g. `captain extension add
14
38
  * ./jokebook --project`: installs, removes, enables, disables and lists the extensions in its two
@@ -101,6 +125,19 @@ export async function runExtensionCommand(argv, options) {
101
125
  const confirmed = async (question, accepts) => flag("yes") || accepts((await ask(question)).trim());
102
126
  // The agent's own marketplace, known without asking; a folder is copied afresh each time (it's the agent's own code).
103
127
  const knownMarketplaces = () => catalogMarketplaces(options.dirs, options.official);
128
+ // Its usage with --help, and a flag it doesn't take refused, before anything changes (#81).
129
+ const known = ACTIONS[action ?? ""];
130
+ if (known) {
131
+ const usage = known.usage.map((line, index) => `${index === 0 ? "Usage: " : " "}${command} extension ${line}`).join("\n");
132
+ const flags = rest.filter((arg, index) => (arg.startsWith("--") || arg === "-h") && !VALUE_FLAGS.includes(rest[index - 1]));
133
+ if (flags.some((arg) => arg === "--help" || arg === "-h")) {
134
+ write(usage);
135
+ return true;
136
+ }
137
+ const unknown = flags.filter((arg) => !known.flags.includes(arg.slice(2)));
138
+ if (unknown.length)
139
+ return fail(`${command} extension ${action} doesn't take ${unknown.join(", ")}.\n${usage}`);
140
+ }
104
141
  try {
105
142
  switch (action) {
106
143
  case "list": {
@@ -198,6 +198,7 @@ export async function runChatInk(options, tuiOptions = {}) {
198
198
  toolLabels: tuiOptions.toolLabels,
199
199
  formatAction: tuiOptions.formatAction,
200
200
  toolPhrase: tuiOptions.toolPhrase,
201
+ formatResult: tuiOptions.formatResult,
201
202
  ...(tuiOptions.openBrowser !== undefined ? { openBrowser: tuiOptions.openBrowser } : {}),
202
203
  promptSuggestions: tuiOptions.promptSuggestions ?? true,
203
204
  // The /resume list in place of the prompt.
@@ -221,7 +222,8 @@ export async function runChatInk(options, tuiOptions = {}) {
221
222
  formatAction: controller.formatAction,
222
223
  toolPhrase: controller.toolPhrase,
223
224
  toolDetail: tuiOptions.toolDetail,
224
- formatResult: tuiOptions.formatResult,
225
+ // The agent's, then the labels' (#79).
226
+ formatResult: controller.formatResult,
225
227
  agentLabel: tuiOptions.agentLabel,
226
228
  width: () => liveWidth(stdout.columns),
227
229
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falkenslab/agent-kit",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "description": "Library for building an agent on top of the Claude Agent SDK",
5
5
  "author": "Francisco Vargas Ruiz",
6
6
  "license": "MIT",
@@ -35,7 +35,12 @@
35
35
  "items": { "type": "string", "minLength": 1 }
36
36
  },
37
37
  "confirmTools": {
38
- "description": "Its tools that always ask the person in guided mode (short names), even inside a batch they approved, and are never offered \"always\": what creates, edits or publishes in their account. Without it, its servers' destructiveHint says which.",
38
+ "description": "Its tools that always ask the person in guided mode (short names), on every call, even inside a batch they approved, never offered \"this session\" or \"always\": what creates, edits or publishes in their account. Without it, its servers' destructiveHint says which.",
39
+ "type": "array",
40
+ "items": { "type": "string", "minLength": 1 }
41
+ },
42
+ "serverConfirmTools": {
43
+ "description": "Its tools whose server asks the person itself before acting, through MCP elicitation (short names): guided mode doesn't ask too while the person can be asked, so it's one question, the server's. Plan mode still stops them; autonomous mode still denies them unless the agent allows them.",
39
44
  "type": "array",
40
45
  "items": { "type": "string", "minLength": 1 }
41
46
  },
@@ -57,6 +62,11 @@
57
62
  "prefixItems": [{ "type": "string" }, { "type": "string" }],
58
63
  "minItems": 2,
59
64
  "maxItems": 2
65
+ },
66
+ "result": {
67
+ "description": "The line under the call when it succeeded, in this language, instead of the result's first line (written for the model, in English). A failure still shows its own.",
68
+ "type": "string",
69
+ "minLength": 1
60
70
  }
61
71
  }
62
72
  }
@@ -1,94 +1,133 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://falkenslab.github.io/agent-kit/schemas/marketplace-catalog.schema.json",
4
- "title": "A marketplace's catalog.json",
5
- "description": "A Claude Code marketplace's extensions as one file, for a page or a client to show without cloning the repository: written by @falkenslab/agent-kit-catalog from .claude-plugin/marketplace.json and each extension's plugin.json.",
6
- "type": "object",
7
- "additionalProperties": false,
8
- "required": ["marketplace", "description", "repository", "add", "extensions"],
9
- "properties": {
10
- "marketplace": { "description": "The marketplace's name, as <extension>@<marketplace> names an extension.", "type": "string", "minLength": 1 },
11
- "description": { "type": "string" },
12
- "repository": { "description": "The GitHub repository's page.", "type": "string", "pattern": "^https://github\\.com/[\\w.-]+/[\\w.-]+$" },
13
- "add": {
14
- "description": "How to add the marketplace.",
15
- "type": "object",
16
- "additionalProperties": false,
17
- "required": ["claudeCode", "agentKit"],
18
- "properties": {
19
- "claudeCode": { "type": "string" },
20
- "agentKit": { "description": "After the agent's own command (<agent> …).", "type": "string" }
21
- }
22
- },
23
- "extensions": {
24
- "description": "Sorted by name.",
25
- "type": "array",
26
- "items": { "$ref": "#/$defs/extension" }
27
- }
28
- },
29
- "$defs": {
30
- "url": { "type": "string", "pattern": "^https://" },
31
- "extension": {
32
- "type": "object",
33
- "additionalProperties": false,
34
- "required": ["name", "version", "description", "category", "topics", "author", "license", "homepage", "readme", "install"],
35
- "properties": {
36
- "name": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" },
37
- "version": { "description": "Its plugin.json's, the same as its marketplace entry's.", "type": "string", "minLength": 1 },
38
- "description": { "description": "In English: its marketplace entry's, else its plugin.json's.", "type": "string" },
39
- "descriptions": {
40
- "description": "Its description per language, English among them; only when it has one in another language.",
41
- "type": "object",
42
- "propertyNames": { "enum": ["en", "es", "fr", "de"] },
43
- "required": ["en"],
44
- "additionalProperties": { "type": "string" }
45
- },
46
- "category": { "type": ["string", "null"] },
47
- "topics": {
48
- "description": "Its plugin.json's keywords and its entry's tags, keywords and category, lowercased, without repeats.",
49
- "type": "array",
50
- "uniqueItems": true,
51
- "items": { "type": "string", "pattern": "^[^A-Z]+$" }
52
- },
53
- "author": { "type": ["string", "null"] },
54
- "license": { "type": ["string", "null"] },
55
- "homepage": { "$ref": "#/$defs/url" },
56
- "readme": { "$ref": "#/$defs/url" },
57
- "readmes": {
58
- "description": "Its README per language (README.md, README.es.md…), as URLs; only when it has one in another language.",
59
- "type": "object",
60
- "propertyNames": { "enum": ["en", "es", "fr", "de"] },
61
- "required": ["en"],
62
- "additionalProperties": { "$ref": "#/$defs/url" }
63
- },
64
- "install": {
65
- "type": "object",
66
- "additionalProperties": false,
67
- "required": ["claudeCode", "agentKit"],
68
- "properties": {
69
- "claudeCode": { "type": "string" },
70
- "agentKit": { "description": "After the agent's own command (<agent> …).", "type": "string" }
71
- }
72
- },
73
- "capabilities": {
74
- "description": "What it lets the agent do: its metadata[\"agent-kit\"].capabilities as it declares them, texts in every language it gives.",
75
- "type": "array",
76
- "items": { "$ref": "https://falkenslab.github.io/agent-kit/schemas/agent-kit-metadata.schema.json#/$defs/capability" }
77
- },
78
- "requirements": {
79
- "description": "The programs it needs on the computer: its metadata[\"agent-kit\"].requirements as it declares them.",
80
- "type": "array",
81
- "items": { "$ref": "https://falkenslab.github.io/agent-kit/schemas/agent-kit-metadata.schema.json#/$defs/requirement" }
82
- },
83
- "remote": {
84
- "description": "The hosts its remote MCP servers talk to (type http or sse in its .mcp.json): what an agent sends there leaves the computer. Only when it has some.",
85
- "type": "array",
86
- "uniqueItems": true,
87
- "items": { "type": "string", "minLength": 1 }
88
- },
89
- "logo": { "description": "A URL anyone can load.", "$ref": "#/$defs/url" },
90
- "banner": { "description": "A URL anyone can load.", "$ref": "#/$defs/url" }
91
- }
92
- }
93
- }
94
- }
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://falkenslab.github.io/agent-kit/schemas/marketplace-catalog.schema.json",
4
+ "title": "A marketplace's catalog.json",
5
+ "description": "A Claude Code marketplace's extensions as one file, for a page or a client to show without cloning the repository: written by @falkenslab/agent-kit-catalog from .claude-plugin/marketplace.json and each extension's plugin.json.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["marketplace", "description", "repository", "add", "extensions"],
9
+ "properties": {
10
+ "marketplace": { "description": "The marketplace's name, as <extension>@<marketplace> names an extension.", "type": "string", "minLength": 1 },
11
+ "description": { "type": "string" },
12
+ "repository": { "description": "The GitHub repository's page.", "type": "string", "pattern": "^https://github\\.com/[\\w.-]+/[\\w.-]+$" },
13
+ "add": {
14
+ "description": "How to add the marketplace.",
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "required": ["claudeCode", "agentKit"],
18
+ "properties": {
19
+ "claudeCode": { "type": "string" },
20
+ "agentKit": { "description": "After the agent's own command (<agent> …).", "type": "string" }
21
+ }
22
+ },
23
+ "extensions": {
24
+ "description": "Sorted by name.",
25
+ "type": "array",
26
+ "items": { "$ref": "#/$defs/extension" }
27
+ }
28
+ },
29
+ "$defs": {
30
+ "url": { "type": "string", "pattern": "^https://" },
31
+ "named": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["name"], "properties": { "name": { "type": "string", "minLength": 1 }, "description": { "type": "string" } } } },
32
+ "extension": {
33
+ "type": "object",
34
+ "additionalProperties": false,
35
+ "required": ["name", "version", "description", "category", "topics", "author", "license", "homepage", "readme", "install"],
36
+ "properties": {
37
+ "name": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" },
38
+ "version": { "description": "Its plugin.json's, the same as its marketplace entry's.", "type": "string", "minLength": 1 },
39
+ "description": { "description": "In English: its marketplace entry's, else its plugin.json's.", "type": "string" },
40
+ "descriptions": {
41
+ "description": "Its description per language, English among them; only when it has one in another language.",
42
+ "type": "object",
43
+ "propertyNames": { "enum": ["en", "es", "fr", "de"] },
44
+ "required": ["en"],
45
+ "additionalProperties": { "type": "string" }
46
+ },
47
+ "category": { "type": ["string", "null"] },
48
+ "topics": {
49
+ "description": "Its plugin.json's keywords and its entry's tags, keywords and category, lowercased, without repeats.",
50
+ "type": "array",
51
+ "uniqueItems": true,
52
+ "items": { "type": "string", "pattern": "^[^A-Z]+$" }
53
+ },
54
+ "author": { "type": ["string", "null"] },
55
+ "license": { "type": ["string", "null"] },
56
+ "homepage": { "$ref": "#/$defs/url" },
57
+ "readme": { "$ref": "#/$defs/url" },
58
+ "readmes": {
59
+ "description": "Its README per language (README.md, README.es.md…), as URLs; only when it has one in another language.",
60
+ "type": "object",
61
+ "propertyNames": { "enum": ["en", "es", "fr", "de"] },
62
+ "required": ["en"],
63
+ "additionalProperties": { "$ref": "#/$defs/url" }
64
+ },
65
+ "install": {
66
+ "type": "object",
67
+ "additionalProperties": false,
68
+ "required": ["claudeCode", "agentKit"],
69
+ "properties": {
70
+ "claudeCode": { "type": "string" },
71
+ "agentKit": { "description": "After the agent's own command (<agent> …).", "type": "string" }
72
+ }
73
+ },
74
+ "capabilities": {
75
+ "description": "What it lets the agent do: its metadata[\"agent-kit\"].capabilities as it declares them, texts in every language it gives.",
76
+ "type": "array",
77
+ "items": { "$ref": "https://falkenslab.github.io/agent-kit/schemas/agent-kit-metadata.schema.json#/$defs/capability" }
78
+ },
79
+ "requirements": {
80
+ "description": "The programs it needs on the computer: its metadata[\"agent-kit\"].requirements as it declares them.",
81
+ "type": "array",
82
+ "items": { "$ref": "https://falkenslab.github.io/agent-kit/schemas/agent-kit-metadata.schema.json#/$defs/requirement" }
83
+ },
84
+ "remote": {
85
+ "description": "The hosts its remote MCP servers talk to (type http or sse in its .mcp.json): what an agent sends there leaves the computer. Only when it has some.",
86
+ "type": "array",
87
+ "uniqueItems": true,
88
+ "items": { "type": "string", "minLength": 1 }
89
+ },
90
+ "servers": {
91
+ "description": "Its MCP servers (plugin.json's mcpServers, else .mcp.json): a package run with npx, bunx or uvx (its package, version and npm page), a program it brings (bundled: its command as written), a remote server (its transport and host), or another command (its name only). Never an argument's secret nor an env value.",
92
+ "type": "array",
93
+ "items": {
94
+ "type": "object",
95
+ "additionalProperties": false,
96
+ "required": ["name", "kind"],
97
+ "properties": {
98
+ "name": { "type": "string", "minLength": 1 },
99
+ "kind": { "enum": ["package", "bundled", "remote", "command"] },
100
+ "package": { "type": "string" },
101
+ "version": { "type": "string" },
102
+ "url": { "$ref": "#/$defs/url" },
103
+ "command": { "type": "string" },
104
+ "transport": { "enum": ["http", "sse"] },
105
+ "host": { "type": "string" }
106
+ }
107
+ }
108
+ },
109
+ "skills": { "description": "Its skills: each one's folder name and its SKILL.md's description, sorted by name.", "$ref": "#/$defs/named" },
110
+ "commands": { "description": "Its commands: each one's file name and its description, sorted by name.", "$ref": "#/$defs/named" },
111
+ "agents": { "description": "Its subagents: each one's file name and its description, sorted by name.", "$ref": "#/$defs/named" },
112
+ "tools": {
113
+ "description": "The tools its metadata[\"agent-kit\"] knows (labels, capabilities, readOnlyTools, confirmTools, serverConfirmTools), by short name: a static read, a running server may list more.",
114
+ "type": "array",
115
+ "items": {
116
+ "type": "object",
117
+ "additionalProperties": false,
118
+ "required": ["name"],
119
+ "properties": {
120
+ "name": { "type": "string", "minLength": 1 },
121
+ "labels": { "description": "Its label per language, {field} as written.", "type": "object", "propertyNames": { "enum": ["en", "es", "fr", "de"] }, "additionalProperties": { "type": "string" } },
122
+ "readOnly": { "description": "It only reads.", "const": true },
123
+ "confirm": { "description": "always: the person says yes before every call; server: its server asks the person itself.", "enum": ["always", "server"] }
124
+ }
125
+ }
126
+ },
127
+ "kit": { "description": "The agent-kit versions it works with, as its metadata[\"agent-kit\"].kit says.", "type": "string" },
128
+ "logo": { "description": "A URL anyone can load.", "$ref": "#/$defs/url" },
129
+ "banner": { "description": "A URL anyone can load.", "$ref": "#/$defs/url" }
130
+ }
131
+ }
132
+ }
133
+ }