@amalgm/tools 0.1.7-preview.34176071261 → 0.1.7-preview.34545371724

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,9 +66,15 @@ amalgm-tools apply ./tool.mjs
66
66
  amalgm-tools list
67
67
  amalgm-tools run hello.say --input '{"name":"Ada"}'
68
68
  amalgm-tools disable hello.say
69
+ amalgm-tools link canvas app-3f1c
69
70
  amalgm-tools connections
70
71
  ```
71
72
 
73
+ A tool may belong to an app: `appId` on the tool is the reverse edge of the
74
+ app manifest's `toolIds`, and sharing either side carries the other. The host
75
+ that composes Tools and Apps may pass `environment(tool)` so a linked tool
76
+ runs knowing where its app listens.
77
+
72
78
  `amalgm-tools-mcp` serves Toolbox management and enabled action tools over
73
79
  newline-delimited MCP stdio.
74
80
 
@@ -1,3 +1,4 @@
1
- import type { ToolDriver } from './types.js';
2
- declare const cliDriver: ToolDriver;
3
- export { cliDriver };
1
+ import type { ToolDriver, ToolRecord } from './types.js';
2
+ type Environment = (tool: ToolRecord) => Record<string, string> | Promise<Record<string, string>>;
3
+ declare function createCliDriver(environment?: Environment): ToolDriver;
4
+ export { createCliDriver };
@@ -8,45 +8,47 @@ function inputArguments(input) {
8
8
  }
9
9
  return args;
10
10
  }
11
- const cliDriver = {
12
- type: 'cli',
13
- async call({ tool, action, input, options }) {
14
- const source = tool.source;
15
- const target = action.target;
16
- const environment = await resolveReferences(source.secretEnv, options.resolveSecret);
17
- const args = [...(source.args || []), ...(target.args || [])];
18
- if (source.inputMode === 'argv')
19
- args.push(...inputArguments(input));
20
- const output = await runCommand({
21
- command: source.command, args, cwd: source.cwd,
22
- env: { ...process.env, ...environment },
23
- stdin: source.inputMode === 'json-stdin' ? `${JSON.stringify(input)}\n` : undefined,
24
- timeoutMs: source.timeoutMs, maximumBytes: source.maxOutputBytes, signal: options.signal,
25
- });
26
- const stdout = output.stdout.trim();
27
- const stderr = output.stderr.trim();
28
- if (output.timedOut)
29
- return errorResult(`Tool ${action.id} timed out after ${source.timeoutMs}ms`);
30
- if (output.aborted)
31
- return errorResult(`Tool ${action.id} was cancelled`);
32
- if (output.code !== 0) {
33
- return errorResult([
34
- `Tool ${action.id} exited with ${output.code ?? output.signal ?? 'unknown'}.`,
35
- stdout && `stdout:\n${stdout}`, stderr && `stderr:\n${stderr}`,
36
- ].filter(Boolean).join('\n\n'));
37
- }
38
- let result;
39
- if (source.outputMode === 'json' && stdout) {
40
- try {
41
- result = structuredResult(JSON.parse(stdout));
11
+ function createCliDriver(environment = () => ({})) {
12
+ return {
13
+ type: 'cli',
14
+ async call({ tool, action, input, options }) {
15
+ const source = tool.source;
16
+ const target = action.target;
17
+ const secrets = await resolveReferences(source.secretEnv, options.resolveSecret);
18
+ const args = [...(source.args || []), ...(target.args || [])];
19
+ if (source.inputMode === 'argv')
20
+ args.push(...inputArguments(input));
21
+ const output = await runCommand({
22
+ command: source.command, args, cwd: source.cwd,
23
+ env: { ...process.env, ...(await environment(tool)), ...secrets },
24
+ stdin: source.inputMode === 'json-stdin' ? `${JSON.stringify(input)}\n` : undefined,
25
+ timeoutMs: source.timeoutMs, maximumBytes: source.maxOutputBytes, signal: options.signal,
26
+ });
27
+ const stdout = output.stdout.trim();
28
+ const stderr = output.stderr.trim();
29
+ if (output.timedOut)
30
+ return errorResult(`Tool ${action.id} timed out after ${source.timeoutMs}ms`);
31
+ if (output.aborted)
32
+ return errorResult(`Tool ${action.id} was cancelled`);
33
+ if (output.code !== 0) {
34
+ return errorResult([
35
+ `Tool ${action.id} exited with ${output.code ?? output.signal ?? 'unknown'}.`,
36
+ stdout && `stdout:\n${stdout}`, stderr && `stderr:\n${stderr}`,
37
+ ].filter(Boolean).join('\n\n'));
42
38
  }
43
- catch {
44
- return errorResult(`Tool ${action.id} returned invalid JSON`);
39
+ let result;
40
+ if (source.outputMode === 'json' && stdout) {
41
+ try {
42
+ result = structuredResult(JSON.parse(stdout));
43
+ }
44
+ catch {
45
+ return errorResult(`Tool ${action.id} returned invalid JSON`);
46
+ }
45
47
  }
46
- }
47
- else
48
- result = textResult(stdout || `Tool ${action.id} completed.`);
49
- return { ...result, ...(output.truncated ? { truncated: true } : {}) };
50
- },
51
- };
52
- export { cliDriver };
48
+ else
49
+ result = textResult(stdout || `Tool ${action.id} completed.`);
50
+ return { ...result, ...(output.truncated ? { truncated: true } : {}) };
51
+ },
52
+ };
53
+ }
54
+ export { createCliDriver };
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CliIo, LoadoutInput } from './types.js';
2
- declare const HELP = "Usage: amalgm-tools [--state-dir DIR] [--loadout IDS] <command>\n\nCommands:\n apply <definition.js|json> Apply one tool or an array of tools\n list List tools and enabled actions\n catalog Print the complete canonical catalog\n show <tool-or-action-id> Show one record\n enable <tool-or-action-id> Enable one record\n disable <tool-or-action-id> Disable one record\n remove <tool-or-action-id> Remove one record\n run <action-id> [--input JSON]\n Run an action\n connections List MCP connections for a composing host";
2
+ declare const HELP = "Usage: amalgm-tools [--state-dir DIR] [--loadout IDS] <command>\n\nCommands:\n apply <definition.js|json> Apply one tool or an array of tools\n list List tools and enabled actions\n catalog Print the complete canonical catalog\n show <tool-or-action-id> Show one record\n enable <tool-or-action-id> Enable one record\n disable <tool-or-action-id> Disable one record\n remove <tool-or-action-id> Remove one record\n link <tool-id> <app-id> Link one tool to the app it belongs to\n unlink <tool-id> Detach one tool from its app\n run <action-id> [--input JSON]\n Run an action\n connections List MCP connections for a composing host";
3
3
  interface Parsed {
4
4
  command?: string;
5
5
  args: string[];
package/dist/cli.js CHANGED
@@ -10,6 +10,8 @@ Commands:
10
10
  enable <tool-or-action-id> Enable one record
11
11
  disable <tool-or-action-id> Disable one record
12
12
  remove <tool-or-action-id> Remove one record
13
+ link <tool-id> <app-id> Link one tool to the app it belongs to
14
+ unlink <tool-id> Detach one tool from its app
13
15
  run <action-id> [--input JSON]
14
16
  Run an action
15
17
  connections List MCP connections for a composing host`;
@@ -101,6 +103,12 @@ async function runCli(argv, io = {}) {
101
103
  case 'remove':
102
104
  write(stdout, await toolbox.remove(required(recordId, 'record id')));
103
105
  break;
106
+ case 'link':
107
+ write(stdout, await toolbox.link(required(recordId, 'tool id'), required(parsed.args[1], 'app id')));
108
+ break;
109
+ case 'unlink':
110
+ write(stdout, await toolbox.link(required(recordId, 'tool id'), null));
111
+ break;
104
112
  case 'run':
105
113
  write(stdout, await toolbox.call(required(recordId, 'action id'), parsed.input, { loadout: parsed.loadout }));
106
114
  break;
@@ -140,6 +140,7 @@ function normalizeDefinition(definition, existing, now = new Date().toISOString(
140
140
  owner: definition.owner?.trim() || 'user', origin,
141
141
  status: definition.status || 'enabled',
142
142
  ...(definition.description?.trim() ? { description: definition.description.trim() } : {}),
143
+ ...(definition.appId?.trim() ? { appId: definition.appId.trim() } : {}),
143
144
  ...(definition.display ? { display: structuredClone(definition.display) } : {}),
144
145
  ...(definition.policy ? { policy: structuredClone(definition.policy) } : {}),
145
146
  ...(definition.metadata ? { metadata: structuredClone(definition.metadata) } : {}),
@@ -1,4 +1,20 @@
1
- import type { HostMcpToolDefinitionInput, ToolDefinition } from './types.js';
1
+ import type { JsonSchema, ToolDefinition } from './types.js';
2
+ interface HostMcpToolDescriptor {
3
+ name: string;
4
+ description?: string;
5
+ inputSchema?: JsonSchema;
6
+ }
7
+ interface HostMcpToolDefinitionInput {
8
+ id: string;
9
+ name: string;
10
+ description?: string;
11
+ serverName: string;
12
+ route: string;
13
+ tools: readonly HostMcpToolDescriptor[];
14
+ owner?: string;
15
+ display?: Record<string, unknown>;
16
+ metadata?: Record<string, unknown>;
17
+ }
2
18
  /**
3
19
  * Project an owning SDK's MCP descriptors into one immutable first-party
4
20
  * Toolbox tool. The descriptors remain authoritative; this adapter does not
@@ -6,3 +22,4 @@ import type { HostMcpToolDefinitionInput, ToolDefinition } from './types.js';
6
22
  */
7
23
  declare function defineHostMcpTool(input: HostMcpToolDefinitionInput): ToolDefinition;
8
24
  export { defineHostMcpTool };
25
+ export type { HostMcpToolDefinitionInput, HostMcpToolDescriptor };
package/dist/mcp.js CHANGED
@@ -51,6 +51,17 @@ function managementToolDescriptors() {
51
51
  name: 'toolbox_record_remove', description: 'Remove one user-owned tool or action.',
52
52
  inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
53
53
  },
54
+ {
55
+ name: 'toolbox_tool_link', description: 'Link one user-owned tool to the app it belongs to.',
56
+ inputSchema: {
57
+ type: 'object', required: ['id', 'app_id'], additionalProperties: false,
58
+ properties: { ...idProperty, app_id: { type: 'string', description: 'App id' } },
59
+ },
60
+ },
61
+ {
62
+ name: 'toolbox_tool_unlink', description: 'Detach one user-owned tool from its app.',
63
+ inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
64
+ },
54
65
  {
55
66
  name: 'toolbox_mcp_connections', description: 'List external MCP connections for the composing host.',
56
67
  inputSchema: { type: 'object', properties: {}, additionalProperties: false },
@@ -58,7 +69,7 @@ function managementToolDescriptors() {
58
69
  ];
59
70
  }
60
71
  function managementTools(toolbox) {
61
- const [list, get, apply, enable, disable, remove, connections] = managementToolDescriptors();
72
+ const [list, get, apply, enable, disable, remove, link, unlink, connections] = managementToolDescriptors();
62
73
  return [
63
74
  managed(list, () => toolbox.catalog()),
64
75
  managed(get, ({ id }) => {
@@ -71,6 +82,8 @@ function managementTools(toolbox) {
71
82
  managed(enable, ({ id }) => toolbox.setStatus(String(id), 'enabled')),
72
83
  managed(disable, ({ id }) => toolbox.setStatus(String(id), 'disabled')),
73
84
  managed(remove, ({ id }) => toolbox.remove(String(id))),
85
+ managed(link, ({ id, app_id }) => toolbox.link(String(id), String(app_id))),
86
+ managed(unlink, ({ id }) => toolbox.link(String(id), null)),
74
87
  managed(connections, () => toolbox.connections()),
75
88
  ];
76
89
  }
package/dist/toolbox.d.ts CHANGED
@@ -34,8 +34,11 @@ declare class Toolbox {
34
34
  update(toolId: string, patch: ToolPatch): Promise<ApplyResult>;
35
35
  upsertAction(toolId: string, definition: ActionDefinition): Promise<ActionRecord>;
36
36
  updateAction(actionId: string, patch: ActionPatch): Promise<ActionRecord>;
37
- setStatus(recordId: string, status: 'enabled' | 'disabled'): Promise<ToolRecord | ActionRecord>;
38
- remove(recordId: string): Promise<ToolRecord | ActionRecord>;
37
+ setStatus(recordId: string, status: 'enabled' | 'disabled'): Promise<ActionRecord | ToolRecord>;
38
+ /** Link one user tool to the app it belongs to; `null` unlinks it. */
39
+ link(toolId: string, appId: string | null): Promise<ToolRecord>;
40
+ isSystemTool(toolId: string): boolean;
41
+ remove(recordId: string): Promise<ActionRecord | ToolRecord>;
39
42
  currentDeployment(toolId: string): ToolDeployment | null;
40
43
  deployment(deploymentId: string): ToolDeployment | null;
41
44
  deploymentHistory(toolId: string): ToolDeployment[];
package/dist/toolbox.js CHANGED
@@ -5,7 +5,7 @@ import { apiDriver } from './api-driver.js';
5
5
  import { applyDefinition } from './apply-definition.js';
6
6
  import { ArtifactFiles } from './artifact-files.js';
7
7
  import { preserveProjectionTime } from './artifacts.js';
8
- import { cliDriver } from './cli-driver.js';
8
+ import { createCliDriver } from './cli-driver.js';
9
9
  import { DeploymentFiles } from './deployment-files.js';
10
10
  import { deploymentRevisionId } from './deployments.js';
11
11
  import { normalizeDefinition } from './definition.js';
@@ -14,7 +14,7 @@ import { Store } from './store.js';
14
14
  import { ToolboxDeployments } from './toolbox-deployments.js';
15
15
  import { ToolboxProjection } from './toolbox-projection.js';
16
16
  import { ToolboxView } from './toolbox-view.js';
17
- import { removeAction, updateAction, updateTool, upsertAction } from './updates.js';
17
+ import { linkTool, removeAction, updateAction, updateTool, upsertAction } from './updates.js';
18
18
  function defaultDatabase(options) {
19
19
  if (options.databaseFile)
20
20
  return path.resolve(options.databaseFile);
@@ -43,7 +43,7 @@ class Toolbox {
43
43
  const artifacts = new ArtifactFiles(path.dirname(databaseFile));
44
44
  const deploymentFiles = new DeploymentFiles(path.resolve(options.deploymentDir || options.stateDir || path.dirname(databaseFile)));
45
45
  const priorIndex = artifacts.readIndex();
46
- for (const driver of [cliDriver, apiDriver, ...(options.drivers || [])])
46
+ for (const driver of [createCliDriver(options.environment), apiDriver, ...(options.drivers || [])])
47
47
  this.drivers.set(driver.type, driver);
48
48
  const tools = [];
49
49
  const actions = [];
@@ -138,6 +138,11 @@ class Toolbox {
138
138
  throw new Error(`Unknown tool or action: ${value}`);
139
139
  return action.status === status ? action : this.updateAction(value, { status });
140
140
  }
141
+ /** Link one user tool to the app it belongs to; `null` unlinks it. */
142
+ async link(toolId, appId) {
143
+ return linkTool(this, toolId, appId);
144
+ }
145
+ isSystemTool(toolId) { return this.system.tools.some((tool) => tool.id === toolId); }
141
146
  async remove(recordId) {
142
147
  const value = id(recordId, 'record id');
143
148
  if (this.system.tools.some((tool) => tool.id === value)
package/dist/types.d.ts CHANGED
@@ -75,6 +75,8 @@ interface ToolDefinition {
75
75
  id: string;
76
76
  name: string;
77
77
  description?: string;
78
+ /** The app this tool belongs to: the reverse edge of the app manifest's `toolIds`. */
79
+ appId?: string;
78
80
  owner?: string;
79
81
  origin?: Origin;
80
82
  status?: Status;
@@ -163,6 +165,8 @@ interface ToolboxOptions {
163
165
  deploymentDir?: string;
164
166
  systemTools?: ToolDefinition[];
165
167
  drivers?: ToolDriver[];
168
+ /** Host-supplied process environment for one CLI tool call, such as where its linked app listens. */
169
+ environment?: (tool: ToolRecord) => Record<string, string> | Promise<Record<string, string>>;
166
170
  onChange?: (catalog: Catalog) => void | Promise<void>;
167
171
  /** Outbound local Live operation. Remote application never calls it. */
168
172
  onDeployment?: (activation: ToolDeploymentActivation) => void | Promise<void>;
@@ -176,26 +180,11 @@ interface McpTool {
176
180
  interface McpOptions extends CallOptions {
177
181
  includeManagement?: boolean;
178
182
  }
179
- interface HostMcpToolDescriptor {
180
- name: string;
181
- description?: string;
182
- inputSchema?: JsonSchema;
183
- }
184
- interface HostMcpToolDefinitionInput {
185
- id: string;
186
- name: string;
187
- description?: string;
188
- serverName: string;
189
- route: string;
190
- tools: readonly HostMcpToolDescriptor[];
191
- owner?: string;
192
- display?: Record<string, unknown>;
193
- metadata?: Record<string, unknown>;
194
- }
195
183
  interface CliIo {
196
184
  stdout?: Writable;
197
185
  stderr?: Writable;
198
186
  toolbox?: import('./toolbox.js').Toolbox;
199
187
  }
200
- export type { ActionDefinition, ActionRecord, ActionTarget, ApiSource, ApiTarget, ApplyResult, CallOptions, Catalog, CliIo, CliSource, CliTarget, DriverCall, HostMcpToolDefinitionInput, HostMcpToolDescriptor, JsonSchema, Loadout, LoadoutInput, McpOptions, McpSource, McpTarget, McpTool, Origin, SecretResolver, Status, ToolDefinition, ToolDriver, ToolboxOptions, ToolRecord, ToolResult, ToolPatch, ActionPatch, ToolQuery, ToolSource, ToolType, };
188
+ export type { ActionDefinition, ActionRecord, ActionTarget, ApiSource, ApiTarget, ApplyResult, CallOptions, Catalog, CliIo, CliSource, CliTarget, DriverCall, JsonSchema, Loadout, LoadoutInput, McpOptions, McpSource, McpTarget, McpTool, Origin, SecretResolver, Status, ToolDefinition, ToolDriver, ToolboxOptions, ToolRecord, ToolResult, ToolPatch, ActionPatch, ToolQuery, ToolSource, ToolType, };
201
189
  export type { ToolDeployment, ToolDeploymentActivation, ToolDeploymentSnapshot, ToolDeploymentSurface, } from './deployment-types.js';
190
+ export type { HostMcpToolDefinitionInput, HostMcpToolDescriptor } from './host-mcp-tool.js';
package/dist/updates.d.ts CHANGED
@@ -1,8 +1,13 @@
1
1
  import type { Toolbox } from './toolbox.js';
2
- import type { ActionDefinition, ActionPatch, ActionRecord, ApplyResult, ToolPatch } from './types.js';
2
+ import type { ActionDefinition, ActionPatch, ActionRecord, ApplyResult, ToolPatch, ToolRecord } from './types.js';
3
3
  type UpdatePort = Pick<Toolbox, 'action' | 'apply' | 'get'>;
4
+ type LinkPort = UpdatePort & Pick<Toolbox, 'update'> & {
5
+ isSystemTool(toolId: string): boolean;
6
+ };
4
7
  declare function updateTool(toolbox: UpdatePort, toolId: string, patch: ToolPatch): Promise<ApplyResult>;
8
+ /** Link one user tool to its app; `null` unlinks. The record is unchanged when the link already holds. */
9
+ declare function linkTool(toolbox: LinkPort, toolId: string, appId: string | null): Promise<ToolRecord>;
5
10
  declare function upsertAction(toolbox: UpdatePort, toolId: string, definition: ActionDefinition): Promise<ActionRecord>;
6
11
  declare function updateAction(toolbox: UpdatePort, actionId: string, patch: ActionPatch): Promise<ActionRecord>;
7
12
  declare function removeAction(toolbox: UpdatePort, actionId: string): Promise<ActionRecord>;
8
- export { removeAction, updateAction, updateTool, upsertAction };
13
+ export { linkTool, removeAction, updateAction, updateTool, upsertAction };
package/dist/updates.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { id } from './ids.js';
1
2
  function actionDefinition(action) {
2
3
  const { id, toolId: _toolId, createdAt: _createdAt, updatedAt: _updatedAt, ...definition } = action;
3
4
  return { ...structuredClone(definition), id };
@@ -25,6 +26,19 @@ async function updateTool(toolbox, toolId, patch) {
25
26
  actions: patch.actions === undefined ? definition.actions : patch.actions,
26
27
  });
27
28
  }
29
+ /** Link one user tool to its app; `null` unlinks. The record is unchanged when the link already holds. */
30
+ async function linkTool(toolbox, toolId, appId) {
31
+ const value = id(toolId, 'tool id');
32
+ if (toolbox.isSystemTool(value))
33
+ throw new Error(`System record is immutable: ${value}`);
34
+ const current = toolbox.get(value);
35
+ if (!current)
36
+ throw new Error(`Unknown tool: ${value}`);
37
+ const trimmed = appId?.trim() || null;
38
+ if ((current.tool.appId ?? null) === trimmed)
39
+ return current.tool;
40
+ return (await toolbox.update(value, { appId: trimmed ?? undefined })).tool;
41
+ }
28
42
  async function upsertAction(toolbox, toolId, definition) {
29
43
  const current = toolbox.get(toolId);
30
44
  if (!current)
@@ -65,4 +79,4 @@ async function removeAction(toolbox, actionId) {
65
79
  });
66
80
  return action;
67
81
  }
68
- export { removeAction, updateAction, updateTool, upsertAction };
82
+ export { linkTool, removeAction, updateAction, updateTool, upsertAction };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amalgm/tools",
3
- "version": "0.1.7-preview.34176071261",
3
+ "version": "0.1.7-preview.34545371724",
4
4
  "description": "Local-first tool definitions, Toolbox registry, and agent execution surfaces.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -56,7 +56,7 @@
56
56
  "node": ">=24"
57
57
  },
58
58
  "dependencies": {
59
- "@amalgm/core": "0.4.8-preview.34168975242",
59
+ "@amalgm/core": "0.5.1-preview.34403852460",
60
60
  "better-sqlite3": "^12.6.2"
61
61
  },
62
62
  "devDependencies": {
@@ -87,11 +87,11 @@
87
87
  "package.json"
88
88
  ]
89
89
  },
90
- "gitHead": "ba5c024d8a0975ce99ac75ab5af0a201d296ee72",
90
+ "gitHead": "53f469e2ce16c667c6883239cc3f98c0af6943fc",
91
91
  "amalgmSource": {
92
92
  "repository": "amalgm-inc/amalgm-tools",
93
93
  "branch": "preview",
94
- "commit": "ba5c024d8a0975ce99ac75ab5af0a201d296ee72",
95
- "occurrence": "34176071261"
94
+ "commit": "53f469e2ce16c667c6883239cc3f98c0af6943fc",
95
+ "occurrence": "34545371724"
96
96
  }
97
97
  }
@@ -144,10 +144,16 @@ amalgm-tools show hello.say
144
144
  amalgm-tools enable hello.say
145
145
  amalgm-tools disable hello.say
146
146
  amalgm-tools remove hello
147
+ amalgm-tools link canvas app-3f1c
148
+ amalgm-tools unlink canvas
147
149
  amalgm-tools run hello.say --input '{"name":"Ada"}'
148
150
  amalgm-tools connections
149
151
  ```
150
152
 
153
+ `link` records the app a tool belongs to (`appId`, the reverse edge of the app
154
+ manifest's `toolIds`); sharing the app carries the tool and sharing the tool
155
+ carries the app. The MCP twins are `toolbox_tool_link` and `toolbox_tool_unlink`.
156
+
151
157
  Use `--state-dir DIR` to select a Toolbox and `--loadout one,two` to bound
152
158
  `list`, `run`, or connection projection.
153
159