amalgm 0.1.257 → 0.1.258

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/lib/cli.js CHANGED
@@ -120,7 +120,7 @@ function usage() {
120
120
  ' amalgm stop',
121
121
  ' amalgm register [path ...] Register local ground (default: current directory)',
122
122
  ' amalgm add <entity-uuid> [directory] Add a cloud-backed tree to this machine',
123
- ' amalgm status',
123
+ ' amalgm status [path-or-uuid] Show cloud-backed entity and watcher state',
124
124
  ' amalgm doctor',
125
125
  ' amalgm service [install|start|stop|status|uninstall] [--mode auto|systemd|launchd|windows|portable]',
126
126
  ' amalgm update [--tag latest|canary]',
@@ -1112,7 +1112,7 @@ async function waitForServices(timeoutMs) {
1112
1112
  return latest;
1113
1113
  }
1114
1114
 
1115
- async function status() {
1115
+ async function runtimeStatus() {
1116
1116
  const pid = readPid();
1117
1117
  const record = loadComputerRecord({ migrate: true });
1118
1118
  const runtimeState = readJson(RUNTIME_STATE_FILE, null);
@@ -1177,6 +1177,75 @@ async function status() {
1177
1177
  }
1178
1178
  }
1179
1179
 
1180
+ /** `amalgm status [path-or-uuid]` is a read-only view of entity truth. The
1181
+ * runtime owns paths, bindings, and cloud freshness; the CLI only narrows and
1182
+ * renders the returned facts. Daemon status remains available as
1183
+ * `amalgm service status`. */
1184
+ async function entityStatus(positionals) {
1185
+ if (positionals.length > 1) throw new Error('Usage: amalgm status [path-or-uuid]');
1186
+ const port = servicePorts().find(([name]) => name === 'amalgm-mcp')?.[1];
1187
+ let answer = null;
1188
+ if (port) {
1189
+ try {
1190
+ answer = await runtimeGet(port, '/entities/status');
1191
+ } catch (error) {
1192
+ if (error.code !== 'ECONNREFUSED') throw error;
1193
+ }
1194
+ }
1195
+ if (!answer) {
1196
+ console.error('Amalgm runtime is not running. Start it with `amalgm start`.');
1197
+ process.exitCode = 1;
1198
+ return;
1199
+ }
1200
+ if (answer.status !== 200 || !answer.payload) {
1201
+ console.error(`Error: ${answer.payload?.error || `runtime responded ${answer.status}`}`);
1202
+ process.exitCode = 1;
1203
+ return;
1204
+ }
1205
+
1206
+ const status = answer.payload;
1207
+ const selector = positionals[0] || null;
1208
+ const uuid = selector && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(selector)
1209
+ ? selector.toLowerCase()
1210
+ : null;
1211
+ const localPath = selector && !uuid ? path.resolve(selector) : null;
1212
+ const entities = (status.entities || []).filter((entity) => !selector
1213
+ || entity.uuid === uuid || entity.localPath === localPath);
1214
+ if (selector && entities.length === 0) {
1215
+ console.error(`No cloud-backed entity matches ${selector}.`);
1216
+ process.exitCode = 1;
1217
+ return;
1218
+ }
1219
+
1220
+ const cloud = status.cloud || {};
1221
+ const replica = cloud.replica;
1222
+ if (!replica) {
1223
+ console.log(`Cloud: ${cloud.identity?.enabled ? 'initializing' : 'not signed in'}`);
1224
+ } else {
1225
+ console.log(`Cloud: ${cloud.catalog?.current ? 'current' : 'catching up'} (head ${replica.headVersion})`);
1226
+ }
1227
+ const outbox = cloud.outbox || {};
1228
+ console.log(`Delivery: ${outbox.pending || 0} pending, ${outbox.syncing || 0} sending, ${outbox.rejected || 0} rejected`);
1229
+ const jobs = cloud.materializations || {};
1230
+ if ((jobs.waitingForContent || 0) + (jobs.materializing || 0) + (jobs.failed || 0) > 0) {
1231
+ console.log(`Materializations: ${jobs.waitingForContent || 0} waiting, ${jobs.materializing || 0} applying, ${jobs.failed || 0} failed`);
1232
+ }
1233
+ if (entities.length === 0) {
1234
+ console.log('Entities: none on this cloud replica.');
1235
+ return;
1236
+ }
1237
+ console.log('Entities:');
1238
+ for (const entity of entities) {
1239
+ console.log(` ${entity.uuid} ${entity.type} ${entity.status}`);
1240
+ if (entity.localPath) console.log(` Local: ${entity.localPath}`);
1241
+ else console.log(' Local: not materialized on this machine');
1242
+ if (entity.root) {
1243
+ const watcher = entity.root.watching && !entity.root.degraded ? 'on' : 'unavailable';
1244
+ console.log(` Watching: ${watcher}`);
1245
+ }
1246
+ }
1247
+ }
1248
+
1180
1249
  async function doctor() {
1181
1250
  ensureBaseDirs();
1182
1251
 
@@ -1649,7 +1718,7 @@ async function main(argv) {
1649
1718
  if (command === 'stop') return stop();
1650
1719
  if (command === 'register') return register(positionals);
1651
1720
  if (command === 'add') return add(positionals);
1652
- if (command === 'status') return status();
1721
+ if (command === 'status') return entityStatus(positionals);
1653
1722
  if (command === 'doctor') return doctor();
1654
1723
  if (command === 'service') return serviceCommand(options, positionals);
1655
1724
  if (command === 'update') return update(options);
package/lib/layout.js CHANGED
@@ -33,7 +33,9 @@ const ROOT_MANIFEST_NAME = '.amalgm-root.json';
33
33
  const USER_MANIFEST_NAME = '.amalgm-user.json';
34
34
  const LAYOUT_VERSION = 1;
35
35
  const USER_HOME_MANIFEST_NAME = 'home.json';
36
- const USER_HOME_SCHEMA_VERSION = 1;
36
+ // Changing the declared exact graph changes the identity contract. A client
37
+ // must never accept a prior graph and silently invent a new managed child.
38
+ const USER_HOME_SCHEMA_VERSION = 2;
37
39
 
38
40
  // Stable per-user product namespaces. Their authority is NOT inferred from
39
41
  // their path: user content and exact platform-managed targets are declared
@@ -59,6 +61,19 @@ const USER_HOME_ROOTS = Object.freeze([
59
61
  // Platform management is an exact-node fact. It deliberately does not
60
62
  // cascade from `system/`: human permissions may still inherit normally, while
61
63
  // only these particular entities are product-managed.
64
+ //
65
+ // A Toolbox tool is one complete entity: its definition and actions travel in
66
+ // the same file. These names are deliberately stable and human-readable.
67
+ const SYSTEM_TOOL_ARTIFACTS = Object.freeze([
68
+ { key: 'amalgm-automations', toolId: 'automations', relativePath: 'toolbox/amalgm-automations.json', rootKey: 'toolbox' },
69
+ { key: 'amalgm-notifications', toolId: 'notifications', relativePath: 'toolbox/amalgm-notifications.json', rootKey: 'toolbox' },
70
+ { key: 'amalgm-memories', toolId: 'memories', relativePath: 'toolbox/amalgm-memories.json', rootKey: 'toolbox' },
71
+ { key: 'amalgm-agents', toolId: 'agents', relativePath: 'toolbox/amalgm-agents.json', rootKey: 'toolbox' },
72
+ { key: 'amalgm-apps', toolId: 'apps', relativePath: 'toolbox/amalgm-apps.json', rootKey: 'toolbox' },
73
+ { key: 'amalgm-cua', toolId: 'computer-use', relativePath: 'toolbox/amalgm-cua.json', rootKey: 'toolbox' },
74
+ { key: 'amalgm-browser', toolId: 'browser', relativePath: 'toolbox/amalgm-browser.json', rootKey: 'toolbox' },
75
+ ]);
76
+
62
77
  const USER_HOME_EXACT_MANAGED = Object.freeze([
63
78
  ...USER_HOME_ROOTS.map(({ key, relativePath }) => ({
64
79
  key: `${key}-root`, relativePath, rootKey: key,
@@ -66,6 +81,7 @@ const USER_HOME_EXACT_MANAGED = Object.freeze([
66
81
  { key: 'home-manifest', relativePath: 'system/home.json', rootKey: 'system' },
67
82
  { key: 'defaults', relativePath: 'system/defaults', rootKey: 'system' },
68
83
  { key: 'policy', relativePath: 'system/policy', rootKey: 'system' },
84
+ ...SYSTEM_TOOL_ARTIFACTS,
69
85
  ]);
70
86
 
71
87
  function readJson(file, fallback = null) {
@@ -106,6 +122,7 @@ function declaredUserHome(userDir) {
106
122
  */
107
123
  function ensureUserHome(userDir) {
108
124
  const declaration = declaredUserHome(userDir);
125
+ const materializedExactPaths = [];
109
126
  for (const root of declaration.roots) {
110
127
  fs.mkdirSync(root.path, { recursive: true, mode: 0o700 });
111
128
  }
@@ -115,15 +132,18 @@ function ensureUserHome(userDir) {
115
132
  }
116
133
 
117
134
  const manifest = declaration.exactManaged.find((entry) => entry.key === 'home-manifest');
118
- if (manifest && !fs.existsSync(manifest.path)) {
119
- writeJson(manifest.path, {
120
- schemaVersion: USER_HOME_SCHEMA_VERSION,
121
- kind: 'amalgm-user-home',
122
- roots: declaration.roots.map(({ key, relativePath }) => ({ key, path: relativePath })),
123
- exactManaged: declaration.exactManaged.map(({ key, relativePath, scope }) => ({ key, path: relativePath, scope })),
124
- });
135
+ const manifestContents = {
136
+ schemaVersion: USER_HOME_SCHEMA_VERSION,
137
+ kind: 'amalgm-user-home',
138
+ roots: declaration.roots.map(({ key, relativePath }) => ({ key, path: relativePath })),
139
+ exactManaged: declaration.exactManaged.map(({ key, relativePath, scope }) => ({ key, path: relativePath, scope })),
140
+ };
141
+ if (manifest && JSON.stringify(readJson(manifest.path, null)) !== JSON.stringify(manifestContents)) {
142
+ writeJson(manifest.path, manifestContents);
143
+ materializedExactPaths.push(manifest.relativePath);
125
144
  }
126
- return declaration;
145
+
146
+ return { ...declaration, materializedExactPaths };
127
147
  }
128
148
 
129
149
  function lstatOrNull(target) {
@@ -401,6 +421,7 @@ module.exports = {
401
421
  LAYOUT_VERSION,
402
422
  PRIMITIVE_DIRS,
403
423
  ROOT_MANIFEST_NAME,
424
+ SYSTEM_TOOL_ARTIFACTS,
404
425
  USER_MANIFEST_NAME,
405
426
  USER_HOME_EXACT_MANAGED,
406
427
  USER_HOME_MANIFEST_NAME,
@@ -19,8 +19,8 @@ const RETRY_MS = 1_000;
19
19
  const REAUTHORIZE_MS = 30_000;
20
20
  const REPO_OBJECT_REQUEST_TIMEOUT_MS = 15_000;
21
21
  const COOKIE_PRIVATE_CONTRACT = 'browser-cookie-jar@1';
22
- const USER_HOME_PRIVATE_CONTRACT = 'amalgm-user-home@1';
23
- const ENTITY_REGISTRY_PRIVATE_CONTRACT = 'amalgm-entity-registry@2';
22
+ const USER_HOME_PRIVATE_CONTRACT = 'amalgm-user-home@2';
23
+ const ENTITY_REGISTRY_PRIVATE_CONTRACT = 'amalgm-entity-registry@3';
24
24
 
25
25
  function createSharedRealtimeTunnel({
26
26
  send,
@@ -938,13 +938,10 @@ function createSharedRealtimeTunnel({
938
938
  committedAt: frame.committed_at,
939
939
  }).then((ack) => {
940
940
  entityMutationInflight.delete(frame.request_id);
941
- // An acknowledgement advances the official head. Re-read the
942
- // private resource immediately: the gateway compacts that head into
943
- // one immutable snapshot before replying, so this device never has
944
- // to use its mutation tail as an `amalgm add` starting point. A
945
- // legacy empty entity resource is seeded in ordered records first;
946
- // intermediate heads are intentionally not imported as a usable
947
- // snapshot, because they are not the complete declared home yet.
941
+ // The runtime reports whether it converged its catalog locally from
942
+ // this acknowledgement. Only when it could not (a version gap, a
943
+ // reconciliation handshake, a stale local view) is the compacted
944
+ // snapshot re-downloaded; the ordinary in-order ack costs nothing.
948
945
  if (ack?.requiresSnapshot !== false) void ensurePrivateEntityRegistry();
949
946
  scheduleEntityMutationDrain();
950
947
  }).catch(() => {
@@ -1109,11 +1106,12 @@ function createSharedRealtimeTunnel({
1109
1106
  version: frame.version,
1110
1107
  committedAt: frame.committed_at,
1111
1108
  actorUserId: frame.actor_user_id,
1112
- })).then(() => {
1113
- if (entityRegistryMutation) {
1114
- // The live frame is useful for collaboration, but cannot become the
1115
- // reconstruction path for a new local binding. Ask the authority to
1116
- // compact and return its active snapshot before `add` can continue.
1109
+ })).then((result) => {
1110
+ if (entityRegistryMutation && result?.requiresSnapshot !== false) {
1111
+ // The runtime could not converge its catalog from this frame alone
1112
+ // (a gap, a snapshot replacement, or a stale local view). Ask the
1113
+ // authority to compact and return its active snapshot; an in-order
1114
+ // frame needs no re-download.
1117
1115
  void ensurePrivateEntityRegistry();
1118
1116
  }
1119
1117
  }).catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.257",
3
+ "version": "0.1.258",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -17,7 +17,7 @@
17
17
  "sync-runtime": "node ../../scripts/sync-npm-package-runtime.mjs",
18
18
  "prepack": "node ../../scripts/sync-npm-package-runtime.mjs",
19
19
  "pack:dry": "npm pack --dry-run",
20
- "check": "node --check bin/amalgm.js && node --check lib/app-chat.js && node --check lib/react.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/identity-adopt.js && node --check lib/layout.js && node --check lib/migrate-layout.js && node --check lib/paths.js && node --check lib/process-cleanup.js && node --check lib/runtime-identity.js && node --check lib/runtime-manifest.js && node --check lib/service.js && node --check lib/state-migration.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check lib/updater.js && node --check runtime/lib/runtime-manifest.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/chat-core/tooling/runtime-home.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js && node --check runtime/scripts/amalgm-mcp/events/pr-check-runner.js && node --check runtime/scripts/amalgm-mcp/registration/entity-cloud.js && node --check runtime/scripts/amalgm-mcp/registration/entity-content.js && node --check runtime/scripts/amalgm-mcp/registration/service.js && node --check runtime/scripts/amalgm-mcp/server/routes/entities.js && node --check runtime/scripts/lib/project-paths.js && node --check runtime/scripts/lib/runtime-paths.js"
20
+ "check": "node --check bin/amalgm.js && node --check lib/app-chat.js && node --check lib/react.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/identity-adopt.js && node --check lib/layout.js && node --check lib/migrate-layout.js && node --check lib/paths.js && node --check lib/process-cleanup.js && node --check lib/runtime-identity.js && node --check lib/runtime-manifest.js && node --check lib/service.js && node --check lib/state-migration.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check lib/updater.js && node --check runtime/lib/runtime-manifest.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/chat-core/tooling/runtime-home.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js && node --check runtime/scripts/amalgm-mcp/events/pr-check-runner.js && node --check runtime/scripts/amalgm-mcp/registration/entity-cloud.js && node --check runtime/scripts/amalgm-mcp/registration/entity-content.js && node --check runtime/scripts/amalgm-mcp/registration/service.js && node --check runtime/scripts/amalgm-mcp/toolbox/artifacts.js && node --check runtime/scripts/amalgm-mcp/server/routes/entities.js && node --check runtime/scripts/lib/project-paths.js && node --check runtime/scripts/lib/runtime-paths.js"
21
21
  },
22
22
  "engines": {
23
23
  "node": ">=20"
@@ -33,7 +33,9 @@ const ROOT_MANIFEST_NAME = '.amalgm-root.json';
33
33
  const USER_MANIFEST_NAME = '.amalgm-user.json';
34
34
  const LAYOUT_VERSION = 1;
35
35
  const USER_HOME_MANIFEST_NAME = 'home.json';
36
- const USER_HOME_SCHEMA_VERSION = 1;
36
+ // Changing the declared exact graph changes the identity contract. A client
37
+ // must never accept a prior graph and silently invent a new managed child.
38
+ const USER_HOME_SCHEMA_VERSION = 2;
37
39
 
38
40
  // Stable per-user product namespaces. Their authority is NOT inferred from
39
41
  // their path: user content and exact platform-managed targets are declared
@@ -59,6 +61,21 @@ const USER_HOME_ROOTS = Object.freeze([
59
61
  // Platform management is an exact-node fact. It deliberately does not
60
62
  // cascade from `system/`: human permissions may still inherit normally, while
61
63
  // only these particular entities are product-managed.
64
+ //
65
+ // A Toolbox tool is one complete entity: its definition and actions travel in
66
+ // the same file. These names are deliberately stable and human-readable. They
67
+ // are not an `amalgm/` subtree because that would falsely imply ownership
68
+ // cascades to every future child of that directory.
69
+ const SYSTEM_TOOL_ARTIFACTS = Object.freeze([
70
+ { key: 'amalgm-automations', toolId: 'automations', relativePath: 'toolbox/amalgm-automations.json', rootKey: 'toolbox' },
71
+ { key: 'amalgm-notifications', toolId: 'notifications', relativePath: 'toolbox/amalgm-notifications.json', rootKey: 'toolbox' },
72
+ { key: 'amalgm-memories', toolId: 'memories', relativePath: 'toolbox/amalgm-memories.json', rootKey: 'toolbox' },
73
+ { key: 'amalgm-agents', toolId: 'agents', relativePath: 'toolbox/amalgm-agents.json', rootKey: 'toolbox' },
74
+ { key: 'amalgm-apps', toolId: 'apps', relativePath: 'toolbox/amalgm-apps.json', rootKey: 'toolbox' },
75
+ { key: 'amalgm-cua', toolId: 'computer-use', relativePath: 'toolbox/amalgm-cua.json', rootKey: 'toolbox' },
76
+ { key: 'amalgm-browser', toolId: 'browser', relativePath: 'toolbox/amalgm-browser.json', rootKey: 'toolbox' },
77
+ ]);
78
+
62
79
  const USER_HOME_EXACT_MANAGED = Object.freeze([
63
80
  ...USER_HOME_ROOTS.map(({ key, relativePath }) => ({
64
81
  key: `${key}-root`, relativePath, rootKey: key,
@@ -66,6 +83,7 @@ const USER_HOME_EXACT_MANAGED = Object.freeze([
66
83
  { key: 'home-manifest', relativePath: 'system/home.json', rootKey: 'system' },
67
84
  { key: 'defaults', relativePath: 'system/defaults', rootKey: 'system' },
68
85
  { key: 'policy', relativePath: 'system/policy', rootKey: 'system' },
86
+ ...SYSTEM_TOOL_ARTIFACTS,
69
87
  ]);
70
88
 
71
89
  function readJson(file, fallback = null) {
@@ -106,6 +124,7 @@ function declaredUserHome(userDir) {
106
124
  */
107
125
  function ensureUserHome(userDir) {
108
126
  const declaration = declaredUserHome(userDir);
127
+ const materializedExactPaths = [];
109
128
  for (const root of declaration.roots) {
110
129
  fs.mkdirSync(root.path, { recursive: true, mode: 0o700 });
111
130
  }
@@ -115,15 +134,18 @@ function ensureUserHome(userDir) {
115
134
  }
116
135
 
117
136
  const manifest = declaration.exactManaged.find((entry) => entry.key === 'home-manifest');
118
- if (manifest && !fs.existsSync(manifest.path)) {
119
- writeJson(manifest.path, {
120
- schemaVersion: USER_HOME_SCHEMA_VERSION,
121
- kind: 'amalgm-user-home',
122
- roots: declaration.roots.map(({ key, relativePath }) => ({ key, path: relativePath })),
123
- exactManaged: declaration.exactManaged.map(({ key, relativePath, scope }) => ({ key, path: relativePath, scope })),
124
- });
137
+ const manifestContents = {
138
+ schemaVersion: USER_HOME_SCHEMA_VERSION,
139
+ kind: 'amalgm-user-home',
140
+ roots: declaration.roots.map(({ key, relativePath }) => ({ key, path: relativePath })),
141
+ exactManaged: declaration.exactManaged.map(({ key, relativePath, scope }) => ({ key, path: relativePath, scope })),
142
+ };
143
+ if (manifest && JSON.stringify(readJson(manifest.path, null)) !== JSON.stringify(manifestContents)) {
144
+ writeJson(manifest.path, manifestContents);
145
+ materializedExactPaths.push(manifest.relativePath);
125
146
  }
126
- return declaration;
147
+
148
+ return { ...declaration, materializedExactPaths };
127
149
  }
128
150
 
129
151
  function lstatOrNull(target) {
@@ -401,6 +423,7 @@ module.exports = {
401
423
  LAYOUT_VERSION,
402
424
  PRIMITIVE_DIRS,
403
425
  ROOT_MANIFEST_NAME,
426
+ SYSTEM_TOOL_ARTIFACTS,
404
427
  USER_MANIFEST_NAME,
405
428
  USER_HOME_EXACT_MANAGED,
406
429
  USER_HOME_MANIFEST_NAME,