@falkenslab/agent-kit 0.19.0 → 0.20.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/dist/chat/chatController.d.ts +4 -1
- package/dist/chat/chatController.js +43 -17
- package/dist/chat/extensions.d.ts +7 -3
- package/dist/chat/extensions.js +11 -8
- package/dist/chat/runs.d.ts +3 -0
- package/dist/chat/runs.js +1 -1
- package/dist/core/agentSpec.d.ts +20 -56
- package/dist/core/extensions.d.ts +21 -12
- package/dist/core/extensions.js +8 -18
- package/dist/core/externalExtensions.d.ts +15 -3
- package/dist/core/externalExtensions.js +33 -5
- package/dist/core/liveExtensions.d.ts +51 -0
- package/dist/core/liveExtensions.js +70 -0
- package/dist/core/marketplaces.d.ts +118 -0
- package/dist/core/marketplaces.js +242 -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/pluginDownloads.d.ts +14 -0
- package/dist/core/pluginDownloads.js +141 -0
- package/dist/core/runner.js +6 -1
- package/dist/core/session.d.ts +11 -4
- package/dist/core/session.js +74 -22
- package/dist/core/sessionFacts.d.ts +5 -0
- package/dist/core/sessionFacts.js +1 -0
- package/dist/extensions/awareness/index.d.ts +12 -3
- package/dist/extensions/awareness/index.js +18 -15
- package/dist/extensions/awareness/tools.d.ts +1 -1
- package/dist/extensions/awareness/tools.js +3 -3
- package/dist/extensions/knowledge/index.d.ts +20 -6
- package/dist/extensions/knowledge/index.js +32 -29
- package/dist/extensions/knowledge/knowledgeStore.d.ts +1 -1
- package/dist/extensions/knowledge/prompt.d.ts +4 -4
- package/dist/extensions/knowledge/prompt.js +4 -4
- package/dist/extensions/memory/index.d.ts +15 -6
- package/dist/extensions/memory/index.js +34 -31
- package/dist/extensions/sources/index.d.ts +17 -5
- package/dist/extensions/sources/index.js +31 -29
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -0
- package/dist/tui/extensionCommand.d.ts +11 -2
- package/dist/tui/extensionCommand.js +129 -5
- package/package.json +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ExtensionsStatus } from "./session.js";
|
|
2
|
+
import type { SessionLive, StaticSessionFacts } from "./sessionFacts.js";
|
|
3
|
+
import type { ToolLabels } from "./toolLabels.js";
|
|
4
|
+
/**
|
|
5
|
+
* Turning an installed extension off and on in a running session (#49), without reopening it.
|
|
6
|
+
* Only one that was running when the session opened: its MCP servers are toggled
|
|
7
|
+
* (`toggleMcpServer()`, which removes their tools from the model's context and brings them back,
|
|
8
|
+
* confirmed empirically) and its plugin, a copy in the run's folder, is emptied or filled again
|
|
9
|
+
* before `reloadPlugins()` (its skills, commands and subagents go and come back, confirmed
|
|
10
|
+
* empirically). The kit's own lists follow: the plan gate's read-only tools, the subagent gates'
|
|
11
|
+
* allowed types, the chat's labels, `/extensions` and what `about_me` says. One that wasn't
|
|
12
|
+
* running has no plugin path the session knows: it takes reopening the session.
|
|
13
|
+
*
|
|
14
|
+
* Not through `setMcpServers()`: it leaves the servers given at start alone (confirmed
|
|
15
|
+
* empirically: removing one from its set removes nothing).
|
|
16
|
+
*/
|
|
17
|
+
/** What an installed extension brought to this session, to take it away and bring it back. */
|
|
18
|
+
export interface LiveExtension {
|
|
19
|
+
name: string;
|
|
20
|
+
/** Where it's installed (its scope's folder): the plugin copy is filled from there. */
|
|
21
|
+
installedDir: string;
|
|
22
|
+
scopeDir: string;
|
|
23
|
+
/** Its plugin, copied into the run's folder for this session. */
|
|
24
|
+
pluginCopy: string;
|
|
25
|
+
servers: string[];
|
|
26
|
+
readOnlyTools: string[];
|
|
27
|
+
toolLabels: ToolLabels;
|
|
28
|
+
/** Its subagents, as the type gate names them (`<plugin>:<name>`). */
|
|
29
|
+
agents: string[];
|
|
30
|
+
description: string;
|
|
31
|
+
provides: string[];
|
|
32
|
+
help: string[];
|
|
33
|
+
}
|
|
34
|
+
/** The session's own lists the switch keeps in step: the same objects the gates, the chat and the view read. */
|
|
35
|
+
export interface LiveTargets {
|
|
36
|
+
readOnlyTools: string[];
|
|
37
|
+
allowedSubagentTypes: string[];
|
|
38
|
+
toolLabels: ToolLabels;
|
|
39
|
+
status: ExtensionsStatus;
|
|
40
|
+
facts: StaticSessionFacts;
|
|
41
|
+
/** The running session's controls, once it has started. */
|
|
42
|
+
controls: () => SessionLive;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Turns `name` on or off in the running session; false when it can't be done here (it wasn't
|
|
46
|
+
* running when the session opened, the session hasn't started, or its files changed since it
|
|
47
|
+
* was installed), and the session must be opened again.
|
|
48
|
+
*/
|
|
49
|
+
export type ExtensionSwitch = (name: string, enabled: boolean) => Promise<boolean>;
|
|
50
|
+
/** The switch for a session's installed extensions. */
|
|
51
|
+
export declare function createExtensionSwitch(extensions: readonly LiveExtension[], targets: LiveTargets): ExtensionSwitch;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { cp, readdir, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { hashExtension, listInstalled, readLock } from "./externalExtensions.js";
|
|
4
|
+
/** The switch for a session's installed extensions. */
|
|
5
|
+
export function createExtensionSwitch(extensions, targets) {
|
|
6
|
+
const off = new Set();
|
|
7
|
+
return async (name, enabled) => {
|
|
8
|
+
const extension = extensions.find((candidate) => candidate.name === name);
|
|
9
|
+
const controls = targets.controls();
|
|
10
|
+
if (!extension || !controls.toggleMcpServer || !controls.reloadPlugins)
|
|
11
|
+
return false;
|
|
12
|
+
if (enabled === !off.has(name))
|
|
13
|
+
return true; // already as asked
|
|
14
|
+
if (enabled) {
|
|
15
|
+
// Its files as they were installed, or nothing: a changed one is for a new session to judge.
|
|
16
|
+
const lock = (await readLock(extension.scopeDir))[name];
|
|
17
|
+
if (!lock || lock.sha256 !== (await hashExtension(extension.installedDir)))
|
|
18
|
+
return false;
|
|
19
|
+
await cp(extension.installedDir, extension.pluginCopy, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
for (const entry of await readdir(extension.pluginCopy))
|
|
23
|
+
await rm(path.join(extension.pluginCopy, entry), { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
await controls.reloadPlugins();
|
|
26
|
+
for (const server of extension.servers)
|
|
27
|
+
await controls.toggleMcpServer(server, enabled);
|
|
28
|
+
if (enabled)
|
|
29
|
+
off.delete(name);
|
|
30
|
+
else
|
|
31
|
+
off.add(name);
|
|
32
|
+
follow(extension, enabled, targets);
|
|
33
|
+
targets.status.installed = await listInstalled(targets.status.dirs);
|
|
34
|
+
return true;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** The kit's lists, as the extension is now. */
|
|
38
|
+
function follow(extension, enabled, targets) {
|
|
39
|
+
const without = (list, items) => {
|
|
40
|
+
for (const item of items) {
|
|
41
|
+
const index = list.indexOf(item);
|
|
42
|
+
if (index >= 0)
|
|
43
|
+
list.splice(index, 1);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const { status, facts } = targets;
|
|
47
|
+
if (enabled) {
|
|
48
|
+
targets.readOnlyTools.push(...extension.readOnlyTools);
|
|
49
|
+
targets.allowedSubagentTypes.push(...extension.agents);
|
|
50
|
+
Object.assign(targets.toolLabels, extension.toolLabels);
|
|
51
|
+
status.active.push(extension.name);
|
|
52
|
+
status.about[extension.name] = { description: extension.description, provides: extension.provides };
|
|
53
|
+
status.inactive = status.inactive.filter((entry) => entry.name !== extension.name);
|
|
54
|
+
facts.extensions.active.push({ name: extension.name, description: extension.description, provides: extension.provides, servers: extension.servers, help: extension.help });
|
|
55
|
+
facts.extensions.inactive = facts.extensions.inactive.filter((entry) => entry.name !== extension.name);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
without(targets.readOnlyTools, extension.readOnlyTools);
|
|
59
|
+
without(targets.allowedSubagentTypes, extension.agents);
|
|
60
|
+
// The same object the chat reads its labels from: changed in place, readonly to everyone else.
|
|
61
|
+
for (const tool of Object.keys(extension.toolLabels))
|
|
62
|
+
delete targets.toolLabels[tool];
|
|
63
|
+
without(status.active, [extension.name]);
|
|
64
|
+
delete status.about[extension.name];
|
|
65
|
+
const reason = { name: extension.name, reason: "was turned off in this session" };
|
|
66
|
+
status.inactive.push(reason);
|
|
67
|
+
facts.extensions.active = facts.extensions.active.filter((entry) => entry.name !== extension.name);
|
|
68
|
+
facts.extensions.inactive.push(reason);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Where a marketplace's plugin comes from, as `marketplace.json` says (Claude Code's forms). */
|
|
2
|
+
export type PluginSource = string | {
|
|
3
|
+
source: "github";
|
|
4
|
+
repo: string;
|
|
5
|
+
ref?: string;
|
|
6
|
+
sha?: string;
|
|
7
|
+
} | {
|
|
8
|
+
source: "url";
|
|
9
|
+
url: string;
|
|
10
|
+
ref?: string;
|
|
11
|
+
sha?: string;
|
|
12
|
+
} | {
|
|
13
|
+
source: "git-subdir";
|
|
14
|
+
url: string;
|
|
15
|
+
path: string;
|
|
16
|
+
ref?: string;
|
|
17
|
+
sha?: string;
|
|
18
|
+
} | {
|
|
19
|
+
source: string;
|
|
20
|
+
[field: string]: unknown;
|
|
21
|
+
};
|
|
22
|
+
/** A plugin a marketplace offers. */
|
|
23
|
+
export interface MarketplacePlugin {
|
|
24
|
+
name: string;
|
|
25
|
+
source: PluginSource;
|
|
26
|
+
description?: string;
|
|
27
|
+
version?: string;
|
|
28
|
+
category?: string;
|
|
29
|
+
tags?: string[];
|
|
30
|
+
keywords?: string[];
|
|
31
|
+
}
|
|
32
|
+
/** A marketplace's `marketplace.json`, as the kit reads it. */
|
|
33
|
+
export interface MarketplaceManifest {
|
|
34
|
+
name: string;
|
|
35
|
+
owner: {
|
|
36
|
+
name: string;
|
|
37
|
+
email?: string;
|
|
38
|
+
url?: string;
|
|
39
|
+
};
|
|
40
|
+
description?: string;
|
|
41
|
+
/** Where bare plugin sources resolve, relative to the marketplace's root. */
|
|
42
|
+
pluginRoot?: string;
|
|
43
|
+
plugins: MarketplacePlugin[];
|
|
44
|
+
}
|
|
45
|
+
/** A marketplace an agent knows. */
|
|
46
|
+
export interface KnownMarketplace {
|
|
47
|
+
name: string;
|
|
48
|
+
/** Where it came from: a folder, a git URL (`#ref`), or `owner/repo`. */
|
|
49
|
+
source: string;
|
|
50
|
+
/** The commit its copy was taken at, for a git one. */
|
|
51
|
+
commit?: string;
|
|
52
|
+
/** The agent's own: known without asking, and its plugins installed without a confirmation. */
|
|
53
|
+
official: boolean;
|
|
54
|
+
/** When its copy was taken, ISO 8601 in UTC. */
|
|
55
|
+
updated: string;
|
|
56
|
+
/** Its copy, inside the agent's extensions folder. */
|
|
57
|
+
dir: string;
|
|
58
|
+
manifest?: MarketplaceManifest;
|
|
59
|
+
}
|
|
60
|
+
/** Reads and checks a marketplace's `.claude-plugin/marketplace.json`. */
|
|
61
|
+
export declare function readMarketplaceManifest(root: string): Promise<MarketplaceManifest>;
|
|
62
|
+
/** Reads a marketplace from a folder, a git URL (`#ref`) or `owner/repo` without adding it: what to show before asking. */
|
|
63
|
+
export declare function inspectMarketplace(source: string): Promise<MarketplaceManifest>;
|
|
64
|
+
/**
|
|
65
|
+
* Adds a marketplace to the agent (its extensions folder, `dir`) from a folder, a git URL
|
|
66
|
+
* (`#ref`) or `owner/repo`, taking a copy; one with the same name is replaced. Asking the person
|
|
67
|
+
* first is the caller's: the kit's command asks them to type its name, unless it's `official`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function addMarketplace(source: string, dir: string, options?: {
|
|
70
|
+
official?: boolean;
|
|
71
|
+
}): Promise<{
|
|
72
|
+
name: string;
|
|
73
|
+
replaced: boolean;
|
|
74
|
+
manifest: MarketplaceManifest;
|
|
75
|
+
commit?: string;
|
|
76
|
+
}>;
|
|
77
|
+
/** The marketplaces the agent knows, with their manifests when they can be read. */
|
|
78
|
+
export declare function listMarketplaces(dir: string): Promise<KnownMarketplace[]>;
|
|
79
|
+
/** Takes a fresh copy of a known marketplace from where it came from. */
|
|
80
|
+
export declare function updateMarketplace(dir: string, name: string): Promise<{
|
|
81
|
+
commit?: string;
|
|
82
|
+
}>;
|
|
83
|
+
/** Forgets a marketplace (its copy and its entry); the extensions installed from it stay. False when it wasn't known. */
|
|
84
|
+
export declare function removeMarketplace(dir: string, name: string): Promise<boolean>;
|
|
85
|
+
/** A plugin found in the known marketplaces: `<name>@<marketplace>`, or a name only one of them offers. */
|
|
86
|
+
export declare function findPlugin(dir: string, spec: string): Promise<{
|
|
87
|
+
marketplace: KnownMarketplace;
|
|
88
|
+
plugin: MarketplacePlugin;
|
|
89
|
+
}>;
|
|
90
|
+
/** Where a plugin comes from: a folder or git repository for `addExtension()`, or a download (npm, archive). */
|
|
91
|
+
export type ResolvedSource = {
|
|
92
|
+
source: string;
|
|
93
|
+
subdir?: string;
|
|
94
|
+
sha?: string;
|
|
95
|
+
} | {
|
|
96
|
+
npm: {
|
|
97
|
+
name: string;
|
|
98
|
+
version?: string;
|
|
99
|
+
registry?: string;
|
|
100
|
+
};
|
|
101
|
+
} | {
|
|
102
|
+
archive: {
|
|
103
|
+
url: string;
|
|
104
|
+
sha256?: string;
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
/** Where a plugin's source points: a folder of the marketplace's copy, a git repository, an npm package or a zip. */
|
|
108
|
+
export declare function resolvePluginSource(marketplace: KnownMarketplace, plugin: MarketplacePlugin): ResolvedSource;
|
|
109
|
+
/**
|
|
110
|
+
* Installs `<plugin>@<marketplace>` (or a plugin only one known marketplace offers) into a scope,
|
|
111
|
+
* locked with where it came from. Its name in the agent is its `plugin.json`'s, as for any extension.
|
|
112
|
+
*/
|
|
113
|
+
export declare function installFromMarketplace(dir: string, spec: string, scopeDir: string): Promise<{
|
|
114
|
+
name: string;
|
|
115
|
+
replaced: boolean;
|
|
116
|
+
commit?: string;
|
|
117
|
+
marketplace: string;
|
|
118
|
+
}>;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { addExtension, isGitSource } from "./externalExtensions.js";
|
|
7
|
+
import { fetchArchivePlugin, fetchNpmPlugin } from "./pluginDownloads.js";
|
|
8
|
+
/**
|
|
9
|
+
* Marketplaces (ADR-025, #29): a repository of extensions is a Claude Code marketplace as it is,
|
|
10
|
+
* a folder or git repository with `.claude-plugin/marketplace.json` listing plugins and where
|
|
11
|
+
* each comes from. An agent knows the marketplaces added to it, kept in its extensions folder
|
|
12
|
+
* (`marketplaces.json`, and a copy of each in `.marketplaces/<name>/`), and installs from them
|
|
13
|
+
* by `<plugin>@<marketplace>` into either scope, through `addExtension()`: the same copy, hash and
|
|
14
|
+
* lock as any extension. Its official marketplace is known without asking; any other is added
|
|
15
|
+
* only by hand, after a warning (the command asks to type its name).
|
|
16
|
+
*/
|
|
17
|
+
const exec = promisify(execFile);
|
|
18
|
+
const REGISTRY = "marketplaces.json";
|
|
19
|
+
const COPIES = ".marketplaces";
|
|
20
|
+
const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
21
|
+
/** Reads and checks a marketplace's `.claude-plugin/marketplace.json`. */
|
|
22
|
+
export async function readMarketplaceManifest(root) {
|
|
23
|
+
const file = path.join(root, ".claude-plugin", "marketplace.json");
|
|
24
|
+
const raw = JSON.parse(await readFile(file, "utf8").catch(() => {
|
|
25
|
+
throw new Error(`${root} isn't a marketplace: it has no .claude-plugin/marketplace.json.`);
|
|
26
|
+
}));
|
|
27
|
+
const name = raw.name;
|
|
28
|
+
if (typeof name !== "string" || !NAME.test(name) || name.includes(".."))
|
|
29
|
+
throw new Error(`${file}: "name" must be letters, digits, ".", "_" or "-".`);
|
|
30
|
+
const owner = raw.owner;
|
|
31
|
+
if (!owner || typeof owner.name !== "string")
|
|
32
|
+
throw new Error(`${file}: "owner" needs a "name".`);
|
|
33
|
+
if (!Array.isArray(raw.plugins))
|
|
34
|
+
throw new Error(`${file}: "plugins" must be a list.`);
|
|
35
|
+
const metadata = (raw.metadata ?? {});
|
|
36
|
+
const plugins = raw.plugins.map((plugin, index) => {
|
|
37
|
+
if (typeof plugin?.name !== "string" || !NAME.test(plugin.name))
|
|
38
|
+
throw new Error(`${file}: plugin ${index + 1} has no valid "name".`);
|
|
39
|
+
if (plugin.source === undefined)
|
|
40
|
+
throw new Error(`${file}: ${plugin.name} has no "source".`);
|
|
41
|
+
return plugin;
|
|
42
|
+
});
|
|
43
|
+
const description = raw.description ?? metadata.description;
|
|
44
|
+
return { name, owner, ...(description ? { description } : {}), ...(metadata.pluginRoot ? { pluginRoot: metadata.pluginRoot } : {}), plugins };
|
|
45
|
+
}
|
|
46
|
+
async function readRegistry(dir) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(await readFile(path.join(dir, REGISTRY), "utf8"));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === "ENOENT")
|
|
52
|
+
return {};
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function writeRegistry(dir, registry) {
|
|
57
|
+
await mkdir(dir, { recursive: true });
|
|
58
|
+
const sorted = Object.fromEntries(Object.keys(registry).sort().map((name) => [name, registry[name]]));
|
|
59
|
+
await writeFile(path.join(dir, REGISTRY), `${JSON.stringify(sorted, null, 2)}\n`, "utf8");
|
|
60
|
+
}
|
|
61
|
+
/** A git URL for a marketplace or plugin source: `owner/repo` is GitHub's. */
|
|
62
|
+
function gitUrl(source) {
|
|
63
|
+
return /^[\w.-]+\/[\w.-]+$/.test(source) ? `https://github.com/${source}.git` : source.replace(/^github:/, "https://github.com/");
|
|
64
|
+
}
|
|
65
|
+
/** Whether a marketplace source is a git repository rather than a folder (`owner/repo` included, unless such a folder exists). */
|
|
66
|
+
async function isGitMarketplace(source) {
|
|
67
|
+
const [location] = source.split("#");
|
|
68
|
+
if (isGitSource(location))
|
|
69
|
+
return true;
|
|
70
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(location))
|
|
71
|
+
return false;
|
|
72
|
+
return !(await stat(path.resolve(location)).then((found) => found.isDirectory(), () => false));
|
|
73
|
+
}
|
|
74
|
+
/** Takes a copy of a marketplace (a folder, or a git clone at its ref) into a temporary folder. */
|
|
75
|
+
async function fetchMarketplace(source) {
|
|
76
|
+
const copy = await mkdtemp(path.join(os.tmpdir(), "agent-kit-marketplace-"));
|
|
77
|
+
try {
|
|
78
|
+
if (await isGitMarketplace(source)) {
|
|
79
|
+
const [location, ref] = source.split("#");
|
|
80
|
+
await exec("git", ["clone", "--depth", "1", ...(ref ? ["--branch", ref] : []), gitUrl(location), copy]);
|
|
81
|
+
const commit = (await exec("git", ["-C", copy, "rev-parse", "HEAD"])).stdout.trim();
|
|
82
|
+
await rm(path.join(copy, ".git"), { recursive: true, force: true });
|
|
83
|
+
return { copy, commit };
|
|
84
|
+
}
|
|
85
|
+
await cp(path.resolve(source), copy, { recursive: true, filter: (file) => path.basename(file) !== ".git" });
|
|
86
|
+
return { copy };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
await rm(copy, { recursive: true, force: true });
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Reads a marketplace from a folder, a git URL (`#ref`) or `owner/repo` without adding it: what to show before asking. */
|
|
94
|
+
export async function inspectMarketplace(source) {
|
|
95
|
+
const { copy } = await fetchMarketplace((await isGitMarketplace(source)) ? source : path.resolve(source));
|
|
96
|
+
try {
|
|
97
|
+
return await readMarketplaceManifest(copy);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await rm(copy, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Adds a marketplace to the agent (its extensions folder, `dir`) from a folder, a git URL
|
|
105
|
+
* (`#ref`) or `owner/repo`, taking a copy; one with the same name is replaced. Asking the person
|
|
106
|
+
* first is the caller's: the kit's command asks them to type its name, unless it's `official`.
|
|
107
|
+
*/
|
|
108
|
+
export async function addMarketplace(source, dir, options = {}) {
|
|
109
|
+
const location = (await isGitMarketplace(source)) ? source : path.resolve(source);
|
|
110
|
+
const { copy, commit } = await fetchMarketplace(location);
|
|
111
|
+
try {
|
|
112
|
+
const manifest = await readMarketplaceManifest(copy);
|
|
113
|
+
const registry = await readRegistry(dir);
|
|
114
|
+
const replaced = Boolean(registry[manifest.name]);
|
|
115
|
+
const target = path.join(dir, COPIES, manifest.name);
|
|
116
|
+
await rm(target, { recursive: true, force: true });
|
|
117
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
118
|
+
await cp(copy, target, { recursive: true });
|
|
119
|
+
registry[manifest.name] = { source: location, ...(commit ? { commit } : {}), official: Boolean(options.official || registry[manifest.name]?.official), updated: new Date().toISOString() };
|
|
120
|
+
await writeRegistry(dir, registry);
|
|
121
|
+
return { name: manifest.name, replaced, manifest, ...(commit ? { commit } : {}) };
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
await rm(copy, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** The marketplaces the agent knows, with their manifests when they can be read. */
|
|
128
|
+
export async function listMarketplaces(dir) {
|
|
129
|
+
const registry = await readRegistry(dir);
|
|
130
|
+
return Promise.all(Object.entries(registry).map(async ([name, entry]) => {
|
|
131
|
+
const copy = path.join(dir, COPIES, name);
|
|
132
|
+
const manifest = await readMarketplaceManifest(copy).catch(() => undefined);
|
|
133
|
+
return { name, ...entry, dir: copy, ...(manifest ? { manifest } : {}) };
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
/** Takes a fresh copy of a known marketplace from where it came from. */
|
|
137
|
+
export async function updateMarketplace(dir, name) {
|
|
138
|
+
const known = (await readRegistry(dir))[name];
|
|
139
|
+
if (!known)
|
|
140
|
+
throw new Error(`No marketplace ${name}.`);
|
|
141
|
+
const result = await addMarketplace(known.source, dir, { official: known.official });
|
|
142
|
+
if (result.name !== name)
|
|
143
|
+
throw new Error(`${known.source} is now the marketplace ${result.name}, not ${name}.`);
|
|
144
|
+
return result.commit ? { commit: result.commit } : {};
|
|
145
|
+
}
|
|
146
|
+
/** Forgets a marketplace (its copy and its entry); the extensions installed from it stay. False when it wasn't known. */
|
|
147
|
+
export async function removeMarketplace(dir, name) {
|
|
148
|
+
const registry = await readRegistry(dir);
|
|
149
|
+
if (!registry[name])
|
|
150
|
+
return false;
|
|
151
|
+
delete registry[name];
|
|
152
|
+
await rm(path.join(dir, COPIES, name), { recursive: true, force: true });
|
|
153
|
+
await writeRegistry(dir, registry);
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
/** A plugin found in the known marketplaces: `<name>@<marketplace>`, or a name only one of them offers. */
|
|
157
|
+
export async function findPlugin(dir, spec) {
|
|
158
|
+
const [name, wanted] = spec.split("@");
|
|
159
|
+
const marketplaces = (await listMarketplaces(dir)).filter((marketplace) => !wanted || marketplace.name === wanted);
|
|
160
|
+
if (wanted && !marketplaces.length)
|
|
161
|
+
throw new Error(`No marketplace ${wanted}.`);
|
|
162
|
+
const found = marketplaces.flatMap((marketplace) => (marketplace.manifest?.plugins ?? []).filter((plugin) => plugin.name === name).map((plugin) => ({ marketplace, plugin })));
|
|
163
|
+
if (!found.length)
|
|
164
|
+
throw new Error(`No marketplace offers ${name}${wanted ? ` (${wanted} doesn't)` : ""}.`);
|
|
165
|
+
if (found.length > 1)
|
|
166
|
+
throw new Error(`${name} is in several marketplaces (${found.map(({ marketplace }) => marketplace.name).join(", ")}): say which, ${name}@<marketplace>.`);
|
|
167
|
+
return found[0];
|
|
168
|
+
}
|
|
169
|
+
/** Where a plugin's source points: a folder of the marketplace's copy, a git repository, an npm package or a zip. */
|
|
170
|
+
export function resolvePluginSource(marketplace, plugin) {
|
|
171
|
+
const { source } = plugin;
|
|
172
|
+
if (typeof source === "string") {
|
|
173
|
+
// From the marketplace's root: `./plugins/x`, or a bare name under `pluginRoot`; never "..", as Claude Code.
|
|
174
|
+
if (source.split(/[\\/]/).includes(".."))
|
|
175
|
+
throw new Error(`${plugin.name}'s source leaves its marketplace: ${source}.`);
|
|
176
|
+
const relative = source.startsWith("./") || source === "." ? source : path.join(marketplace.manifest?.pluginRoot ?? ".", source);
|
|
177
|
+
const root = path.resolve(marketplace.dir);
|
|
178
|
+
const folder = path.resolve(root, relative);
|
|
179
|
+
if (folder !== root && !folder.startsWith(`${root}${path.sep}`))
|
|
180
|
+
throw new Error(`${plugin.name}'s source leaves its marketplace: ${source}.`);
|
|
181
|
+
return { source: folder };
|
|
182
|
+
}
|
|
183
|
+
// Its fields, as written: the union above only names the forms the kit knows.
|
|
184
|
+
const entry = source;
|
|
185
|
+
const ref = typeof entry.ref === "string" ? `#${entry.ref}` : "";
|
|
186
|
+
const sha = typeof entry.sha === "string" ? { sha: entry.sha } : {};
|
|
187
|
+
switch (source.source) {
|
|
188
|
+
case "github":
|
|
189
|
+
return { source: `https://github.com/${String(entry.repo)}.git${ref}`, ...sha };
|
|
190
|
+
case "url":
|
|
191
|
+
return { source: `${String(entry.url)}${ref}`, ...sha };
|
|
192
|
+
case "git-subdir":
|
|
193
|
+
return { source: `${gitUrl(String(entry.url))}${ref}`, subdir: String(entry.path), ...sha };
|
|
194
|
+
case "npm": {
|
|
195
|
+
// "package" may carry its version ("@scope/name@^2.0.0"); "version" wins.
|
|
196
|
+
const spec = String(entry.package);
|
|
197
|
+
const at = spec.lastIndexOf("@");
|
|
198
|
+
const [name, inline] = at > 0 ? [spec.slice(0, at), spec.slice(at + 1)] : [spec, undefined];
|
|
199
|
+
const version = typeof entry.version === "string" ? entry.version : inline;
|
|
200
|
+
return { npm: { name, ...(version ? { version } : {}), ...(typeof entry.registry === "string" ? { registry: entry.registry } : {}) } };
|
|
201
|
+
}
|
|
202
|
+
case "archive":
|
|
203
|
+
return { archive: { url: String(entry.url), ...(typeof entry.sha256 === "string" ? { sha256: entry.sha256 } : {}) } };
|
|
204
|
+
default:
|
|
205
|
+
// command runs a program of the marketplace's on this machine: never.
|
|
206
|
+
throw new Error(`${plugin.name} comes from a "${source.source}" source, which agent-kit doesn't install: only folders of the marketplace, github, url, git-subdir, npm and archive.`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Installs `<plugin>@<marketplace>` (or a plugin only one known marketplace offers) into a scope,
|
|
211
|
+
* locked with where it came from. Its name in the agent is its `plugin.json`'s, as for any extension.
|
|
212
|
+
*/
|
|
213
|
+
export async function installFromMarketplace(dir, spec, scopeDir) {
|
|
214
|
+
const { marketplace, plugin } = await findPlugin(dir, spec);
|
|
215
|
+
const resolved = resolvePluginSource(marketplace, plugin);
|
|
216
|
+
const from = `${plugin.name}@${marketplace.name}`;
|
|
217
|
+
if ("npm" in resolved) {
|
|
218
|
+
const { name, version, registry } = resolved.npm;
|
|
219
|
+
const fetched = await fetchNpmPlugin(name, version, registry);
|
|
220
|
+
try {
|
|
221
|
+
return { ...(await addExtension(fetched.dir, scopeDir, { marketplace: from, origin: `npm:${name}@${fetched.version}` })), marketplace: marketplace.name };
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
await rm(fetched.dir, { recursive: true, force: true });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if ("archive" in resolved) {
|
|
228
|
+
const fetched = await fetchArchivePlugin(resolved.archive.url, resolved.archive.sha256);
|
|
229
|
+
try {
|
|
230
|
+
return { ...(await addExtension(fetched.root, scopeDir, { marketplace: from, origin: resolved.archive.url })), marketplace: marketplace.name };
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
await rm(fetched.dir, { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const result = await addExtension(resolved.source, scopeDir, {
|
|
237
|
+
...(resolved.subdir ? { subdir: resolved.subdir } : {}),
|
|
238
|
+
...(resolved.sha ? { sha: resolved.sha } : {}),
|
|
239
|
+
marketplace: from,
|
|
240
|
+
});
|
|
241
|
+
return { ...result, marketplace: marketplace.name };
|
|
242
|
+
}
|
package/dist/core/messages/de.js
CHANGED
|
@@ -29,6 +29,7 @@ export const de = {
|
|
|
29
29
|
extensionsNone: "(keine Erweiterungen)",
|
|
30
30
|
extensionOff: (name, reason) => `${name} ist aus: ${reason}`,
|
|
31
31
|
extensionsInstalled: "Installiert:",
|
|
32
|
+
extensionSwitched: (name, enabled) => `(${name} ${enabled ? "aktiviert" : "deaktiviert"}, in derselben Sitzung)`,
|
|
32
33
|
extensionToggled: (name, enabled) => `(${name} ${enabled ? "aktiviert" : "deaktiviert"}: die Sitzung wird mit derselben Unterhaltung neu geöffnet)`,
|
|
33
34
|
extensionNotInstalled: (name) => `(${name} ist nicht installiert: /extensions listet sie auf)`,
|
|
34
35
|
extensionsUsage: "(Verwendung: /extensions, /extensions enable <Name>, /extensions disable <Name>)",
|
|
@@ -38,6 +38,8 @@ export interface Messages {
|
|
|
38
38
|
extensionsNone: string;
|
|
39
39
|
extensionOff(name: string, reason: string): string;
|
|
40
40
|
extensionsInstalled: string;
|
|
41
|
+
/** An installed extension turned off or on in the running session (#49). */
|
|
42
|
+
extensionSwitched(name: string, enabled: boolean): string;
|
|
41
43
|
extensionToggled(name: string, enabled: boolean): string;
|
|
42
44
|
extensionNotInstalled(name: string): string;
|
|
43
45
|
extensionsUsage: string;
|
package/dist/core/messages/en.js
CHANGED
|
@@ -28,6 +28,7 @@ export const en = {
|
|
|
28
28
|
extensionsNone: "(no extensions)",
|
|
29
29
|
extensionOff: (name, reason) => `${name} is off: it ${reason}`,
|
|
30
30
|
extensionsInstalled: "Installed:",
|
|
31
|
+
extensionSwitched: (name, enabled) => `(${name} ${enabled ? "enabled" : "disabled"}, in this same session)`,
|
|
31
32
|
extensionToggled: (name, enabled) => `(${name} ${enabled ? "enabled" : "disabled"}: the session reopens, with the same conversation)`,
|
|
32
33
|
extensionNotInstalled: (name) => `(${name} isn't installed: /extensions lists them)`,
|
|
33
34
|
extensionsUsage: "(usage: /extensions, /extensions enable <name>, /extensions disable <name>)",
|
package/dist/core/messages/es.js
CHANGED
|
@@ -29,6 +29,7 @@ export const es = {
|
|
|
29
29
|
extensionsNone: "(ninguna extensión)",
|
|
30
30
|
extensionOff: (name, reason) => `${name} está desactivada: ${reason}`,
|
|
31
31
|
extensionsInstalled: "Instaladas:",
|
|
32
|
+
extensionSwitched: (name, enabled) => `(${name} ${enabled ? "activada" : "desactivada"}, en esta misma sesión)`,
|
|
32
33
|
extensionToggled: (name, enabled) => `(${name} ${enabled ? "activada" : "desactivada"}: la sesión se reabre, con la misma conversación)`,
|
|
33
34
|
extensionNotInstalled: (name) => `(${name} no está instalada: /extensions las lista)`,
|
|
34
35
|
extensionsUsage: "(uso: /extensions, /extensions enable <nombre>, /extensions disable <nombre>)",
|
package/dist/core/messages/fr.js
CHANGED
|
@@ -29,6 +29,7 @@ export const fr = {
|
|
|
29
29
|
extensionsNone: "(aucune extension)",
|
|
30
30
|
extensionOff: (name, reason) => `${name} est désactivée : ${reason}`,
|
|
31
31
|
extensionsInstalled: "Installées :",
|
|
32
|
+
extensionSwitched: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"}, dans cette même session)`,
|
|
32
33
|
extensionToggled: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"} : la session se rouvre, avec la même conversation)`,
|
|
33
34
|
extensionNotInstalled: (name) => `(${name} n'est pas installée : /extensions les liste)`,
|
|
34
35
|
extensionsUsage: "(usage : /extensions, /extensions enable <nom>, /extensions disable <nom>)",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The regular files of a tar archive, by path (ustar, with pax and GNU long names). */
|
|
2
|
+
export declare function untar(tar: Uint8Array): Map<string, Uint8Array>;
|
|
3
|
+
/** The highest of `versions` that `wanted` asks for: an exact version, or a range (`^`, `~`, comparators, `*`). */
|
|
4
|
+
export declare function pickVersion(versions: readonly string[], wanted: string): string | undefined;
|
|
5
|
+
/** An npm package into a new temporary folder: `{ dir, version }`. */
|
|
6
|
+
export declare function fetchNpmPlugin(name: string, wanted?: string, registry?: string): Promise<{
|
|
7
|
+
dir: string;
|
|
8
|
+
version: string;
|
|
9
|
+
}>;
|
|
10
|
+
/** A zip archive into a new temporary folder; the plugin is its root, or its only folder. */
|
|
11
|
+
export declare function fetchArchivePlugin(url: string, sha256?: string): Promise<{
|
|
12
|
+
dir: string;
|
|
13
|
+
root: string;
|
|
14
|
+
}>;
|