@falkenslab/agent-kit 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/chat/chatController.d.ts +10 -0
- package/dist/chat/chatController.js +39 -1
- package/dist/chat/extensions.d.ts +9 -0
- package/dist/chat/extensions.js +14 -2
- package/dist/chat/runs.d.ts +3 -1
- package/dist/chat/runs.js +1 -1
- package/dist/core/extensionCatalog.d.ts +8 -1
- package/dist/core/extensionCatalog.js +13 -9
- package/dist/core/extensionOptions.d.ts +5 -0
- package/dist/core/extensionOptions.js +10 -0
- package/dist/core/extensions.d.ts +13 -0
- package/dist/core/extensions.js +13 -0
- package/dist/core/externalExtensions.d.ts +27 -1
- package/dist/core/externalExtensions.js +80 -13
- package/dist/core/liveExtensions.d.ts +38 -0
- package/dist/core/liveExtensions.js +69 -0
- package/dist/core/marketplaces.d.ts +1 -0
- package/dist/core/messages/de.js +10 -1
- package/dist/core/messages/en.d.ts +14 -0
- package/dist/core/messages/en.js +10 -1
- package/dist/core/messages/es.js +10 -1
- package/dist/core/messages/fr.js +10 -1
- package/dist/core/npxPackages.d.ts +2 -2
- package/dist/core/runner.js +17 -0
- package/dist/core/session.d.ts +6 -2
- package/dist/core/session.js +5 -2
- package/dist/core/sessionFacts.d.ts +14 -0
- package/dist/extensions/awareness/catalog.d.ts +8 -0
- package/dist/extensions/awareness/catalog.js +92 -0
- package/dist/extensions/awareness/index.d.ts +3 -1
- package/dist/extensions/awareness/index.js +15 -6
- package/dist/extensions/awareness/labels.js +8 -5
- package/dist/extensions/awareness/tools.d.ts +2 -2
- package/dist/extensions/awareness/tools.js +4 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/tui/extensionCommand.js +17 -9
- package/extensions/awareness/skills/extensions/SKILL.md +56 -0
- package/package.json +1 -1
- package/schemas/agent-kit-metadata.schema.json +6 -0
- package/schemas/marketplace-catalog.schema.json +21 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { tool } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { describeCapability } from "../../core/extensions.js";
|
|
4
|
+
import { extensionCatalog, knownMarketplaces, marketplacesDir } from "../../core/extensionCatalog.js";
|
|
5
|
+
import { extensionOptionStates, listInstalled } from "../../core/externalExtensions.js";
|
|
6
|
+
import { updateMarketplace } from "../../core/marketplaces.js";
|
|
7
|
+
/**
|
|
8
|
+
* The agent as an advisor on extensions (#64): `extensions_catalog` tells it what's installed and
|
|
9
|
+
* how it stands, what its catalogs offer (from their copies on this computer, instant and offline),
|
|
10
|
+
* and, with `refresh`, what's new in them, so it can suggest an extension the person lacks. It
|
|
11
|
+
* never installs, enables or configures anything: refreshing renews the catalogs' copies only, so
|
|
12
|
+
* the tool stays read-only for plan mode.
|
|
13
|
+
*/
|
|
14
|
+
const ok = (text) => ({ content: [{ type: "text", text }] });
|
|
15
|
+
/** How long ago an ISO date was, as the model reads it. */
|
|
16
|
+
function age(iso, now) {
|
|
17
|
+
const days = Math.floor((now - Date.parse(iso)) / 86_400_000);
|
|
18
|
+
return Number.isNaN(days) ? "at an unknown time" : days <= 0 ? "today" : days === 1 ? "yesterday" : `${days} days ago`;
|
|
19
|
+
}
|
|
20
|
+
/** A catalog entry in one line: what installs it, what it is and does, what it needs, where it talks to, how it stands here. */
|
|
21
|
+
function describeEntry(entry) {
|
|
22
|
+
const parts = [`- **${entry.spec}**${entry.version ? ` ${entry.version}` : ""}: ${entry.description || "(no description)"}`];
|
|
23
|
+
if (entry.capabilities.length)
|
|
24
|
+
parts.push(`Can: ${entry.capabilities.map(({ id, label }) => `${label} (${id})`).join(", ")}.`);
|
|
25
|
+
if (entry.requirements.length)
|
|
26
|
+
parts.push(`Needs on the computer: ${entry.requirements.map(({ name, for: needed }) => (needed.length ? `${name} (for ${needed.join(", ")})` : name)).join(", ")}.`);
|
|
27
|
+
if (entry.remote.length)
|
|
28
|
+
parts.push(`Talks to ${entry.remote.join(", ")}.`);
|
|
29
|
+
if (entry.topics.length)
|
|
30
|
+
parts.push(`Topics: ${entry.topics.join(", ")}.`);
|
|
31
|
+
parts.push(entry.installed ? (entry.update ? `Installed, with an update here: \`/extensions update ${entry.plugin.name}\`.` : "Installed.") : `Not installed: \`/extensions install ${entry.spec}\`.`);
|
|
32
|
+
return parts.join(" ");
|
|
33
|
+
}
|
|
34
|
+
/** The awareness's catalog tool, over the agent's extension folders and its own marketplace and topics. */
|
|
35
|
+
export function createCatalogTool(config, session) {
|
|
36
|
+
const dirs = config.extensionDirs;
|
|
37
|
+
const official = config.officialMarketplace;
|
|
38
|
+
const topics = config.catalogTopics;
|
|
39
|
+
// At most one fetch per marketplace and session: a second "what's new" answers from that copy.
|
|
40
|
+
const refreshed = new Set();
|
|
41
|
+
return tool("extensions_catalog", "Your extensions and your catalogs: what's installed here and how it stands (running, or off and why, options missing), what the catalogs you know offer (from their copies on this computer, with each copy's age) and, with `refresh: true`, what's new in them online. With `words`, only what matches (name, description, capabilities, topics). Read-only: it never installs, enables or configures anything; installing is the person's, with `/extensions install <name>@<marketplace>`.", {
|
|
42
|
+
words: z.string().optional().describe("A few words of the task or extension to look for, e.g. `grading rubrics` or `weather`. Without them, everything."),
|
|
43
|
+
refresh: z.boolean().optional().describe("Fetch the catalogs again from their source first, and say what's new: only when the person asks what's new, or the copies are over a week old."),
|
|
44
|
+
}, async ({ words, refresh }) => {
|
|
45
|
+
if (!dirs || !marketplacesDir(dirs))
|
|
46
|
+
return ok("This agent has no folder for extensions: it can't install any. Say so if the person asks for one.");
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const sections = [];
|
|
49
|
+
// What's installed, and how it stands in this session.
|
|
50
|
+
const facts = await session().catch(() => undefined);
|
|
51
|
+
const running = new Set(facts?.extensions.active.map(({ name }) => name) ?? []);
|
|
52
|
+
const off = new Map(facts?.extensions.inactive.map(({ name, reason }) => [name, reason]) ?? []);
|
|
53
|
+
const installed = (await listInstalled(dirs)).filter((extension) => !extension.shadowed);
|
|
54
|
+
const installedLines = await Promise.all(installed.map(async (extension) => {
|
|
55
|
+
const state = !extension.lock.enabled ? "disabled (`/extensions enable " + extension.name + "`)" : running.has(extension.name) ? "running" : off.has(extension.name) ? `off: it ${off.get(extension.name)}` : "enabled, from the next session";
|
|
56
|
+
const missing = (await extensionOptionStates(extension).catch(() => [])).filter(({ option, set }) => option.required && !set).map(({ key }) => key);
|
|
57
|
+
const can = (extension.manifest?.capabilities ?? []).map((capability) => describeCapability(capability, "en").label);
|
|
58
|
+
return `- **${extension.name}**${extension.manifest?.version ? ` ${extension.manifest.version}` : ""} (${extension.scope} scope): ${state}.${can.length ? ` Can: ${can.join(", ")}.` : ""}${missing.length ? ` Options to set: ${missing.join(", ")} (\`/extensions config ${extension.name}\`; a secret through the agent's settings or \`extension config\`, never in the chat).` : ""}`;
|
|
59
|
+
}));
|
|
60
|
+
sections.push(`## Installed here\n\n${installedLines.join("\n") || "None."}`);
|
|
61
|
+
// Fetching the catalogs again, when asked: what's new, compared with the copies before.
|
|
62
|
+
let before = [];
|
|
63
|
+
const failed = [];
|
|
64
|
+
if (refresh) {
|
|
65
|
+
before = await extensionCatalog(dirs, { ...(official ? { official } : {}), ...(topics?.length ? { topics } : {}), language: "en" }).catch(() => []);
|
|
66
|
+
for (const marketplace of await knownMarketplaces(dirs, official).catch(() => [])) {
|
|
67
|
+
if (refreshed.has(marketplace.name))
|
|
68
|
+
continue;
|
|
69
|
+
refreshed.add(marketplace.name);
|
|
70
|
+
await updateMarketplace(marketplacesDir(dirs), marketplace.name).catch(() => failed.push(marketplace.name));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const known = await knownMarketplaces(dirs, official).catch(() => []);
|
|
74
|
+
const entries = await extensionCatalog(dirs, {
|
|
75
|
+
...(official ? { official } : {}),
|
|
76
|
+
...(topics?.length ? { topics } : {}),
|
|
77
|
+
...(words?.trim() ? { words: words.trim().split(/\s+/) } : {}),
|
|
78
|
+
language: "en",
|
|
79
|
+
}).catch(() => []);
|
|
80
|
+
const copies = known.map((marketplace) => `${marketplace.name}${marketplace.official ? " (yours)" : ""}, copied ${age(marketplace.updated, now)}`).join("; ");
|
|
81
|
+
sections.push(`## Offered by your catalogs\n\n${known.length ? `From their copies on this computer: ${copies}.` : "No catalog is known: the person adds one with `/extensions` (a marketplace)."}${words?.trim() ? ` Matching "${words.trim()}".` : ""}\n\n${entries.map(describeEntry).join("\n") || "Nothing matches."}`);
|
|
82
|
+
if (refresh) {
|
|
83
|
+
const was = new Map(before.map((entry) => [entry.spec, entry.version]));
|
|
84
|
+
const added = entries.filter((entry) => !was.has(entry.spec));
|
|
85
|
+
const updates = entries.filter((entry) => entry.update);
|
|
86
|
+
const lines = [...added.map((entry) => `- New: **${entry.spec}**${entry.version ? ` ${entry.version}` : ""}: ${entry.description}`), ...updates.map((entry) => `- A newer version of **${entry.plugin.name}**: ${entry.version}.`)];
|
|
87
|
+
const unreachable = failed.length ? `\n\nCouldn't fetch ${failed.join(", ")} (no network, or it's unavailable): what's above is its copy on this computer.` : "";
|
|
88
|
+
sections.push(`## What's new online\n\n${lines.join("\n") || "Nothing new since the last copies."}${unreachable}`);
|
|
89
|
+
}
|
|
90
|
+
return ok(sections.join("\n\n"));
|
|
91
|
+
}, { annotations: { readOnlyHint: true } });
|
|
92
|
+
}
|
|
@@ -3,7 +3,9 @@ import type { Extension } from "../../core/extensions.js";
|
|
|
3
3
|
/** Absolute path of the awareness extension's plugin, next to `dist/` (or `src/` under tsx). */
|
|
4
4
|
export declare function awarenessPluginRoot(): string;
|
|
5
5
|
/** The "Who you are" section of the system prompt: name, version, what it is, the kit's version, and where to look. */
|
|
6
|
-
export declare function identityPromptSection(identity: AgentIdentity
|
|
6
|
+
export declare function identityPromptSection(identity: AgentIdentity, options?: {
|
|
7
|
+
catalog?: boolean;
|
|
8
|
+
}): string;
|
|
7
9
|
/** The awareness extension's options. */
|
|
8
10
|
export interface AwarenessOptions {
|
|
9
11
|
/**
|
|
@@ -3,12 +3,13 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import { agentKitVersion } from "../../core/version.js";
|
|
4
4
|
import { awarenessToolLabels } from "./labels.js";
|
|
5
5
|
import { createAwarenessServer } from "./tools.js";
|
|
6
|
+
import { createCatalogTool } from "./catalog.js";
|
|
6
7
|
/** Absolute path of the awareness extension's plugin, next to `dist/` (or `src/` under tsx). */
|
|
7
8
|
export function awarenessPluginRoot() {
|
|
8
9
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "extensions", "awareness");
|
|
9
10
|
}
|
|
10
11
|
/** The "Who you are" section of the system prompt: name, version, what it is, the kit's version, and where to look. */
|
|
11
|
-
export function identityPromptSection(identity) {
|
|
12
|
+
export function identityPromptSection(identity, options = {}) {
|
|
12
13
|
const who = `${identity.name}${identity.version ? ` ${identity.version}` : ""}${identity.description ? `: ${identity.description}` : ""}`;
|
|
13
14
|
return `## Who you are
|
|
14
15
|
You are ${who}. You are built on agent-kit ${agentKitVersion()}, which provides your chat (its modes, keys and slash commands) and your built-in tools.
|
|
@@ -16,8 +17,14 @@ You are ${who}. You are built on agent-kit ${agentKitVersion()}, which provides
|
|
|
16
17
|
- Your mode is the kit's (guided, interactive, autonomous or plan), not a permission mode you may see elsewhere: you don't know which one is on until \`about_me\` says.
|
|
17
18
|
- When they ask about your domain (your own commands, configuration, folders), \`about_me\` with \`part: "guide"\`.
|
|
18
19
|
- When they ask how to use you (the chat's modes, keys and commands, approvals, resuming, the command line), load the \`help\` skill.
|
|
19
|
-
- Don't invent what none of them says
|
|
20
|
+
- Don't invent what none of them says.${options.catalog ? `\n${CATALOG_RULES}` : ""}`;
|
|
20
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* When the agent can install extensions (#64): look at its catalogs itself, suggest what fits in
|
|
24
|
+
* one sentence, and leave installing to the person. The tool is read-only; these are the rules.
|
|
25
|
+
*/
|
|
26
|
+
const CATALOG_RULES = `- When the person asks for something none of your tools or skills does well (or that an extension of yours that's off would do), call \`extensions_catalog\` with a few words of the task. If an extension there fits, suggest it in one sentence (what it adds, what it needs, and \`/extensions install <name>@<marketplace>\`) and go on with what you can do without it. Suggest only what it returns; never install, enable or configure an extension yourself; once the person declines one, don't suggest it again in this conversation.
|
|
27
|
+
- When they ask what extensions they have, what one does, how to set it up or why it's off, call \`extensions_catalog\` and load the \`extensions\` skill; when they ask what's new, or its copies are over a week old, call it with \`refresh: true\`.`;
|
|
21
28
|
/**
|
|
22
29
|
* The awareness (#43): the agent knows who it is and what it is at each moment of a session,
|
|
23
30
|
* and the person can ask it. A "Who you are" section, `about_me` (the session's facts, read
|
|
@@ -30,11 +37,13 @@ export function awareness(options = {}) {
|
|
|
30
37
|
name: "awareness",
|
|
31
38
|
plugin: awarenessPluginRoot(),
|
|
32
39
|
missing: ({ spec }) => (spec.identity ? undefined : "needs `identity` in the spec"),
|
|
33
|
-
async contribute({ spec, session }) {
|
|
40
|
+
async contribute({ spec, session, config }) {
|
|
41
|
+
// The catalog only where the agent has a folder for extensions: elsewhere it can't install any (#64).
|
|
42
|
+
const catalog = Boolean(config.extensionDirs);
|
|
34
43
|
return {
|
|
35
|
-
mcpServers: { awareness: createAwarenessServer(session, options.guide) },
|
|
36
|
-
promptSection: identityPromptSection(spec.identity),
|
|
37
|
-
readOnlyTools: ["mcp__awareness__about_me"],
|
|
44
|
+
mcpServers: { awareness: createAwarenessServer(session, options.guide, catalog ? [createCatalogTool(config, session)] : []) },
|
|
45
|
+
promptSection: identityPromptSection(spec.identity, { catalog }),
|
|
46
|
+
readOnlyTools: ["mcp__awareness__about_me", ...(catalog ? ["mcp__awareness__extensions_catalog"] : [])],
|
|
38
47
|
toolLabels: awarenessToolLabels(),
|
|
39
48
|
};
|
|
40
49
|
},
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { getLanguage } from "../../core/messages/index.js";
|
|
2
2
|
const TEXTS = {
|
|
3
|
-
en: { now: "Looking at itself", guide: "Reading its own guide", phrase: ["looked at itself", "looked at itself {n} times"] },
|
|
4
|
-
es: { now: "Mirándose a sí mismo", guide: "Leyendo su propia guía", phrase: ["se miró a sí mismo", "se miró a sí mismo {n} veces"] },
|
|
5
|
-
fr: { now: "Se regarde lui-même", guide: "Lit son propre guide", phrase: ["s'est regardé lui-même", "s'est regardé lui-même {n} fois"] },
|
|
6
|
-
de: { now: "Betrachtet sich selbst", guide: "Liest seine eigene Anleitung", phrase: ["sich selbst betrachtet", "sich selbst {n}-mal betrachtet"] },
|
|
3
|
+
en: { now: "Looking at itself", guide: "Reading its own guide", phrase: ["looked at itself", "looked at itself {n} times"], catalog: "Looking at its extensions", catalogNew: "Looking for new extensions", catalogPhrase: ["looked at its extensions", "looked at its extensions {n} times"] },
|
|
4
|
+
es: { now: "Mirándose a sí mismo", guide: "Leyendo su propia guía", phrase: ["se miró a sí mismo", "se miró a sí mismo {n} veces"], catalog: "Mirando sus extensiones", catalogNew: "Buscando extensiones nuevas", catalogPhrase: ["miró sus extensiones", "miró sus extensiones {n} veces"] },
|
|
5
|
+
fr: { now: "Se regarde lui-même", guide: "Lit son propre guide", phrase: ["s'est regardé lui-même", "s'est regardé lui-même {n} fois"], catalog: "Regarde ses extensions", catalogNew: "Cherche de nouvelles extensions", catalogPhrase: ["a regardé ses extensions", "a regardé ses extensions {n} fois"] },
|
|
6
|
+
de: { now: "Betrachtet sich selbst", guide: "Liest seine eigene Anleitung", phrase: ["sich selbst betrachtet", "sich selbst {n}-mal betrachtet"], catalog: "Sieht seine Erweiterungen an", catalogNew: "Sucht neue Erweiterungen", catalogPhrase: ["seine Erweiterungen angesehen", "seine Erweiterungen {n}-mal angesehen"] },
|
|
7
7
|
};
|
|
8
8
|
/** How the chat shows the awareness's tool (server `awareness`), in the kit's language. */
|
|
9
9
|
export function awarenessToolLabels() {
|
|
10
10
|
const texts = TEXTS[getLanguage()];
|
|
11
|
-
return {
|
|
11
|
+
return {
|
|
12
|
+
mcp__awareness__about_me: { label: (input) => (input.part === "guide" ? texts.guide : texts.now), phrase: texts.phrase },
|
|
13
|
+
mcp__awareness__extensions_catalog: { label: (input) => (input.refresh ? texts.catalogNew : texts.catalog), phrase: texts.catalogPhrase },
|
|
14
|
+
};
|
|
12
15
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SessionFacts } from "../../core/sessionFacts.js";
|
|
2
2
|
/** The session's facts as the model reads them: markdown, in English (ADR-019). */
|
|
3
3
|
export declare function describeSession(facts: SessionFacts): string;
|
|
4
|
-
/** The awareness's MCP server: `about_me`, from the session's facts and the agent's guide. */
|
|
5
|
-
export declare function createAwarenessServer(session: () => Promise<SessionFacts>, guide?: string): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
4
|
+
/** The awareness's MCP server: `about_me`, from the session's facts and the agent's guide, and any more tools it's given (the catalog, #64). */
|
|
5
|
+
export declare function createAwarenessServer(session: () => Promise<SessionFacts>, guide?: string, more?: readonly unknown[]): import("@anthropic-ai/claude-agent-sdk").McpSdkServerConfigWithInstance;
|
|
@@ -44,8 +44,8 @@ export function describeSession(facts) {
|
|
|
44
44
|
}
|
|
45
45
|
return sections.join("\n\n");
|
|
46
46
|
}
|
|
47
|
-
/** The awareness's MCP server: `about_me`, from the session's facts and the agent's guide. */
|
|
48
|
-
export function createAwarenessServer(session, guide) {
|
|
47
|
+
/** The awareness's MCP server: `about_me`, from the session's facts and the agent's guide, and any more tools it's given (the catalog, #64). */
|
|
48
|
+
export function createAwarenessServer(session, guide, more = []) {
|
|
49
49
|
const aboutMe = tool("about_me", "What you are right now: your mode and the ones the person can switch to, your extensions with their tools and the ones off (with why), your subagents, the commands the person can type, how full your context is. With `part: \"guide\"`, your own guide to your domain (your commands, configuration, folders). It changes during a session: call it whenever the person asks about you, rather than answering from memory.", { part: z.enum(["now", "guide"]).optional().describe("`now` (the default): what you are right now. `guide`: your own guide to your domain.") }, async ({ part }) => {
|
|
50
50
|
if (part === "guide") {
|
|
51
51
|
if (!guide)
|
|
@@ -54,5 +54,6 @@ export function createAwarenessServer(session, guide) {
|
|
|
54
54
|
}
|
|
55
55
|
return ok(describeSession(await session()));
|
|
56
56
|
}, { annotations: { readOnlyHint: true } });
|
|
57
|
-
|
|
57
|
+
// Tools with different input schemas: as the sources' server does.
|
|
58
|
+
return createSdkMcpServer({ name: "awareness", version: "1.0.0", tools: [aboutMe, ...more] });
|
|
58
59
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export { createFriendlyToolLabel, truncate, truncatePath, withToolLabels, withTo
|
|
|
9
9
|
export type { Mode, BaseSessionConfig, AgentSpec, AgentIdentity, PlanModeSpec } from "./core/agentSpec.js";
|
|
10
10
|
export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, togglePlanMode, type ModeControl, type ExtensionsStatus } from "./core/session.js";
|
|
11
11
|
export { runQuery, type AgentEvent, type AgentRun, type SessionUsage, type ContextUsage } from "./core/runner.js";
|
|
12
|
-
export type { SessionFacts } from "./core/sessionFacts.js";
|
|
12
|
+
export type { McpAuthentication, SessionFacts } from "./core/sessionFacts.js";
|
|
13
13
|
export { knowledgePluginRoot, knowledgePromptSection } from "./extensions/knowledge/prompt.js";
|
|
14
14
|
export { awareness, type AwarenessOptions } from "./extensions/awareness/index.js";
|
|
15
15
|
export { sources, type SourcesOptions } from "./extensions/sources/index.js";
|
|
@@ -17,9 +17,9 @@ export { knowledge, type KnowledgeOptions } from "./extensions/knowledge/index.j
|
|
|
17
17
|
export { memory, type MemoryOptions } from "./extensions/memory/index.js";
|
|
18
18
|
export { describeCapability, readExtensionManifest, type AboutExtension, type CapabilityText, type DescribedCapability, type Extension, type ExtensionCapability, type ExtensionContext, type ExtensionContribution, type ExtensionManifest, type FolderOption, type UnavailableCapability, } from "./core/extensions.js";
|
|
19
19
|
export { checkRequirements, type ExtensionRequirement, type RequirementCheck } from "./core/extensionRequirements.js";
|
|
20
|
-
export { addExtension, codeStoreDir, installTarget, scopePrivateDir, extensionDataDir, extensionOptionStates, setExtensionOptions, removeExtension, setExtensionEnabled, listInstalled, readExternalManifest, type ExtensionAuthor, type ExtensionDirs, type ExtensionScope, type ExternalManifest, type ExternalToolLabel, type PluginServer, type InstalledExtension, type LockEntry, type ExtensionOptionState, } from "./core/externalExtensions.js";
|
|
20
|
+
export { addExtension, codeStoreDir, installTarget, scopePrivateDir, extensionDataDir, extensionOptionStates, setExtensionOptions, removeExtension, setExtensionEnabled, listInstalled, readExternalManifest, type ExtensionAuthor, type ExtensionDirs, type ExtensionScope, type ExternalManifest, type ExternalToolLabel, type PluginServer, type LocalServer, type RemoteServer, isRemoteServer, remoteHosts, remoteServerName, type InstalledExtension, type LockEntry, type ExtensionOptionState, } from "./core/externalExtensions.js";
|
|
21
21
|
export type { ExtensionOption, OptionValue } from "./core/extensionOptions.js";
|
|
22
|
-
export type { ExtensionRestart, ExtensionSwitch } from "./core/liveExtensions.js";
|
|
22
|
+
export type { ExtensionAuth, ExtensionRestart, ExtensionSwitch, SignIn } from "./core/liveExtensions.js";
|
|
23
23
|
export { packageCacheDir, type CachedPackage } from "./core/npxPackages.js";
|
|
24
24
|
export { addMarketplace, findPlugin, inspectMarketplace, installFromMarketplace, listMarketplaces, readMarketplaceManifest, removeMarketplace, resolvePluginSource, updateMarketplace, type KnownMarketplace, type MarketplaceManifest, type MarketplacePlugin, type PluginSource, } from "./core/marketplaces.js";
|
|
25
25
|
export { extensionCatalog, installExtension, restoreExtensions, updateExtension, type CatalogEntry } from "./core/extensionCatalog.js";
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ export { knowledge } from "./extensions/knowledge/index.js";
|
|
|
17
17
|
export { memory } from "./extensions/memory/index.js";
|
|
18
18
|
export { describeCapability, readExtensionManifest, } from "./core/extensions.js";
|
|
19
19
|
export { checkRequirements } from "./core/extensionRequirements.js";
|
|
20
|
-
export { addExtension, codeStoreDir, installTarget, scopePrivateDir, extensionDataDir, extensionOptionStates, setExtensionOptions, removeExtension, setExtensionEnabled, listInstalled, readExternalManifest, } from "./core/externalExtensions.js";
|
|
20
|
+
export { addExtension, codeStoreDir, installTarget, scopePrivateDir, extensionDataDir, extensionOptionStates, setExtensionOptions, removeExtension, setExtensionEnabled, listInstalled, readExternalManifest, isRemoteServer, remoteHosts, remoteServerName, } from "./core/externalExtensions.js";
|
|
21
21
|
export { packageCacheDir } from "./core/npxPackages.js";
|
|
22
22
|
export { addMarketplace, findPlugin, inspectMarketplace, installFromMarketplace, listMarketplaces, readMarketplaceManifest, removeMarketplace, resolvePluginSource, updateMarketplace, } from "./core/marketplaces.js";
|
|
23
23
|
export { extensionCatalog, installExtension, restoreExtensions, updateExtension } from "./core/extensionCatalog.js";
|
|
@@ -2,11 +2,11 @@ import { stat } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createInterface } from "node:readline/promises";
|
|
4
4
|
import { addMarketplace, findPlugin, inspectMarketplace, installFromMarketplace, removeMarketplace, updateMarketplace, } from "../core/marketplaces.js";
|
|
5
|
-
import { addExtension, extensionOptionStates, installTarget, folderSize, isGitSource, listInstalled, removeExtension, setExtensionEnabled, setExtensionOptions, } from "../core/externalExtensions.js";
|
|
5
|
+
import { addExtension, extensionOptionStates, installTarget, folderSize, isGitSource, isRemoteServer, remoteHosts, listInstalled, removeExtension, setExtensionEnabled, setExtensionOptions, } from "../core/externalExtensions.js";
|
|
6
6
|
import { describe, describeOptions } from "../chat/extensions.js";
|
|
7
7
|
import { extensionCatalog, knownMarketplaces as catalogMarketplaces, restoreExtensions, updateExtension } from "../core/extensionCatalog.js";
|
|
8
8
|
import { trustWorkspace } from "../core/workspaceTrust.js";
|
|
9
|
-
import { describeCapability } from "../core/extensions.js";
|
|
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
12
|
/**
|
|
@@ -129,9 +129,12 @@ export async function runExtensionCommand(argv, options) {
|
|
|
129
129
|
// Installed into the workspace by the person: trusted there as it is.
|
|
130
130
|
const trusted = async (name) => void (scope === "project" && (await trustWorkspace(options.dirs, [name])));
|
|
131
131
|
// What it needs and this computer lacks (#63): said, never installed.
|
|
132
|
-
const lacking = (missing) => {
|
|
132
|
+
const lacking = (missing, remote = []) => {
|
|
133
133
|
for (const line of missing)
|
|
134
134
|
write(`It ${line}.`);
|
|
135
|
+
// Where what the agent sends goes (#61).
|
|
136
|
+
if (remote.length)
|
|
137
|
+
write(`It talks to ${remote.join(", ")}: what the agent sends there leaves this computer.`);
|
|
135
138
|
};
|
|
136
139
|
// A plugin of a marketplace, unless it's a folder here or a git URL.
|
|
137
140
|
const isFolder = await stat(path.resolve(source)).then((found) => found.isDirectory(), () => false);
|
|
@@ -142,7 +145,7 @@ export async function runExtensionCommand(argv, options) {
|
|
|
142
145
|
await trusted(result.name);
|
|
143
146
|
await configure(result.name, dir);
|
|
144
147
|
write(`Linked ${result.name} in the ${scope} scope: it runs from ${path.resolve(source)}, whose files aren't checked. A rebuild takes effect when its server starts again (a new session, or \`/extensions disable\` and \`enable\` in the chat).`);
|
|
145
|
-
lacking(result.missing);
|
|
148
|
+
lacking(result.missing, result.remote);
|
|
146
149
|
return true;
|
|
147
150
|
}
|
|
148
151
|
if (!isFolder && !isGitSource(source) && /^[A-Za-z0-9][\w.-]*(@[A-Za-z0-9][\w.-]*)?$/.test(source)) {
|
|
@@ -158,7 +161,7 @@ export async function runExtensionCommand(argv, options) {
|
|
|
158
161
|
await trusted(result.name);
|
|
159
162
|
await configure(result.name, dir);
|
|
160
163
|
write(`${result.replaced ? "Reinstalled" : "Installed"} ${result.name} from ${result.marketplace} in the ${scope} scope (${dir})${result.commit ? `, at ${result.commit.slice(0, 12)}` : ""}. It's enabled: it runs from the next session.`);
|
|
161
|
-
lacking(result.missing);
|
|
164
|
+
lacking(result.missing, result.remote);
|
|
162
165
|
return true;
|
|
163
166
|
}
|
|
164
167
|
const subdir = value("path");
|
|
@@ -166,7 +169,7 @@ export async function runExtensionCommand(argv, options) {
|
|
|
166
169
|
await trusted(result.name);
|
|
167
170
|
await configure(result.name, dir);
|
|
168
171
|
write(`${result.replaced ? "Reinstalled" : "Installed"} ${result.name} in the ${scope} scope (${dir})${result.commit ? `, at ${result.commit.slice(0, 12)}` : ""}. It's enabled: it runs from the next session.`);
|
|
169
|
-
lacking(result.missing);
|
|
172
|
+
lacking(result.missing, result.remote);
|
|
170
173
|
return true;
|
|
171
174
|
}
|
|
172
175
|
case "remove":
|
|
@@ -402,10 +405,13 @@ async function info(extension) {
|
|
|
402
405
|
if (!manifest)
|
|
403
406
|
return [describe(extension), "Its manifest can't be read."];
|
|
404
407
|
const author = manifest.author ? [manifest.author.name, manifest.author.email && `<${manifest.author.email}>`, manifest.author.url && `(${manifest.author.url})`].filter(Boolean).join(" ") : undefined;
|
|
405
|
-
|
|
408
|
+
// Its README in the kit's language when it has one (README.es.md), else the English (#67).
|
|
409
|
+
const language = getLanguage();
|
|
410
|
+
const translated = path.join(extension.dir, `README.${language}.md`);
|
|
411
|
+
const readme = language !== "en" && (await stat(translated).catch(() => null))?.isFile() ? translated : path.join(extension.dir, "README.md");
|
|
406
412
|
const field = (label, value) => (value ? [`${label.padEnd(13)}${value}`] : []);
|
|
407
413
|
return [
|
|
408
|
-
`${manifest.name}${manifest.version ? ` ${manifest.version}` : ""}: ${manifest.description}`,
|
|
414
|
+
`${manifest.name}${manifest.version ? ` ${manifest.version}` : ""}: ${describeInLanguage(manifest.description, manifest.descriptions, language)}`,
|
|
409
415
|
...field("Author", author),
|
|
410
416
|
...field("License", manifest.license),
|
|
411
417
|
...field("Homepage", manifest.homepage),
|
|
@@ -431,7 +437,9 @@ async function info(extension) {
|
|
|
431
437
|
...field("Options", Object.entries(manifest.userConfig)
|
|
432
438
|
.map(([key, option]) => `${key} (${[option.title, option.required && "required", option.sensitive && "secret"].filter(Boolean).join(", ")})`)
|
|
433
439
|
.join(", ") || undefined),
|
|
434
|
-
...field("Variables", Object.values(manifest.servers).flatMap((server) => Object.keys(server.env ?? {})).join(", ") || undefined),
|
|
440
|
+
...field("Variables", Object.values(manifest.servers).flatMap((server) => (isRemoteServer(server) ? [] : Object.keys(server.env ?? {}))).join(", ") || undefined),
|
|
441
|
+
// Where what the agent sends goes (#61): a remote server's host.
|
|
442
|
+
...field("Talks to", remoteHosts(manifest).join(", ") || undefined),
|
|
435
443
|
...field("Installed", `${extension.scope} scope, ${extension.lock.enabled ? "enabled" : "disabled"}, on ${extension.lock.installed}, from ${describe(extension).split(" from ")[1]}`),
|
|
436
444
|
...field("Data", (await stat(extension.dataDir).catch(() => null))?.isDirectory() ? `${extension.dataDir} (${formatSize(await folderSize(extension.dataDir))})` : undefined),
|
|
437
445
|
...field("README", (await stat(readme).catch(() => null))?.isFile() ? readme : undefined),
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: extensions
|
|
3
|
+
description: Helping the person with this agent's extensions - what each installed one lets you do, setting its options, why one is off, what the catalogs offer for a task, signing in to a remote one, installing, updating and removing, adding a catalog. Load it whenever the person asks about extensions, or when you're about to suggest one.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Helping with extensions
|
|
7
|
+
|
|
8
|
+
Extensions add what you can do: tools, skills and the know-how to use them (a Moodle classroom, a weather forecast, a lab). What's installed and what the catalogs offer come from `extensions_catalog`, read anew each time: never answer from memory. Answer in the language you reply in, briefly, with the exact command; commands are typed as written here. Without `extensions_catalog` among your tools, this agent has no folder for extensions and can't install any: say so, and suggest nothing.
|
|
9
|
+
|
|
10
|
+
## What you never do
|
|
11
|
+
|
|
12
|
+
- Install, enable, disable, configure or remove an extension yourself: the person does, with the commands below. You suggest and explain.
|
|
13
|
+
- Ask for a secret (a token, a password) in the chat: what's typed there is kept in its history. Secrets are set in the agent's settings or from a terminal (`<agent> extension config <name> <key>`, which asks without showing it).
|
|
14
|
+
- Suggest an extension the catalog didn't return, or one the person already declined in this conversation.
|
|
15
|
+
|
|
16
|
+
## Suggesting one
|
|
17
|
+
|
|
18
|
+
When the task needs what an extension does and it isn't installed, or it's off: one sentence with what it adds, what it needs (a program, a site, an account) and how to install it, then go on with what you can do without it.
|
|
19
|
+
|
|
20
|
+
> The **weather** extension would give me today's forecast for your city; install it with `/extensions install weather@falkenslab` and tell me your city. Meanwhile, here's what I can plan without it…
|
|
21
|
+
|
|
22
|
+
An extension that talks to a remote service sends what you give it there: say so when the person's data is involved.
|
|
23
|
+
|
|
24
|
+
## Commands in the chat
|
|
25
|
+
|
|
26
|
+
| Command | What it does |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `/extensions` | What's running, what's off and why, what's installed |
|
|
29
|
+
| `/extensions search <words>` | What the catalogs offer |
|
|
30
|
+
| `/extensions install <name>@<marketplace>` | Installs one (one from a catalog that isn't the agent's own asks for confirmation) |
|
|
31
|
+
| `/extensions reload` | Opens the session again with what's installed now, keeping the conversation |
|
|
32
|
+
| `/extensions update [name]` | Updates one, or every one with a newer version |
|
|
33
|
+
| `/extensions remove <name>` | Uninstalls one (`--delete-data` deletes its data too) |
|
|
34
|
+
| `/extensions enable <name>` | Turns one on |
|
|
35
|
+
| `/extensions disable <name>` | Turns one off, keeping it installed |
|
|
36
|
+
| `/extensions config <name> [key=value…]` | Its options, and setting the ones that aren't secret |
|
|
37
|
+
| `/extensions sign-in <name>` | Signs in to its remote service: gives the link to open |
|
|
38
|
+
| `/extensions sign-out <name>` | Signs out of its remote service |
|
|
39
|
+
| `/extensions trust` | Lets the workspace's extensions run |
|
|
40
|
+
|
|
41
|
+
A new catalog (a marketplace) is added from a terminal: `<agent> extension marketplace add <owner/repo>`.
|
|
42
|
+
|
|
43
|
+
## Why one is off
|
|
44
|
+
|
|
45
|
+
`extensions_catalog` and `/extensions` give the reason; explain it and its remedy in plain words:
|
|
46
|
+
|
|
47
|
+
- **An option without a value** (its site, its account): `/extensions config <name> <key>=<value>`, or the settings for a secret.
|
|
48
|
+
- **A program missing on the computer** (Docker, ffmpeg): what it is, and the install command for this system the reason gives. The extension never installs it.
|
|
49
|
+
- **It needs signing in**: `/extensions sign-in <name>`, then open the link.
|
|
50
|
+
- **It requires another extension's capability**: install or enable the one that provides it.
|
|
51
|
+
- **Its files changed** or **its workspace isn't trusted**: reinstall it, or `/extensions trust`.
|
|
52
|
+
- **This agent's kit version is outside its range**: an update of the agent or of the extension.
|
|
53
|
+
|
|
54
|
+
## What's new
|
|
55
|
+
|
|
56
|
+
`extensions_catalog` with `refresh: true` fetches the catalogs again and says what's new: extensions added since the last copy, and newer versions of installed ones (`/extensions update`). Without network it answers from the copies on this computer and says how old they are.
|
package/package.json
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
"type": "string",
|
|
12
12
|
"pattern": "^\\s*(?:(?:>=|<=|>|<|=)?\\s*v?\\d+(?:\\.\\d+){0,2})(?:\\s+(?:>=|<=|>|<|=)?\\s*v?\\d+(?:\\.\\d+){0,2})*\\s*$"
|
|
13
13
|
},
|
|
14
|
+
"description": {
|
|
15
|
+
"description": "Its description in other languages than English, which stays plugin.json's own description (Claude Code reads that one).",
|
|
16
|
+
"type": "object",
|
|
17
|
+
"propertyNames": { "enum": ["es", "fr", "de"] },
|
|
18
|
+
"additionalProperties": { "type": "string", "minLength": 1 }
|
|
19
|
+
},
|
|
14
20
|
"provides": { "description": "Capabilities it lets the agent use, by name only. Its capabilities' ids count as provided too.", "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
15
21
|
"capabilities": {
|
|
16
22
|
"description": "What it lets the agent do, for the person: each one's skills and tools together, described per language. Its id is what requires names; neutral ids (lms-grading) let skills depend on what can be done, not on which extension does it.",
|
|
@@ -35,7 +35,14 @@
|
|
|
35
35
|
"properties": {
|
|
36
36
|
"name": { "type": "string", "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$" },
|
|
37
37
|
"version": { "description": "Its plugin.json's, the same as its marketplace entry's.", "type": "string", "minLength": 1 },
|
|
38
|
-
"description": { "type": "string" },
|
|
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
|
+
},
|
|
39
46
|
"category": { "type": ["string", "null"] },
|
|
40
47
|
"topics": {
|
|
41
48
|
"description": "Its plugin.json's keywords and its entry's tags, keywords and category, lowercased, without repeats.",
|
|
@@ -47,6 +54,13 @@
|
|
|
47
54
|
"license": { "type": ["string", "null"] },
|
|
48
55
|
"homepage": { "$ref": "#/$defs/url" },
|
|
49
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
|
+
},
|
|
50
64
|
"install": {
|
|
51
65
|
"type": "object",
|
|
52
66
|
"additionalProperties": false,
|
|
@@ -66,6 +80,12 @@
|
|
|
66
80
|
"type": "array",
|
|
67
81
|
"items": { "$ref": "https://falkenslab.github.io/agent-kit/schemas/agent-kit-metadata.schema.json#/$defs/requirement" }
|
|
68
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
|
+
},
|
|
69
89
|
"logo": { "description": "A URL anyone can load.", "$ref": "#/$defs/url" },
|
|
70
90
|
"banner": { "description": "A URL anyone can load.", "$ref": "#/$defs/url" }
|
|
71
91
|
}
|