amalgm 0.1.36 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.36",
3
+ "version": "0.1.37",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -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
- return updateArtifactMeta(artifact.id, { lastDeployedAt: new Date().toISOString() });
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
- return updateArtifactMeta(artifactId, { lastDeployedAt: new Date().toISOString() });
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
  }
@@ -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: legacy.cwd || task.projectPath || null,
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: legacy.cwd || DEFAULT_CWD,
160
+ cwd: projectPath || DEFAULT_CWD,
160
161
  authMethod: legacy.authMethod || null,
161
162
  mcpServers,
162
- taskId: task.id,
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,
@@ -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,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
+ });
@@ -16,6 +16,7 @@ const { AMALGM_DIR, PORT } = require('../config');
16
16
  const { ensureDir, readJson, writeJsonAtomic } = require('../lib/storage');
17
17
  const { openLocalDb } = require('../state/db');
18
18
  const { insertStateEvent, publishStateEvent } = require('../state/events');
19
+ const activeMemory = require('../../chat-core/tooling/active-memory');
19
20
 
20
21
  const TOOLBOX_VERSION = 1;
21
22
  const TOOLBOX_DIR = path.join(AMALGM_DIR, 'toolbox');
@@ -726,6 +727,13 @@ function upsertTool(input) {
726
727
  return inserted;
727
728
  })();
728
729
  publishCommittedEvents(events);
730
+ if (tool.origin !== 'system') {
731
+ activeMemory.ensureConstructMemory({
732
+ type: 'tool',
733
+ id: tool.id,
734
+ name: tool.name,
735
+ }, { source: 'toolbox:upsert-tool' });
736
+ }
729
737
  return { tool, actions };
730
738
  }
731
739
 
@@ -802,6 +810,13 @@ function updateTool(input) {
802
810
  })];
803
811
  })();
804
812
  publishCommittedEvents(events);
813
+ if (tool.origin !== 'system') {
814
+ activeMemory.ensureConstructMemory({
815
+ type: 'tool',
816
+ id: tool.id,
817
+ name: tool.name,
818
+ }, { source: 'toolbox:update-tool' });
819
+ }
805
820
  return { tool, actions: toolActionsForTool(toolbox, tool.id) };
806
821
  }
807
822