amalgm 0.1.35 → 0.1.37
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/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +29 -2
- package/runtime/scripts/amalgm-mcp/events/executor.js +6 -0
- package/runtime/scripts/amalgm-mcp/events/rest.js +13 -0
- package/runtime/scripts/amalgm-mcp/events/tools.js +13 -0
- package/runtime/scripts/amalgm-mcp/fs/rest.js +6 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +9 -0
- package/runtime/scripts/amalgm-mcp/state/db.js +25 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +39 -11
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +9 -3
- package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +6 -3
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +13 -0
- package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +45 -0
- package/runtime/scripts/amalgm-mcp/tests/local-live-snapshot.test.js +44 -0
- package/runtime/scripts/amalgm-mcp/tests/scheduler.test.js +64 -0
- package/runtime/scripts/amalgm-mcp/tests/workspace-store.test.js +84 -0
- package/runtime/scripts/amalgm-mcp/toolbox/store.js +15 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +162 -22
- package/runtime/scripts/amalgm-mcp/workspace/store.js +278 -0
- package/runtime/scripts/chat-core/adapters/codex.js +44 -4
- package/runtime/scripts/chat-core/contract.js +57 -0
- package/runtime/scripts/chat-core/engine.js +5 -5
- package/runtime/scripts/chat-core/server.js +17 -4
- package/runtime/scripts/chat-core/sse.js +8 -1
- package/runtime/scripts/chat-core/stores.js +3 -1
- package/runtime/scripts/chat-core/tooling/active-memory.js +396 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +3 -0
- package/runtime/scripts/local-gateway.js +158 -0
package/package.json
CHANGED
|
@@ -9,6 +9,7 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { spawn } = require('child_process');
|
|
11
11
|
const { DEFAULT_CWD } = require('../config');
|
|
12
|
+
const activeMemory = require('../../chat-core/tooling/active-memory');
|
|
12
13
|
const { syncArtifactRoutesToGateway } = require('./advertise');
|
|
13
14
|
const {
|
|
14
15
|
allocatePort,
|
|
@@ -319,10 +320,23 @@ async function registerArtifact(input) {
|
|
|
319
320
|
const data = loadArtifacts();
|
|
320
321
|
data.artifacts.push(artifact);
|
|
321
322
|
saveArtifacts(data);
|
|
323
|
+
activeMemory.ensureConstructMemory({
|
|
324
|
+
type: 'artifact',
|
|
325
|
+
id: artifact.id,
|
|
326
|
+
name: artifact.name,
|
|
327
|
+
projectPath: artifact.cwd,
|
|
328
|
+
}, { source: 'artifact:register' });
|
|
322
329
|
|
|
323
330
|
if (artifact.buildCommand) await runBuild(artifact);
|
|
324
331
|
await startArtifact(artifact.id);
|
|
325
|
-
|
|
332
|
+
const deployed = updateArtifactMeta(artifact.id, { lastDeployedAt: new Date().toISOString() });
|
|
333
|
+
activeMemory.ensureConstructMemory({
|
|
334
|
+
type: 'artifact',
|
|
335
|
+
id: deployed.id,
|
|
336
|
+
name: deployed.name,
|
|
337
|
+
projectPath: deployed.cwd,
|
|
338
|
+
}, { source: 'artifact:deploy' });
|
|
339
|
+
return deployed;
|
|
326
340
|
}
|
|
327
341
|
|
|
328
342
|
async function redeployArtifact(artifactId, updates = {}) {
|
|
@@ -343,6 +357,12 @@ async function redeployArtifact(artifactId, updates = {}) {
|
|
|
343
357
|
if (updates.keep_alive !== undefined) patch.keepAlive = updates.keep_alive !== false;
|
|
344
358
|
if (updates.autostart !== undefined) patch.autostart = updates.autostart !== false;
|
|
345
359
|
if (Object.keys(patch).length > 0) artifact = updateArtifactMeta(artifactId, patch);
|
|
360
|
+
activeMemory.ensureConstructMemory({
|
|
361
|
+
type: 'artifact',
|
|
362
|
+
id: artifact.id,
|
|
363
|
+
name: artifact.name,
|
|
364
|
+
projectPath: artifact.cwd,
|
|
365
|
+
}, { source: 'artifact:update' });
|
|
346
366
|
|
|
347
367
|
if (!artifact.startCommand) throw new Error('startCommand is required');
|
|
348
368
|
if (!fs.existsSync(artifact.cwd)) throw new Error(`cwd does not exist: ${artifact.cwd}`);
|
|
@@ -350,7 +370,14 @@ async function redeployArtifact(artifactId, updates = {}) {
|
|
|
350
370
|
if (artifact.buildCommand) await runBuild(artifact);
|
|
351
371
|
await stopArtifact(artifactId, { desiredState: 'running' });
|
|
352
372
|
await startArtifact(artifactId);
|
|
353
|
-
|
|
373
|
+
const deployed = updateArtifactMeta(artifactId, { lastDeployedAt: new Date().toISOString() });
|
|
374
|
+
activeMemory.ensureConstructMemory({
|
|
375
|
+
type: 'artifact',
|
|
376
|
+
id: deployed.id,
|
|
377
|
+
name: deployed.name,
|
|
378
|
+
projectPath: deployed.cwd,
|
|
379
|
+
}, { source: 'artifact:deploy' });
|
|
380
|
+
return deployed;
|
|
354
381
|
}
|
|
355
382
|
|
|
356
383
|
function cryptoRandomId() {
|
|
@@ -139,6 +139,12 @@ async function executeArtifactEvent(artifactOrTrigger, eventDef, payload, opts =
|
|
|
139
139
|
cwd,
|
|
140
140
|
authMethod: legacy.authMethod || authMethod,
|
|
141
141
|
mcpServers,
|
|
142
|
+
origin: {
|
|
143
|
+
type: 'event',
|
|
144
|
+
id: triggerId || artifactOrTrigger.id,
|
|
145
|
+
name: eventDef.name,
|
|
146
|
+
projectPath: projectPath || null,
|
|
147
|
+
},
|
|
142
148
|
automated: true,
|
|
143
149
|
});
|
|
144
150
|
|
|
@@ -17,6 +17,7 @@ const {
|
|
|
17
17
|
hydrateModelPreferences,
|
|
18
18
|
} = require('../lib/prefs');
|
|
19
19
|
const credentialAdapter = require('../../credential-adapter');
|
|
20
|
+
const activeMemory = require('../../chat-core/tooling/active-memory');
|
|
20
21
|
|
|
21
22
|
function resolveEventHarness(harness, chatInput) {
|
|
22
23
|
return (
|
|
@@ -134,6 +135,12 @@ async function handleCreate(body, sendJson) {
|
|
|
134
135
|
});
|
|
135
136
|
data.triggers.push(trigger);
|
|
136
137
|
saveEventTriggers(data);
|
|
138
|
+
activeMemory.ensureConstructMemory({
|
|
139
|
+
type: 'event',
|
|
140
|
+
id: trigger.id,
|
|
141
|
+
name: trigger.name,
|
|
142
|
+
projectPath: trigger.projectPath,
|
|
143
|
+
}, { source: 'event:create' });
|
|
137
144
|
console.log(`[AmalgmMCP:Events] Created event trigger: ${name} (${source}:${event})`);
|
|
138
145
|
sendJson(200, { ok: true, trigger });
|
|
139
146
|
}
|
|
@@ -182,6 +189,12 @@ async function handleUpdate(body, sendJson) {
|
|
|
182
189
|
}
|
|
183
190
|
Object.assign(trigger, normalizeEventTriggerRecord(trigger, trigger));
|
|
184
191
|
saveEventTriggers(data);
|
|
192
|
+
activeMemory.ensureConstructMemory({
|
|
193
|
+
type: 'event',
|
|
194
|
+
id: trigger.id,
|
|
195
|
+
name: trigger.name,
|
|
196
|
+
projectPath: trigger.projectPath,
|
|
197
|
+
}, { source: 'event:update' });
|
|
185
198
|
console.log(`[AmalgmMCP:Events] Updated trigger: ${trigger_id}`);
|
|
186
199
|
sendJson(200, { ok: true, trigger });
|
|
187
200
|
}
|
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
hydrateModelPreferences,
|
|
21
21
|
} = require('../lib/prefs');
|
|
22
22
|
const credentialAdapter = require('../../credential-adapter');
|
|
23
|
+
const activeMemory = require('../../chat-core/tooling/active-memory');
|
|
23
24
|
|
|
24
25
|
function resolveEventHarness(harness, chatInput) {
|
|
25
26
|
return (
|
|
@@ -159,6 +160,12 @@ module.exports = [
|
|
|
159
160
|
});
|
|
160
161
|
data.triggers.push(trigger);
|
|
161
162
|
saveEventTriggers(data);
|
|
163
|
+
activeMemory.ensureConstructMemory({
|
|
164
|
+
type: 'event',
|
|
165
|
+
id: trigger.id,
|
|
166
|
+
name: trigger.name,
|
|
167
|
+
projectPath: trigger.projectPath,
|
|
168
|
+
}, { source: 'event:create' });
|
|
162
169
|
console.log(`[AmalgmMCP:Events] Created trigger via MCP: ${name} (${source}:${event})`);
|
|
163
170
|
return textResult(
|
|
164
171
|
`Event trigger created.\n\nWebhook URL: ${trigger.webhookUrl}\nSecret: ${trigger.secret}\n\nConfigure the sender to either sign each request with this secret or send it directly in a secret header, then POST JSON to the webhook URL.\n\n${JSON.stringify(trigger, null, 2)}`,
|
|
@@ -281,6 +288,12 @@ module.exports = [
|
|
|
281
288
|
Object.assign(trigger, normalizeEventTriggerRecord(trigger, trigger));
|
|
282
289
|
|
|
283
290
|
saveEventTriggers(data);
|
|
291
|
+
activeMemory.ensureConstructMemory({
|
|
292
|
+
type: 'event',
|
|
293
|
+
id: trigger.id,
|
|
294
|
+
name: trigger.name,
|
|
295
|
+
projectPath: trigger.projectPath,
|
|
296
|
+
}, { source: 'event:update' });
|
|
284
297
|
console.log(`[AmalgmMCP:Events] Updated trigger via MCP: ${trigger_id}`);
|
|
285
298
|
return textResult(`Trigger updated.\n\n${JSON.stringify(trigger, null, 2)}`);
|
|
286
299
|
},
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const activeMemory = require('../../chat-core/tooling/active-memory');
|
|
9
10
|
|
|
10
11
|
const TEXT_EXTENSIONS = new Set([
|
|
11
12
|
'txt', 'md', 'json', 'js', 'ts', 'tsx', 'jsx', 'py', 'html', 'css',
|
|
@@ -236,6 +237,7 @@ async function handleWrite(body, sendJson) {
|
|
|
236
237
|
|
|
237
238
|
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
|
|
238
239
|
await fs.promises.writeFile(targetPath, data);
|
|
240
|
+
activeMemory.notifyMemoryPathChanged(targetPath, 'fs:write');
|
|
239
241
|
|
|
240
242
|
sendJson(200, { success: true, path: targetPath });
|
|
241
243
|
} catch (error) {
|
|
@@ -248,6 +250,7 @@ async function handleDelete(body, sendJson) {
|
|
|
248
250
|
try {
|
|
249
251
|
const targetPath = await resolveSafePath(body.path);
|
|
250
252
|
await fs.promises.rm(targetPath, { recursive: true, force: true });
|
|
253
|
+
activeMemory.notifyMemoryPathChanged(targetPath, 'fs:delete');
|
|
251
254
|
sendJson(200, { success: true, path: targetPath });
|
|
252
255
|
} catch (error) {
|
|
253
256
|
const message = error instanceof Error ? error.message : 'Failed to delete';
|
|
@@ -259,6 +262,7 @@ async function handleMkdir(body, sendJson) {
|
|
|
259
262
|
try {
|
|
260
263
|
const targetPath = await resolveSafePath(body.path, { forCreate: true });
|
|
261
264
|
await fs.promises.mkdir(targetPath, { recursive: true, mode: 0o755 });
|
|
265
|
+
activeMemory.notifyMemoryPathChanged(targetPath, 'fs:mkdir');
|
|
262
266
|
sendJson(200, { success: true, path: targetPath });
|
|
263
267
|
} catch (error) {
|
|
264
268
|
const message = error instanceof Error ? error.message : 'Failed to create folder';
|
|
@@ -272,6 +276,8 @@ async function handleRename(body, sendJson) {
|
|
|
272
276
|
const newPath = await resolveSafePath(body.newPath, { forCreate: true });
|
|
273
277
|
await fs.promises.mkdir(path.dirname(newPath), { recursive: true });
|
|
274
278
|
await fs.promises.rename(oldPath, newPath);
|
|
279
|
+
activeMemory.notifyMemoryPathChanged(oldPath, 'fs:rename');
|
|
280
|
+
activeMemory.notifyMemoryPathChanged(newPath, 'fs:rename');
|
|
275
281
|
sendJson(200, { success: true, oldPath, newPath });
|
|
276
282
|
} catch (error) {
|
|
277
283
|
const message = error instanceof Error ? error.message : 'Failed to rename';
|
|
@@ -195,6 +195,15 @@ function createServer() {
|
|
|
195
195
|
if (req.url === '/workspace/projects' && req.method === 'GET') {
|
|
196
196
|
return workspaceRest.handleProjects(sendJson);
|
|
197
197
|
}
|
|
198
|
+
if (req.url === '/workspace/projects' && req.method === 'POST') {
|
|
199
|
+
return workspaceRest.handleProjectRegister(await readJsonBody(), sendJson);
|
|
200
|
+
}
|
|
201
|
+
if (req.url === '/workspace/projects' && req.method === 'PATCH') {
|
|
202
|
+
return workspaceRest.handleProjectUpdate(await readJsonBody(), sendJson);
|
|
203
|
+
}
|
|
204
|
+
if (req.url === '/workspace/projects' && req.method === 'DELETE') {
|
|
205
|
+
return workspaceRest.handleProjectDelete(await readJsonBody(), sendJson);
|
|
206
|
+
}
|
|
198
207
|
if (req.url === '/workspace/import' && req.method === 'POST') {
|
|
199
208
|
return workspaceRest.handleImport(await readJsonBody(), sendJson);
|
|
200
209
|
}
|
|
@@ -104,6 +104,31 @@ function migrate(database = openLocalDb()) {
|
|
|
104
104
|
|
|
105
105
|
CREATE INDEX IF NOT EXISTS agents_builtin_idx
|
|
106
106
|
ON agents(builtin);
|
|
107
|
+
|
|
108
|
+
CREATE TABLE IF NOT EXISTS workspaces (
|
|
109
|
+
id TEXT PRIMARY KEY,
|
|
110
|
+
name TEXT NOT NULL,
|
|
111
|
+
uri TEXT NOT NULL UNIQUE,
|
|
112
|
+
path TEXT,
|
|
113
|
+
location_type TEXT NOT NULL,
|
|
114
|
+
computer_id TEXT,
|
|
115
|
+
source TEXT NOT NULL,
|
|
116
|
+
managed INTEGER NOT NULL DEFAULT 0,
|
|
117
|
+
status TEXT NOT NULL,
|
|
118
|
+
created_at TEXT NOT NULL,
|
|
119
|
+
updated_at TEXT NOT NULL,
|
|
120
|
+
workspace_json TEXT NOT NULL
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
CREATE INDEX IF NOT EXISTS workspaces_path_idx
|
|
124
|
+
ON workspaces(path);
|
|
125
|
+
|
|
126
|
+
CREATE INDEX IF NOT EXISTS workspaces_computer_id_idx
|
|
127
|
+
ON workspaces(computer_id);
|
|
128
|
+
|
|
129
|
+
CREATE INDEX IF NOT EXISTS workspaces_location_type_idx
|
|
130
|
+
ON workspaces(location_type);
|
|
131
|
+
|
|
107
132
|
`);
|
|
108
133
|
migrateLegacyToolboxTables(database);
|
|
109
134
|
removeInheritedHarnessTools(database);
|
|
@@ -10,6 +10,9 @@ const DEFAULT_RESOURCES = [
|
|
|
10
10
|
'toolbox',
|
|
11
11
|
'tools',
|
|
12
12
|
'tool_actions',
|
|
13
|
+
'projects',
|
|
14
|
+
'workspaces',
|
|
15
|
+
'memories',
|
|
13
16
|
];
|
|
14
17
|
|
|
15
18
|
function normalizeResources(resources) {
|
|
@@ -45,6 +48,13 @@ function readResource(resource, cache) {
|
|
|
45
48
|
cache.toolbox ||= require('../toolbox/store').readToolbox();
|
|
46
49
|
return cache.toolbox.toolActions;
|
|
47
50
|
}
|
|
51
|
+
case 'projects':
|
|
52
|
+
case 'workspaces': {
|
|
53
|
+
const workspaceStore = require('../workspace/store');
|
|
54
|
+
return workspaceStore.listWorkspaces();
|
|
55
|
+
}
|
|
56
|
+
case 'memories':
|
|
57
|
+
return require('../../chat-core/tooling/active-memory').collectActiveMemoryFiles();
|
|
48
58
|
default:
|
|
49
59
|
return undefined;
|
|
50
60
|
}
|
|
@@ -52,20 +62,38 @@ function readResource(resource, cache) {
|
|
|
52
62
|
|
|
53
63
|
function buildSnapshot(resourcesInput) {
|
|
54
64
|
const resources = normalizeResources(resourcesInput);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
let lastUnstable = null;
|
|
66
|
+
|
|
67
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
68
|
+
const beforeSeq = currentSeq();
|
|
69
|
+
const cache = {};
|
|
70
|
+
const data = {};
|
|
71
|
+
|
|
72
|
+
for (const resource of resources) {
|
|
73
|
+
const value = readResource(resource, cache);
|
|
74
|
+
if (value !== undefined) data[resource] = value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const afterSeq = currentSeq();
|
|
78
|
+
if (beforeSeq === afterSeq) {
|
|
79
|
+
return {
|
|
80
|
+
seq: afterSeq,
|
|
81
|
+
stable: true,
|
|
82
|
+
resources: data,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
58
85
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
lastUnstable = {
|
|
87
|
+
seq: beforeSeq,
|
|
88
|
+
stable: false,
|
|
89
|
+
resources: data,
|
|
90
|
+
};
|
|
62
91
|
}
|
|
63
92
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
resources: data,
|
|
93
|
+
return lastUnstable || {
|
|
94
|
+
seq: currentSeq(),
|
|
95
|
+
stable: true,
|
|
96
|
+
resources: {},
|
|
69
97
|
};
|
|
70
98
|
}
|
|
71
99
|
|
|
@@ -106,6 +106,7 @@ async function executeTask(task) {
|
|
|
106
106
|
const runId = crypto.randomUUID();
|
|
107
107
|
const startedAt = new Date().toISOString();
|
|
108
108
|
const { harness, chatInput, legacy, modelSelection } = resolveTaskRequest(task);
|
|
109
|
+
const projectPath = legacy.cwd || task.projectPath || null;
|
|
109
110
|
const mcpServers = await buildLocalMcpServerConfigs(legacy.mcpAppIds || []);
|
|
110
111
|
|
|
111
112
|
console.log(`[AmalgmMCP:Exec] Starting task ${task.id} (${task.name}), run ${runId}`);
|
|
@@ -127,7 +128,7 @@ async function executeTask(task) {
|
|
|
127
128
|
title: 'New Chat',
|
|
128
129
|
origin: 'task',
|
|
129
130
|
origin_id: task.id,
|
|
130
|
-
project_path:
|
|
131
|
+
project_path: projectPath,
|
|
131
132
|
status: 'in-progress',
|
|
132
133
|
metadata: { taskRunId: runId, automated: true, originName: task.name },
|
|
133
134
|
});
|
|
@@ -156,10 +157,15 @@ async function executeTask(task) {
|
|
|
156
157
|
modelId: modelSelection.modelId || legacy.modelId || null,
|
|
157
158
|
cliModel: modelSelection.cliModel || null,
|
|
158
159
|
...(modelSelection.reasoningEffort ? { reasoningEffort: modelSelection.reasoningEffort } : {}),
|
|
159
|
-
cwd:
|
|
160
|
+
cwd: projectPath || DEFAULT_CWD,
|
|
160
161
|
authMethod: legacy.authMethod || null,
|
|
161
162
|
mcpServers,
|
|
162
|
-
|
|
163
|
+
origin: {
|
|
164
|
+
type: 'task',
|
|
165
|
+
id: task.id,
|
|
166
|
+
name: task.name,
|
|
167
|
+
projectPath,
|
|
168
|
+
},
|
|
163
169
|
automated: true,
|
|
164
170
|
},
|
|
165
171
|
abortController.signal,
|
|
@@ -57,7 +57,9 @@ function isTaskDue(task, now) {
|
|
|
57
57
|
currentDate: now,
|
|
58
58
|
tz: schedule.tz || 'UTC',
|
|
59
59
|
});
|
|
60
|
-
const prev = interval.
|
|
60
|
+
const prev = interval.includesDate(now)
|
|
61
|
+
? new Date(Math.floor(now.getTime() / 1000) * 1000)
|
|
62
|
+
: interval.prev().toDate();
|
|
61
63
|
const createdAt = validDate(task.createdAt);
|
|
62
64
|
const lastRunAt = validDate(task.lastRunAt);
|
|
63
65
|
|
|
@@ -68,8 +70,9 @@ function isTaskDue(task, now) {
|
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
if (schedule.kind === 'interval') {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
const baseline = validDate(task.lastRunAt) || validDate(task.createdAt);
|
|
74
|
+
if (!baseline) return true;
|
|
75
|
+
return now - baseline >= schedule.ms;
|
|
73
76
|
}
|
|
74
77
|
return false;
|
|
75
78
|
}
|
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
hydrateModelPreferences,
|
|
24
24
|
} = require('../lib/prefs');
|
|
25
25
|
const credentialAdapter = require('../../credential-adapter');
|
|
26
|
+
const activeMemory = require('../../chat-core/tooling/active-memory');
|
|
26
27
|
|
|
27
28
|
function resolveTaskHarness(harness, chatInput) {
|
|
28
29
|
const candidate =
|
|
@@ -153,6 +154,12 @@ module.exports = [
|
|
|
153
154
|
const data = loadTasks();
|
|
154
155
|
data.tasks.push(task);
|
|
155
156
|
saveTasks(data);
|
|
157
|
+
activeMemory.ensureConstructMemory({
|
|
158
|
+
type: 'task',
|
|
159
|
+
id: task.id,
|
|
160
|
+
name: task.name,
|
|
161
|
+
projectPath: task.projectPath,
|
|
162
|
+
}, { source: 'task:create' });
|
|
156
163
|
console.log(`[AmalgmMCP] Created task: ${task.id} (${task.name})`);
|
|
157
164
|
return textResult(`Task created successfully.\n\n${JSON.stringify(task, null, 2)}`);
|
|
158
165
|
},
|
|
@@ -309,6 +316,12 @@ module.exports = [
|
|
|
309
316
|
Object.assign(task, normalizedTask);
|
|
310
317
|
|
|
311
318
|
saveTasks(data);
|
|
319
|
+
activeMemory.ensureConstructMemory({
|
|
320
|
+
type: 'task',
|
|
321
|
+
id: task.id,
|
|
322
|
+
name: task.name,
|
|
323
|
+
projectPath: task.projectPath,
|
|
324
|
+
}, { source: 'task:update' });
|
|
312
325
|
console.log(`[AmalgmMCP] Updated task: ${task.id} (${task.name})`);
|
|
313
326
|
return textResult(`Task updated.\n\n${JSON.stringify(task, null, 2)}`);
|
|
314
327
|
},
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
computeSignature,
|
|
8
|
+
extractSignature,
|
|
9
|
+
matchBySignature,
|
|
10
|
+
} = require('../events/matcher');
|
|
11
|
+
|
|
12
|
+
function trigger(overrides = {}) {
|
|
13
|
+
return {
|
|
14
|
+
id: 'trigger-1',
|
|
15
|
+
enabled: true,
|
|
16
|
+
secret: 'super-secret',
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('event matcher accepts generic HMAC webhook signatures', () => {
|
|
22
|
+
const rawBody = JSON.stringify({ ok: true });
|
|
23
|
+
const signature = extractSignature({
|
|
24
|
+
'x-webhook-signature': computeSignature('super-secret', rawBody),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
assert.equal(matchBySignature([trigger()], signature, rawBody)?.id, 'trigger-1');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('event matcher accepts bearer token webhook secrets', () => {
|
|
31
|
+
const signature = extractSignature({
|
|
32
|
+
authorization: 'Bearer super-secret',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
assert.equal(matchBySignature([trigger()], signature, '{}')?.id, 'trigger-1');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('event matcher rejects disabled triggers and wrong secrets', () => {
|
|
39
|
+
const rawBody = JSON.stringify({ ok: true });
|
|
40
|
+
const signature = extractSignature({
|
|
41
|
+
'x-webhook-signature': computeSignature('wrong-secret', rawBody),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
assert.equal(matchBySignature([trigger(), trigger({ id: 'disabled', enabled: false })], signature, rawBody), null);
|
|
45
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
let seq = 0;
|
|
7
|
+
const eventsPath = require.resolve('../state/events');
|
|
8
|
+
require.cache[eventsPath] = {
|
|
9
|
+
id: eventsPath,
|
|
10
|
+
filename: eventsPath,
|
|
11
|
+
loaded: true,
|
|
12
|
+
exports: {
|
|
13
|
+
currentSeq: () => seq,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const taskStore = require('../tasks/store');
|
|
18
|
+
const { buildSnapshot } = require('../state/snapshot');
|
|
19
|
+
|
|
20
|
+
test.after(() => {
|
|
21
|
+
taskStore.loadTasks = originalLoadTasks;
|
|
22
|
+
delete require.cache[eventsPath];
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const originalLoadTasks = taskStore.loadTasks;
|
|
26
|
+
|
|
27
|
+
test('snapshot retries when a state event lands during resource reads', () => {
|
|
28
|
+
let calls = 0;
|
|
29
|
+
taskStore.loadTasks = () => {
|
|
30
|
+
calls += 1;
|
|
31
|
+
if (calls === 1) {
|
|
32
|
+
seq = 1;
|
|
33
|
+
return { version: 1, tasks: [{ id: 'stale-task', name: 'Stale task' }] };
|
|
34
|
+
}
|
|
35
|
+
return { version: 1, tasks: [{ id: 'new-task', name: 'New task' }] };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const snapshot = buildSnapshot('tasks');
|
|
39
|
+
|
|
40
|
+
assert.equal(calls, 2);
|
|
41
|
+
assert.equal(snapshot.stable, true);
|
|
42
|
+
assert.equal(snapshot.seq, 1);
|
|
43
|
+
assert.deepEqual(snapshot.resources.tasks, [{ id: 'new-task', name: 'New task' }]);
|
|
44
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const { isTaskDue } = require('../tasks/scheduler');
|
|
7
|
+
|
|
8
|
+
function baseTask(overrides = {}) {
|
|
9
|
+
return {
|
|
10
|
+
id: 'task-1',
|
|
11
|
+
enabled: true,
|
|
12
|
+
createdAt: '2026-05-18T10:00:00.000Z',
|
|
13
|
+
lastRunAt: null,
|
|
14
|
+
endsAt: null,
|
|
15
|
+
schedule: { kind: 'once', at: '2026-05-18T10:01:00.000Z' },
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('interval task waits for the first interval after creation', () => {
|
|
21
|
+
const task = baseTask({
|
|
22
|
+
schedule: { kind: 'interval', ms: 60_000 },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
assert.equal(isTaskDue(task, new Date('2026-05-18T10:00:30.000Z')), false);
|
|
26
|
+
assert.equal(isTaskDue(task, new Date('2026-05-18T10:01:00.000Z')), true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('interval task uses lastRunAt after the first run', () => {
|
|
30
|
+
const task = baseTask({
|
|
31
|
+
lastRunAt: '2026-05-18T10:03:00.000Z',
|
|
32
|
+
schedule: { kind: 'interval', ms: 60_000 },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
assert.equal(isTaskDue(task, new Date('2026-05-18T10:03:59.000Z')), false);
|
|
36
|
+
assert.equal(isTaskDue(task, new Date('2026-05-18T10:04:00.000Z')), true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('cron task is due at the exact scheduled boundary in its timezone', () => {
|
|
40
|
+
const task = baseTask({
|
|
41
|
+
createdAt: '2026-05-18T15:55:00.000Z',
|
|
42
|
+
schedule: {
|
|
43
|
+
kind: 'cron',
|
|
44
|
+
expr: '0 9 * * *',
|
|
45
|
+
tz: 'America/Los_Angeles',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.equal(isTaskDue(task, new Date('2026-05-18T16:00:00.000Z')), true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('cron task does not rerun for the same scheduled instant', () => {
|
|
53
|
+
const task = baseTask({
|
|
54
|
+
createdAt: '2026-05-18T15:55:00.000Z',
|
|
55
|
+
lastRunAt: '2026-05-18T16:00:00.000Z',
|
|
56
|
+
schedule: {
|
|
57
|
+
kind: 'cron',
|
|
58
|
+
expr: '0 9 * * *',
|
|
59
|
+
tz: 'America/Los_Angeles',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert.equal(isTaskDue(task, new Date('2026-05-18T16:00:30.000Z')), false);
|
|
64
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-workspaces-test-'));
|
|
10
|
+
process.env.AMALGM_DIR = path.join(tempRoot, '.amalgm');
|
|
11
|
+
process.env.AMALGM_WORKSPACES_DIR = path.join(process.env.AMALGM_DIR, 'workspaces');
|
|
12
|
+
process.env.AMALGM_COMPUTER_ID = 'computer-test';
|
|
13
|
+
|
|
14
|
+
const { closeLocalDb } = require('../state/db');
|
|
15
|
+
const {
|
|
16
|
+
listWorkspaces,
|
|
17
|
+
removeWorkspace,
|
|
18
|
+
renameWorkspace,
|
|
19
|
+
upsertWorkspace,
|
|
20
|
+
workspaceUriForPath,
|
|
21
|
+
} = require('../workspace/store');
|
|
22
|
+
|
|
23
|
+
test.after(() => {
|
|
24
|
+
closeLocalDb();
|
|
25
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('workspace rows store a stable computer URI and local path', () => {
|
|
29
|
+
const projectPath = path.join(tempRoot, 'existing-project');
|
|
30
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
31
|
+
|
|
32
|
+
const workspace = upsertWorkspace({
|
|
33
|
+
path: projectPath,
|
|
34
|
+
name: 'Existing project',
|
|
35
|
+
source: 'computer',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
assert.equal(workspace.path, projectPath);
|
|
39
|
+
assert.equal(workspace.computerId, 'computer-test');
|
|
40
|
+
assert.equal(workspace.managed, false);
|
|
41
|
+
assert.equal(workspace.uri, workspaceUriForPath(projectPath, 'computer-test'));
|
|
42
|
+
assert.equal(listWorkspaces().length, 1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('workspace rename and remove publish durable row changes', () => {
|
|
46
|
+
const projectPath = path.join(tempRoot, 'managed-project');
|
|
47
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
48
|
+
|
|
49
|
+
const workspace = upsertWorkspace({
|
|
50
|
+
path: projectPath,
|
|
51
|
+
name: 'Managed project',
|
|
52
|
+
source: 'managed',
|
|
53
|
+
managed: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const renamed = renameWorkspace(workspace.id, 'Renamed project');
|
|
57
|
+
assert.equal(renamed.name, 'Renamed project');
|
|
58
|
+
assert.equal(listWorkspaces().some((item) => item.name === 'Renamed project'), true);
|
|
59
|
+
|
|
60
|
+
const removed = removeWorkspace(workspace.id);
|
|
61
|
+
assert.equal(removed.id, workspace.id);
|
|
62
|
+
assert.equal(listWorkspaces().some((item) => item.id === workspace.id), false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('workspace registry does not auto-discover managed folders', () => {
|
|
66
|
+
const workspaceRoot = path.join(tempRoot, 'workspace-root');
|
|
67
|
+
const projectPath = path.join(workspaceRoot, 'folder-on-disk');
|
|
68
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
69
|
+
|
|
70
|
+
assert.equal(listWorkspaces().some((item) => item.path === projectPath), false);
|
|
71
|
+
|
|
72
|
+
const workspace = upsertWorkspace({
|
|
73
|
+
path: projectPath,
|
|
74
|
+
name: 'Folder on disk',
|
|
75
|
+
source: 'managed',
|
|
76
|
+
managed: true,
|
|
77
|
+
}, { workspaceRoot });
|
|
78
|
+
|
|
79
|
+
assert.equal(listWorkspaces().some((item) => item.id === workspace.id), true);
|
|
80
|
+
removeWorkspace(workspace.id);
|
|
81
|
+
|
|
82
|
+
assert.equal(fs.existsSync(projectPath), true);
|
|
83
|
+
assert.equal(listWorkspaces().some((item) => item.path === projectPath), false);
|
|
84
|
+
});
|