agentbox-app 0.1.13 → 0.1.16
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/README.md +24 -9
- package/bin/agentbox.mjs +23 -1
- package/briefs/message-rules.md +36 -8
- package/briefs/worker.md +46 -23
- package/main/account-discovery.mjs +122 -30
- package/main/account-tooling.mjs +29 -1
- package/main/account-usage.mjs +46 -0
- package/main/agent-sessions.mjs +282 -268
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +12 -2
- package/main/answer-modes.mjs +103 -37
- package/main/approval-prompt-core.mjs +197 -0
- package/main/approval-prompt-server.mjs +4 -138
- package/main/approvals.mjs +101 -297
- package/main/artifact-path.mjs +47 -18
- package/main/claude-input.mjs +48 -4
- package/main/claude-plan.mjs +48 -0
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +18 -40
- package/main/cloud/account.mjs +179 -0
- package/main/cloud/aws.mjs +127 -0
- package/main/cloud/browser-return.mjs +58 -0
- package/main/cloud/clients.mjs +15 -0
- package/main/cloud/codex-cloud.mjs +53 -0
- package/main/cloud/config.mjs +107 -0
- package/main/cloud/docs.mjs +219 -0
- package/main/cloud/exec.mjs +27 -0
- package/main/cloud/host.mjs +165 -0
- package/main/cloud/kit.mjs +80 -0
- package/main/cloud/logins.mjs +543 -0
- package/main/cloud/managed.mjs +115 -0
- package/main/cloud/marker.mjs +104 -0
- package/main/cloud/org.mjs +53 -0
- package/main/cloud/reuse.mjs +208 -0
- package/main/cloud/run-cli.mjs +177 -0
- package/main/cloud/run.mjs +1934 -0
- package/main/cloud/usage.mjs +76 -0
- package/main/cloud-permission.mjs +51 -0
- package/main/codex-account.mjs +78 -0
- package/main/codex-app-server.mjs +276 -409
- package/main/codex-approvals.mjs +148 -433
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +212 -507
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +71 -3
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +17 -3
- package/main/codex.mjs +95 -363
- package/main/config.mjs +11 -1
- package/main/corner-tag.mjs +340 -19
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +232 -282
- package/main/engine-setup.mjs +21 -9
- package/main/feedback.mjs +3 -25
- package/main/folder-pictures.mjs +2 -2
- package/main/freeze-recorder-thread.mjs +86 -0
- package/main/freeze-recorder.mjs +146 -0
- package/main/fresh-user.mjs +20 -9
- package/main/git-change-offthread.mjs +17 -3
- package/main/git-change-thread.mjs +5 -2
- package/main/harnesses/claude.mjs +32 -11
- package/main/harnesses/codex.mjs +9 -2
- package/main/importable-threads.mjs +64 -6
- package/main/ipc.mjs +362 -111
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +199 -84
- package/main/memory-gate-history.mjs +36 -10
- package/main/memory-gate-server.mjs +62 -0
- package/main/memory-gate.mjs +14 -1
- package/main/menu.mjs +4 -2
- package/main/message-priority.mjs +2 -2
- package/main/mic.mjs +38 -0
- package/main/native/liquid-glass/build/liquid-glass.node +0 -0
- package/main/new-chats-watch.mjs +110 -0
- package/main/new-user-window.mjs +25 -0
- package/main/notify.mjs +24 -8
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +29 -9
- package/main/phone-uploads.mjs +476 -0
- package/main/picture-shape.mjs +114 -0
- package/main/quit-sequence.mjs +143 -0
- package/main/refresh-agent-models.mjs +15 -2
- package/main/relay-client.mjs +69 -1
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +12 -41
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +45 -3
- package/main/settings.mjs +220 -44
- package/main/shell-path.mjs +185 -37
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +115 -19
- package/main/source-updater.mjs +18 -10
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/store/home.mjs +87 -232
- package/main/store/paths.mjs +11 -0
- package/main/store/teammates-files.mjs +99 -0
- package/main/store/work-items.mjs +231 -13
- package/main/store.mjs +226 -52
- package/main/supervisor.mjs +3103 -341
- package/main/task-folders.mjs +430 -30
- package/main/task-terminals.mjs +110 -34
- package/main/team/index.mjs +114 -10
- package/main/team/memory-cloud.mjs +183 -1
- package/main/team/room-hours.mjs +48 -0
- package/main/team/room-setup.mjs +31 -0
- package/main/team/rooms-memory.mjs +454 -0
- package/main/team/rooms-shape.mjs +42 -0
- package/main/team/rooms-supabase.mjs +324 -0
- package/main/team/rooms.mjs +575 -0
- package/main/team/session.mjs +96 -26
- package/main/team/supabase-backend.mjs +180 -0
- package/main/team/sync.mjs +109 -1
- package/main/teammates.mjs +580 -0
- package/main/terminal-host.mjs +143 -0
- package/main/terminal-hosts.mjs +261 -0
- package/main/transcript-search.mjs +80 -0
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +90 -219
- package/mcp/core/documents.mjs +3 -1
- package/mcp/core/repeats.mjs +88 -0
- package/mcp/core/teammates.mjs +222 -0
- package/mcp/core/work.mjs +76 -4
- package/mcp/stdio.mjs +24 -11
- package/mcp/tools.mjs +116 -8
- package/package.json +21 -5
- package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
- package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
- package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
- package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
- package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
- package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
- package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
- package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
- package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
- package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
- package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
- package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
- package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
- package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
- package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
- package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
- package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
- package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
- package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
- package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
- package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
- package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
- package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
- package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
- package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
- package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
- package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
- package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
- package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
- package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
- package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
- package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
- package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
- package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
- package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
- package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
- package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
- package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
- package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
- package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
- package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
- package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
- package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
- package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
- package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
- package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
- package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
- package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
- package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
- package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
- package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
- package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
- package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
- package/renderer/dist/corner-tag.html +3 -3
- package/renderer/dist/index.html +3 -3
- package/scripts/after-install.mjs +30 -8
- package/scripts/cloud-shim.pl +57 -0
- package/shared/account-names.d.mts +6 -0
- package/shared/account-names.mjs +15 -0
- package/shared/agent-hours.mjs +136 -0
- package/shared/agent-import.mjs +60 -34
- package/shared/agents.mjs +126 -522
- package/shared/analytics-events.mjs +12 -0
- package/shared/approval-card.mjs +36 -0
- package/shared/bridge-map.mjs +27 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-usage.d.mts +1 -0
- package/shared/claude-usage.mjs +42 -1
- package/shared/cloud-hand-back.d.mts +7 -0
- package/shared/cloud-hand-back.mjs +20 -0
- package/shared/cloud-kit.d.mts +219 -0
- package/shared/cloud-kit.mjs +341 -0
- package/shared/cloud-login-name.mjs +4 -0
- package/shared/cloud-start.mjs +17 -0
- package/shared/cloud-versions.mjs +23 -0
- package/shared/codex-import.mjs +2 -2
- package/shared/codex-modes.d.mts +22 -9
- package/shared/codex-modes.mjs +67 -11
- package/shared/codex-usage.mjs +108 -2
- package/shared/corner-tag.d.mts +23 -1
- package/shared/corner-tag.mjs +284 -29
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +70 -309
- package/shared/feedback.d.mts +7 -0
- package/shared/feedback.mjs +25 -0
- package/shared/first-run-practice.mjs +17 -24
- package/shared/harness-definitions.mjs +8 -0
- package/shared/instruction-defaults.json +2 -2
- package/shared/key-caps.d.mts +14 -0
- package/shared/key-caps.mjs +68 -0
- package/shared/message-edits.d.mts +7 -0
- package/shared/message-edits.mjs +35 -0
- package/shared/move-thread.d.mts +24 -0
- package/shared/move-thread.mjs +112 -0
- package/shared/named-threads.mjs +72 -0
- package/shared/not-shipped.d.mts +22 -0
- package/shared/not-shipped.mjs +89 -0
- package/shared/notify-rules.d.mts +6 -0
- package/shared/notify-rules.mjs +36 -8
- package/shared/phone-files.mjs +28 -0
- package/shared/phone-settings.mjs +118 -0
- package/shared/phone-snapshot.d.mts +16 -0
- package/shared/phone-snapshot.mjs +348 -0
- package/shared/picture-formats.d.mts +1 -0
- package/shared/picture-formats.mjs +13 -0
- package/shared/rank.d.mts +2 -1
- package/shared/rank.mjs +32 -19
- package/shared/repeats.d.mts +8 -0
- package/shared/repeats.mjs +33 -2
- package/shared/room-cards.mjs +167 -0
- package/shared/room-links.d.mts +4 -0
- package/shared/room-links.mjs +21 -0
- package/shared/runs-on.d.mts +3 -3
- package/shared/runs-on.mjs +8 -5
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +14 -6
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +443 -40
- package/shared/team-rules.mjs +9 -2
- package/shared/teammates.d.mts +19 -0
- package/shared/teammates.mjs +756 -0
- package/shared/terminal-focus.d.mts +1 -0
- package/shared/terminal-focus.mjs +13 -0
- package/shared/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +9 -0
- package/shared/usage.mjs +49 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/work-items.mjs +127 -30
- package/shared/work-lines.mjs +92 -244
- package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
- package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
- package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
- package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
- package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// STAGING A FILE FOR A REPLY BOX, before it is sent: the bytes of a paste, or
|
|
2
|
+
// a copy of a picture dropped in, written into the account's .staging folder,
|
|
3
|
+
// which the window may draw from (main/ipc.mjs `zero:stage-attachment` says
|
|
4
|
+
// why). A picture the window cannot draw is turned into one that it can here,
|
|
5
|
+
// so the box, the message and the teammate all get a picture.
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { turnedName } from '../shared/picture-formats.mjs';
|
|
10
|
+
|
|
11
|
+
const WEEK = 7 * 24 * 60 * 60 * 1000;
|
|
12
|
+
const SIPS_MS = 30_000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Answers `{ path }`, and `name` too when the file was turned into another
|
|
16
|
+
* format, which is the name it goes on under.
|
|
17
|
+
*/
|
|
18
|
+
export async function stageAttachment(dir, { name, dataBase64, srcPath } = {}, { turn = turnIntoPicture } = {}) {
|
|
19
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
20
|
+
// Sweep on the way in rather than on a timer: an abandoned paste is only
|
|
21
|
+
// ever discovered by the next one, and a week is longer than any draft has
|
|
22
|
+
// been left open. Failure to sweep must never fail the paste.
|
|
23
|
+
try {
|
|
24
|
+
const week = Date.now() - WEEK;
|
|
25
|
+
for (const f of fs.readdirSync(dir)) {
|
|
26
|
+
const p = path.join(dir, f);
|
|
27
|
+
try { if (fs.statSync(p).mtimeMs < week) fs.unlinkSync(p); } catch { /* leave it */ }
|
|
28
|
+
}
|
|
29
|
+
} catch { /* a sweep that cannot run is not a paste that failed */ }
|
|
30
|
+
const safe = String(name ?? 'pasted.png').replace(/[^\w.-]+/g, '-').slice(0, 80);
|
|
31
|
+
const abs = path.join(dir, `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}-${safe}`);
|
|
32
|
+
// A path is a drag, bytes are a paste. Already inside the folder we serve
|
|
33
|
+
// from is left alone: copying it would only make a second file to sweep.
|
|
34
|
+
if (typeof srcPath === 'string' && srcPath) {
|
|
35
|
+
if (path.resolve(srcPath) === path.resolve(path.join(dir, path.basename(srcPath)))) return { path: srcPath };
|
|
36
|
+
fs.copyFileSync(srcPath, abs);
|
|
37
|
+
} else {
|
|
38
|
+
fs.writeFileSync(abs, Buffer.from(String(dataBase64 ?? ''), 'base64'));
|
|
39
|
+
}
|
|
40
|
+
const as = turnedName(name ?? safe);
|
|
41
|
+
if (!as) return { path: abs };
|
|
42
|
+
// One that cannot be turned goes as the file it is, which is how every TIFF went before.
|
|
43
|
+
const drawn = await Promise.resolve().then(() => turn(abs)).catch(() => null);
|
|
44
|
+
if (!drawn) return { path: abs };
|
|
45
|
+
try { fs.unlinkSync(abs); } catch { /* swept within the week */ }
|
|
46
|
+
return { path: drawn, name: as };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Turns a TIFF or HEIC into a PNG or JPEG beside it with the Mac's own sips,
|
|
51
|
+
* and answers the new file's path. Null off a Mac, for any other file, or
|
|
52
|
+
* when sips fails or writes nothing.
|
|
53
|
+
*/
|
|
54
|
+
export async function turnIntoPicture(file, { platform = process.platform, run = sips } = {}) {
|
|
55
|
+
const as = turnedName(path.basename(file));
|
|
56
|
+
if (!as || platform !== 'darwin') return null;
|
|
57
|
+
const out = path.join(path.dirname(file), as);
|
|
58
|
+
try {
|
|
59
|
+
await run(['-s', 'format', as.endsWith('.png') ? 'png' : 'jpeg', file, '--out', out]);
|
|
60
|
+
return fs.statSync(out).size > 0 ? out : null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sips = (args) => new Promise((resolve, reject) => {
|
|
67
|
+
execFile('/usr/bin/sips', args, { timeout: SIPS_MS }, (err) => (err ? reject(err) : resolve()));
|
|
68
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// WHICH REQUEST HELD THE APP. A spinning wheel on a Mac is the main process
|
|
2
|
+
// not answering, and nothing used to record what it was doing instead. This
|
|
3
|
+
// times every request the window makes and keeps the longest freeze; the
|
|
4
|
+
// memory log writes both into its minute line (main/memory-log.mjs).
|
|
5
|
+
//
|
|
6
|
+
// Only the synchronous part of a handler is timed. That is the part that holds
|
|
7
|
+
// the process: once a handler waits on a promise the window is free again.
|
|
8
|
+
import { monitorEventLoopDelay, performance } from 'node:perf_hooks';
|
|
9
|
+
|
|
10
|
+
export const SLOW_MS = 100;
|
|
11
|
+
|
|
12
|
+
export function stallWatch({ clock = () => performance.now(), loop = monitorEventLoopDelay({ resolution: 20 }) } = {}) {
|
|
13
|
+
loop.enable();
|
|
14
|
+
let slow = new Map();
|
|
15
|
+
let freezes = [];
|
|
16
|
+
return {
|
|
17
|
+
/** A freeze main/freeze-recorder.mjs caught, with the function that held it. */
|
|
18
|
+
noteFreeze({ ms, where }) {
|
|
19
|
+
freezes.push({ ms, where });
|
|
20
|
+
},
|
|
21
|
+
time(channel, fn) {
|
|
22
|
+
return (...args) => {
|
|
23
|
+
const start = clock();
|
|
24
|
+
try {
|
|
25
|
+
return fn(...args);
|
|
26
|
+
} finally {
|
|
27
|
+
const ms = Math.round(clock() - start);
|
|
28
|
+
if (ms >= SLOW_MS) {
|
|
29
|
+
const seen = slow.get(channel) ?? { channel, times: 0, maxMs: 0 };
|
|
30
|
+
seen.times += 1;
|
|
31
|
+
seen.maxMs = Math.max(seen.maxMs, ms);
|
|
32
|
+
slow.set(channel, seen);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
/** The minute so far, and a fresh start. */
|
|
38
|
+
take() {
|
|
39
|
+
const out = {
|
|
40
|
+
longestStallMs: Math.round(loop.max / 1e6),
|
|
41
|
+
slowRequests: [...slow.values()].sort((a, b) => b.maxMs - a.maxMs).slice(0, 5),
|
|
42
|
+
...(freezes.length ? { freezes: freezes.sort((a, b) => b.ms - a.ms).slice(0, 5) } : {}),
|
|
43
|
+
};
|
|
44
|
+
loop.reset();
|
|
45
|
+
slow = new Map();
|
|
46
|
+
freezes = [];
|
|
47
|
+
return out;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
package/main/store/home.mjs
CHANGED
|
@@ -1,40 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// The app's own home, so nothing Agentbox generates lands in the folder a
|
|
2
|
+
// worker runs in. Claude Code keeps per-project state under
|
|
3
|
+
// `~/.claude/projects/<encoded path>/` and Codex under `~/.codex`; this is the
|
|
4
|
+
// same convention. A product with no code repo makes its docs folder the
|
|
5
|
+
// worker's cwd (main/supervisor.mjs), so without this our records sat there.
|
|
3
6
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
+
// Our records move: the ledger, session traces, run records, repeat rules and
|
|
8
|
+
// parked writes. What describes the folder stays in it (`project.json`,
|
|
9
|
+
// `index.json` and `.lock`), so the folder still opens when copied to another
|
|
10
|
+
// Mac.
|
|
7
11
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// edit history to `~/.claude/file-history`. Codex does the same under `~/.codex`.
|
|
12
|
-
// Agentbox did the opposite: a product with no code repo registered makes the
|
|
13
|
-
// product docs directory the worker's own cwd (main/supervisor.mjs), which is
|
|
14
|
-
// every new user's first product, and our ledger, session traces, run records
|
|
15
|
-
// and repeat rules all sat in it.
|
|
16
|
-
//
|
|
17
|
-
// So this is the same convention, one name over: a dot-folder in her home
|
|
18
|
-
// named after this app. On Linux a fresh install does not get that dot-folder,
|
|
19
|
-
// and it does not get a visible folder of the app's name in $HOME. Application
|
|
20
|
-
// data belongs in $XDG_DATA_HOME (see defaultStoreRoot). An existing dot-folder
|
|
21
|
-
// is still read, so a store already on disk is not orphaned.
|
|
22
|
-
//
|
|
23
|
-
// WHAT MOVES AND WHAT DOES NOT, because the line is not "everything of ours".
|
|
24
|
-
// The app's own RECORDS move: the work item ledger, session traces, run
|
|
25
|
-
// records, repeat rules, parked writes. What DESCRIBES THE FOLDER ITSELF stays
|
|
26
|
-
// in it: `project.json` says this directory is a product at all, `index.json`
|
|
27
|
-
// catalogs the founder's own documents by their paths inside it, and `.lock`
|
|
28
|
-
// guards that catalog. A folder you could copy to another Mac and still open is
|
|
29
|
-
// worth more than four fewer files, and none of those three is what a worker
|
|
30
|
-
// scribbles.
|
|
31
|
-
//
|
|
32
|
-
// WHAT THIS DOES NOT DO, said out loud so nobody reads it as more than it is.
|
|
33
|
-
// It does not make the ledger unforgeable. A worker with Bash reaches that
|
|
34
|
-
// folder exactly as easily as it reaches its own cwd. Claude Code has the
|
|
35
|
-
// identical property and does not pretend otherwise. This moves our files out
|
|
36
|
-
// of her folder; the grant that used to ride on the ledger is dealt with
|
|
37
|
-
// separately, by not keeping it on the ledger at all (main/answer-modes.mjs).
|
|
12
|
+
// This does not protect the ledger: a worker with Bash reaches this folder as
|
|
13
|
+
// easily as its own cwd. The grant that used to sit on the ledger is handled in
|
|
14
|
+
// main/answer-modes.mjs.
|
|
38
15
|
|
|
39
16
|
import fs from 'node:fs';
|
|
40
17
|
import os from 'node:os';
|
|
@@ -45,18 +22,10 @@ import { Name, WAS, envNames, nameSlug, readEnv } from '../../shared/product-nam
|
|
|
45
22
|
const isDir = (dir) => { try { return fs.statSync(dir).isDirectory(); } catch { return false; } };
|
|
46
23
|
|
|
47
24
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* the root whichever spelling it is given. What is NOT in this repo does not:
|
|
53
|
-
* the store MCP server of an older install, her own launchd jobs, a script she
|
|
54
|
-
* wrote. Those read the one variable they were written against, and a worker
|
|
55
|
-
* whose store root is missing writes its whole session to nowhere, which is the
|
|
56
|
-
* 2026-08-05 failure exactly.
|
|
57
|
-
*
|
|
58
|
-
* So both are exported. It costs one string per spawn and it makes the rename
|
|
59
|
-
* invisible to everything outside this checkout.
|
|
25
|
+
* The store root under every env name this app has had, for every process we
|
|
26
|
+
* start. Code outside this repo (an older install's MCP server, a launchd job,
|
|
27
|
+
* a user's own script) reads only the name it was written against, and a
|
|
28
|
+
* worker with no store root writes its session nowhere.
|
|
60
29
|
*/
|
|
61
30
|
export function storeRootEnv(storeRoot) {
|
|
62
31
|
const env = {};
|
|
@@ -64,18 +33,10 @@ export function storeRootEnv(storeRoot) {
|
|
|
64
33
|
return env;
|
|
65
34
|
}
|
|
66
35
|
|
|
67
|
-
// Where a config that names no store keeps the inbox.
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// Linux: $XDG_DATA_HOME/<slug>, or ~/.local/share/<slug> when the variable is
|
|
72
|
-
// unset, blank, or not an absolute path. A visible directory in $HOME is not
|
|
73
|
-
// where an application writes. Measured 2026-10-07: a config that named no
|
|
74
|
-
// store created one.
|
|
75
|
-
//
|
|
76
|
-
// The home and the environment are arguments so a test can ask about another
|
|
77
|
-
// machine. A caller that passes a stand-in home must pass an environment that
|
|
78
|
-
// belongs to it. This function does not decide that; it uses the env it is given.
|
|
36
|
+
// Where a config that names no store keeps the inbox. Anything but Linux: a
|
|
37
|
+
// folder of the app's name in the home directory. Linux: $XDG_DATA_HOME/<slug>,
|
|
38
|
+
// or ~/.local/share/<slug> when that is unset, blank or not absolute. A caller
|
|
39
|
+
// that passes a stand-in home must pass an env that belongs to it.
|
|
79
40
|
export function defaultStoreRoot({ home = os.homedir(), platform = process.platform, env = process.env } = {}) {
|
|
80
41
|
if (platform !== 'linux') return path.join(home, Name);
|
|
81
42
|
const given = typeof env?.XDG_DATA_HOME === 'string' ? env.XDG_DATA_HOME.trim() : '';
|
|
@@ -83,21 +44,11 @@ export function defaultStoreRoot({ home = os.homedir(), platform = process.platf
|
|
|
83
44
|
return path.join(base, nameSlug);
|
|
84
45
|
}
|
|
85
46
|
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
// `
|
|
91
|
-
// being called that, so both go through the name module now. The fallbacks are
|
|
92
|
-
// not politeness: a launchd job, a login shell and any process started before a
|
|
93
|
-
// rename still export the old variable, and a dot-folder written under an old
|
|
94
|
-
// name holds a real store. The current name wins when both are there;
|
|
95
|
-
// otherwise the newest old name that is actually on disk does, so nobody's
|
|
96
|
-
// store goes missing on the release that renames the app.
|
|
97
|
-
// `homeDir` is injected so a test can ask what this answers on a machine with
|
|
98
|
-
// an old dot-folder on it, without one having to exist on the machine running
|
|
99
|
-
// the test. `platform` and `env` are the same kind of injection: this suite
|
|
100
|
-
// runs on a Mac and still has to answer a Linux home. Production passes neither.
|
|
47
|
+
// The env override is read at call time because a test sets it per case. Old
|
|
48
|
+
// env names and old dot-folders still answer: a launchd job or a process
|
|
49
|
+
// started before a rename exports the old variable, and an old dot-folder holds
|
|
50
|
+
// a real store. The current name wins, then the newest old name on disk.
|
|
51
|
+
// `homeDir`, `platform` and `env` are for tests; production passes none.
|
|
101
52
|
export function appHome(homeDir = os.homedir(), { platform = process.platform, env } = {}) {
|
|
102
53
|
const override = readEnv('HOME', env);
|
|
103
54
|
if (override) return override;
|
|
@@ -107,11 +58,10 @@ export function appHome(homeDir = os.homedir(), { platform = process.platform, e
|
|
|
107
58
|
const older = path.join(homeDir, `.${was.toLowerCase()}`);
|
|
108
59
|
if (isDir(older)) return older;
|
|
109
60
|
}
|
|
110
|
-
// A fresh Linux
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// A caller that passes `env` is asking about that environment, not this one.
|
|
61
|
+
// A fresh Linux home has nothing of ours yet, so answer the path loadConfig
|
|
62
|
+
// opens, or the two write separate stores. This machine's XDG_DATA_HOME
|
|
63
|
+
// belongs to the real home only; a stand-in home or an explicit `env` does
|
|
64
|
+
// not get it.
|
|
115
65
|
if (platform === 'linux') {
|
|
116
66
|
const real = env === undefined && path.resolve(homeDir) === path.resolve(os.homedir());
|
|
117
67
|
return defaultStoreRoot({ home: homeDir, platform: 'linux', env: real ? process.env : (env ?? {}) });
|
|
@@ -119,19 +69,15 @@ export function appHome(homeDir = os.homedir(), { platform = process.platform, e
|
|
|
119
69
|
return here;
|
|
120
70
|
}
|
|
121
71
|
|
|
122
|
-
// Claude Code's own encoding
|
|
123
|
-
// `/Users/you/
|
|
124
|
-
// `-Users-you-Zero-accounts-00000...-agentbox`. Every character that is not
|
|
125
|
-
// a letter or a digit becomes a dash, which is readable at a glance and safe on
|
|
126
|
-
// every filesystem.
|
|
72
|
+
// Claude Code's own encoding: every character that is not a letter or a digit
|
|
73
|
+
// becomes a dash, so `/Users/you/a.b` becomes `-Users-you-a-b`.
|
|
127
74
|
export function encodeProjectPath(dir) {
|
|
128
75
|
return String(dir).replace(/[^A-Za-z0-9]/g, '-');
|
|
129
76
|
}
|
|
130
77
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
// path it belongs to and a mismatch takes a short hash suffix instead.
|
|
78
|
+
// That encoding is lossy: `/a/b.c` and `/a/b/c` encode the same. Two products
|
|
79
|
+
// sharing one ledger would merge their inboxes, so each folder records the
|
|
80
|
+
// path it belongs to in `.origin` and a mismatch gets a short hash suffix.
|
|
135
81
|
const ORIGIN = '.origin';
|
|
136
82
|
|
|
137
83
|
function readOrigin(dir) {
|
|
@@ -139,28 +85,11 @@ function readOrigin(dir) {
|
|
|
139
85
|
}
|
|
140
86
|
|
|
141
87
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* `<home>/projects`, and handed each of those directories to `readWorkItems` as
|
|
148
|
-
* if it were a product folder. `readWorkItems` asks for the ledger, the ledger
|
|
149
|
-
* asks for the machinery directory, and the machinery directory of a machinery
|
|
150
|
-
* directory is a SECOND one nested inside the home. Then the sweep below did
|
|
151
|
-
* what it is for and carried the ledger, the session traces, the run records,
|
|
152
|
-
* the repeat rules and the parked writes one level down into it. Thirty eight
|
|
153
|
-
* products in two seconds, and her inbox read empty until the folders were
|
|
154
|
-
* carried back at 15:58.
|
|
155
|
-
*
|
|
156
|
-
* Nothing about that was exotic. Any agent with a shell can write that script,
|
|
157
|
-
* and reading is supposed to be the safe thing to do. So the refusal lives
|
|
158
|
-
* HERE, in the one place every caller passes through, rather than in a rule
|
|
159
|
-
* about how to write scripts that nobody reading this will have been told.
|
|
160
|
-
*
|
|
161
|
-
* The test is the path itself: anything inside `<home>/projects` is ours
|
|
162
|
-
* already. A product folder can never be one, because products live under the
|
|
163
|
-
* account root and the home is a separate tree.
|
|
88
|
+
* True for a path inside `<home>/projects`, which is ours and never a product
|
|
89
|
+
* folder, since products live under the account root. Treating one as a
|
|
90
|
+
* product folder nests a second machinery directory inside it, and the sweep
|
|
91
|
+
* then moves every record one level down. Every caller passes through here, so
|
|
92
|
+
* this is where that is refused.
|
|
164
93
|
*/
|
|
165
94
|
export function isMachineryDir(dir, home = appHome()) {
|
|
166
95
|
const root = path.resolve(path.join(home, 'projects'));
|
|
@@ -170,10 +99,8 @@ export function isMachineryDir(dir, home = appHome()) {
|
|
|
170
99
|
/** `<home>/projects/<encoded>` for this project directory. Creates nothing. */
|
|
171
100
|
export function machineryDir(projectDir, home = appHome()) {
|
|
172
101
|
const real = path.resolve(projectDir);
|
|
173
|
-
// Already
|
|
174
|
-
//
|
|
175
|
-
// rather than silent, because a caller that guessed this path still gets the
|
|
176
|
-
// right ledger back.
|
|
102
|
+
// Already a machinery directory: return it as is, so a caller that passed
|
|
103
|
+
// one still gets the right ledger rather than a nested one.
|
|
177
104
|
if (isMachineryDir(real, home)) return real;
|
|
178
105
|
const base = path.join(home, 'projects', encodeProjectPath(real));
|
|
179
106
|
const claimed = readOrigin(base);
|
|
@@ -182,52 +109,26 @@ export function machineryDir(projectDir, home = appHome()) {
|
|
|
182
109
|
return `${base}-${tag}`;
|
|
183
110
|
}
|
|
184
111
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
// FOR. It moves a file the first time something asks for it, so the ledger goes
|
|
189
|
-
// the moment the inbox opens and `sessions/` goes one item at a time, whenever a
|
|
190
|
-
// trace happens to be read, which for a row nobody opens again is never.
|
|
191
|
-
// Measured on her folder 2026-08-26: 1,785 of the 1,830 files that leave are
|
|
192
|
-
// session traces. Lazily she would still be looking at nearly all of it. The
|
|
193
|
-
// point is that her folder is clean, so all of it goes at once, the first time
|
|
194
|
-
// this process touches the product.
|
|
112
|
+
// Every name that is ours. All of them move together when a product is first
|
|
113
|
+
// touched, not one at a time when read, because most of the files are session
|
|
114
|
+
// traces that nothing reads again.
|
|
195
115
|
export const MACHINERY = ['work-items.jsonl', 'repeats.jsonl', 'sessions', 'runs', 'pending-writes'];
|
|
196
116
|
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
// rather than moved over it. Everything else here is a whole file that stands
|
|
200
|
-
// on its own.
|
|
117
|
+
// The append-only logs. A stray copy holds lines the real file never got, so
|
|
118
|
+
// it is appended onto the real file rather than moved over it.
|
|
201
119
|
const LOGS = new Set(['work-items.jsonl', 'repeats.jsonl']);
|
|
202
120
|
|
|
203
121
|
/**
|
|
204
|
-
* The
|
|
205
|
-
* record of ours carried in.
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* behind a `Set`, and `machineryPath` below only adopted a name when there was
|
|
209
|
-
* NOTHING at the new place. Between them that meant the move happened exactly
|
|
210
|
-
* once, and one of our files that appeared in her folder afterwards was never
|
|
211
|
-
* picked up again: nothing read it, nothing moved it, and it sat there.
|
|
212
|
-
*
|
|
213
|
-
* MEASURED on her real folder the day the move landed, not reasoned about: two
|
|
214
|
-
* release lines for two different cards written into a stray `work-items.jsonl`
|
|
215
|
-
* at 14:10 and never folded in, and two run records stranded under `runs/`. So
|
|
216
|
-
* this is not a theoretical window, it is where her folder actually was.
|
|
217
|
-
*
|
|
218
|
-
* The cost of running it every time is one stat per name against a path that is
|
|
219
|
-
* normally absent. Five syscalls that find nothing is cheaper than being wrong,
|
|
220
|
-
* and being wrong here is silent.
|
|
122
|
+
* The machinery directory for a project, created, with its `.origin` written
|
|
123
|
+
* once and every stray record of ours carried in. The sweep runs on every
|
|
124
|
+
* call, not once per process, because a file of ours can turn up in the
|
|
125
|
+
* product folder after the first move. It costs one stat per name.
|
|
221
126
|
*/
|
|
222
127
|
export function ensureMachineryDir(projectDir, home = appHome()) {
|
|
223
128
|
const dir = machineryDir(projectDir, home);
|
|
224
129
|
fs.mkdirSync(dir, { recursive: true });
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
// and there is nothing to carry across from a folder into itself. The origin
|
|
228
|
-
// marker is left alone too: it names the product this machinery belongs to,
|
|
229
|
-
// and overwriting it with the machinery's own path is how the folded copies
|
|
230
|
-
// came to claim themselves.
|
|
130
|
+
// A directory that is already ours is not swept, and its `.origin` is left
|
|
131
|
+
// alone: overwritten with its own path, it would claim itself.
|
|
231
132
|
if (dir === path.resolve(projectDir)) return dir;
|
|
232
133
|
if (readOrigin(dir) === null) {
|
|
233
134
|
try { fs.writeFileSync(path.join(dir, ORIGIN), `${path.resolve(projectDir)}\n`); } catch { /* best effort */ }
|
|
@@ -237,24 +138,18 @@ export function ensureMachineryDir(projectDir, home = appHome()) {
|
|
|
237
138
|
}
|
|
238
139
|
|
|
239
140
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
141
|
+
* Carries one of our names from the product folder to the machinery directory.
|
|
142
|
+
* Nothing is ever destroyed here. In order:
|
|
242
143
|
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
* -
|
|
246
|
-
*
|
|
247
|
-
* first move has always done.
|
|
248
|
-
* - Both directories: merge them entry by entry by these same rules, then
|
|
249
|
-
* drop the stray directory once the merge has emptied it.
|
|
144
|
+
* - Nothing there: return.
|
|
145
|
+
* - Nothing at the target: rename it across whole.
|
|
146
|
+
* - Both directories: merge entry by entry by these rules, then remove the
|
|
147
|
+
* stray directory if the merge emptied it.
|
|
250
148
|
* - Both append-only logs: append the stray's bytes onto the real file and
|
|
251
|
-
* delete the stray.
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* run record case, and it is rare on purpose, because the writer of those
|
|
256
|
-
* now writes straight here (main/code-change.mjs). A conflict only happens
|
|
257
|
-
* while a process started before the move is still running.
|
|
149
|
+
* delete the stray.
|
|
150
|
+
* - Both ordinary files: the newer takes the name and the older is kept as
|
|
151
|
+
* `<name>.superseded-<mtime>`. This is rare, because run records are now
|
|
152
|
+
* written straight here (main/code-change.mjs).
|
|
258
153
|
*/
|
|
259
154
|
export function foldStray(from, to, rel = path.basename(to)) {
|
|
260
155
|
let stray;
|
|
@@ -267,14 +162,11 @@ export function foldStray(from, to, rel = path.basename(to)) {
|
|
|
267
162
|
let entries = [];
|
|
268
163
|
try { entries = fs.readdirSync(from); } catch { return false; }
|
|
269
164
|
for (const name of entries) foldStray(path.join(from, name), path.join(to, name), name);
|
|
270
|
-
// Only if the merge emptied it. A directory with anything left in it holds
|
|
271
|
-
// something we did not understand, and it stays where it is.
|
|
272
165
|
try { fs.rmdirSync(from); } catch { /* not empty, or gone already */ }
|
|
273
166
|
return true;
|
|
274
167
|
}
|
|
275
168
|
|
|
276
|
-
// A directory on one side and a file on the other
|
|
277
|
-
// right answer for, so the stray is left alone and it is said out loud.
|
|
169
|
+
// A directory on one side and a file on the other has no right answer.
|
|
278
170
|
if (stray.isDirectory() !== landed.isDirectory()) {
|
|
279
171
|
console.warn(`${nameSlug}: ${from} and ${to} are not the same kind of thing; leaving the first alone`);
|
|
280
172
|
return false;
|
|
@@ -307,23 +199,13 @@ export function foldStray(from, to, rel = path.basename(to)) {
|
|
|
307
199
|
}
|
|
308
200
|
|
|
309
201
|
/**
|
|
310
|
-
* Where one of our records lives for this project,
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* the time this returns: anything still sitting in the product folder under one
|
|
315
|
-
* of our names is RENAMED into the home. A rename on the same volume is atomic
|
|
316
|
-
* and costs nothing, whether it is a 6 MB ledger or a directory of 1,785 session
|
|
317
|
-
* traces, and it is undone by moving the file back.
|
|
318
|
-
*
|
|
319
|
-
* It is a move rather than a read-both-write-new because the ledger is an
|
|
320
|
-
* append-only log: two halves of one log in two places is not a store, it is
|
|
321
|
-
* two stores that disagree. Everything that reads it must therefore agree about
|
|
322
|
-
* where it is, which is why the MCP server ships the same file.
|
|
202
|
+
* Where one of our records lives for this project, after any stray copy in the
|
|
203
|
+
* product folder has been renamed across. It is a move, not read-both-write-new,
|
|
204
|
+
* because two halves of an append-only ledger are two stores that disagree.
|
|
205
|
+
* The MCP server ships this same file so every reader agrees where it is.
|
|
323
206
|
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* was nothing to move, EEXIST/ENOTEMPTY means somebody else moved it first.
|
|
207
|
+
* Many processes may call this at once. A failed rename is benign: ENOENT means
|
|
208
|
+
* nothing to move, EEXIST or ENOTEMPTY means another process moved it first.
|
|
327
209
|
*/
|
|
328
210
|
export function machineryPath(projectDir, rel, home = appHome()) {
|
|
329
211
|
if (typeof rel !== 'string' || !rel || path.isAbsolute(rel) || rel.split(/[\\/]/).includes('..')) {
|
|
@@ -336,25 +218,14 @@ export function machineryPath(projectDir, rel, home = appHome()) {
|
|
|
336
218
|
}
|
|
337
219
|
|
|
338
220
|
/**
|
|
339
|
-
*
|
|
221
|
+
* Carries records a copy wrote under the wrong home over to `toHome`. The
|
|
222
|
+
* headless door (main/serve.mjs) once wrote its ledgers under whatever home it
|
|
223
|
+
* inherited, and this keeps the rows it wrote then.
|
|
340
224
|
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
* agent session. Its workers were handed `storeRoot`, found nothing there, and
|
|
346
|
-
* answered "no work item" to every store call. Pointing the home at `storeRoot`
|
|
347
|
-
* fixes the next write; this is what keeps the rows already written from
|
|
348
|
-
* vanishing on the first boot after the fix.
|
|
349
|
-
*
|
|
350
|
-
* ONLY WHAT IS PROVABLY THIS STORE'S MOVES. A machinery directory names the
|
|
351
|
-
* product it belongs to in `.origin`; one is carried only when that product is
|
|
352
|
-
* inside `storeRoot`. Everything else under `fromHome` is somebody else's
|
|
353
|
-
* store and is not touched. Each record goes through `foldStray`, so a ledger
|
|
354
|
-
* is appended rather than overwritten and nothing is ever destroyed; the old
|
|
355
|
-
* directory is removed only once nothing but its marker is left in it.
|
|
356
|
-
*
|
|
357
|
-
* Returns the product folders whose records were carried.
|
|
225
|
+
* Only a machinery directory whose `.origin` is inside `storeRoot` moves;
|
|
226
|
+
* anything else under `fromHome` belongs to another store. Records go through
|
|
227
|
+
* `foldStray`, and the old directory is removed only when just its marker is
|
|
228
|
+
* left. Returns the product folders whose records were carried.
|
|
358
229
|
*/
|
|
359
230
|
export function carryMisplacedMachinery(fromHome, storeRoot, toHome = storeRoot) {
|
|
360
231
|
if (!fromHome || !storeRoot) return [];
|
|
@@ -381,29 +252,14 @@ export function carryMisplacedMachinery(fromHome, storeRoot, toHome = storeRoot)
|
|
|
381
252
|
}
|
|
382
253
|
|
|
383
254
|
/**
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
* has now ruled must not survive its own tutorial. Deleting the folder in her
|
|
388
|
-
* store and leaving the ledger, the session traces and the run records behind
|
|
389
|
-
* would be a half removal, and it is the half nobody can see: MEASURED on her
|
|
390
|
-
* real Mac 2026-08-28, the practice folder in her account held two files and
|
|
391
|
-
* the machinery for it held 168 ledger lines and 29 session traces from six
|
|
392
|
-
* walks.
|
|
393
|
-
*
|
|
394
|
-
* IT WILL ONLY EVER DELETE A DIRECTORY THAT SAYS IT BELONGS TO THIS PROJECT.
|
|
395
|
-
* `.origin` is written by `ensureMachineryDir` on first use and holds the
|
|
396
|
-
* resolved path of the project the directory is the machinery for; this refuses
|
|
397
|
-
* unless it is there and it matches. A directory with no `.origin` is one we did
|
|
398
|
-
* not make, or one made by a version that did not write the marker, and either
|
|
399
|
-
* way it is somebody else's to delete. Same instinct as `foldStray` above: when
|
|
400
|
-
* two things are not the same kind of thing, leave the first alone and say so.
|
|
401
|
-
*
|
|
402
|
-
* Never throws. A machinery directory that will not delete is a few kilobytes
|
|
403
|
-
* under the app's own home; failing a launch over it would be the worse bug.
|
|
255
|
+
* Deletes every record of ours for one project. Its caller is the practice
|
|
256
|
+
* project, which must not outlive its tutorial; deleting only its folder would
|
|
257
|
+
* leave the ledger and traces behind.
|
|
404
258
|
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
259
|
+
* Deletes only a directory whose `.origin` matches this project. One with no
|
|
260
|
+
* marker was made by someone else or by an older version, and is left alone.
|
|
261
|
+
* Never throws, because a leftover directory is a few kilobytes and failing a
|
|
262
|
+
* launch over it would be worse. Returns { removed, why }.
|
|
407
263
|
*/
|
|
408
264
|
export function removeMachinery(projectDir, home = appHome()) {
|
|
409
265
|
const real = path.resolve(projectDir);
|
|
@@ -425,8 +281,8 @@ export function removeMachinery(projectDir, home = appHome()) {
|
|
|
425
281
|
}
|
|
426
282
|
}
|
|
427
283
|
|
|
428
|
-
//
|
|
429
|
-
// migrated
|
|
284
|
+
// Moves one legacy path in, if it is there. Never throws, so a store that
|
|
285
|
+
// cannot be migrated still opens on the new path.
|
|
430
286
|
export function adoptLegacy(from, to) {
|
|
431
287
|
let there = false;
|
|
432
288
|
try { there = fs.existsSync(from); } catch { return false; }
|
|
@@ -445,9 +301,8 @@ export function adoptLegacy(from, to) {
|
|
|
445
301
|
}
|
|
446
302
|
}
|
|
447
303
|
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
// leaves the original where it was rather than nothing anywhere.
|
|
304
|
+
// A rename cannot cross volumes, so copy and then remove. In that order, a
|
|
305
|
+
// failure halfway leaves the original where it was.
|
|
451
306
|
function copyAcross(from, to) {
|
|
452
307
|
try {
|
|
453
308
|
fs.cpSync(from, to, { recursive: true, errorOnExist: true, force: false });
|
package/main/store/paths.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { MACHINERY } from './home.mjs';
|
|
3
4
|
|
|
4
5
|
// The .git / .env deny-list, shared by EVERY creation-file access point: the
|
|
5
6
|
// agent tools (main/agent/tools.mjs), the folder-backed materialize walk
|
|
@@ -31,6 +32,16 @@ export function deniesGitOrEnvPath(rel) {
|
|
|
31
32
|
return GIT_PATH_RE.test(norm) || ENV_PATH_RE.test(norm);
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
// True iff any segment of `rel` is a MACHINERY name (main/store/home.mjs). A
|
|
36
|
+
// document written to one of those names is swept into the real ledger by
|
|
37
|
+
// ensureMachineryDir, so the store write boundary must reserve them. Normalized
|
|
38
|
+
// like deniesGitOrEnvPath so case or a backslash cannot alias one. null -> false.
|
|
39
|
+
const MACHINERY_SET = new Set(MACHINERY);
|
|
40
|
+
export function deniesMachineryPath(rel) {
|
|
41
|
+
if (rel == null) return false;
|
|
42
|
+
return normalizeForDenyCheck(rel).split('/').some((seg) => MACHINERY_SET.has(seg));
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
// Containment: resolve rel against root; realpath the nearest EXISTING
|
|
35
46
|
// ancestor so a symlinked directory cannot smuggle the path outside; reject
|
|
36
47
|
// a symlink as the final component (lstat). Throws on any violation, returns
|