@falkenslab/agent-kit 0.29.0 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chat/extensions.d.ts +3 -2
- package/dist/chat/extensions.js +14 -5
- package/dist/core/hooks/guidedGate.js +7 -1
- package/dist/core/hooks/signInReminder.d.ts +24 -0
- package/dist/core/hooks/signInReminder.js +43 -0
- package/dist/core/messages/de.js +1 -0
- package/dist/core/messages/en.d.ts +2 -0
- package/dist/core/messages/en.js +1 -0
- package/dist/core/messages/es.js +1 -0
- package/dist/core/messages/fr.js +1 -0
- package/dist/core/projectServers.d.ts +30 -0
- package/dist/core/projectServers.js +51 -0
- package/dist/core/session.d.ts +3 -0
- package/dist/core/session.js +30 -3
- package/dist/core/sessionFacts.d.ts +16 -0
- package/dist/core/sessionFacts.js +7 -1
- package/dist/extensions/awareness/tools.js +11 -0
- package/dist/tui/extensionCommand.js +37 -0
- package/dist/tui/ink/SessionView.d.ts +15 -0
- package/dist/tui/ink/SessionView.js +21 -1
- package/package.json +1 -1
- package/schemas/marketplace-catalog.schema.json +133 -94
|
@@ -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";
|
package/dist/chat/extensions.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { askForChoice } from "./humanInput.js";
|
|
2
2
|
import { readLock, setExtensionConfirm } from "../externalExtensions.js";
|
|
3
3
|
import { t } from "../messages/index.js";
|
|
4
|
+
/** How much of a labelled tool's parameters its panel shows, as one line (#84). */
|
|
5
|
+
const MAX_PARAMETERS = 300;
|
|
4
6
|
/** The kit's approval tool: asking it ends a batch, and its yes starts one. */
|
|
5
7
|
const APPROVAL_TOOL = "mcp__approvals__request_human_approval";
|
|
6
8
|
/**
|
|
@@ -70,9 +72,13 @@ export function createGuidedGate(options) {
|
|
|
70
72
|
// For one that always asks, only what covers this call, or makes it stricter (#78).
|
|
71
73
|
const choices = alwaysAsks ? [answers.yes, answers.no, answers.never] : [answers.yes, answers.session, answers.always, answers.no, answers.never];
|
|
72
74
|
const label = options.labels?.()[full]?.label((pre.tool_input ?? {}));
|
|
75
|
+
// With a label saying what it does, its parameters in one line, cut short (#84): a rubric or a
|
|
76
|
+
// page's HTML as indented JSON filled the panel. Without one, the parameters are all it shows.
|
|
77
|
+
const compact = JSON.stringify(pre.tool_input ?? {});
|
|
78
|
+
const parameters = label ? (compact.length > MAX_PARAMETERS ? `${compact.slice(0, MAX_PARAMETERS)}…` : compact) : JSON.stringify(pre.tool_input, null, 2);
|
|
73
79
|
const answer = await askForChoice(options.runDir, {
|
|
74
80
|
title: t().guidedTitle(extension.name),
|
|
75
|
-
lines: [...(label ? [label] : []), t().toolLine(full), t().parametersLine(
|
|
81
|
+
lines: [...(label ? [label] : []), t().toolLine(full), t().parametersLine(parameters)],
|
|
76
82
|
}, choices);
|
|
77
83
|
const picked = answer.chosen[0];
|
|
78
84
|
if (picked === answers.yes || picked === answers.session || picked === answers.always) {
|
|
@@ -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())]);
|
package/dist/core/messages/de.js
CHANGED
|
@@ -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;
|
package/dist/core/messages/en.js
CHANGED
|
@@ -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)",
|
package/dist/core/messages/es.js
CHANGED
|
@@ -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)`,
|
package/dist/core/messages/fr.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/core/session.d.ts
CHANGED
|
@@ -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
|
package/dist/core/session.js
CHANGED
|
@@ -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
|
|
@@ -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 (
|
|
404
|
-
...(guidedGate
|
|
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:
|
|
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,
|
|
@@ -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__"));
|
|
@@ -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": {
|
|
@@ -6,6 +6,21 @@ import { type SessionModel } from "./sessionModel.js";
|
|
|
6
6
|
import type { InkInteraction } from "./inkInteraction.js";
|
|
7
7
|
/** Replaces the default preview (title and lines) of an approval panel; the choices stay. */
|
|
8
8
|
export type RenderApproval = (prompt: ApprovalPrompt) => ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* The rows a checkpoint panel needs besides its preview, by its kind (#84): a choice has its
|
|
11
|
+
* options, "Other" and a line of keys, so the guided gate's five answers took four rows more than a
|
|
12
|
+
* decision's, and pushed them below the screen under a tool's long parameters. `reservedRows` is
|
|
13
|
+
* what the screen keeps for a decision's panel; the difference in frame is added to it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function panelReservedRows(checkpoint: {
|
|
16
|
+
kind: string;
|
|
17
|
+
prompt: {
|
|
18
|
+
question?: string;
|
|
19
|
+
};
|
|
20
|
+
choice?: {
|
|
21
|
+
options: readonly string[];
|
|
22
|
+
};
|
|
23
|
+
}, reservedRows?: number): number;
|
|
9
24
|
/**
|
|
10
25
|
* The preview's lines, capped so the whole live area stays shorter than the terminal: Ink
|
|
11
26
|
* clears the entire screen (scrollback included) to redraw anything taller, and a step-gate
|
|
@@ -28,6 +28,26 @@ const DECISION_KEYS = { "1": "y", "2": "n", "3": "q", y: "y", n: "n", q: "q" };
|
|
|
28
28
|
const PANEL_RESERVED_ROWS = 12;
|
|
29
29
|
/** The panel's border and horizontal padding, on both sides. */
|
|
30
30
|
const PANEL_FRAME_COLUMNS = 4;
|
|
31
|
+
/** The rows `PANEL_RESERVED_ROWS` counts for the panel's own frame: a decision's (borders, title, a gap, "Proceed?", its three options). */
|
|
32
|
+
const DECISION_FRAME_ROWS = 8;
|
|
33
|
+
/**
|
|
34
|
+
* The rows a checkpoint panel needs besides its preview, by its kind (#84): a choice has its
|
|
35
|
+
* options, "Other" and a line of keys, so the guided gate's five answers took four rows more than a
|
|
36
|
+
* decision's, and pushed them below the screen under a tool's long parameters. `reservedRows` is
|
|
37
|
+
* what the screen keeps for a decision's panel; the difference in frame is added to it.
|
|
38
|
+
*/
|
|
39
|
+
export function panelReservedRows(checkpoint, reservedRows = PANEL_RESERVED_ROWS) {
|
|
40
|
+
const question = checkpoint.prompt.question ? 1 : 0;
|
|
41
|
+
// Borders and the title (3), then each kind's own rows.
|
|
42
|
+
const frame = checkpoint.kind === "decision"
|
|
43
|
+
? DECISION_FRAME_ROWS
|
|
44
|
+
: checkpoint.kind === "choice"
|
|
45
|
+
? 3 + question + 1 + (checkpoint.choice?.options.length ?? 0) + 1 + 1
|
|
46
|
+
: checkpoint.kind === "text"
|
|
47
|
+
? 3 + question + 1 + 1 + 1
|
|
48
|
+
: 3 + question + 1 + 1;
|
|
49
|
+
return reservedRows - DECISION_FRAME_ROWS + frame;
|
|
50
|
+
}
|
|
31
51
|
/**
|
|
32
52
|
* The preview's lines, capped so the whole live area stays shorter than the terminal: Ink
|
|
33
53
|
* clears the entire screen (scrollback included) to redraw anything taller, and a step-gate
|
|
@@ -62,7 +82,7 @@ function CheckpointPanel({ checkpoint, renderApproval, reservedRows, width, }) {
|
|
|
62
82
|
});
|
|
63
83
|
const { prompt } = checkpoint;
|
|
64
84
|
const { stdout } = useStdout();
|
|
65
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: inkColor("accent"), paddingX: 1, children: [renderApproval ? (renderApproval(prompt)) : (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: prompt.title }), previewLines(prompt.markdown ? renderMarkdown(prompt.lines.join("\n"), width - PANEL_FRAME_COLUMNS) : prompt.lines, stdout.rows, reservedRows, width - PANEL_FRAME_COLUMNS).map((line, index) => (_jsx(Text, { children: line }, index))), !decision && prompt.question ? _jsx(Text, { dimColor: true, children: prompt.question }) : null] })), decision ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: t().proceed }) })) : null, choice && checkpoint.choice ? (_jsx(ChoiceInput, { choice: checkpoint.choice, onAnswer: (value) => checkpoint.answer(value) }, checkpoint.id)) : text ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { children: "> " }), _jsx(TextInput, { placeholder: t().textAnswerHint, onSubmit: (value) => checkpoint.answer(value.trim()) }, checkpoint.id)] }), _jsx(Text, { dimColor: true, children: t().textAnswerKeys })] })) : (_jsx(Box, { marginTop: decision ? 0 : 1, children: _jsx(Select, { options: decision ? decisionOptions() : manualOptions(), onChange: (value) => checkpoint.answer(decision ? value : "") }, checkpoint.id) }))] }));
|
|
85
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: inkColor("accent"), paddingX: 1, children: [renderApproval ? (renderApproval(prompt)) : (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: prompt.title }), previewLines(prompt.markdown ? renderMarkdown(prompt.lines.join("\n"), width - PANEL_FRAME_COLUMNS) : prompt.lines, stdout.rows, panelReservedRows(checkpoint, reservedRows), width - PANEL_FRAME_COLUMNS).map((line, index) => (_jsx(Text, { children: line }, index))), !decision && prompt.question ? _jsx(Text, { dimColor: true, children: prompt.question }) : null] })), decision ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: t().proceed }) })) : null, choice && checkpoint.choice ? (_jsx(ChoiceInput, { choice: checkpoint.choice, onAnswer: (value) => checkpoint.answer(value) }, checkpoint.id)) : text ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { children: "> " }), _jsx(TextInput, { placeholder: t().textAnswerHint, onSubmit: (value) => checkpoint.answer(value.trim()) }, checkpoint.id)] }), _jsx(Text, { dimColor: true, children: t().textAnswerKeys })] })) : (_jsx(Box, { marginTop: decision ? 0 : 1, children: _jsx(Select, { options: decision ? decisionOptions() : manualOptions(), onChange: (value) => checkpoint.answer(decision ? value : "") }, checkpoint.id) }))] }));
|
|
66
86
|
}
|
|
67
87
|
/**
|
|
68
88
|
* A choice's options (one with Select, several with MultiSelect), plus "Other" to type an
|
package/package.json
CHANGED
|
@@ -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
|
-
"
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"
|
|
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
|
+
}
|