@50arms/platform-mcp 0.0.0-stage → 0.1.436

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.
Files changed (75) hide show
  1. package/README.md +114 -2
  2. package/dist/access-request.js +101 -0
  3. package/dist/agent-gate.js +69 -0
  4. package/dist/board-id.js +27 -0
  5. package/dist/bounded-retry.js +25 -0
  6. package/dist/checkout-docs-root.js +54 -0
  7. package/dist/clear-control.js +77 -0
  8. package/dist/compaction-control.js +142 -0
  9. package/dist/credential.js +104 -0
  10. package/dist/danxbot-api-examples.js +97 -0
  11. package/dist/dashboard-identity.js +14 -0
  12. package/dist/design-screenshot.js +138 -0
  13. package/dist/digest-hold.js +67 -0
  14. package/dist/docs-tools.js +140 -0
  15. package/dist/durable-write.js +56 -0
  16. package/dist/entrypoint.js +35 -0
  17. package/dist/forbidden-hint.js +23 -0
  18. package/dist/guarded-calls.js +21 -0
  19. package/dist/handlers.js +385 -0
  20. package/dist/heartbeat-reporter.js +156 -0
  21. package/dist/http-client.js +190 -0
  22. package/dist/index.js +1128 -0
  23. package/dist/json-body.js +12 -0
  24. package/dist/key-revoked-halt.js +30 -0
  25. package/dist/lifecycle.js +113 -0
  26. package/dist/listen.js +999 -0
  27. package/dist/listener-reminder.js +119 -0
  28. package/dist/machine-daemon/autostart.js +160 -0
  29. package/dist/machine-daemon/beat.js +77 -0
  30. package/dist/machine-daemon/cli.js +42 -0
  31. package/dist/machine-daemon/install.js +168 -0
  32. package/dist/machine-daemon/state.js +64 -0
  33. package/dist/machine-daemon/version.js +13 -0
  34. package/dist/machine-daemon.js +553 -0
  35. package/dist/machine-toolkit.js +460 -0
  36. package/dist/mini-app-build.js +159 -0
  37. package/dist/mini-app-checkout.js +215 -0
  38. package/dist/mini-app-output.js +111 -0
  39. package/dist/mini-app-publish.js +154 -0
  40. package/dist/mini-app-restore.js +175 -0
  41. package/dist/one-line.js +25 -0
  42. package/dist/pacing-relay.js +16 -0
  43. package/dist/permission-request.js +134 -0
  44. package/dist/permission-watch.js +145 -0
  45. package/dist/plan-events-relay.js +572 -0
  46. package/dist/plan-events-tool.js +9 -0
  47. package/dist/plugin-publish.js +38 -0
  48. package/dist/plugin-screenshot.js +81 -0
  49. package/dist/request-bound.js +21 -0
  50. package/dist/restart-notice.js +271 -0
  51. package/dist/result-limit.js +31 -0
  52. package/dist/role-instruction-file.js +36 -0
  53. package/dist/route-board-scope.generated.js +93 -0
  54. package/dist/route-board-scope.js +71 -0
  55. package/dist/session-access.js +322 -0
  56. package/dist/session-connection.js +264 -0
  57. package/dist/session-control-file.js +58 -0
  58. package/dist/session-key.js +125 -0
  59. package/dist/session-options.js +60 -0
  60. package/dist/session-stats.js +235 -0
  61. package/dist/session-stream.js +345 -0
  62. package/dist/session-title.js +129 -0
  63. package/dist/shutdown.js +38 -0
  64. package/dist/stop-fixes.js +50 -0
  65. package/dist/subagents-live.js +162 -0
  66. package/dist/terms-halt.js +39 -0
  67. package/dist/text-limit.js +24 -0
  68. package/dist/tool-access.js +114 -0
  69. package/dist/tool-defs.js +58 -0
  70. package/dist/tool-surface.js +59 -0
  71. package/dist/transcript-lineage.js +105 -0
  72. package/dist/types.js +45 -0
  73. package/dist/urgent-records.js +47 -0
  74. package/dist/version-floor.js +146 -0
  75. package/package.json +51 -4
@@ -0,0 +1,553 @@
1
+ // src/machine-daemon/run.ts
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync as existsSync2 } from "node:fs";
4
+ import { homedir as homedir2 } from "node:os";
5
+
6
+ // ../machine-vitals/src/probes.ts
7
+ import { statfsSync, readFileSync } from "node:fs";
8
+ import { cpus as osCpus, freemem, totalmem, platform as osPlatform } from "node:os";
9
+ import { execFileSync } from "node:child_process";
10
+ function readDisk(path, statfs = statfsSync) {
11
+ const stats = statfs(path);
12
+ return {
13
+ free_disk_bytes: stats.bavail * stats.bsize,
14
+ total_disk_bytes: stats.blocks * stats.bsize
15
+ };
16
+ }
17
+ function readMemory(getFree = freemem, getTotal = totalmem) {
18
+ const total = getTotal();
19
+ const free = getFree();
20
+ return { mem_used_bytes: total - free, mem_total_bytes: total };
21
+ }
22
+ function sumCpuTotals(cpus) {
23
+ let busy = 0;
24
+ let total = 0;
25
+ for (const cpu of cpus) {
26
+ const cpuBusy = cpu.user + cpu.nice + cpu.sys + cpu.irq;
27
+ busy += cpuBusy;
28
+ total += cpuBusy + cpu.idle;
29
+ }
30
+ return { busy, total };
31
+ }
32
+ function osCpuTimes() {
33
+ return osCpus().map((c) => c.times);
34
+ }
35
+ function readCpuCores(getCpus = osCpuTimes) {
36
+ return getCpus().length;
37
+ }
38
+ var CpuBusyWindow = class {
39
+ constructor(windowMs = 5 * 6e4, getCpus = osCpuTimes, now = Date.now) {
40
+ this.windowMs = windowMs;
41
+ this.getCpus = getCpus;
42
+ this.now = now;
43
+ }
44
+ windowMs;
45
+ getCpus;
46
+ now;
47
+ samples = [];
48
+ sample() {
49
+ const at = this.now();
50
+ const totals = sumCpuTotals(this.getCpus());
51
+ this.samples.push({ at, ...totals });
52
+ while (this.samples.length > 2 && this.samples[1].at <= at - this.windowMs) {
53
+ this.samples.shift();
54
+ }
55
+ const first = this.samples[0];
56
+ const last = this.samples[this.samples.length - 1];
57
+ const deltaTotal = last.total - first.total;
58
+ if (this.samples.length < 2 || deltaTotal <= 0) return null;
59
+ const pct = (last.busy - first.busy) / deltaTotal * 100;
60
+ return Math.round(pct * 10) / 10;
61
+ }
62
+ };
63
+ function parseMeminfoSwap(meminfo) {
64
+ const field = (name) => {
65
+ const match = new RegExp(`^${name}:\\s+(\\d+)\\s+kB$`, "m").exec(meminfo);
66
+ if (!match) throw new Error(`readSwap: ${name} missing from /proc/meminfo`);
67
+ return Number(match[1]) * 1024;
68
+ };
69
+ const total = field("SwapTotal");
70
+ const free = field("SwapFree");
71
+ return { swap_total_bytes: total, swap_used_bytes: total - free };
72
+ }
73
+ function parsePageFileJson(json) {
74
+ const parsed = JSON.parse(json);
75
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
76
+ let total = 0;
77
+ let used = 0;
78
+ for (const row of rows) {
79
+ if (typeof row !== "object" || row === null) {
80
+ throw new Error("readSwap: Win32_PageFileUsage row is not an object");
81
+ }
82
+ const r = row;
83
+ if (typeof r.AllocatedBaseSize !== "number" || typeof r.CurrentUsage !== "number") {
84
+ throw new Error("readSwap: Win32_PageFileUsage row lacks AllocatedBaseSize/CurrentUsage");
85
+ }
86
+ total += r.AllocatedBaseSize * 1024 * 1024;
87
+ used += r.CurrentUsage * 1024 * 1024;
88
+ }
89
+ return { swap_total_bytes: total, swap_used_bytes: used };
90
+ }
91
+ var PAGEFILE_SCRIPT = "Get-CimInstance Win32_PageFileUsage | Select-Object AllocatedBaseSize,CurrentUsage | ConvertTo-Json -Compress";
92
+ function readSwap(deps = {}) {
93
+ const platform = deps.platform ?? osPlatform();
94
+ if (platform === "linux") {
95
+ return parseMeminfoSwap((deps.readFile ?? ((p) => readFileSync(p, "utf8")))("/proc/meminfo"));
96
+ }
97
+ if (platform === "win32") {
98
+ const run = deps.runPowerShell ?? ((script) => execFileSync(
99
+ "powershell.exe",
100
+ ["-NoProfile", "-NonInteractive", "-Command", script],
101
+ { encoding: "utf8", windowsHide: true }
102
+ ));
103
+ return parsePageFileJson(run(PAGEFILE_SCRIPT).trim() || "[]");
104
+ }
105
+ throw new Error(`readSwap: unsupported platform ${platform}`);
106
+ }
107
+ function createMachineVitalsCollector(deps) {
108
+ const getCpus = deps.getCpus ?? osCpuTimes;
109
+ const now = deps.now ?? Date.now;
110
+ const cpuWindow = new CpuBusyWindow(deps.cpuWindowMs ?? 5 * 6e4, getCpus, now);
111
+ return () => {
112
+ const disk = readDisk(deps.diskPath, deps.statfs);
113
+ const memory = readMemory(deps.getFree, deps.getTotal);
114
+ const swap = readSwap(deps.swap);
115
+ const cpu_busy_pct_5m = cpuWindow.sample();
116
+ return {
117
+ ...disk,
118
+ ...memory,
119
+ ...swap,
120
+ cpu_cores: readCpuCores(getCpus),
121
+ cpu_busy_pct_5m,
122
+ sampled_at: now()
123
+ };
124
+ };
125
+ }
126
+
127
+ // src/machine-daemon/autostart.ts
128
+ import { execFileSync as execFileSync2 } from "node:child_process";
129
+ var TASK_NAME = "danxbot-machine-daemon";
130
+ var UNIT_NAME = "danxbot-machine-daemon.service";
131
+ function runHidden(file, args) {
132
+ return execFileSync2(file, args, { encoding: "utf8", windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
133
+ }
134
+ function disableAutostart(deps) {
135
+ if (deps.platform === "win32") {
136
+ deps.run("schtasks", ["/Change", "/TN", TASK_NAME, "/DISABLE"]);
137
+ return;
138
+ }
139
+ if (deps.platform === "linux") {
140
+ deps.run("systemctl", ["--user", "disable", UNIT_NAME]);
141
+ return;
142
+ }
143
+ throw new Error(`no start-at-logon entry for platform ${deps.platform}`);
144
+ }
145
+
146
+ // src/machine-daemon/beat.ts
147
+ var BEAT_INTERVAL_MS = 6e4;
148
+ function stopOnRefusal(deps) {
149
+ const onDisk = deps.readBearer();
150
+ if (onDisk !== null && onDisk !== deps.config.bearer) {
151
+ deps.log("install-daemon replaced this machine's reporting bearer: this process is the old bundle, so it exits and the start entry runs the new one.");
152
+ return { reason: "replaced" };
153
+ }
154
+ deps.log("the dashboard refused this machine's reporting bearer (HTTP 401): re-run install-daemon. The daemon has stopped beating.");
155
+ return { reason: "refused" };
156
+ }
157
+ async function postAsMachine(config, fetchFn, route, payload) {
158
+ try {
159
+ const res = await fetchFn(`${config.dashboard_url.replace(/\/+$/, "")}${route}`, {
160
+ method: "POST",
161
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${config.bearer}` },
162
+ body: JSON.stringify({ machine_id: config.machine_id, ...payload })
163
+ });
164
+ return { status: res.status, error: res.status === 200 ? null : `HTTP ${res.status}`, text: await res.text() };
165
+ } catch (err) {
166
+ return { status: null, error: err instanceof Error ? err.message : String(err), text: "" };
167
+ }
168
+ }
169
+ async function sendBeat(deps, vitals) {
170
+ const { status, error } = await postAsMachine(deps.config, deps.fetchFn, "/api/machines/heartbeat", { vitals });
171
+ return { status, error };
172
+ }
173
+ async function runBeatLoop(deps) {
174
+ let ticks = 0;
175
+ for (; ; ) {
176
+ if (deps.maxTicks !== void 0 && ticks >= deps.maxTicks) return { reason: "ticks-exhausted" };
177
+ if (!deps.installed()) {
178
+ deps.log("the daemon's config is gone (uninstall-daemon removed it): stopping.");
179
+ return { reason: "uninstalled" };
180
+ }
181
+ ticks += 1;
182
+ const beatAt = deps.now();
183
+ let vitals = null;
184
+ let outcome;
185
+ try {
186
+ vitals = deps.collect();
187
+ outcome = await sendBeat(deps, vitals);
188
+ } catch (err) {
189
+ outcome = { status: null, error: `vitals: ${err instanceof Error ? err.message : String(err)}` };
190
+ }
191
+ const next = new Date(beatAt.getTime() + BEAT_INTERVAL_MS);
192
+ deps.writeStatus({
193
+ pid: deps.pid,
194
+ last_beat_at: beatAt.toISOString(),
195
+ last_beat_ok: outcome.status === 200,
196
+ last_error: outcome.error,
197
+ next_beat_at: next.toISOString()
198
+ });
199
+ if (outcome.status === 200) {
200
+ deps.log(`beat ok at ${beatAt.toISOString()}`);
201
+ } else if (outcome.status === 401) {
202
+ return stopOnRefusal(deps);
203
+ } else {
204
+ deps.log(`beat failed at ${beatAt.toISOString()}: ${outcome.error}; retrying on the next tick`);
205
+ }
206
+ if (deps.reporter.refused()) return stopOnRefusal(deps);
207
+ deps.reporter.tick(beatAt);
208
+ if (deps.maxTicks !== void 0 && ticks >= deps.maxTicks) return { reason: "ticks-exhausted" };
209
+ await deps.sleep(Math.max(0, next.getTime() - deps.now().getTime()));
210
+ }
211
+ }
212
+
213
+ // src/machine-toolkit/contract.ts
214
+ var CONTRACT_VERSION = 2;
215
+ var REQUIRED_METRICS = ["docker_data_disk_bytes", "unowned_docker_resource_count", "orphan_process_count"];
216
+ var HEALTH_ITEM_LISTS = ["unowned_docker_resources", "orphan_processes"];
217
+ var ContractViolation = class extends Error {
218
+ constructor(command, path, what) {
219
+ super(`${command} output does not match machine-toolkit contract v${CONTRACT_VERSION}: ${path} ${what}`);
220
+ this.name = "ContractViolation";
221
+ }
222
+ };
223
+ var isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
224
+ var join = (path, key) => path === "" ? key : `${path}.${key}`;
225
+ function objectAt(v, path, fail) {
226
+ if (!isObject(v)) return fail(path, "must be an object");
227
+ return v;
228
+ }
229
+ function exactKeys(o, path, keys, fail) {
230
+ for (const k of keys) if (!(k in o)) fail(join(path, k), "is required");
231
+ for (const k of Object.keys(o)) if (!keys.includes(k)) fail(join(path, k), "is not part of the contract");
232
+ }
233
+ function stringAt(o, key, path, fail) {
234
+ const v = o[key];
235
+ if (typeof v !== "string" || v === "") return fail(join(path, key), "must be a non-empty string");
236
+ return v;
237
+ }
238
+ function listAt(v, path, fail) {
239
+ if (!Array.isArray(v)) return fail(path, "must be a list");
240
+ return v;
241
+ }
242
+ function checkItem(v, path, extra, fail) {
243
+ const o = objectAt(v, path, fail);
244
+ exactKeys(o, path, ["id", "detail", ...extra], fail);
245
+ stringAt(o, "id", path, fail);
246
+ if (typeof o.detail !== "string") fail(join(path, "detail"), "must be a string");
247
+ for (const k of extra) stringAt(o, k, path, fail);
248
+ }
249
+ function checkItems(v, path, extra, fail) {
250
+ listAt(v, path, fail).forEach((item, i) => checkItem(item, `${path}[${i}]`, extra, fail));
251
+ }
252
+ function checkMetric(v, path, fail) {
253
+ const o = objectAt(v, path, fail);
254
+ exactKeys(o, path, ["value", "reason", "source", "age_seconds"], fail);
255
+ if (o.value === null) {
256
+ if (typeof o.reason !== "string" || o.reason === "") fail(`${path}.reason`, "must say why the value is null");
257
+ } else {
258
+ if (typeof o.value !== "number" || !Number.isFinite(o.value) || o.value < 0) fail(`${path}.value`, "must be null or a finite number of at least 0");
259
+ if (o.reason !== null) fail(`${path}.reason`, "must be null when the value was read");
260
+ }
261
+ if (o.source !== "fresh" && o.source !== "cached") fail(`${path}.source`, "must be 'fresh' or 'cached'");
262
+ if (typeof o.age_seconds !== "number" || !Number.isFinite(o.age_seconds) || o.age_seconds < 0) fail(`${path}.age_seconds`, "must be a number of at least 0");
263
+ if (o.source === "fresh" && o.age_seconds !== 0) fail(`${path}.age_seconds`, "must be 0 for a fresh reading");
264
+ }
265
+ function checkEnvelope(o, fail) {
266
+ if (o.contract_version !== CONTRACT_VERSION) fail("contract_version", `must be ${CONTRACT_VERSION}`);
267
+ stringAt(o, "toolkit_version", "", fail);
268
+ stringAt(o, "profile", "", fail);
269
+ const at = stringAt(o, "measured_at", "", fail);
270
+ if (Number.isNaN(new Date(at).getTime())) fail("measured_at", "must be an ISO time");
271
+ }
272
+ var ENVELOPE_KEYS = ["contract_version", "toolkit_version", "profile", "measured_at"];
273
+ var VALIDATORS = {
274
+ health(o, fail) {
275
+ exactKeys(o, "", [...ENVELOPE_KEYS, "metrics", "items"], fail);
276
+ checkEnvelope(o, fail);
277
+ const metrics = objectAt(o.metrics, "metrics", fail);
278
+ for (const name of REQUIRED_METRICS) if (!(name in metrics)) fail(`metrics.${name}`, "is required");
279
+ for (const [name, metric] of Object.entries(metrics)) checkMetric(metric, `metrics.${name}`, fail);
280
+ const items = objectAt(o.items, "items", fail);
281
+ exactKeys(items, "items", HEALTH_ITEM_LISTS, fail);
282
+ for (const list of HEALTH_ITEM_LISTS) checkItems(items[list], `items.${list}`, [], fail);
283
+ },
284
+ sweep(o, fail) {
285
+ exactKeys(o, "", [...ENVELOPE_KEYS, "dry_run", "kinds"], fail);
286
+ checkEnvelope(o, fail);
287
+ if (typeof o.dry_run !== "boolean") fail("dry_run", "must be true or false");
288
+ listAt(o.kinds, "kinds", fail).forEach((k, i) => {
289
+ const path = `kinds[${i}]`;
290
+ const kind = objectAt(k, path, fail);
291
+ exactKeys(kind, path, ["kind", "removed", "kept", "failed", "flagged"], fail);
292
+ stringAt(kind, "kind", path, fail);
293
+ checkItems(kind.removed, `${path}.removed`, [], fail);
294
+ checkItems(kind.kept, `${path}.kept`, ["reason"], fail);
295
+ checkItems(kind.failed, `${path}.failed`, ["error"], fail);
296
+ checkItems(kind.flagged, `${path}.flagged`, ["reason"], fail);
297
+ });
298
+ },
299
+ maintain(o, fail) {
300
+ exactKeys(o, "", [...ENVELOPE_KEYS, "steps"], fail);
301
+ checkEnvelope(o, fail);
302
+ listAt(o.steps, "steps", fail).forEach((s, i) => {
303
+ const path = `steps[${i}]`;
304
+ const step = objectAt(s, path, fail);
305
+ exactKeys(step, path, ["name", "status", "detail"], fail);
306
+ stringAt(step, "name", path, fail);
307
+ if (step.status !== "done" && step.status !== "skipped" && step.status !== "failed") fail(`${path}.status`, "must be 'done', 'skipped' or 'failed'");
308
+ if (typeof step.detail !== "string") fail(join(path, "detail"), "must be a string");
309
+ });
310
+ },
311
+ "label-check"(o, fail) {
312
+ exactKeys(o, "", [...ENVELOPE_KEYS, "unowned", "malformed"], fail);
313
+ checkEnvelope(o, fail);
314
+ checkItems(o.unowned, "unowned", ["reason"], fail);
315
+ checkItems(o.malformed, "malformed", ["reason"], fail);
316
+ }
317
+ };
318
+ function assertContract(command, report) {
319
+ const fail = (path, what) => {
320
+ throw new ContractViolation(command, path === "" ? "(document)" : path, what);
321
+ };
322
+ VALIDATORS[command](objectAt(report, "", fail), fail);
323
+ }
324
+
325
+ // src/machine-toolkit/deadline.ts
326
+ var realScheduler = (fire, ms) => {
327
+ const timer = setTimeout(fire, ms);
328
+ return () => clearTimeout(timer);
329
+ };
330
+
331
+ // src/machine-toolkit/errors.ts
332
+ function errorCode(err) {
333
+ const failure = err;
334
+ if (typeof failure?.code === "string" || typeof failure?.code === "number") return String(failure.code);
335
+ return failure?.killed === true ? "timed out" : "unexpected error";
336
+ }
337
+ function errMessage(err) {
338
+ return err instanceof Error ? err.message : String(err);
339
+ }
340
+
341
+ // src/machine-daemon/report.ts
342
+ var REPORT_INTERVAL_MS = 5 * 60 * 1e3;
343
+ var REPORT_TICK_TOLERANCE_MS = 1e3;
344
+ var REPORT_TIMEOUT_MS = 3e4;
345
+ function runHealthChild(opts) {
346
+ return new Promise((resolve) => {
347
+ const timeoutMs = opts.timeoutMs ?? REPORT_TIMEOUT_MS;
348
+ const schedule = opts.schedule ?? realScheduler;
349
+ let stdout = "";
350
+ let settled = false;
351
+ let cancel = () => {
352
+ };
353
+ const finish = (run) => {
354
+ if (settled) return;
355
+ settled = true;
356
+ cancel();
357
+ resolve(run);
358
+ };
359
+ const child = opts.spawnFn(opts.nodePath, [opts.toolkitPath, "health", "--json"], { windowsHide: true, stdio: ["ignore", "pipe", "ignore"] });
360
+ cancel = schedule(() => {
361
+ child.kill();
362
+ finish({ kind: "timed-out" });
363
+ }, timeoutMs);
364
+ child.stdout?.on("data", (chunk) => {
365
+ stdout += chunk.toString();
366
+ });
367
+ child.on("error", (err) => finish({ kind: "failed", reason: errorCode(err) }));
368
+ child.on("close", (code) => finish({ kind: "exited", code, stdout }));
369
+ });
370
+ }
371
+ function reportPayload(run) {
372
+ if (run.kind === "timed-out") return { report_error: `health did not finish within ${REPORT_TIMEOUT_MS / 1e3} s: the machine is saturated` };
373
+ if (run.kind === "failed") return { report_error: `health could not start (${run.reason})` };
374
+ if (run.code !== 0 && run.code !== 2) return { report_error: `health exited ${run.code ?? "on a signal"}` };
375
+ let parsed;
376
+ try {
377
+ parsed = JSON.parse(run.stdout);
378
+ } catch {
379
+ return { report_error: "health printed output that is not JSON" };
380
+ }
381
+ try {
382
+ assertContract("health", parsed);
383
+ } catch (err) {
384
+ if (err instanceof ContractViolation) return { report_error: err.message };
385
+ throw err;
386
+ }
387
+ return { report: parsed };
388
+ }
389
+ function storedKind(text) {
390
+ try {
391
+ const { stored } = JSON.parse(text);
392
+ return stored === "report" || stored === "report_error" ? stored : null;
393
+ } catch {
394
+ return null;
395
+ }
396
+ }
397
+ function createReporter(deps) {
398
+ let lastStartMs = null;
399
+ let running = false;
400
+ let refused = false;
401
+ const run = async (startedAt) => {
402
+ const payload = reportPayload(await deps.runHealth());
403
+ const reportError = "report_error" in payload ? payload.report_error : null;
404
+ const sent = await postAsMachine(deps.config, deps.fetchFn, "/api/machines/report", payload);
405
+ let outcome;
406
+ let error = reportError ?? sent.error;
407
+ if (sent.status === 200) {
408
+ const stored = storedKind(sent.text);
409
+ if (stored === null) {
410
+ outcome = "failed";
411
+ error = "the dashboard answered 200 without saying what it stored";
412
+ deps.log(`report failed at ${startedAt.toISOString()}: ${error}`);
413
+ } else if (stored === "report_error" && reportError === null) {
414
+ outcome = "report-refused";
415
+ error = "the dashboard stored this report as a report error";
416
+ deps.log(`report refused at ${startedAt.toISOString()}: ${error}`);
417
+ } else {
418
+ outcome = reportError === null ? "sent" : "error-sent";
419
+ deps.log(reportError === null ? `report sent at ${startedAt.toISOString()}` : `report error sent at ${startedAt.toISOString()}: ${reportError}`);
420
+ }
421
+ } else if (sent.status === 429) {
422
+ outcome = "throttled";
423
+ deps.log("the dashboard refused this report as too soon (HTTP 429): the next due run tries again");
424
+ } else if (sent.status === 401) {
425
+ outcome = "refused";
426
+ refused = true;
427
+ deps.log("the dashboard refused this machine's reporting bearer on a report (HTTP 401)");
428
+ } else {
429
+ outcome = "failed";
430
+ deps.log(`report failed at ${startedAt.toISOString()}: ${sent.error}; the next due run tries again`);
431
+ }
432
+ if (deps.installed()) deps.writeStatus({ last_report_at: startedAt.toISOString(), last_report_outcome: outcome, last_report_error: error });
433
+ };
434
+ return {
435
+ tick(startedAt) {
436
+ if (lastStartMs !== null && startedAt.getTime() - lastStartMs < REPORT_INTERVAL_MS - REPORT_TICK_TOLERANCE_MS) return null;
437
+ if (running) {
438
+ deps.log("the last health report is still running: skipping this one");
439
+ return null;
440
+ }
441
+ lastStartMs = startedAt.getTime();
442
+ running = true;
443
+ return run(startedAt).catch((err) => deps.log(`report failed at ${startedAt.toISOString()}: ${errMessage(err)}; the next due run tries again`)).finally(() => {
444
+ running = false;
445
+ });
446
+ },
447
+ refused: () => refused
448
+ };
449
+ }
450
+
451
+ // src/machine-daemon/state.ts
452
+ import { chmodSync, existsSync, mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "node:fs";
453
+ import { homedir } from "node:os";
454
+ import { dirname, join as join2 } from "node:path";
455
+ var MACHINE_DAEMON_DIR_NAME = ".danxbot";
456
+ function createStatusWriter(write) {
457
+ let current = {
458
+ pid: process.pid,
459
+ last_beat_at: null,
460
+ last_beat_ok: null,
461
+ last_error: null,
462
+ next_beat_at: null,
463
+ last_report_at: null,
464
+ last_report_outcome: null,
465
+ last_report_error: null
466
+ };
467
+ const merge = (fields) => {
468
+ current = { ...current, ...fields };
469
+ write(current);
470
+ };
471
+ return { beat: merge, report: merge };
472
+ }
473
+ function danxbotHome() {
474
+ return join2(homedir(), MACHINE_DAEMON_DIR_NAME);
475
+ }
476
+ function daemonConfigPath(home = danxbotHome()) {
477
+ return join2(home, "machine-daemon.json");
478
+ }
479
+ function daemonStatusPath(home = danxbotHome()) {
480
+ return join2(home, "machine-daemon-status.json");
481
+ }
482
+ function daemonBundleDir(home = danxbotHome()) {
483
+ return join2(home, "machine-daemon");
484
+ }
485
+ function toolkitBundlePath(home = danxbotHome()) {
486
+ return join2(daemonBundleDir(home), "machine-toolkit.js");
487
+ }
488
+ function writeOwnerOnlyJson(file, value) {
489
+ mkdirSync(dirname(file), { recursive: true });
490
+ const tmp = `${file}.${process.pid}.tmp`;
491
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}
492
+ `, { encoding: "utf8", mode: 384 });
493
+ if (process.platform !== "win32") chmodSync(tmp, 384);
494
+ renameSync(tmp, file);
495
+ }
496
+ function readJsonIfPresent(file) {
497
+ if (!existsSync(file)) return null;
498
+ return JSON.parse(readFileSync2(file, "utf8"));
499
+ }
500
+
501
+ // src/machine-daemon/run.ts
502
+ function finishBeatStop(stop, disable) {
503
+ if (stop.reason === "refused") disable();
504
+ return 0;
505
+ }
506
+ function readBearerOnDisk() {
507
+ try {
508
+ return readJsonIfPresent(daemonConfigPath())?.bearer ?? null;
509
+ } catch {
510
+ return null;
511
+ }
512
+ }
513
+ async function runDaemonProcess() {
514
+ const config = readJsonIfPresent(daemonConfigPath());
515
+ if (config === null) {
516
+ console.error("[machine-daemon] no config: run npx @50arms/platform-mcp install-daemon first");
517
+ return 2;
518
+ }
519
+ const fetchFn = fetch;
520
+ const now = () => /* @__PURE__ */ new Date();
521
+ const log = (line) => console.log(`[machine-daemon] ${line}`);
522
+ const status = createStatusWriter((value) => writeOwnerOnlyJson(daemonStatusPath(), value));
523
+ const stop = await runBeatLoop({
524
+ config,
525
+ collect: createMachineVitalsCollector({ diskPath: homedir2() }),
526
+ fetchFn,
527
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
528
+ now,
529
+ writeStatus: status.beat,
530
+ log,
531
+ pid: process.pid,
532
+ installed: () => existsSync2(daemonConfigPath()),
533
+ readBearer: readBearerOnDisk,
534
+ reporter: createReporter({
535
+ config,
536
+ runHealth: () => runHealthChild({ spawnFn: spawn, nodePath: process.execPath, toolkitPath: toolkitBundlePath() }),
537
+ fetchFn,
538
+ writeStatus: status.report,
539
+ log,
540
+ installed: () => existsSync2(daemonConfigPath())
541
+ })
542
+ });
543
+ return finishBeatStop(stop, () => disableAutostart({ platform: process.platform, run: runHidden }));
544
+ }
545
+
546
+ // src/machine-daemon/entry.ts
547
+ runDaemonProcess().then(
548
+ (code) => process.exit(code),
549
+ (err) => {
550
+ console.error(`[machine-daemon] fatal: ${err instanceof Error ? err.message : String(err)}`);
551
+ process.exit(1);
552
+ }
553
+ );