@amalgm/tools 0.1.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 (52) hide show
  1. package/PURPOSE.md +87 -0
  2. package/README.md +62 -0
  3. package/dist/api-driver.d.ts +3 -0
  4. package/dist/api-driver.js +92 -0
  5. package/dist/artifact-files.d.ts +28 -0
  6. package/dist/artifact-files.js +115 -0
  7. package/dist/artifacts.d.ts +66 -0
  8. package/dist/artifacts.js +112 -0
  9. package/dist/bin/mcp.d.ts +2 -0
  10. package/dist/bin/mcp.js +13 -0
  11. package/dist/bin/tools.d.ts +2 -0
  12. package/dist/bin/tools.js +3 -0
  13. package/dist/cli-driver.d.ts +3 -0
  14. package/dist/cli-driver.js +52 -0
  15. package/dist/cli.d.ts +13 -0
  16. package/dist/cli.js +123 -0
  17. package/dist/definition.d.ts +7 -0
  18. package/dist/definition.js +170 -0
  19. package/dist/ids.d.ts +7 -0
  20. package/dist/ids.js +38 -0
  21. package/dist/index.d.ts +9 -0
  22. package/dist/index.js +7 -0
  23. package/dist/input.d.ts +4 -0
  24. package/dist/input.js +43 -0
  25. package/dist/mcp-server.d.ts +18 -0
  26. package/dist/mcp-server.js +61 -0
  27. package/dist/mcp.d.ts +9 -0
  28. package/dist/mcp.js +95 -0
  29. package/dist/module.d.ts +3 -0
  30. package/dist/module.js +22 -0
  31. package/dist/process.d.ts +22 -0
  32. package/dist/process.js +46 -0
  33. package/dist/query.d.ts +23 -0
  34. package/dist/query.js +36 -0
  35. package/dist/results.d.ts +6 -0
  36. package/dist/results.js +27 -0
  37. package/dist/schema.d.ts +3 -0
  38. package/dist/schema.js +26 -0
  39. package/dist/secrets.d.ts +3 -0
  40. package/dist/secrets.js +13 -0
  41. package/dist/selection.d.ts +10 -0
  42. package/dist/selection.js +18 -0
  43. package/dist/store.d.ts +17 -0
  44. package/dist/store.js +96 -0
  45. package/dist/toolbox.d.ts +46 -0
  46. package/dist/toolbox.js +204 -0
  47. package/dist/types.d.ts +172 -0
  48. package/dist/types.js +1 -0
  49. package/dist/updates.d.ts +7 -0
  50. package/dist/updates.js +57 -0
  51. package/docs/ENGINE_INTEGRATION.md +57 -0
  52. package/package.json +57 -0
@@ -0,0 +1,46 @@
1
+ import { spawn } from 'node:child_process';
2
+ function runCommand(input) {
3
+ return new Promise((resolve, reject) => {
4
+ const child = spawn(input.command, input.args, {
5
+ cwd: input.cwd || process.cwd(), env: input.env,
6
+ shell: false, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
7
+ });
8
+ let stdout = Buffer.alloc(0);
9
+ let stderr = Buffer.alloc(0);
10
+ let timedOut = false;
11
+ let aborted = false;
12
+ let truncated = false;
13
+ const append = (current, chunk) => {
14
+ const room = Math.max(0, input.maximumBytes - stdout.length - stderr.length);
15
+ if (chunk.length > room)
16
+ truncated = true;
17
+ return room > 0 ? Buffer.concat([current, chunk.subarray(0, room)]) : current;
18
+ };
19
+ child.stdout.on('data', (chunk) => { stdout = append(stdout, chunk); });
20
+ child.stderr.on('data', (chunk) => { stderr = append(stderr, chunk); });
21
+ child.on('error', reject);
22
+ const stop = () => child.kill('SIGTERM');
23
+ let forceTimer;
24
+ const terminate = () => {
25
+ stop();
26
+ forceTimer = setTimeout(() => child.kill('SIGKILL'), 1_000);
27
+ forceTimer.unref();
28
+ };
29
+ const timer = setTimeout(() => { timedOut = true; terminate(); }, input.timeoutMs);
30
+ timer.unref();
31
+ const abort = () => { aborted = true; terminate(); };
32
+ input.signal?.addEventListener('abort', abort, { once: true });
33
+ child.on('close', (code, signal) => {
34
+ clearTimeout(timer);
35
+ if (forceTimer)
36
+ clearTimeout(forceTimer);
37
+ input.signal?.removeEventListener('abort', abort);
38
+ resolve({
39
+ code, signal, timedOut, aborted, truncated,
40
+ stdout: stdout.toString('utf8'), stderr: stderr.toString('utf8'),
41
+ });
42
+ });
43
+ child.stdin.end(input.stdin);
44
+ });
45
+ }
46
+ export { runCommand };
@@ -0,0 +1,23 @@
1
+ import type { Catalog, LoadoutInput, ToolQuery, ToolRecord } from './types.js';
2
+ interface QueryPort {
3
+ catalog(): Catalog;
4
+ hasDriver(tool: ToolRecord): boolean;
5
+ }
6
+ declare function matches(tool: ToolRecord, filters: ToolQuery): boolean;
7
+ declare function queryTools(port: QueryPort, filters?: ToolQuery): {
8
+ tool: ToolRecord;
9
+ actions: import("./types.js").ActionRecord[];
10
+ }[];
11
+ declare function callableActions(port: QueryPort, loadout?: LoadoutInput, filters?: ToolQuery): {
12
+ tool: ToolRecord;
13
+ action: import("./types.js").ActionRecord;
14
+ }[];
15
+ declare function resolveAction(port: QueryPort, toolId: string, actionReference: string, loadout?: LoadoutInput): {
16
+ tool: ToolRecord;
17
+ action: import("./types.js").ActionRecord;
18
+ } | null;
19
+ declare function resolveCallableAction(port: QueryPort, toolId: string, actionReference: string, loadout?: LoadoutInput, filters?: ToolQuery): {
20
+ tool: ToolRecord;
21
+ action: import("./types.js").ActionRecord;
22
+ } | null;
23
+ export { callableActions, matches, queryTools, resolveAction, resolveCallableAction };
package/dist/query.js ADDED
@@ -0,0 +1,36 @@
1
+ import { id } from './ids.js';
2
+ import { findSelected, selected } from './selection.js';
3
+ function matches(tool, filters) {
4
+ return (filters.type === undefined || tool.source.type === filters.type)
5
+ && (filters.owner === undefined || tool.owner === filters.owner)
6
+ && (filters.origin === undefined || tool.origin === filters.origin)
7
+ && (filters.origins === undefined || filters.origins.includes(tool.origin))
8
+ && (filters.status === undefined || tool.status === filters.status);
9
+ }
10
+ function queryTools(port, filters = {}) {
11
+ const catalog = port.catalog();
12
+ return catalog.tools
13
+ .filter((tool) => matches(tool, filters))
14
+ .map((tool) => ({
15
+ tool,
16
+ actions: catalog.actions.filter((action) => action.toolId === tool.id),
17
+ }));
18
+ }
19
+ function callableActions(port, loadout, filters = {}) {
20
+ return selected(port.catalog(), loadout)
21
+ .filter(({ tool }) => port.hasDriver(tool))
22
+ .filter(({ tool }) => matches(tool, filters));
23
+ }
24
+ function resolveAction(port, toolId, actionReference, loadout) {
25
+ const owner = id(toolId, 'tool id');
26
+ const reference = id(actionReference, 'action reference');
27
+ const actionId = reference.startsWith(`${owner}.`) ? reference : `${owner}.${reference}`;
28
+ return findSelected(port.catalog(), actionId, loadout);
29
+ }
30
+ function resolveCallableAction(port, toolId, actionReference, loadout, filters = {}) {
31
+ const resolved = resolveAction(port, toolId, actionReference, loadout);
32
+ return resolved && port.hasDriver(resolved.tool) && matches(resolved.tool, filters)
33
+ ? resolved
34
+ : null;
35
+ }
36
+ export { callableActions, matches, queryTools, resolveAction, resolveCallableAction };
@@ -0,0 +1,6 @@
1
+ import type { ToolResult } from './types.js';
2
+ declare function textResult(value: unknown): ToolResult;
3
+ declare function structuredResult(value: unknown): ToolResult;
4
+ declare function errorResult(value: unknown): ToolResult;
5
+ declare function boundedResult(result: ToolResult, maximum: number): ToolResult;
6
+ export { boundedResult, errorResult, structuredResult, textResult };
@@ -0,0 +1,27 @@
1
+ function textResult(value) {
2
+ const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
3
+ return { content: [{ type: 'text', text }] };
4
+ }
5
+ function structuredResult(value) {
6
+ return { ...textResult(value), structuredContent: value };
7
+ }
8
+ function errorResult(value) {
9
+ return { ...textResult(value instanceof Error ? value.message : value), isError: true };
10
+ }
11
+ function boundedResult(result, maximum) {
12
+ let remaining = Math.max(0, maximum);
13
+ let truncated = false;
14
+ const content = result.content.map((item) => {
15
+ const bytes = Buffer.from(item.text);
16
+ if (bytes.length <= remaining) {
17
+ remaining -= bytes.length;
18
+ return item;
19
+ }
20
+ truncated = true;
21
+ const text = bytes.subarray(0, remaining).toString('utf8');
22
+ remaining = 0;
23
+ return { ...item, text: `${text}\n[output truncated]` };
24
+ });
25
+ return { ...result, content, ...(truncated ? { truncated: true } : {}) };
26
+ }
27
+ export { boundedResult, errorResult, structuredResult, textResult };
@@ -0,0 +1,3 @@
1
+ import type Database from 'better-sqlite3';
2
+ declare function migrate(db: Database.Database): void;
3
+ export { migrate };
package/dist/schema.js ADDED
@@ -0,0 +1,26 @@
1
+ function migrate(db) {
2
+ db.exec(`
3
+ CREATE TABLE IF NOT EXISTS metadata (
4
+ key TEXT PRIMARY KEY,
5
+ value TEXT NOT NULL
6
+ );
7
+ INSERT OR IGNORE INTO metadata (key, value) VALUES ('revision', '0');
8
+
9
+ CREATE TABLE IF NOT EXISTS tools (
10
+ id TEXT PRIMARY KEY,
11
+ origin TEXT NOT NULL,
12
+ status TEXT NOT NULL,
13
+ record_json TEXT NOT NULL
14
+ );
15
+
16
+ CREATE TABLE IF NOT EXISTS actions (
17
+ id TEXT PRIMARY KEY,
18
+ tool_id TEXT NOT NULL REFERENCES tools(id) ON DELETE CASCADE,
19
+ status TEXT NOT NULL,
20
+ record_json TEXT NOT NULL,
21
+ UNIQUE(tool_id, id)
22
+ );
23
+ CREATE INDEX IF NOT EXISTS actions_tool_id ON actions(tool_id);
24
+ `);
25
+ }
26
+ export { migrate };
@@ -0,0 +1,3 @@
1
+ import type { SecretResolver } from './types.js';
2
+ declare function resolveReferences(references: Record<string, string> | undefined, resolver: SecretResolver | undefined): Promise<Record<string, string>>;
3
+ export { resolveReferences };
@@ -0,0 +1,13 @@
1
+ async function resolveReferences(references, resolver) {
2
+ const resolved = {};
3
+ for (const [name, reference] of Object.entries(references || {})) {
4
+ if (!resolver)
5
+ throw new Error(`Secret resolver is required for ${reference}`);
6
+ const value = await resolver(reference);
7
+ if (value === undefined)
8
+ throw new Error(`Secret not found: ${reference}`);
9
+ resolved[name] = value;
10
+ }
11
+ return resolved;
12
+ }
13
+ export { resolveReferences };
@@ -0,0 +1,10 @@
1
+ import type { ActionRecord, Catalog, LoadoutInput, ToolRecord } from './types.js';
2
+ declare function selected(catalog: Catalog, loadout?: LoadoutInput): Array<{
3
+ tool: ToolRecord;
4
+ action: ActionRecord;
5
+ }>;
6
+ declare function findSelected(catalog: Catalog, actionId: string, loadout?: LoadoutInput): {
7
+ tool: ToolRecord;
8
+ action: ActionRecord;
9
+ } | null;
10
+ export { findSelected, selected };
@@ -0,0 +1,18 @@
1
+ function selected(catalog, loadout) {
2
+ const ids = loadout === undefined
3
+ ? null
4
+ : new Set(Array.isArray(loadout) ? loadout : loadout.toolIds);
5
+ const tools = new Map(catalog.tools.map((tool) => [tool.id, tool]));
6
+ return catalog.actions.flatMap((action) => {
7
+ const tool = tools.get(action.toolId);
8
+ if (!tool || tool.status !== 'enabled' || action.status !== 'enabled')
9
+ return [];
10
+ if (ids && !ids.has(tool.id) && !ids.has(action.id))
11
+ return [];
12
+ return [{ tool, action }];
13
+ });
14
+ }
15
+ function findSelected(catalog, actionId, loadout) {
16
+ return selected(catalog, loadout).find(({ action }) => action.id === actionId) || null;
17
+ }
18
+ export { findSelected, selected };
@@ -0,0 +1,17 @@
1
+ import Database from 'better-sqlite3';
2
+ import type { ActionRecord, ApplyResult, Catalog, Status, ToolRecord } from './types.js';
3
+ declare class Store {
4
+ readonly db: Database.Database;
5
+ constructor(file: string);
6
+ revision(): number;
7
+ private bump;
8
+ getTool(id: string): ToolRecord | null;
9
+ getAction(id: string): ActionRecord | null;
10
+ actionsFor(toolId: string): ActionRecord[];
11
+ catalog(): Catalog;
12
+ apply(result: ApplyResult): ApplyResult;
13
+ setStatus(id: string, status: Status, now: string): ToolRecord | ActionRecord | null;
14
+ remove(id: string): ToolRecord | ActionRecord | null;
15
+ close(): void;
16
+ }
17
+ export { Store };
package/dist/store.js ADDED
@@ -0,0 +1,96 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import Database from 'better-sqlite3';
4
+ import { migrate } from './schema.js';
5
+ function parse(row) {
6
+ return row ? JSON.parse(row.record_json) : null;
7
+ }
8
+ class Store {
9
+ db;
10
+ constructor(file) {
11
+ fs.mkdirSync(path.dirname(file), { recursive: true });
12
+ this.db = new Database(file);
13
+ this.db.pragma('journal_mode = WAL');
14
+ this.db.pragma('foreign_keys = ON');
15
+ this.db.pragma('busy_timeout = 5000');
16
+ migrate(this.db);
17
+ }
18
+ revision() {
19
+ const row = this.db.prepare("SELECT value FROM metadata WHERE key = 'revision'").get();
20
+ return Number(row.value);
21
+ }
22
+ bump() {
23
+ this.db.prepare("UPDATE metadata SET value = CAST(value AS INTEGER) + 1 WHERE key = 'revision'").run();
24
+ }
25
+ getTool(id) {
26
+ return parse(this.db.prepare('SELECT record_json FROM tools WHERE id = ?').get(id));
27
+ }
28
+ getAction(id) {
29
+ return parse(this.db.prepare('SELECT record_json FROM actions WHERE id = ?').get(id));
30
+ }
31
+ actionsFor(toolId) {
32
+ return this.db.prepare('SELECT record_json FROM actions WHERE tool_id = ? ORDER BY id')
33
+ .all(toolId).map((row) => parse(row)).filter(Boolean);
34
+ }
35
+ catalog() {
36
+ const tools = this.db.prepare('SELECT record_json FROM tools ORDER BY id')
37
+ .all().map((row) => parse(row)).filter(Boolean);
38
+ const actions = this.db.prepare('SELECT record_json FROM actions ORDER BY id')
39
+ .all().map((row) => parse(row)).filter(Boolean);
40
+ return { version: 1, revision: this.revision(), tools, actions };
41
+ }
42
+ apply(result) {
43
+ this.db.transaction(() => {
44
+ this.db.prepare(`
45
+ INSERT INTO tools (id, origin, status, record_json)
46
+ VALUES (@id, @origin, @status, @json)
47
+ ON CONFLICT(id) DO UPDATE SET
48
+ origin = excluded.origin, status = excluded.status, record_json = excluded.record_json
49
+ `).run({ id: result.tool.id, origin: result.tool.origin, status: result.tool.status, json: JSON.stringify(result.tool) });
50
+ this.db.prepare('DELETE FROM actions WHERE tool_id = ?').run(result.tool.id);
51
+ const insert = this.db.prepare('INSERT INTO actions (id, tool_id, status, record_json) VALUES (?, ?, ?, ?)');
52
+ for (const action of result.actions)
53
+ insert.run(action.id, action.toolId, action.status, JSON.stringify(action));
54
+ this.bump();
55
+ })();
56
+ return result;
57
+ }
58
+ setStatus(id, status, now) {
59
+ return this.db.transaction(() => {
60
+ const tool = this.getTool(id);
61
+ if (tool) {
62
+ const updated = { ...tool, status, updatedAt: now };
63
+ this.db.prepare('UPDATE tools SET status = ?, record_json = ? WHERE id = ?')
64
+ .run(status, JSON.stringify(updated), id);
65
+ this.bump();
66
+ return updated;
67
+ }
68
+ const action = this.getAction(id);
69
+ if (!action)
70
+ return null;
71
+ const updated = { ...action, status, updatedAt: now };
72
+ this.db.prepare('UPDATE actions SET status = ?, record_json = ? WHERE id = ?')
73
+ .run(status, JSON.stringify(updated), id);
74
+ this.bump();
75
+ return updated;
76
+ })();
77
+ }
78
+ remove(id) {
79
+ return this.db.transaction(() => {
80
+ const tool = this.getTool(id);
81
+ if (tool) {
82
+ this.db.prepare('DELETE FROM tools WHERE id = ?').run(id);
83
+ this.bump();
84
+ return tool;
85
+ }
86
+ const action = this.getAction(id);
87
+ if (!action)
88
+ return null;
89
+ this.db.prepare('DELETE FROM actions WHERE id = ?').run(id);
90
+ this.bump();
91
+ return action;
92
+ })();
93
+ }
94
+ close() { this.db.close(); }
95
+ }
96
+ export { Store };
@@ -0,0 +1,46 @@
1
+ import type { ActionDefinition, ActionPatch, ActionRecord, ApplyResult, CallOptions, Catalog, LoadoutInput, ToolDefinition, ToolboxOptions, ToolPatch, ToolQuery, ToolRecord, ToolResult } from './types.js';
2
+ declare class Toolbox {
3
+ private readonly store;
4
+ private readonly artifacts;
5
+ private readonly system;
6
+ private readonly drivers;
7
+ private readonly onChange?;
8
+ constructor(options?: ToolboxOptions);
9
+ catalog(): Catalog;
10
+ list(loadout?: LoadoutInput): Array<{
11
+ tool: ToolRecord;
12
+ actions: ActionRecord[];
13
+ }>;
14
+ query(filters?: ToolQuery): {
15
+ tool: ToolRecord;
16
+ actions: ActionRecord[];
17
+ }[];
18
+ callable(loadout?: LoadoutInput, filters?: ToolQuery): {
19
+ tool: ToolRecord;
20
+ action: ActionRecord;
21
+ }[];
22
+ get(toolId: string): ApplyResult | null;
23
+ action(actionId: string): ActionRecord | null;
24
+ resolve(toolId: string, actionReference: string, loadout?: LoadoutInput): {
25
+ tool: ToolRecord;
26
+ action: ActionRecord;
27
+ } | null;
28
+ resolveCallable(toolId: string, actionReference: string, loadout?: LoadoutInput, filters?: ToolQuery): {
29
+ tool: ToolRecord;
30
+ action: ActionRecord;
31
+ } | null;
32
+ apply(definition: ToolDefinition): Promise<ApplyResult>;
33
+ update(toolId: string, patch: ToolPatch): Promise<ApplyResult>;
34
+ upsertAction(toolId: string, definition: ActionDefinition): Promise<ActionRecord>;
35
+ updateAction(actionId: string, patch: ActionPatch): Promise<ActionRecord>;
36
+ setStatus(recordId: string, status: 'enabled' | 'disabled'): Promise<ToolRecord | ActionRecord>;
37
+ remove(recordId: string): Promise<ToolRecord | ActionRecord>;
38
+ connections(loadout?: LoadoutInput): ToolRecord[];
39
+ call(actionId: string, input?: unknown, options?: CallOptions): Promise<ToolResult>;
40
+ hasDriver(tool: ToolRecord): boolean;
41
+ callMcp(name: string, input?: unknown, options?: CallOptions): Promise<ToolResult>;
42
+ private changed;
43
+ private driverFor;
44
+ close(): void;
45
+ }
46
+ export { Toolbox };
@@ -0,0 +1,204 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { resolveProductStateDir } from '@amalgm/core/identity';
4
+ import { apiDriver } from './api-driver.js';
5
+ import { ArtifactFiles } from './artifact-files.js';
6
+ import { cliDriver } from './cli-driver.js';
7
+ import { normalizeDefinition } from './definition.js';
8
+ import { assertMcpNamesUnique, id, mcpName } from './ids.js';
9
+ import { validateInput } from './input.js';
10
+ import { callableActions, queryTools, resolveAction, resolveCallableAction } from './query.js';
11
+ import { findSelected, selected } from './selection.js';
12
+ import { Store } from './store.js';
13
+ import { updateAction, updateTool, upsertAction } from './updates.js';
14
+ function semantic(value) {
15
+ const { createdAt, updatedAt, ...record } = value;
16
+ return JSON.stringify(record);
17
+ }
18
+ function defaultDatabase(options) {
19
+ if (options.databaseFile)
20
+ return path.resolve(options.databaseFile);
21
+ // The state-dir LAW lives in @amalgm/core; only the effectful reads
22
+ // (process.env, os.homedir) happen here at the construction boundary.
23
+ // Precedence: AMALGM_TOOLS_DIR, then AMALGM_DIR/toolbox, then the
24
+ // scoped layout <home>/.amalgm/users/<scope>/toolbox.
25
+ const state = options.stateDir || resolveProductStateDir({
26
+ product: 'tools',
27
+ primitive: 'toolbox',
28
+ env: process.env,
29
+ homedir: path.join(os.homedir(), '.amalgm'),
30
+ });
31
+ return path.join(path.resolve(state), 'tools.db');
32
+ }
33
+ class Toolbox {
34
+ store;
35
+ artifacts;
36
+ system;
37
+ drivers = new Map();
38
+ onChange;
39
+ constructor(options = {}) {
40
+ const databaseFile = defaultDatabase(options);
41
+ this.store = new Store(databaseFile);
42
+ this.artifacts = new ArtifactFiles(path.dirname(databaseFile));
43
+ this.onChange = options.onChange;
44
+ for (const driver of [cliDriver, apiDriver, ...(options.drivers || [])])
45
+ this.drivers.set(driver.type, driver);
46
+ const tools = [];
47
+ const actions = [];
48
+ for (const definition of options.systemTools || []) {
49
+ const normalized = normalizeDefinition({ ...definition, origin: 'system' });
50
+ if (tools.some((tool) => tool.id === normalized.tool.id))
51
+ throw new Error(`Duplicate system tool: ${normalized.tool.id}`);
52
+ tools.push(normalized.tool);
53
+ actions.push(...normalized.actions);
54
+ }
55
+ assertMcpNamesUnique(actions);
56
+ this.system = { version: 1, revision: 0, tools, actions };
57
+ this.artifacts.migrateLegacy();
58
+ this.artifacts.materialize(this.catalog());
59
+ }
60
+ catalog() {
61
+ const persisted = this.store.catalog();
62
+ const systemIds = new Set(this.system.tools.map((tool) => tool.id));
63
+ const userTools = persisted.tools.filter((tool) => !systemIds.has(tool.id));
64
+ const userActions = persisted.actions.filter((action) => !systemIds.has(action.toolId));
65
+ const catalog = {
66
+ version: 1, revision: persisted.revision,
67
+ tools: [...this.system.tools, ...userTools].sort((a, b) => a.id.localeCompare(b.id)),
68
+ actions: [...this.system.actions, ...userActions].sort((a, b) => a.id.localeCompare(b.id)),
69
+ };
70
+ assertMcpNamesUnique(catalog.actions);
71
+ return catalog;
72
+ }
73
+ list(loadout) {
74
+ const catalog = this.catalog();
75
+ const runnable = selected(catalog, loadout);
76
+ return catalog.tools.flatMap((tool) => {
77
+ const actions = runnable.filter((item) => item.tool.id === tool.id).map((item) => item.action);
78
+ if (loadout !== undefined && actions.length === 0)
79
+ return [];
80
+ return [{ tool, actions }];
81
+ });
82
+ }
83
+ query(filters = {}) { return queryTools(this, filters); }
84
+ callable(loadout, filters = {}) { return callableActions(this, loadout, filters); }
85
+ get(toolId) {
86
+ const value = id(toolId, 'tool id');
87
+ const catalog = this.catalog();
88
+ const tool = catalog.tools.find((candidate) => candidate.id === value);
89
+ return tool ? { tool, actions: catalog.actions.filter((action) => action.toolId === value) } : null;
90
+ }
91
+ action(actionId) {
92
+ const value = id(actionId, 'action id');
93
+ return this.catalog().actions.find((action) => action.id === value) || null;
94
+ }
95
+ resolve(toolId, actionReference, loadout) {
96
+ return resolveAction(this, toolId, actionReference, loadout);
97
+ }
98
+ resolveCallable(toolId, actionReference, loadout, filters = {}) {
99
+ return resolveCallableAction(this, toolId, actionReference, loadout, filters);
100
+ }
101
+ async apply(definition) {
102
+ const normalizedId = id(definition.id, 'tool.id', 64);
103
+ if (definition.origin === 'system')
104
+ throw new Error('System tools can only be supplied by the embedder');
105
+ if (this.system.tools.some((tool) => tool.id === normalizedId))
106
+ throw new Error(`System tool is immutable: ${normalizedId}`);
107
+ const existing = this.store.getTool(normalizedId);
108
+ const oldActions = new Map(this.store.actionsFor(normalizedId).map((action) => [action.id, action]));
109
+ const normalized = normalizeDefinition(definition, existing || undefined);
110
+ normalized.actions = normalized.actions.map((action) => ({
111
+ ...action, createdAt: oldActions.get(action.id)?.createdAt || action.createdAt,
112
+ }));
113
+ const candidateActions = [
114
+ ...this.system.actions,
115
+ ...this.store.catalog().actions.filter((action) => action.toolId !== normalized.tool.id),
116
+ ...normalized.actions,
117
+ ];
118
+ assertMcpNamesUnique(candidateActions);
119
+ const unchanged = existing && semantic(existing) === semantic(normalized.tool)
120
+ && normalized.actions.length === oldActions.size
121
+ && normalized.actions.every((action) => semantic(action) === semantic(oldActions.get(action.id)));
122
+ if (unchanged)
123
+ return { tool: existing, actions: [...oldActions.values()].sort((a, b) => a.id.localeCompare(b.id)) };
124
+ const result = this.store.apply(normalized);
125
+ await this.changed(normalized.tool.id);
126
+ return result;
127
+ }
128
+ async update(toolId, patch) {
129
+ return updateTool(this, toolId, patch);
130
+ }
131
+ async upsertAction(toolId, definition) {
132
+ return upsertAction(this, toolId, definition);
133
+ }
134
+ async updateAction(actionId, patch) {
135
+ return updateAction(this, actionId, patch);
136
+ }
137
+ async setStatus(recordId, status) {
138
+ const value = id(recordId, 'record id');
139
+ if (this.system.tools.some((tool) => tool.id === value)
140
+ || this.system.actions.some((action) => action.id === value))
141
+ throw new Error(`System record is immutable: ${value}`);
142
+ const updated = this.store.setStatus(value, status, new Date().toISOString());
143
+ if (!updated)
144
+ throw new Error(`Unknown tool or action: ${value}`);
145
+ await this.changed('toolId' in updated ? updated.toolId : updated.id);
146
+ return updated;
147
+ }
148
+ async remove(recordId) {
149
+ const value = id(recordId, 'record id');
150
+ if (this.system.tools.some((tool) => tool.id === value)
151
+ || this.system.actions.some((action) => action.id === value))
152
+ throw new Error(`System record is immutable: ${value}`);
153
+ const removed = this.store.remove(value);
154
+ if (!removed)
155
+ throw new Error(`Unknown tool or action: ${value}`);
156
+ await this.changed('toolId' in removed ? removed.toolId : removed.id);
157
+ return removed;
158
+ }
159
+ connections(loadout) {
160
+ const catalog = this.catalog();
161
+ const ids = loadout === undefined ? null : new Set(Array.isArray(loadout) ? loadout : loadout.toolIds);
162
+ return catalog.tools.filter((tool) => {
163
+ if (tool.status !== 'enabled' || tool.source.type !== 'mcp' || tool.origin === 'system')
164
+ return false;
165
+ if (!ids)
166
+ return true;
167
+ return ids.has(tool.id) || catalog.actions.some((action) => action.toolId === tool.id && ids.has(action.id));
168
+ });
169
+ }
170
+ async call(actionId, input = {}, options = {}) {
171
+ const resolved = findSelected(this.catalog(), id(actionId, 'action id'), options.loadout);
172
+ if (!resolved)
173
+ throw new Error(`Action is unavailable: ${actionId}`);
174
+ const driver = this.driverFor(resolved.tool);
175
+ if (!driver)
176
+ throw new Error(`No ${resolved.tool.source.type} driver is configured for ${resolved.tool.id}`);
177
+ return driver.call({ ...resolved, input: validateInput(resolved.action.inputSchema, input), options });
178
+ }
179
+ hasDriver(tool) { return this.driverFor(tool) !== null; }
180
+ async callMcp(name, input = {}, options = {}) {
181
+ const resolved = selected(this.catalog(), options.loadout).find(({ action }) => mcpName(action) === name);
182
+ if (!resolved)
183
+ throw new Error(`Unknown MCP tool: ${name}`);
184
+ return this.call(resolved.action.id, input, options);
185
+ }
186
+ async changed(toolId) {
187
+ const catalog = this.catalog();
188
+ const tool = catalog.tools.find((candidate) => candidate.id === toolId);
189
+ if (tool && tool.origin !== 'system') {
190
+ this.artifacts.writeTool({ tool, actions: catalog.actions.filter((action) => action.toolId === toolId) });
191
+ }
192
+ else if (!tool) {
193
+ this.artifacts.removeTool(toolId);
194
+ }
195
+ this.artifacts.writeIndex(catalog);
196
+ await this.onChange?.(catalog);
197
+ }
198
+ driverFor(tool) {
199
+ const driver = this.drivers.get(tool.source.type);
200
+ return driver && (driver.supports?.(tool) ?? true) ? driver : null;
201
+ }
202
+ close() { this.store.close(); }
203
+ }
204
+ export { Toolbox };