@amalgm/tools 0.1.3 → 0.1.4
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/PURPOSE.md +32 -8
- package/README.md +22 -0
- package/dist/apply-definition.d.ts +12 -0
- package/dist/apply-definition.js +33 -0
- package/dist/artifact-files.d.ts +1 -1
- package/dist/artifact-files.js +4 -3
- package/dist/artifacts.d.ts +5 -5
- package/dist/artifacts.js +5 -5
- package/dist/definition.js +10 -0
- package/dist/deployment-files.d.ts +10 -0
- package/dist/deployment-files.js +38 -0
- package/dist/deployment-types.d.ts +32 -0
- package/dist/deployment-types.js +1 -0
- package/dist/deployments.d.ts +19 -0
- package/dist/deployments.js +98 -0
- package/dist/host-mcp-tool.d.ts +8 -0
- package/dist/host-mcp-tool.js +40 -0
- package/dist/http.js +6 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/mcp-server.d.ts +2 -1
- package/dist/mcp-server.js +3 -2
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.js +33 -21
- package/dist/notifications.js +4 -3
- package/dist/schema.js +15 -0
- package/dist/store.d.ts +17 -4
- package/dist/store.js +105 -41
- package/dist/toolbox-deployments.d.ts +30 -0
- package/dist/toolbox-deployments.js +125 -0
- package/dist/toolbox-projection.d.ts +13 -0
- package/dist/toolbox-projection.js +41 -0
- package/dist/toolbox-view.d.ts +35 -0
- package/dist/toolbox-view.js +75 -0
- package/dist/toolbox.d.ts +13 -5
- package/dist/toolbox.js +88 -111
- package/dist/types.d.ts +30 -1
- package/dist/updates.d.ts +2 -1
- package/dist/updates.js +12 -1
- package/docs/ENGINE_INTEGRATION.md +5 -2
- package/package.json +1 -1
package/dist/mcp.js
CHANGED
|
@@ -21,45 +21,57 @@ function managed(definition, handler) {
|
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
function
|
|
24
|
+
function managementToolDescriptors() {
|
|
25
25
|
const idProperty = { id: { type: 'string', description: 'Tool or action id' } };
|
|
26
26
|
return [
|
|
27
|
-
|
|
27
|
+
{
|
|
28
28
|
name: 'toolbox_tools_list', description: 'List the canonical Toolbox catalog.',
|
|
29
29
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
30
|
-
},
|
|
31
|
-
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
32
|
name: 'toolbox_tool_get', description: 'Get one tool and its actions.',
|
|
33
33
|
inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
if (!result)
|
|
37
|
-
throw new Error(`Unknown tool: ${id}`);
|
|
38
|
-
return result;
|
|
39
|
-
}),
|
|
40
|
-
managed({
|
|
34
|
+
},
|
|
35
|
+
{
|
|
41
36
|
name: 'toolbox_tool_apply', description: 'Atomically apply an authoritative tool definition.',
|
|
42
37
|
inputSchema: {
|
|
43
38
|
type: 'object', required: ['tool'], additionalProperties: false,
|
|
44
39
|
properties: { tool: { type: 'object', description: 'Complete tool definition' } },
|
|
45
40
|
},
|
|
46
|
-
},
|
|
47
|
-
|
|
41
|
+
},
|
|
42
|
+
{
|
|
48
43
|
name: 'toolbox_record_enable', description: 'Enable one user-owned tool or action.',
|
|
49
44
|
inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
|
|
50
|
-
},
|
|
51
|
-
|
|
45
|
+
},
|
|
46
|
+
{
|
|
52
47
|
name: 'toolbox_record_disable', description: 'Disable one user-owned tool or action.',
|
|
53
48
|
inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
|
|
54
|
-
},
|
|
55
|
-
|
|
49
|
+
},
|
|
50
|
+
{
|
|
56
51
|
name: 'toolbox_record_remove', description: 'Remove one user-owned tool or action.',
|
|
57
52
|
inputSchema: { type: 'object', properties: idProperty, required: ['id'], additionalProperties: false },
|
|
58
|
-
},
|
|
59
|
-
|
|
53
|
+
},
|
|
54
|
+
{
|
|
60
55
|
name: 'toolbox_mcp_connections', description: 'List external MCP connections for the composing host.',
|
|
61
56
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
62
|
-
},
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
function managementTools(toolbox) {
|
|
61
|
+
const [list, get, apply, enable, disable, remove, connections] = managementToolDescriptors();
|
|
62
|
+
return [
|
|
63
|
+
managed(list, () => toolbox.catalog()),
|
|
64
|
+
managed(get, ({ id }) => {
|
|
65
|
+
const result = toolbox.get(String(id));
|
|
66
|
+
if (!result)
|
|
67
|
+
throw new Error(`Unknown tool: ${id}`);
|
|
68
|
+
return result;
|
|
69
|
+
}),
|
|
70
|
+
managed(apply, ({ tool }) => toolbox.apply(tool)),
|
|
71
|
+
managed(enable, ({ id }) => toolbox.setStatus(String(id), 'enabled')),
|
|
72
|
+
managed(disable, ({ id }) => toolbox.setStatus(String(id), 'disabled')),
|
|
73
|
+
managed(remove, ({ id }) => toolbox.remove(String(id))),
|
|
74
|
+
managed(connections, () => toolbox.connections()),
|
|
63
75
|
];
|
|
64
76
|
}
|
|
65
77
|
function actionTools(toolbox, options = {}) {
|
|
@@ -92,4 +104,4 @@ async function callMcpTool(toolbox, name, input = {}, options = {}) {
|
|
|
92
104
|
throw new Error(`Unknown tool: ${name}`);
|
|
93
105
|
return tool.handler(input);
|
|
94
106
|
}
|
|
95
|
-
export { actionTools, callMcpTool, createMcpTools, findMcpTool, managementTools, portable, };
|
|
107
|
+
export { actionTools, callMcpTool, createMcpTools, findMcpTool, managementTools, managementToolDescriptors, portable, };
|
package/dist/notifications.js
CHANGED
|
@@ -18,11 +18,12 @@ function contextSessionId(value) {
|
|
|
18
18
|
function notificationToolDefinition() {
|
|
19
19
|
return {
|
|
20
20
|
id: 'notifications',
|
|
21
|
-
name: '
|
|
21
|
+
name: 'Channels',
|
|
22
22
|
description: 'Notify the current user about important results, completions, and failures.',
|
|
23
23
|
owner: 'amalgm',
|
|
24
24
|
origin: 'system',
|
|
25
|
-
source: { type: 'mcp', transport: 'host', serverName: '
|
|
25
|
+
source: { type: 'mcp', transport: 'host', serverName: 'channels', route: '/mcp/channels' },
|
|
26
|
+
policy: { firstParty: true },
|
|
26
27
|
actions: [{
|
|
27
28
|
name: 'notify_user',
|
|
28
29
|
description: 'Send the current user an email notification. Keep it concise and actionable.',
|
|
@@ -91,7 +92,7 @@ function createNotificationsDriver(notifications) {
|
|
|
91
92
|
return tool.id === 'notifications'
|
|
92
93
|
&& tool.source.type === 'mcp'
|
|
93
94
|
&& tool.source.transport === 'host'
|
|
94
|
-
&& tool.source.serverName === '
|
|
95
|
+
&& tool.source.serverName === 'channels';
|
|
95
96
|
},
|
|
96
97
|
async call({ action, input, options }) {
|
|
97
98
|
if (action.id !== 'notifications.notify_user')
|
package/dist/schema.js
CHANGED
|
@@ -21,6 +21,21 @@ function migrate(db) {
|
|
|
21
21
|
UNIQUE(tool_id, id)
|
|
22
22
|
);
|
|
23
23
|
CREATE INDEX IF NOT EXISTS actions_tool_id ON actions(tool_id);
|
|
24
|
+
|
|
25
|
+
CREATE TABLE IF NOT EXISTS tool_deployments (
|
|
26
|
+
id TEXT PRIMARY KEY,
|
|
27
|
+
tool_id TEXT NOT NULL,
|
|
28
|
+
previous_deployment_id TEXT,
|
|
29
|
+
created_at TEXT NOT NULL,
|
|
30
|
+
record_json TEXT NOT NULL
|
|
31
|
+
);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS tool_deployments_tool_id
|
|
33
|
+
ON tool_deployments(tool_id, created_at, id);
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS tool_heads (
|
|
36
|
+
tool_id TEXT PRIMARY KEY,
|
|
37
|
+
deployment_id TEXT NOT NULL REFERENCES tool_deployments(id)
|
|
38
|
+
);
|
|
24
39
|
`);
|
|
25
40
|
}
|
|
26
41
|
export { migrate };
|
package/dist/store.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
|
-
import type { ActionRecord, ApplyResult, Catalog,
|
|
2
|
+
import type { ActionRecord, ApplyResult, Catalog, ToolDeployment, ToolDeploymentActivation, ToolDeploymentSnapshot, ToolRecord } from './types.js';
|
|
3
3
|
declare class Store {
|
|
4
4
|
readonly db: Database.Database;
|
|
5
5
|
constructor(file: string);
|
|
@@ -8,10 +8,23 @@ declare class Store {
|
|
|
8
8
|
getTool(id: string): ToolRecord | null;
|
|
9
9
|
getAction(id: string): ActionRecord | null;
|
|
10
10
|
actionsFor(toolId: string): ActionRecord[];
|
|
11
|
+
heads(): Record<string, string>;
|
|
11
12
|
catalog(): Catalog;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
currentDeploymentId(toolId: string): string | null;
|
|
14
|
+
getDeployment(id: string): ToolDeployment | null;
|
|
15
|
+
deploymentsFor(toolId: string): ToolDeployment[];
|
|
16
|
+
allDeployments(): ToolDeployment[];
|
|
17
|
+
deploymentSnapshot(): ToolDeploymentSnapshot;
|
|
18
|
+
/** Add a baseline for a pre-deployment database without changing its catalog bytes. */
|
|
19
|
+
adoptDeployment(activation: ToolDeploymentActivation): void;
|
|
20
|
+
applyDeployment(activation: ToolDeploymentActivation): {
|
|
21
|
+
changed: boolean;
|
|
22
|
+
result: ApplyResult | null;
|
|
23
|
+
};
|
|
24
|
+
replaceDeploymentSnapshot(snapshot: ToolDeploymentSnapshot): void;
|
|
25
|
+
private insertDeployment;
|
|
26
|
+
private setHead;
|
|
27
|
+
private materializeDefinition;
|
|
15
28
|
close(): void;
|
|
16
29
|
}
|
|
17
30
|
export { Store };
|
package/dist/store.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import Database from 'better-sqlite3';
|
|
4
|
+
import { canonicalJson, deploymentRevisionId } from './deployments.js';
|
|
4
5
|
import { migrate } from './schema.js';
|
|
5
6
|
function parse(row) {
|
|
6
7
|
return row ? JSON.parse(row.record_json) : null;
|
|
@@ -32,65 +33,128 @@ class Store {
|
|
|
32
33
|
return this.db.prepare('SELECT record_json FROM actions WHERE tool_id = ? ORDER BY id')
|
|
33
34
|
.all(toolId).map((row) => parse(row)).filter(Boolean);
|
|
34
35
|
}
|
|
36
|
+
heads() {
|
|
37
|
+
return Object.fromEntries(this.db.prepare('SELECT tool_id, deployment_id FROM tool_heads ORDER BY tool_id').all()
|
|
38
|
+
.map((row) => [row.tool_id, row.deployment_id]));
|
|
39
|
+
}
|
|
35
40
|
catalog() {
|
|
36
41
|
const tools = this.db.prepare('SELECT record_json FROM tools ORDER BY id')
|
|
37
42
|
.all().map((row) => parse(row)).filter(Boolean);
|
|
38
43
|
const actions = this.db.prepare('SELECT record_json FROM actions ORDER BY id')
|
|
39
44
|
.all().map((row) => parse(row)).filter(Boolean);
|
|
40
|
-
|
|
45
|
+
const deployments = this.heads();
|
|
46
|
+
return {
|
|
47
|
+
version: 1,
|
|
48
|
+
revision: this.revision(),
|
|
49
|
+
revisionId: deploymentRevisionId({ heads: deployments }),
|
|
50
|
+
deployments,
|
|
51
|
+
tools,
|
|
52
|
+
actions,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
currentDeploymentId(toolId) {
|
|
56
|
+
const row = this.db.prepare('SELECT deployment_id FROM tool_heads WHERE tool_id = ?').get(toolId);
|
|
57
|
+
return row?.deployment_id || null;
|
|
58
|
+
}
|
|
59
|
+
getDeployment(id) {
|
|
60
|
+
return parse(this.db.prepare('SELECT record_json FROM tool_deployments WHERE id = ?').get(id));
|
|
61
|
+
}
|
|
62
|
+
deploymentsFor(toolId) {
|
|
63
|
+
return this.db.prepare(`
|
|
64
|
+
SELECT record_json FROM tool_deployments
|
|
65
|
+
WHERE tool_id = ? ORDER BY created_at, id
|
|
66
|
+
`).all(toolId).map((row) => parse(row)).filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
allDeployments() {
|
|
69
|
+
return this.db.prepare(`
|
|
70
|
+
SELECT record_json FROM tool_deployments ORDER BY created_at, id
|
|
71
|
+
`).all().map((row) => parse(row)).filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
deploymentSnapshot() {
|
|
74
|
+
return {
|
|
75
|
+
version: 1,
|
|
76
|
+
deployments: Object.fromEntries(this.allDeployments().map((deployment) => [deployment.id, deployment])),
|
|
77
|
+
heads: this.heads(),
|
|
78
|
+
};
|
|
41
79
|
}
|
|
42
|
-
|
|
80
|
+
/** Add a baseline for a pre-deployment database without changing its catalog bytes. */
|
|
81
|
+
adoptDeployment(activation) {
|
|
82
|
+
const deployment = activation.deployment;
|
|
43
83
|
this.db.transaction(() => {
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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();
|
|
84
|
+
if (this.getDeployment(deployment.id))
|
|
85
|
+
return;
|
|
86
|
+
this.insertDeployment(deployment);
|
|
87
|
+
this.setHead(deployment.subjectId, deployment.id);
|
|
55
88
|
})();
|
|
56
|
-
return result;
|
|
57
89
|
}
|
|
58
|
-
|
|
90
|
+
applyDeployment(activation) {
|
|
91
|
+
const deployment = activation.deployment;
|
|
59
92
|
return this.db.transaction(() => {
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return updated;
|
|
93
|
+
const existing = this.getDeployment(deployment.id);
|
|
94
|
+
if (existing) {
|
|
95
|
+
if (canonicalJson(existing) !== canonicalJson(deployment)) {
|
|
96
|
+
throw new Error(`Deployment id is immutable: ${deployment.id}`);
|
|
97
|
+
}
|
|
98
|
+
return { changed: false, result: existing.definition };
|
|
67
99
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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);
|
|
100
|
+
this.insertDeployment(deployment);
|
|
101
|
+
this.materializeDefinition(deployment.subjectId, deployment.definition);
|
|
102
|
+
this.setHead(deployment.subjectId, deployment.id);
|
|
74
103
|
this.bump();
|
|
75
|
-
return
|
|
104
|
+
return { changed: true, result: deployment.definition };
|
|
76
105
|
})();
|
|
77
106
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
107
|
+
replaceDeploymentSnapshot(snapshot) {
|
|
108
|
+
this.db.transaction(() => {
|
|
109
|
+
this.db.prepare('DELETE FROM actions').run();
|
|
110
|
+
this.db.prepare('DELETE FROM tools').run();
|
|
111
|
+
this.db.prepare('DELETE FROM tool_heads').run();
|
|
112
|
+
this.db.prepare('DELETE FROM tool_deployments').run();
|
|
113
|
+
for (const deployment of Object.values(snapshot.deployments))
|
|
114
|
+
this.insertDeployment(deployment);
|
|
115
|
+
for (const [toolId, deploymentId] of Object.entries(snapshot.heads)) {
|
|
116
|
+
const deployment = snapshot.deployments[deploymentId];
|
|
117
|
+
this.materializeDefinition(toolId, deployment.definition);
|
|
118
|
+
this.setHead(toolId, deploymentId);
|
|
85
119
|
}
|
|
86
|
-
const action = this.getAction(id);
|
|
87
|
-
if (!action)
|
|
88
|
-
return null;
|
|
89
|
-
this.db.prepare('DELETE FROM actions WHERE id = ?').run(id);
|
|
90
120
|
this.bump();
|
|
91
|
-
return action;
|
|
92
121
|
})();
|
|
93
122
|
}
|
|
123
|
+
insertDeployment(deployment) {
|
|
124
|
+
this.db.prepare(`
|
|
125
|
+
INSERT INTO tool_deployments
|
|
126
|
+
(id, tool_id, previous_deployment_id, created_at, record_json)
|
|
127
|
+
VALUES (?, ?, ?, ?, ?)
|
|
128
|
+
`).run(deployment.id, deployment.subjectId, deployment.previousDeploymentId, deployment.createdAt, JSON.stringify(deployment));
|
|
129
|
+
}
|
|
130
|
+
setHead(toolId, deploymentId) {
|
|
131
|
+
this.db.prepare(`
|
|
132
|
+
INSERT INTO tool_heads (tool_id, deployment_id) VALUES (?, ?)
|
|
133
|
+
ON CONFLICT(tool_id) DO UPDATE SET deployment_id = excluded.deployment_id
|
|
134
|
+
`).run(toolId, deploymentId);
|
|
135
|
+
}
|
|
136
|
+
materializeDefinition(toolId, result) {
|
|
137
|
+
if (result === null) {
|
|
138
|
+
this.db.prepare('DELETE FROM tools WHERE id = ?').run(toolId);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
this.db.prepare(`
|
|
142
|
+
INSERT INTO tools (id, origin, status, record_json)
|
|
143
|
+
VALUES (@id, @origin, @status, @json)
|
|
144
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
145
|
+
origin = excluded.origin, status = excluded.status, record_json = excluded.record_json
|
|
146
|
+
`).run({
|
|
147
|
+
id: result.tool.id,
|
|
148
|
+
origin: result.tool.origin,
|
|
149
|
+
status: result.tool.status,
|
|
150
|
+
json: JSON.stringify(result.tool),
|
|
151
|
+
});
|
|
152
|
+
this.db.prepare('DELETE FROM actions WHERE tool_id = ?').run(result.tool.id);
|
|
153
|
+
const insert = this.db.prepare('INSERT INTO actions (id, tool_id, status, record_json) VALUES (?, ?, ?, ?)');
|
|
154
|
+
for (const action of result.actions) {
|
|
155
|
+
insert.run(action.id, action.toolId, action.status, JSON.stringify(action));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
94
158
|
close() { this.db.close(); }
|
|
95
159
|
}
|
|
96
160
|
export { Store };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DeploymentFiles } from './deployment-files.js';
|
|
2
|
+
import { Store } from './store.js';
|
|
3
|
+
import type { ActionRecord, ApplyResult, ToolDeployment, ToolDeploymentActivation, ToolDeploymentSnapshot, ToolDeploymentSurface, ToolRecord, ToolboxOptions } from './types.js';
|
|
4
|
+
interface DeploymentServiceOptions {
|
|
5
|
+
store: Store;
|
|
6
|
+
files: DeploymentFiles;
|
|
7
|
+
systemTools: readonly ToolRecord[];
|
|
8
|
+
systemActions: readonly ActionRecord[];
|
|
9
|
+
project(toolId: string): Promise<void>;
|
|
10
|
+
replaceProjection(previousToolIds: readonly string[]): Promise<void>;
|
|
11
|
+
onDeployment?: ToolboxOptions['onDeployment'];
|
|
12
|
+
}
|
|
13
|
+
/** The Toolbox deployment state machine; adapters only observe its operations. */
|
|
14
|
+
declare class ToolboxDeployments {
|
|
15
|
+
private readonly options;
|
|
16
|
+
private readonly observers;
|
|
17
|
+
constructor(options: DeploymentServiceOptions);
|
|
18
|
+
adoptLegacy(): void;
|
|
19
|
+
activateLocal(definition: ApplyResult | null, deletedToolId?: string): Promise<void>;
|
|
20
|
+
current(toolId: string): ToolDeployment | null;
|
|
21
|
+
get(deploymentId: string): ToolDeployment | null;
|
|
22
|
+
history(toolId: string): ToolDeployment[];
|
|
23
|
+
snapshot(): ToolDeploymentSnapshot;
|
|
24
|
+
activateRemote(activation: ToolDeploymentActivation): Promise<ApplyResult | null>;
|
|
25
|
+
applySnapshot(snapshot: ToolDeploymentSnapshot): Promise<void>;
|
|
26
|
+
surface(): ToolDeploymentSurface;
|
|
27
|
+
private validateActivation;
|
|
28
|
+
private assertNotSystem;
|
|
29
|
+
}
|
|
30
|
+
export { ToolboxDeployments };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { TOOL_DEPLOYMENT_CONTRACT, assertToolDeploymentActivation, createToolDeployment, } from './deployments.js';
|
|
2
|
+
import { assertMcpNamesUnique, id } from './ids.js';
|
|
3
|
+
/** The Toolbox deployment state machine; adapters only observe its operations. */
|
|
4
|
+
class ToolboxDeployments {
|
|
5
|
+
options;
|
|
6
|
+
observers = new Set();
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.options = options;
|
|
9
|
+
}
|
|
10
|
+
adoptLegacy() {
|
|
11
|
+
const { store, systemTools, files } = this.options;
|
|
12
|
+
for (const tool of store.catalog().tools) {
|
|
13
|
+
if (store.currentDeploymentId(tool.id) || systemTools.some((system) => system.id === tool.id))
|
|
14
|
+
continue;
|
|
15
|
+
store.adoptDeployment(createToolDeployment({
|
|
16
|
+
toolId: tool.id,
|
|
17
|
+
previousDeploymentId: null,
|
|
18
|
+
definition: { tool, actions: store.actionsFor(tool.id) },
|
|
19
|
+
createdAt: tool.updatedAt,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
files.materialize(store.allDeployments());
|
|
23
|
+
}
|
|
24
|
+
async activateLocal(definition, deletedToolId) {
|
|
25
|
+
const toolId = definition?.tool.id || deletedToolId;
|
|
26
|
+
if (!toolId)
|
|
27
|
+
throw new Error('Tool deployment requires a stable tool id');
|
|
28
|
+
const activation = createToolDeployment({
|
|
29
|
+
toolId,
|
|
30
|
+
previousDeploymentId: this.options.store.currentDeploymentId(toolId),
|
|
31
|
+
definition,
|
|
32
|
+
createdAt: definition?.tool.updatedAt || new Date().toISOString(),
|
|
33
|
+
});
|
|
34
|
+
const applied = this.options.store.applyDeployment(activation);
|
|
35
|
+
if (!applied.changed)
|
|
36
|
+
return;
|
|
37
|
+
this.options.files.write(activation.deployment);
|
|
38
|
+
await this.options.project(toolId);
|
|
39
|
+
for (const observer of this.observers)
|
|
40
|
+
observer(activation);
|
|
41
|
+
await this.options.onDeployment?.(activation);
|
|
42
|
+
}
|
|
43
|
+
current(toolId) {
|
|
44
|
+
const value = id(toolId, 'tool id');
|
|
45
|
+
const deploymentId = this.options.store.currentDeploymentId(value);
|
|
46
|
+
return deploymentId ? this.options.store.getDeployment(deploymentId) : null;
|
|
47
|
+
}
|
|
48
|
+
get(deploymentId) {
|
|
49
|
+
return this.options.store.getDeployment(id(deploymentId, 'deployment id', 96));
|
|
50
|
+
}
|
|
51
|
+
history(toolId) {
|
|
52
|
+
return this.options.store.deploymentsFor(id(toolId, 'tool id'));
|
|
53
|
+
}
|
|
54
|
+
snapshot() {
|
|
55
|
+
return this.options.store.deploymentSnapshot();
|
|
56
|
+
}
|
|
57
|
+
async activateRemote(activation) {
|
|
58
|
+
this.validateActivation(activation);
|
|
59
|
+
const applied = this.options.store.applyDeployment(activation);
|
|
60
|
+
if (!applied.changed)
|
|
61
|
+
return applied.result;
|
|
62
|
+
this.options.files.write(activation.deployment);
|
|
63
|
+
await this.options.project(activation.deployment.subjectId);
|
|
64
|
+
return applied.result;
|
|
65
|
+
}
|
|
66
|
+
async applySnapshot(snapshot) {
|
|
67
|
+
if (!snapshot || snapshot.version !== 1 || !snapshot.deployments || !snapshot.heads) {
|
|
68
|
+
throw new Error('Invalid tool deployment snapshot');
|
|
69
|
+
}
|
|
70
|
+
for (const [deploymentId, deployment] of Object.entries(snapshot.deployments)) {
|
|
71
|
+
this.validateActivation({ type: 'deployment.activate', deployment }, false);
|
|
72
|
+
if (deploymentId !== deployment.id)
|
|
73
|
+
throw new Error(`Deployment snapshot key mismatch: ${deploymentId}`);
|
|
74
|
+
}
|
|
75
|
+
const currentDefinitions = [];
|
|
76
|
+
for (const [toolId, deploymentId] of Object.entries(snapshot.heads)) {
|
|
77
|
+
const deployment = snapshot.deployments[deploymentId];
|
|
78
|
+
if (!deployment || deployment.subjectId !== toolId) {
|
|
79
|
+
throw new Error(`Deployment snapshot head is invalid: ${toolId}`);
|
|
80
|
+
}
|
|
81
|
+
this.assertNotSystem(toolId);
|
|
82
|
+
if (deployment.definition)
|
|
83
|
+
currentDefinitions.push(deployment.definition);
|
|
84
|
+
}
|
|
85
|
+
assertMcpNamesUnique([
|
|
86
|
+
...this.options.systemActions,
|
|
87
|
+
...currentDefinitions.flatMap((definition) => definition.actions),
|
|
88
|
+
]);
|
|
89
|
+
const previousToolIds = this.options.store.catalog().tools.map((tool) => tool.id);
|
|
90
|
+
this.options.store.replaceDeploymentSnapshot(snapshot);
|
|
91
|
+
this.options.files.materialize(Object.values(snapshot.deployments));
|
|
92
|
+
await this.options.replaceProjection(previousToolIds);
|
|
93
|
+
}
|
|
94
|
+
surface() {
|
|
95
|
+
return Object.freeze({
|
|
96
|
+
contract: TOOL_DEPLOYMENT_CONTRACT,
|
|
97
|
+
observeEdits: (emit) => {
|
|
98
|
+
this.observers.add(emit);
|
|
99
|
+
return () => this.observers.delete(emit);
|
|
100
|
+
},
|
|
101
|
+
applyRemote: async (operation) => {
|
|
102
|
+
await this.activateRemote(operation);
|
|
103
|
+
},
|
|
104
|
+
applySnapshot: (snapshot) => this.applySnapshot(snapshot),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
validateActivation(activation, checkNames = true) {
|
|
108
|
+
assertToolDeploymentActivation(activation);
|
|
109
|
+
const deployment = activation.deployment;
|
|
110
|
+
this.assertNotSystem(deployment.subjectId);
|
|
111
|
+
if (checkNames && deployment.definition) {
|
|
112
|
+
assertMcpNamesUnique([
|
|
113
|
+
...this.options.systemActions,
|
|
114
|
+
...this.options.store.catalog().actions.filter((action) => action.toolId !== deployment.subjectId),
|
|
115
|
+
...deployment.definition.actions,
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
assertNotSystem(toolId) {
|
|
120
|
+
if (this.options.systemTools.some((tool) => tool.id === toolId)) {
|
|
121
|
+
throw new Error(`System tool is immutable: ${toolId}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export { ToolboxDeployments };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ArtifactFiles } from './artifact-files.js';
|
|
2
|
+
import type { Catalog, ToolboxOptions } from './types.js';
|
|
3
|
+
/** Current-catalog compatibility projections; deployments remain authority. */
|
|
4
|
+
declare class ToolboxProjection {
|
|
5
|
+
private readonly artifacts;
|
|
6
|
+
private readonly catalog;
|
|
7
|
+
private readonly onChange?;
|
|
8
|
+
constructor(artifacts: ArtifactFiles, catalog: () => Catalog, onChange?: ToolboxOptions['onChange']);
|
|
9
|
+
boot(): void;
|
|
10
|
+
changed(toolId: string): Promise<void>;
|
|
11
|
+
replaced(previousToolIds: readonly string[]): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export { ToolboxProjection };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Current-catalog compatibility projections; deployments remain authority. */
|
|
2
|
+
class ToolboxProjection {
|
|
3
|
+
artifacts;
|
|
4
|
+
catalog;
|
|
5
|
+
onChange;
|
|
6
|
+
constructor(artifacts, catalog, onChange) {
|
|
7
|
+
this.artifacts = artifacts;
|
|
8
|
+
this.catalog = catalog;
|
|
9
|
+
this.onChange = onChange;
|
|
10
|
+
}
|
|
11
|
+
boot() {
|
|
12
|
+
this.artifacts.migrateLegacy();
|
|
13
|
+
this.artifacts.materialize(this.catalog());
|
|
14
|
+
}
|
|
15
|
+
async changed(toolId) {
|
|
16
|
+
const catalog = this.catalog();
|
|
17
|
+
const tool = catalog.tools.find((candidate) => candidate.id === toolId);
|
|
18
|
+
if (tool && tool.origin !== 'system') {
|
|
19
|
+
this.artifacts.writeTool({
|
|
20
|
+
tool,
|
|
21
|
+
actions: catalog.actions.filter((action) => action.toolId === toolId),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
else if (!tool) {
|
|
25
|
+
this.artifacts.removeTool(toolId);
|
|
26
|
+
}
|
|
27
|
+
this.artifacts.writeIndex(catalog);
|
|
28
|
+
await this.onChange?.(catalog);
|
|
29
|
+
}
|
|
30
|
+
async replaced(previousToolIds) {
|
|
31
|
+
const catalog = this.catalog();
|
|
32
|
+
const currentToolIds = new Set(catalog.tools.map((tool) => tool.id));
|
|
33
|
+
for (const toolId of previousToolIds) {
|
|
34
|
+
if (!currentToolIds.has(toolId))
|
|
35
|
+
this.artifacts.removeTool(toolId);
|
|
36
|
+
}
|
|
37
|
+
this.artifacts.materialize(catalog);
|
|
38
|
+
await this.onChange?.(catalog);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export { ToolboxProjection };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ActionRecord, ApplyResult, CallOptions, Catalog, LoadoutInput, ToolDriver, ToolQuery, ToolRecord, ToolResult } from './types.js';
|
|
2
|
+
declare class ToolboxView {
|
|
3
|
+
private readonly readCatalog;
|
|
4
|
+
private readonly drivers;
|
|
5
|
+
constructor(readCatalog: () => Catalog, drivers: ReadonlyMap<string, ToolDriver>);
|
|
6
|
+
catalog(): Catalog;
|
|
7
|
+
list(loadout?: LoadoutInput): Array<{
|
|
8
|
+
tool: ToolRecord;
|
|
9
|
+
actions: ActionRecord[];
|
|
10
|
+
}>;
|
|
11
|
+
query(filters?: ToolQuery): {
|
|
12
|
+
tool: ToolRecord;
|
|
13
|
+
actions: ActionRecord[];
|
|
14
|
+
}[];
|
|
15
|
+
callable(loadout?: LoadoutInput, filters?: ToolQuery): {
|
|
16
|
+
tool: ToolRecord;
|
|
17
|
+
action: ActionRecord;
|
|
18
|
+
}[];
|
|
19
|
+
get(toolId: string): ApplyResult | null;
|
|
20
|
+
action(actionId: string): ActionRecord | null;
|
|
21
|
+
resolve(toolId: string, actionReference: string, loadout?: LoadoutInput): {
|
|
22
|
+
tool: ToolRecord;
|
|
23
|
+
action: ActionRecord;
|
|
24
|
+
} | null;
|
|
25
|
+
resolveCallable(toolId: string, actionReference: string, loadout?: LoadoutInput, filters?: ToolQuery): {
|
|
26
|
+
tool: ToolRecord;
|
|
27
|
+
action: ActionRecord;
|
|
28
|
+
} | null;
|
|
29
|
+
connections(loadout?: LoadoutInput): ToolRecord[];
|
|
30
|
+
call(actionId: string, input?: unknown, options?: CallOptions): Promise<ToolResult>;
|
|
31
|
+
hasDriver(tool: ToolRecord): boolean;
|
|
32
|
+
callMcp(name: string, input?: unknown, options?: CallOptions): Promise<ToolResult>;
|
|
33
|
+
private driverFor;
|
|
34
|
+
}
|
|
35
|
+
export { ToolboxView };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { id, mcpName } from './ids.js';
|
|
2
|
+
import { validateInput } from './input.js';
|
|
3
|
+
import { callableActions, queryTools, resolveAction, resolveCallableAction } from './query.js';
|
|
4
|
+
import { findSelected, selected } from './selection.js';
|
|
5
|
+
class ToolboxView {
|
|
6
|
+
readCatalog;
|
|
7
|
+
drivers;
|
|
8
|
+
constructor(readCatalog, drivers) {
|
|
9
|
+
this.readCatalog = readCatalog;
|
|
10
|
+
this.drivers = drivers;
|
|
11
|
+
}
|
|
12
|
+
catalog() { return this.readCatalog(); }
|
|
13
|
+
list(loadout) {
|
|
14
|
+
const catalog = this.catalog();
|
|
15
|
+
const runnable = selected(catalog, loadout);
|
|
16
|
+
return catalog.tools.flatMap((tool) => {
|
|
17
|
+
const actions = runnable.filter((item) => item.tool.id === tool.id).map((item) => item.action);
|
|
18
|
+
if (loadout !== undefined && actions.length === 0)
|
|
19
|
+
return [];
|
|
20
|
+
return [{ tool, actions }];
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
query(filters = {}) { return queryTools(this, filters); }
|
|
24
|
+
callable(loadout, filters = {}) {
|
|
25
|
+
return callableActions(this, loadout, filters);
|
|
26
|
+
}
|
|
27
|
+
get(toolId) {
|
|
28
|
+
const value = id(toolId, 'tool id');
|
|
29
|
+
const catalog = this.catalog();
|
|
30
|
+
const tool = catalog.tools.find((candidate) => candidate.id === value);
|
|
31
|
+
return tool ? { tool, actions: catalog.actions.filter((action) => action.toolId === value) } : null;
|
|
32
|
+
}
|
|
33
|
+
action(actionId) {
|
|
34
|
+
const value = id(actionId, 'action id');
|
|
35
|
+
return this.catalog().actions.find((action) => action.id === value) || null;
|
|
36
|
+
}
|
|
37
|
+
resolve(toolId, actionReference, loadout) {
|
|
38
|
+
return resolveAction(this, toolId, actionReference, loadout);
|
|
39
|
+
}
|
|
40
|
+
resolveCallable(toolId, actionReference, loadout, filters = {}) {
|
|
41
|
+
return resolveCallableAction(this, toolId, actionReference, loadout, filters);
|
|
42
|
+
}
|
|
43
|
+
connections(loadout) {
|
|
44
|
+
const catalog = this.catalog();
|
|
45
|
+
const ids = loadout === undefined ? null : new Set(Array.isArray(loadout) ? loadout : loadout.toolIds);
|
|
46
|
+
return catalog.tools.filter((tool) => {
|
|
47
|
+
if (tool.status !== 'enabled' || tool.source.type !== 'mcp' || tool.origin === 'system')
|
|
48
|
+
return false;
|
|
49
|
+
if (!ids)
|
|
50
|
+
return true;
|
|
51
|
+
return ids.has(tool.id) || catalog.actions.some((action) => action.toolId === tool.id && ids.has(action.id));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async call(actionId, input = {}, options = {}) {
|
|
55
|
+
const resolved = findSelected(this.catalog(), id(actionId, 'action id'), options.loadout);
|
|
56
|
+
if (!resolved)
|
|
57
|
+
throw new Error(`Action is unavailable: ${actionId}`);
|
|
58
|
+
const driver = this.driverFor(resolved.tool);
|
|
59
|
+
if (!driver)
|
|
60
|
+
throw new Error(`No ${resolved.tool.source.type} driver is configured for ${resolved.tool.id}`);
|
|
61
|
+
return driver.call({ ...resolved, input: validateInput(resolved.action.inputSchema, input), options });
|
|
62
|
+
}
|
|
63
|
+
hasDriver(tool) { return this.driverFor(tool) !== null; }
|
|
64
|
+
async callMcp(name, input = {}, options = {}) {
|
|
65
|
+
const resolved = selected(this.catalog(), options.loadout).find(({ action }) => mcpName(action) === name);
|
|
66
|
+
if (!resolved)
|
|
67
|
+
throw new Error(`Unknown MCP tool: ${name}`);
|
|
68
|
+
return this.call(resolved.action.id, input, options);
|
|
69
|
+
}
|
|
70
|
+
driverFor(tool) {
|
|
71
|
+
const driver = this.drivers.get(tool.source.type);
|
|
72
|
+
return driver && (driver.supports?.(tool) ?? true) ? driver : null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export { ToolboxView };
|