@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.
Files changed (45) hide show
  1. package/dist/chat/chatController.d.ts +4 -1
  2. package/dist/chat/chatController.js +43 -17
  3. package/dist/chat/extensions.d.ts +7 -3
  4. package/dist/chat/extensions.js +11 -8
  5. package/dist/chat/runs.d.ts +3 -0
  6. package/dist/chat/runs.js +1 -1
  7. package/dist/core/agentSpec.d.ts +20 -56
  8. package/dist/core/extensions.d.ts +21 -12
  9. package/dist/core/extensions.js +8 -18
  10. package/dist/core/externalExtensions.d.ts +15 -3
  11. package/dist/core/externalExtensions.js +33 -5
  12. package/dist/core/liveExtensions.d.ts +51 -0
  13. package/dist/core/liveExtensions.js +70 -0
  14. package/dist/core/marketplaces.d.ts +118 -0
  15. package/dist/core/marketplaces.js +242 -0
  16. package/dist/core/messages/de.js +1 -0
  17. package/dist/core/messages/en.d.ts +2 -0
  18. package/dist/core/messages/en.js +1 -0
  19. package/dist/core/messages/es.js +1 -0
  20. package/dist/core/messages/fr.js +1 -0
  21. package/dist/core/pluginDownloads.d.ts +14 -0
  22. package/dist/core/pluginDownloads.js +141 -0
  23. package/dist/core/runner.js +6 -1
  24. package/dist/core/session.d.ts +11 -4
  25. package/dist/core/session.js +74 -22
  26. package/dist/core/sessionFacts.d.ts +5 -0
  27. package/dist/core/sessionFacts.js +1 -0
  28. package/dist/extensions/awareness/index.d.ts +12 -3
  29. package/dist/extensions/awareness/index.js +18 -15
  30. package/dist/extensions/awareness/tools.d.ts +1 -1
  31. package/dist/extensions/awareness/tools.js +3 -3
  32. package/dist/extensions/knowledge/index.d.ts +20 -6
  33. package/dist/extensions/knowledge/index.js +32 -29
  34. package/dist/extensions/knowledge/knowledgeStore.d.ts +1 -1
  35. package/dist/extensions/knowledge/prompt.d.ts +4 -4
  36. package/dist/extensions/knowledge/prompt.js +4 -4
  37. package/dist/extensions/memory/index.d.ts +15 -6
  38. package/dist/extensions/memory/index.js +34 -31
  39. package/dist/extensions/sources/index.d.ts +17 -5
  40. package/dist/extensions/sources/index.js +31 -29
  41. package/dist/index.d.ts +6 -1
  42. package/dist/index.js +5 -0
  43. package/dist/tui/extensionCommand.d.ts +11 -2
  44. package/dist/tui/extensionCommand.js +129 -5
  45. package/package.json +1 -1
@@ -1,11 +1,11 @@
1
1
  import type { PageType } from "./knowledgeStore.js";
2
2
  /**
3
- * The built-in knowledge base (an "LLM wiki"): `sourcesDir` holds the originals, `knowledgeDir`
4
- * is the wiki the agent maintains through the `knowledge_*` tools (ADR-024), and the rules below
3
+ * The built-in knowledge base (an "LLM wiki"): the sources folder holds the originals, its own
4
+ * folder is the wiki the agent maintains through the `knowledge_*` tools (ADR-024), and the rules below
5
5
  * plus the plugin's skills (knowledge-ingest, knowledge-query, knowledge-lint) and commands
6
6
  * (/knowledge:ingest, /knowledge:query, /knowledge:lint) are the schema that tells it how.
7
- * Domain-agnostic on purpose: an agent adds its own page types (`AgentSpec.knowledgePageTypes`),
8
- * or leaves it out of `AgentSpec.extensions` and writes its own rules for `knowledgeDir`.
7
+ * Domain-agnostic on purpose: an agent adds its own page types (`knowledge({ pageTypes })`), or
8
+ * leaves it out of `AgentSpec.extensions` and keeps notes of its own in `extraWritableDirs`.
9
9
  */
10
10
  /** Absolute path of the knowledge base's plugin shipped with the kit, next to `dist/` (or `src/` under tsx). */
11
11
  export declare function knowledgePluginRoot(): string;
@@ -1,12 +1,12 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  /**
4
- * The built-in knowledge base (an "LLM wiki"): `sourcesDir` holds the originals, `knowledgeDir`
5
- * is the wiki the agent maintains through the `knowledge_*` tools (ADR-024), and the rules below
4
+ * The built-in knowledge base (an "LLM wiki"): the sources folder holds the originals, its own
5
+ * folder is the wiki the agent maintains through the `knowledge_*` tools (ADR-024), and the rules below
6
6
  * plus the plugin's skills (knowledge-ingest, knowledge-query, knowledge-lint) and commands
7
7
  * (/knowledge:ingest, /knowledge:query, /knowledge:lint) are the schema that tells it how.
8
- * Domain-agnostic on purpose: an agent adds its own page types (`AgentSpec.knowledgePageTypes`),
9
- * or leaves it out of `AgentSpec.extensions` and writes its own rules for `knowledgeDir`.
8
+ * Domain-agnostic on purpose: an agent adds its own page types (`knowledge({ pageTypes })`), or
9
+ * leaves it out of `AgentSpec.extensions` and keeps notes of its own in `extraWritableDirs`.
10
10
  */
11
11
  /** Absolute path of the knowledge base's plugin shipped with the kit, next to `dist/` (or `src/` under tsx). */
12
12
  export function knowledgePluginRoot() {
@@ -1,12 +1,21 @@
1
- import type { Extension } from "../../core/extensions.js";
1
+ import type { BaseSessionConfig } from "../../core/agentSpec.js";
2
+ import { type Extension, type FolderOption } from "../../core/extensions.js";
2
3
  /** Absolute path of the memory extension's plugin, next to `dist/` (or `src/` under tsx). */
3
4
  export declare function memoryPluginRoot(): string;
4
5
  /** The memory's section of the system prompt: what it is, what it holds now, and its rules. */
5
6
  export declare function memoryPromptSection(index: string): string;
7
+ /** The memory's options. */
8
+ export interface MemoryOptions<TConfig extends BaseSessionConfig = BaseSessionConfig> {
9
+ /**
10
+ * Its folder, the agent's own outside any project (e.g. `~/.miyagi/memory`), kept across all
11
+ * the person's projects and never shared with another agent. Without a folder the extension is off.
12
+ */
13
+ dir: FolderOption<TConfig>;
14
+ }
6
15
  /**
7
- * The memory of the person (`config.memoryDir`, #34): what the agent learns about the person it
8
- * works for, kept across all their projects in a folder of its own, reached only through its
9
- * tools (`recall`, `remember`, `forget`). What it remembers must quote the person's own messages, which it hears through
10
- * a `UserPromptSubmit` hook (and, for a resumed run, from the run's conversation).
16
+ * The memory of the person (#34): what the agent learns about the person it works for, kept
17
+ * across all their projects in a folder of its own, reached only through its tools (`recall`,
18
+ * `remember`, `forget`). What it remembers must quote the person's own messages, which it hears
19
+ * through a `UserPromptSubmit` hook (and, for a resumed run, from the run's conversation).
11
20
  */
12
- export declare const memoryExtension: Extension;
21
+ export declare function memory<TConfig extends BaseSessionConfig = BaseSessionConfig>(options: MemoryOptions<TConfig>): Extension;
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
+ import { folderOf } from "../../core/extensions.js";
3
4
  import { readConversation } from "../../core/runs.js";
4
5
  import { memoryToolLabels } from "./labels.js";
5
6
  import { createMemoryStore } from "./memoryStore.js";
@@ -22,35 +23,37 @@ ${index}
22
23
  - Write entries in the language you reply in.`;
23
24
  }
24
25
  /**
25
- * The memory of the person (`config.memoryDir`, #34): what the agent learns about the person it
26
- * works for, kept across all their projects in a folder of its own, reached only through its
27
- * tools (`recall`, `remember`, `forget`). What it remembers must quote the person's own messages, which it hears through
28
- * a `UserPromptSubmit` hook (and, for a resumed run, from the run's conversation).
26
+ * The memory of the person (#34): what the agent learns about the person it works for, kept
27
+ * across all their projects in a folder of its own, reached only through its tools (`recall`,
28
+ * `remember`, `forget`). What it remembers must quote the person's own messages, which it hears
29
+ * through a `UserPromptSubmit` hook (and, for a resumed run, from the run's conversation).
29
30
  */
30
- export const memoryExtension = {
31
- name: "memory",
32
- plugin: memoryPluginRoot(),
33
- missing: ({ config }) => (config.memoryDir ? undefined : "needs `memoryDir` in the config"),
34
- async contribute({ config, runDir }) {
35
- const memoryDir = config.memoryDir;
36
- const store = createMemoryStore(memoryDir);
37
- // What the person wrote: earlier in a resumed run, then each message as it's sent.
38
- const said = (await readConversation(runDir)).filter((message) => message.role === "user").map((message) => message.text);
39
- const hear = async (input) => {
40
- if (input.hook_event_name === "UserPromptSubmit")
41
- said.push(input.prompt);
42
- return {};
43
- };
44
- return {
45
- mcpServers: { memory: createMemoryServer(store, said) },
46
- promptSection: memoryPromptSection(memoryIndex(await store.list())),
47
- toolOnlyDirs: [{ dir: memoryDir, instead: "its tools: recall, remember, forget" }],
48
- readOnlyTools: ["mcp__memory__recall"],
49
- hooks: { UserPromptSubmit: [{ hooks: [hear] }] },
50
- toolLabels: memoryToolLabels(),
51
- helpLines: [
52
- "You keep a memory of the person across all their projects (how they like things done, who they are): `/memory:list` shows it, `/memory:forget <entry>` forgets an entry. Only what they tell you goes in it.",
53
- ],
54
- };
55
- },
56
- };
31
+ export function memory(options) {
32
+ return {
33
+ name: "memory",
34
+ plugin: memoryPluginRoot(),
35
+ missing: ({ config }) => (folderOf(options.dir, config) ? undefined : "needs a folder (`dir`)"),
36
+ async contribute({ config, runDir }) {
37
+ const memoryDir = folderOf(options.dir, config);
38
+ const store = createMemoryStore(memoryDir);
39
+ // What the person wrote: earlier in a resumed run, then each message as it's sent.
40
+ const said = (await readConversation(runDir)).filter((message) => message.role === "user").map((message) => message.text);
41
+ const hear = async (input) => {
42
+ if (input.hook_event_name === "UserPromptSubmit")
43
+ said.push(input.prompt);
44
+ return {};
45
+ };
46
+ return {
47
+ mcpServers: { memory: createMemoryServer(store, said) },
48
+ promptSection: memoryPromptSection(memoryIndex(await store.list())),
49
+ toolOnlyDirs: [{ dir: memoryDir, instead: "its tools: recall, remember, forget" }],
50
+ readOnlyTools: ["mcp__memory__recall"],
51
+ hooks: { UserPromptSubmit: [{ hooks: [hear] }] },
52
+ toolLabels: memoryToolLabels(),
53
+ helpLines: [
54
+ "You keep a memory of the person across all their projects (how they like things done, who they are): `/memory:list` shows it, `/memory:forget <entry>` forgets an entry. Only what they tell you goes in it.",
55
+ ],
56
+ };
57
+ },
58
+ };
59
+ }
@@ -1,9 +1,21 @@
1
- import type { Extension } from "../../core/extensions.js";
1
+ import type { BaseSessionConfig } from "../../core/agentSpec.js";
2
+ import { type Extension, type FolderOption } from "../../core/extensions.js";
2
3
  /** Absolute path of the sources extension's plugin, next to `dist/` (or `src/` under tsx). */
3
4
  export declare function sourcesPluginRoot(): string;
5
+ /** The sources extension's options. */
6
+ export interface SourcesOptions<TConfig extends BaseSessionConfig = BaseSessionConfig> {
7
+ /**
8
+ * The sources folder: original files kept as obtained, apart from any notes. Material the
9
+ * person drops in, plus whatever the agent saves with `save_to_sources` (downloaded documents,
10
+ * transcripts…). The agent reads and searches it, never edits it, and `save_to_sources` never
11
+ * overwrites. Without a folder the extension is off.
12
+ */
13
+ dir: FolderOption<TConfig>;
14
+ /** The `save_to_sources` tool's description in the agent's own words, instead of the kit's generic one. */
15
+ saveDescription?: string;
16
+ }
4
17
  /**
5
- * The sources folder (`config.sourcesDir`): originals kept as obtained, read with `Read` and
6
- * `extract_text`, added and retired only through its tools (`sourceFiles`). It knows nothing of
7
- * a knowledge base (#30).
18
+ * The sources folder: originals kept as obtained, read with `Read` and `extract_text`, added and
19
+ * retired only through its tools (`sourceFiles`). It knows nothing of a knowledge base (#30).
8
20
  */
9
- export declare const sourcesExtension: Extension;
21
+ export declare function sources<TConfig extends BaseSessionConfig = BaseSessionConfig>(options: SourcesOptions<TConfig>): Extension;
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
+ import { folderOf } from "../../core/extensions.js";
3
4
  import { sourcesToolLabels } from "./labels.js";
4
5
  import { addSource } from "./sources.js";
5
6
  import { createSaveToSourcesServer, sourcesPromptSection } from "./tools.js";
@@ -8,33 +9,34 @@ export function sourcesPluginRoot() {
8
9
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "extensions", "sources");
9
10
  }
10
11
  /**
11
- * The sources folder (`config.sourcesDir`): originals kept as obtained, read with `Read` and
12
- * `extract_text`, added and retired only through its tools (`sourceFiles`). It knows nothing of
13
- * a knowledge base (#30).
12
+ * The sources folder: originals kept as obtained, read with `Read` and `extract_text`, added and
13
+ * retired only through its tools (`sourceFiles`). It knows nothing of a knowledge base (#30).
14
14
  */
15
- export const sourcesExtension = {
16
- name: "sources",
17
- plugin: sourcesPluginRoot(),
18
- missing: ({ config }) => (config.sourcesDir ? undefined : "needs `sourcesDir` in the config"),
19
- async contribute({ config, spec, runDir, interactive }) {
20
- const sourcesDir = config.sourcesDir;
21
- const folder = `${path.relative(config.projectDir, sourcesDir).split(path.sep).join("/")}/`;
22
- return {
23
- // Asking a person (request_file, retire_source) only where there is one.
24
- mcpServers: { sourceFiles: createSaveToSourcesServer(runDir, sourcesDir, spec.saveToSourcesDescription, { projectDir: config.projectDir, interactive }) },
25
- promptSection: sourcesPromptSection(config.projectDir, sourcesDir),
26
- // The originals are read and searched, never written: the only way in is its tools.
27
- fileTools: ["Read", "Glob", "Grep"],
28
- readOnlyDirs: [sourcesDir],
29
- readOnlyTools: ["mcp__sourceFiles__list_sources", "mcp__sourceFiles__extract_text"],
30
- selfAskingTools: ["mcp__sourceFiles__request_file", "mcp__sourceFiles__retire_source"],
31
- toolLabels: sourcesToolLabels(),
32
- // For the host (a page's "add to the chest"): a file the person gave, with the same rules
33
- // as the tools (never overwriting, a duplicate recognized) and the person as its origin.
34
- api: {
35
- addSource: (file, name, subfolder = "") => addSource(sourcesDir, file, subfolder ? `${subfolder.replace(/[/\\]*$/, "")}/` : name, { kind: "person", from: name }, subfolder ? { name } : {}),
36
- },
37
- helpLines: [`Originals go in \`${folder}\`: the person drops files there (or you ask for one, or download it), and you never change them.`],
38
- };
39
- },
40
- };
15
+ export function sources(options) {
16
+ return {
17
+ name: "sources",
18
+ plugin: sourcesPluginRoot(),
19
+ missing: ({ config }) => (folderOf(options.dir, config) ? undefined : "needs a folder (`dir`)"),
20
+ async contribute({ config, runDir, interactive }) {
21
+ const sourcesDir = folderOf(options.dir, config);
22
+ const folder = `${path.relative(config.projectDir, sourcesDir).split(path.sep).join("/")}/`;
23
+ return {
24
+ // Asking a person (request_file, retire_source) only where there is one.
25
+ mcpServers: { sourceFiles: createSaveToSourcesServer(runDir, sourcesDir, options.saveDescription, { projectDir: config.projectDir, interactive }) },
26
+ promptSection: sourcesPromptSection(config.projectDir, sourcesDir),
27
+ // The originals are read and searched, never written: the only way in is its tools.
28
+ fileTools: ["Read", "Glob", "Grep"],
29
+ readOnlyDirs: [sourcesDir],
30
+ readOnlyTools: ["mcp__sourceFiles__list_sources", "mcp__sourceFiles__extract_text"],
31
+ selfAskingTools: ["mcp__sourceFiles__request_file", "mcp__sourceFiles__retire_source"],
32
+ toolLabels: sourcesToolLabels(),
33
+ // For the host (a page's "add to the chest"): a file the person gave, with the same rules
34
+ // as the tools (never overwriting, a duplicate recognized) and the person as its origin.
35
+ api: {
36
+ addSource: (file, name, subfolder = "") => addSource(sourcesDir, file, subfolder ? `${subfolder.replace(/[/\\]*$/, "")}/` : name, { kind: "person", from: name }, subfolder ? { name } : {}),
37
+ },
38
+ helpLines: [`Originals go in \`${folder}\`: the person drops files there (or you ask for one, or download it), and you never change them.`],
39
+ };
40
+ },
41
+ };
42
+ }
package/dist/index.d.ts CHANGED
@@ -11,8 +11,13 @@ export { buildSessionOptions, createInputQueue, createDeferred, createModeContro
11
11
  export { runQuery, type AgentEvent, type AgentRun, type SessionUsage, type ContextUsage } from "./core/runner.js";
12
12
  export type { SessionFacts } from "./core/sessionFacts.js";
13
13
  export { knowledgePluginRoot, knowledgePromptSection } from "./extensions/knowledge/prompt.js";
14
- export { readExtensionManifest, type Extension, type ExtensionContext, type ExtensionContribution, type ExtensionManifest, } from "./core/extensions.js";
14
+ export { awareness, type AwarenessOptions } from "./extensions/awareness/index.js";
15
+ export { sources, type SourcesOptions } from "./extensions/sources/index.js";
16
+ export { knowledge, type KnowledgeOptions } from "./extensions/knowledge/index.js";
17
+ export { memory, type MemoryOptions } from "./extensions/memory/index.js";
18
+ export { readExtensionManifest, type Extension, type ExtensionContext, type ExtensionContribution, type ExtensionManifest, type FolderOption, } from "./core/extensions.js";
15
19
  export { addExtension, removeExtension, setExtensionEnabled, listInstalled, readExternalManifest, type ExtensionAuthor, type ExtensionDirs, type ExtensionScope, type ExternalManifest, type ExternalToolLabel, type PluginServer, type InstalledExtension, type LockEntry, } from "./core/externalExtensions.js";
20
+ export { addMarketplace, findPlugin, inspectMarketplace, installFromMarketplace, listMarketplaces, readMarketplaceManifest, removeMarketplace, resolvePluginSource, updateMarketplace, type KnownMarketplace, type MarketplaceManifest, type MarketplacePlugin, type PluginSource, } from "./core/marketplaces.js";
16
21
  export { runExtensionCommand } from "./tui/extensionCommand.js";
17
22
  export { agentKitVersion } from "./core/version.js";
18
23
  export { BUILT_IN_PAGE_TYPES, isSlug, parseId, linkedIds, type KnowledgeStore, type PageType, type KnowledgePage, type PageInfo, type SearchHit, type CheckReport, type FieldChanges, type NewPage, } from "./extensions/knowledge/knowledgeStore.js";
package/dist/index.js CHANGED
@@ -11,8 +11,13 @@ export { createFriendlyToolLabel, truncate, truncatePath, withToolLabels, withTo
11
11
  export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, togglePlanMode } from "./core/session.js";
12
12
  export { runQuery } from "./core/runner.js";
13
13
  export { knowledgePluginRoot, knowledgePromptSection } from "./extensions/knowledge/prompt.js";
14
+ export { awareness } from "./extensions/awareness/index.js";
15
+ export { sources } from "./extensions/sources/index.js";
16
+ export { knowledge } from "./extensions/knowledge/index.js";
17
+ export { memory } from "./extensions/memory/index.js";
14
18
  export { readExtensionManifest, } from "./core/extensions.js";
15
19
  export { addExtension, removeExtension, setExtensionEnabled, listInstalled, readExternalManifest, } from "./core/externalExtensions.js";
20
+ export { addMarketplace, findPlugin, inspectMarketplace, installFromMarketplace, listMarketplaces, readMarketplaceManifest, removeMarketplace, resolvePluginSource, updateMarketplace, } from "./core/marketplaces.js";
16
21
  export { runExtensionCommand } from "./tui/extensionCommand.js";
17
22
  export { agentKitVersion } from "./core/version.js";
18
23
  export { BUILT_IN_PAGE_TYPES, isSlug, parseId, linkedIds, } from "./extensions/knowledge/knowledgeStore.js";
@@ -8,14 +8,23 @@ export { chatExtensionsCommand, describe } from "../chat/extensions.js";
8
8
  *
9
9
  * - `extension list`
10
10
  * - `extension info <name>`: its metadata, what it offers and where its README is
11
- * - `extension add <folder | git URL[#ref]> [--project] [--path <subfolder>]`
11
+ * - `extension add <folder | git URL[#ref] | plugin[@marketplace]> [--project] [--path <subfolder>] [--yes]`
12
12
  * - `extension remove|enable|disable <name> [--project | --agent]`
13
+ * - `extension search [words]`: what the known marketplaces offer
14
+ * - `extension marketplace add <folder | git URL[#ref] | owner/repo> [--yes]`, `list`, `update [name]`, `remove <name>`
13
15
  *
14
16
  * Without a scope, `add` installs into the agent's (the project's when the agent has no scope of
15
- * its own); the others act where the extension is, the project's first.
17
+ * its own); the others act where the extension is, the project's first. The marketplaces live in
18
+ * the same folder. `official` is the agent's own marketplace (a folder or git URL): known without
19
+ * asking, and installed from without a confirmation. Any other is added after a warning and
20
+ * typing its name, and installing from it asks first; `--yes` answers for the person (a script).
16
21
  */
17
22
  export declare function runExtensionCommand(argv: readonly string[], options: {
18
23
  dirs: ExtensionDirs;
19
24
  command?: string;
20
25
  write?: (line: string) => void;
26
+ /** The agent's official marketplace: a folder or a git URL (`#ref`). */
27
+ official?: string;
28
+ /** Asks the person (a confirmation); by default on the terminal, and nothing without one. */
29
+ ask?: (question: string) => Promise<string>;
21
30
  }): Promise<boolean>;
@@ -1,6 +1,8 @@
1
1
  import { stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { addExtension, listInstalled, removeExtension, setExtensionEnabled, } from "../core/externalExtensions.js";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { addMarketplace, findPlugin, inspectMarketplace, installFromMarketplace, listMarketplaces, removeMarketplace, updateMarketplace, } from "../core/marketplaces.js";
5
+ import { addExtension, isGitSource, listInstalled, removeExtension, setExtensionEnabled, } from "../core/externalExtensions.js";
4
6
  import { describe } from "../chat/extensions.js";
5
7
  export { chatExtensionsCommand, describe } from "../chat/extensions.js";
6
8
  /**
@@ -11,11 +13,16 @@ export { chatExtensionsCommand, describe } from "../chat/extensions.js";
11
13
  *
12
14
  * - `extension list`
13
15
  * - `extension info <name>`: its metadata, what it offers and where its README is
14
- * - `extension add <folder | git URL[#ref]> [--project] [--path <subfolder>]`
16
+ * - `extension add <folder | git URL[#ref] | plugin[@marketplace]> [--project] [--path <subfolder>] [--yes]`
15
17
  * - `extension remove|enable|disable <name> [--project | --agent]`
18
+ * - `extension search [words]`: what the known marketplaces offer
19
+ * - `extension marketplace add <folder | git URL[#ref] | owner/repo> [--yes]`, `list`, `update [name]`, `remove <name>`
16
20
  *
17
21
  * Without a scope, `add` installs into the agent's (the project's when the agent has no scope of
18
- * its own); the others act where the extension is, the project's first.
22
+ * its own); the others act where the extension is, the project's first. The marketplaces live in
23
+ * the same folder. `official` is the agent's own marketplace (a folder or git URL): known without
24
+ * asking, and installed from without a confirmation. Any other is added after a warning and
25
+ * typing its name, and installing from it asks first; `--yes` answers for the person (a script).
19
26
  */
20
27
  export async function runExtensionCommand(argv, options) {
21
28
  if (argv[0] !== "extension")
@@ -36,6 +43,22 @@ export async function runExtensionCommand(argv, options) {
36
43
  return true;
37
44
  };
38
45
  const scopeDir = (scope) => options.dirs[scope];
46
+ // The marketplaces live with the agent's extensions (the project's when it has no scope of its own).
47
+ const marketsDir = options.dirs.agent ?? options.dirs.project;
48
+ const ask = options.ask ?? askOnTerminal;
49
+ const confirmed = async (question, accepts) => flag("yes") || accepts((await ask(question)).trim());
50
+ // The agent's own marketplace, known without asking; a folder is copied afresh each time (it's the agent's own code).
51
+ const knownMarketplaces = async () => {
52
+ if (!marketsDir)
53
+ throw new Error("This agent has no folder for extensions.");
54
+ const known = await listMarketplaces(marketsDir);
55
+ const official = options.official;
56
+ if (official && !(isGitSource(official) && known.some((marketplace) => marketplace.official))) {
57
+ await addMarketplace(official, marketsDir, { official: true });
58
+ return listMarketplaces(marketsDir);
59
+ }
60
+ return known;
61
+ };
39
62
  try {
40
63
  switch (action) {
41
64
  case "list": {
@@ -62,11 +85,26 @@ export async function runExtensionCommand(argv, options) {
62
85
  case "add": {
63
86
  const source = positional[0];
64
87
  if (!source)
65
- return fail(`Usage: ${command} extension add <folder | git URL[#ref]> [--project] [--path <subfolder>]`);
88
+ return fail(`Usage: ${command} extension add <folder | git URL[#ref] | plugin[@marketplace]> [--project] [--path <subfolder>]`);
66
89
  const scope = askedScope ?? (options.dirs.agent ? "agent" : "project");
67
90
  const dir = scopeDir(scope);
68
91
  if (!dir)
69
92
  return fail(`This agent has no ${scope} scope for extensions.`);
93
+ // A plugin of a marketplace, unless it's a folder here or a git URL.
94
+ const isFolder = await stat(path.resolve(source)).then((found) => found.isDirectory(), () => false);
95
+ if (!isFolder && !isGitSource(source) && /^[A-Za-z0-9][\w.-]*(@[A-Za-z0-9][\w.-]*)?$/.test(source)) {
96
+ await knownMarketplaces();
97
+ const { marketplace, plugin } = await findPlugin(marketsDir, source);
98
+ if (!marketplace.official) {
99
+ write(`${plugin.name}${plugin.version ? ` ${plugin.version}` : ""}, from the marketplace ${marketplace.name} (${marketplace.source}): ${plugin.description ?? "no description"}`);
100
+ write("It will run on this computer with your permissions.");
101
+ if (!(await confirmed("Install it? (y/N) ", (typed) => /^(y|yes|s|si|sí)$/i.test(typed))))
102
+ return fail("Not installed.");
103
+ }
104
+ const result = await installFromMarketplace(marketsDir, source, dir);
105
+ 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.`);
106
+ return true;
107
+ }
70
108
  const subdir = value("path");
71
109
  const result = await addExtension(source, dir, subdir ? { subdir } : {});
72
110
  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.`);
@@ -92,19 +130,104 @@ export async function runExtensionCommand(argv, options) {
92
130
  write(`${name} ${action === "remove" ? "removed" : `${action}d`} in the ${scope} scope. It applies from the next session (in the chat, \`/extensions\` reopens it).`);
93
131
  return true;
94
132
  }
133
+ case "search": {
134
+ const words = positional.map((word) => word.toLowerCase());
135
+ const installed = new Set((await listInstalled(options.dirs)).map((extension) => extension.name));
136
+ const offered = (await knownMarketplaces()).flatMap((marketplace) => (marketplace.manifest?.plugins ?? []).map((plugin) => ({ marketplace, plugin })));
137
+ const matching = offered.filter(({ plugin }) => {
138
+ const text = [plugin.name, plugin.description, plugin.category, ...(plugin.tags ?? []), ...(plugin.keywords ?? [])].join(" ").toLowerCase();
139
+ return words.every((word) => text.includes(word));
140
+ });
141
+ if (!matching.length) {
142
+ write(offered.length ? "No extension matches." : `No marketplace offers extensions yet. \`${command} extension marketplace add <folder | git URL | owner/repo>\` adds one.`);
143
+ return true;
144
+ }
145
+ for (const { marketplace, plugin } of matching) {
146
+ write(`${plugin.name}@${marketplace.name}${plugin.version ? ` ${plugin.version}` : ""}${installed.has(plugin.name) ? " (installed)" : ""}${plugin.description ? ` ${plugin.description}` : ""}`);
147
+ }
148
+ return true;
149
+ }
150
+ case "marketplace":
151
+ return await marketplaceCommand();
95
152
  default:
96
153
  return fail([
97
154
  `Usage: ${command} extension <action>`,
98
155
  " list what's installed, in which scope, on or off",
99
156
  " info <name> its metadata, what it offers, its README",
100
- " add <folder | git URL[#ref]> [--project] [--path <subfolder>]",
157
+ " add <folder | git URL[#ref] | plugin[@marketplace]> [--project] [--path <subfolder>] [--yes]",
101
158
  " remove|enable|disable <name> [--project | --agent]",
159
+ " search [words] what the known marketplaces offer",
160
+ " marketplace add <folder | git URL[#ref] | owner/repo> [--yes]",
161
+ " marketplace list | update [name] | remove <name>",
102
162
  ].join("\n"));
103
163
  }
104
164
  }
105
165
  catch (error) {
106
166
  return fail(error instanceof Error ? error.message : String(error));
107
167
  }
168
+ /** `extension marketplace <add | list | update | remove>`. */
169
+ async function marketplaceCommand() {
170
+ const [what, name] = positional;
171
+ switch (what) {
172
+ case "add": {
173
+ if (!name)
174
+ return fail(`Usage: ${command} extension marketplace add <folder | git URL[#ref] | owner/repo> [--yes]`);
175
+ await knownMarketplaces();
176
+ const manifest = await inspectMarketplace(name);
177
+ write(`The marketplace ${manifest.name}, by ${manifest.owner.name}${manifest.description ? `: ${manifest.description}` : ""} (${manifest.plugins.length} extension${manifest.plugins.length === 1 ? "" : "s"}).`);
178
+ write("Its extensions run on this computer with your permissions, and it isn't this agent's own: add it only if you trust who publishes it.");
179
+ if (!(await confirmed(`Type its name (${manifest.name}) to add it: `, (typed) => typed === manifest.name)))
180
+ return fail("Not added.");
181
+ const result = await addMarketplace(name, marketsDir);
182
+ write(`${result.replaced ? "Updated" : "Added"} the marketplace ${result.name}${result.commit ? `, at ${result.commit.slice(0, 12)}` : ""}. \`${command} extension search\` lists what it offers.`);
183
+ return true;
184
+ }
185
+ case "list": {
186
+ const known = await knownMarketplaces();
187
+ if (!known.length) {
188
+ write(`No marketplaces. \`${command} extension marketplace add <folder | git URL | owner/repo>\` adds one.`);
189
+ return true;
190
+ }
191
+ for (const marketplace of known) {
192
+ write(`${marketplace.name}${marketplace.official ? " (official)" : ""} ${marketplace.manifest ? `${marketplace.manifest.plugins.length} extension${marketplace.manifest.plugins.length === 1 ? "" : "s"}` : "can't be read"} from ${marketplace.source}${marketplace.commit ? ` at ${marketplace.commit.slice(0, 12)}` : ""}, updated ${marketplace.updated.slice(0, 10)}`);
193
+ }
194
+ return true;
195
+ }
196
+ case "update": {
197
+ const known = await knownMarketplaces();
198
+ const chosen = name ? known.filter((marketplace) => marketplace.name === name) : known;
199
+ if (name && !chosen.length)
200
+ return fail(`No marketplace ${name}.`);
201
+ for (const marketplace of chosen) {
202
+ const { commit } = await updateMarketplace(marketsDir, marketplace.name);
203
+ write(`Updated ${marketplace.name}${commit ? `, at ${commit.slice(0, 12)}` : ""}. Its installed extensions stay as they are: \`${command} extension add <name>@${marketplace.name}\` installs one again.`);
204
+ }
205
+ return true;
206
+ }
207
+ case "remove": {
208
+ if (!name)
209
+ return fail(`Usage: ${command} extension marketplace remove <name>`);
210
+ if (!marketsDir || !(await removeMarketplace(marketsDir, name)))
211
+ return fail(`No marketplace ${name}.`);
212
+ write(`Removed the marketplace ${name}. The extensions installed from it stay.`);
213
+ return true;
214
+ }
215
+ default:
216
+ return fail(`Usage: ${command} extension marketplace <add | list | update | remove>`);
217
+ }
218
+ }
219
+ }
220
+ /** A question on the terminal, or no answer without one. */
221
+ async function askOnTerminal(question) {
222
+ if (!process.stdin.isTTY)
223
+ return "";
224
+ const terminal = createInterface({ input: process.stdin, output: process.stdout });
225
+ try {
226
+ return await terminal.question(question);
227
+ }
228
+ finally {
229
+ terminal.close();
230
+ }
108
231
  }
109
232
  /** Everything an installed extension's manifest says about it, for `extension info`. */
110
233
  async function info(extension) {
@@ -124,6 +247,7 @@ async function info(extension) {
124
247
  ...field("Works with", manifest.kit && `agent-kit ${manifest.kit}`),
125
248
  ...field("Provides", manifest.provides.join(", ") || undefined),
126
249
  ...field("Requires", manifest.requires.join(", ") || undefined),
250
+ ...field("Needs", manifest.needs.join(", ") || undefined),
127
251
  ...field("Servers", Object.keys(manifest.servers).join(", ") || undefined),
128
252
  ...field("Only read", manifest.readOnlyTools.join(", ") || undefined),
129
253
  ...field("Variables", Object.values(manifest.servers).flatMap((server) => Object.keys(server.env ?? {})).join(", ") || undefined),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falkenslab/agent-kit",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Library for building an agent on top of the Claude Agent SDK",
5
5
  "author": "Francisco Vargas Ruiz",
6
6
  "license": "MIT",