agentbox-app 0.1.12 → 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/acp.mjs +296 -0
- package/main/agent-homes.mjs +158 -0
- package/main/agent-sessions.mjs +311 -260
- package/main/agent-updates.mjs +9 -22
- package/main/agents.mjs +179 -466
- package/main/analytics.mjs +42 -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-bin.mjs +45 -311
- package/main/claude-input.mjs +57 -7
- package/main/claude-launch-env.mjs +54 -0
- package/main/claude-models.mjs +39 -7
- package/main/claude-plan.mjs +64 -16
- package/main/claude-session-open.mjs +28 -0
- package/main/claude-usage-process.mjs +40 -0
- package/main/claude-usage.mjs +94 -59
- 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-bin.mjs +51 -249
- package/main/codex-models.mjs +20 -2
- package/main/codex-session.mjs +246 -506
- package/main/codex-threads-worker.mjs +12 -0
- package/main/codex-threads.mjs +112 -11
- package/main/codex-usage-file.mjs +5 -3
- package/main/codex-usage.mjs +56 -7
- package/main/codex-watch.mjs +83 -5
- package/main/codex.mjs +95 -363
- package/main/config.mjs +37 -5
- package/main/corner-tag.mjs +810 -0
- package/main/crash-report.mjs +26 -3
- package/main/dev-window.mjs +1 -1
- package/main/engine-bin.mjs +545 -0
- package/main/engine-setup.mjs +25 -24
- package/main/feedback.mjs +14 -31
- 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-stream.mjs +208 -0
- package/main/harnesses/claude.mjs +105 -0
- package/main/harnesses/codex.mjs +72 -0
- package/main/harnesses/common.mjs +78 -0
- package/main/harnesses/instructions-file.mjs +116 -0
- package/main/harnesses/opencode.mjs +234 -0
- package/main/harnesses.mjs +65 -0
- package/main/importable-threads.mjs +96 -0
- package/main/ipc.mjs +520 -135
- package/main/leftovers.mjs +117 -25
- package/main/liquid-glass.mjs +26 -0
- package/main/live-replies.mjs +10 -4
- package/main/local-app-frames.mjs +82 -0
- package/main/main-went-red.mjs +330 -0
- package/main/main.mjs +248 -37
- package/main/memory-gate-history.mjs +85 -20
- package/main/memory-gate-server.mjs +129 -11
- package/main/memory-gate.mjs +62 -8
- package/main/memory-log.mjs +124 -0
- 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 +64 -5
- package/main/opencode-server.mjs +271 -0
- package/main/opencode.mjs +287 -0
- package/main/phone-alerts.mjs +117 -0
- package/main/phone-inbox.mjs +625 -0
- package/main/phone-link.mjs +571 -0
- 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-address.mjs +28 -0
- package/main/relay-client.mjs +314 -0
- package/main/repeats.mjs +10 -4
- package/main/row-label.mjs +18 -47
- package/main/run-change.mjs +13 -0
- package/main/run-ledger.mjs +135 -0
- package/main/runs-on.mjs +32 -35
- package/main/save-attachments.mjs +41 -0
- package/main/serve.mjs +155 -9
- package/main/settings.mjs +364 -50
- package/main/shell-path.mjs +234 -0
- package/main/ship-conflict-brief.mjs +119 -0
- package/main/ship-queue.mjs +134 -20
- package/main/sign-in-files.mjs +3 -7
- package/main/source-updater.mjs +45 -11
- package/main/stage-attachment.mjs +68 -0
- package/main/stall-watch.mjs +50 -0
- package/main/start-hold.mjs +73 -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 +227 -53
- package/main/supervisor.mjs +3865 -699
- package/main/task-commands.mjs +13 -17
- package/main/task-folders-offthread.mjs +6 -1
- package/main/task-folders.mjs +897 -105
- package/main/task-terminals.mjs +147 -41
- 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/updater.mjs +71 -9
- package/main/webview-guard.mjs +45 -0
- package/main/worktree-include.mjs +365 -0
- 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 +30 -5
- package/renderer/dist/assets/abnfDiagram-YUKMZFIV-CoWB6YUK.js +1 -0
- package/renderer/dist/assets/arc-Ch0NrCul.js +1 -0
- package/renderer/dist/assets/architectureDiagram-47P4ROYG-CXz9M5tQ.js +36 -0
- package/renderer/dist/assets/blockDiagram-E7TT5TSB-DchGUuHH.js +140 -0
- package/renderer/dist/assets/c4Diagram-CRA5TL53-Cip-zNyr.js +38 -0
- package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
- package/renderer/dist/assets/chunk-2Q5K7J3B-BoXid_2r.js +1 -0
- package/renderer/dist/assets/chunk-5VM5RSS4-Cpez1rRz.js +15 -0
- package/renderer/dist/assets/chunk-7TKQ45FW-DKA84YkE.js +1 -0
- package/renderer/dist/assets/chunk-AW2ZBBNX-Dg-SumPG.js +88 -0
- package/renderer/dist/assets/chunk-FVRAUYC3-06TCvPm1.js +2 -0
- package/renderer/dist/assets/chunk-HTAEGDNF-CMEBy1Td.js +1 -0
- package/renderer/dist/assets/chunk-JWPE2WC7-BJ2ObmV-.js +1 -0
- package/renderer/dist/assets/chunk-KQW6MTUR-CGWOYXUE.js +1 -0
- package/renderer/dist/assets/chunk-XXDRQBXY-CiZp07XC.js +1 -0
- package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-DJNIX719.js +217 -0
- 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-CbkyZOUH.js +1 -0
- package/renderer/dist/assets/cynefin-EF2NZ3EQ-78iOroZO.js +141 -0
- package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-GcCopMs7.js +62 -0
- package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
- package/renderer/dist/assets/dagre-W4DXFKR2-DHLlXLkz.js +4 -0
- package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/renderer/dist/assets/diagram-2UJZ2QOL-DuZSUHcH.js +200 -0
- package/renderer/dist/assets/diagram-OVF4WLC6-Ckxu3cqH.js +24 -0
- package/renderer/dist/assets/diagram-PFPMY2P6-CsTw9Ycn.js +30 -0
- package/renderer/dist/assets/diagram-UMYRVEAY-CTXd8qVg.js +24 -0
- package/renderer/dist/assets/diagram-YEKJPTXX-CJs1T_9m.js +41 -0
- package/renderer/dist/assets/diagram-ZIFT7M5P-CvjI23mf.js +3 -0
- package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-d_IeAzOJ.js +1 -0
- package/renderer/dist/assets/elk-IJKZMXRS-zo9KFP3N.js +27 -0
- package/renderer/dist/assets/erDiagram-O2IAPWRE-DqfYf5Dt.js +99 -0
- package/renderer/dist/assets/flowDiagram-OXPTDLAJ-DAWsMLmo.js +213 -0
- package/renderer/dist/assets/ganttDiagram-R7TSEDQI-4ml_bhwR.js +292 -0
- package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-B_WcqSId.js +106 -0
- package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
- package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-CI-jYqzc.js +2 -0
- package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
- package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-B56VWRlV.js +70 -0
- package/renderer/dist/assets/journeyDiagram-COXZFDF6-D1-p09Lr.js +139 -0
- package/renderer/dist/assets/kanban-definition-P3RFRI5V-Cv3w7JoN.js +89 -0
- package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
- package/renderer/dist/assets/linear-C35V2Spi.js +1 -0
- package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
- package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
- package/renderer/dist/assets/mermaid.core-BzGT5Vo9.js +314 -0
- package/renderer/dist/assets/mindmap-definition-LZFPQGKD-BZUfuymC.js +96 -0
- package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/renderer/dist/assets/pegDiagram-BGZESJAR-DFh9XNGe.js +1 -0
- package/renderer/dist/assets/pieDiagram-CAPJLFHJ-C65tPVsy.js +39 -0
- package/renderer/dist/assets/product-name-CSNW6d7s.js +40 -0
- package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-Cushj_nk.js +7 -0
- package/renderer/dist/assets/railroadDiagram-SM67HX2B-DZGebGx5.js +1 -0
- package/renderer/dist/assets/requirementDiagram-X7JNWC4A-DpAB0MNo.js +84 -0
- package/renderer/dist/assets/sankeyDiagram-UM26HJRW-D-29rHra.js +40 -0
- package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-FfOQAZol.js +169 -0
- package/renderer/dist/assets/sizeCapture-INFHLROL-C_n6fDBY.js +1 -0
- package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-BZjenzJG.js +291 -0
- package/renderer/dist/assets/swimlanes-N4OXWK64-DxaMU9vX.js +1 -0
- package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
- package/renderer/dist/assets/timeline-definition-YOQAKGHF-DTg5HJKz.js +120 -0
- package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-Bc3MZDsl.js +370 -0
- package/renderer/dist/assets/vennDiagram-BLWOH2XV-GIVGk9Id.js +34 -0
- package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-NmJnbQOg.js +78 -0
- package/renderer/dist/assets/xychartDiagram-TAQBALBS-p-kfGfnW.js +7 -0
- package/renderer/dist/corner-tag.html +13 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +297 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/index.html +16 -3
- package/scripts/after-install.mjs +30 -8
- package/scripts/approval-server.sh +24 -5
- package/scripts/before-publish.mjs +12 -10
- 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 +62 -34
- package/shared/agents.d.mts +1 -0
- 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 +45 -1
- package/shared/claude-code-shape.mjs +6 -0
- package/shared/claude-commands.generated.mjs +6 -4
- package/shared/claude-models.generated.mjs +6 -4
- 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 +36 -0
- package/shared/corner-tag.mjs +396 -0
- package/shared/dead-run-trace.mjs +19 -2
- package/shared/engines.mjs +76 -300
- 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 +111 -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 +31 -0
- package/shared/notify-rules.mjs +55 -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/provider-commands.mjs +10 -2
- package/shared/rank.d.mts +4 -1
- package/shared/rank.mjs +82 -19
- package/shared/relay-address.mjs +6 -0
- package/shared/relay-crypto.mjs +165 -0
- 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 -13
- package/shared/runs-on.mjs +31 -52
- package/shared/shipped-note.d.mts +4 -0
- package/shared/shipped-note.mjs +6 -0
- package/shared/shortcuts.mjs +24 -10
- package/shared/spawn-trouble.d.mts +4 -0
- package/shared/spawn-trouble.mjs +484 -30
- 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/terminal-input.d.mts +1 -0
- package/shared/terminal-input.mjs +30 -0
- package/shared/terminal-keys.d.mts +1 -0
- package/shared/terminal-keys.mjs +16 -0
- package/shared/terminal-size.d.mts +4 -0
- package/shared/terminal-size.mjs +29 -0
- package/shared/thread-cards.mjs +4 -1
- package/shared/usage.d.mts +12 -0
- package/shared/usage.mjs +56 -3
- package/shared/websites.d.mts +15 -0
- package/shared/websites.mjs +61 -0
- package/shared/where-it-runs-from.d.mts +43 -0
- package/shared/where-it-runs-from.mjs +155 -0
- package/shared/work-items.mjs +150 -29
- package/shared/work-lines.mjs +93 -245
- package/shared/xterm-invalid-range.d.mts +2 -0
- package/shared/xterm-invalid-range.mjs +58 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +0 -1169
- package/renderer/dist/assets/index-ClBXnq3B.css +0 -32
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// THE RELAY'S LOCKS (relay/worker.mjs, main/relay-client.mjs, relay/phone/).
|
|
2
|
+
//
|
|
3
|
+
// A phone away from home reaches the Mac through a relay we run. Everything
|
|
4
|
+
// that crosses it is sealed on one end and opened on the other with keys made
|
|
5
|
+
// from the phone's key, which is in the QR code and nowhere else: it never
|
|
6
|
+
// leaves the Mac except into the code, and the phone keeps it in the address's
|
|
7
|
+
// fragment, which a browser never sends. So the relay can pass messages and
|
|
8
|
+
// cannot read, alter or forge one.
|
|
9
|
+
//
|
|
10
|
+
// What the relay does see: a room name, and the Mac's proof that it may hold
|
|
11
|
+
// that room. The room is a hash of the proof, so the relay can check a Mac
|
|
12
|
+
// against it, and neither one tells it the key.
|
|
13
|
+
//
|
|
14
|
+
// Plain WebCrypto, so this one file runs on the Mac (Node), on Cloudflare, and
|
|
15
|
+
// in the phone's service worker.
|
|
16
|
+
|
|
17
|
+
const enc = new TextEncoder();
|
|
18
|
+
const subtle = () => globalThis.crypto.subtle;
|
|
19
|
+
const SALT = enc.encode('agentbox-relay-v1');
|
|
20
|
+
|
|
21
|
+
export function b64u(bytes) {
|
|
22
|
+
let s = '';
|
|
23
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
24
|
+
return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function bits(key, info, length = 256) {
|
|
28
|
+
const ikm = await subtle().importKey('raw', enc.encode(String(key)), 'HKDF', false, ['deriveBits']);
|
|
29
|
+
return new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SALT, info: enc.encode(info) }, ikm, length));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function roomOf(proof) {
|
|
33
|
+
return b64u(new Uint8Array(await subtle().digest('SHA-256', enc.encode(`agentbox-room:${proof}`))));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The room the relay knows this pair by, and the Mac's proof that it is the Mac's. */
|
|
37
|
+
export async function relayIds(key) {
|
|
38
|
+
const proof = b64u(await bits(key, 'mac-proof'));
|
|
39
|
+
return { room: await roomOf(proof), proof };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Is `proof` the proof for `room`? The relay asks this of every Mac. */
|
|
43
|
+
export async function macMayJoin(room, proof) {
|
|
44
|
+
if (typeof room !== 'string' || typeof proof !== 'string' || !proof) return false;
|
|
45
|
+
const want = await roomOf(proof);
|
|
46
|
+
if (want.length !== room.length) return false;
|
|
47
|
+
let diff = 0;
|
|
48
|
+
for (let i = 0; i < want.length; i += 1) diff |= want.charCodeAt(i) ^ room.charCodeAt(i);
|
|
49
|
+
return diff === 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const aes = async (raw) => subtle().importKey('raw', raw, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
|
|
53
|
+
|
|
54
|
+
/** One key for each direction, so nothing sealed one way opens the other. */
|
|
55
|
+
export async function relayKeys(key) {
|
|
56
|
+
return { up: await aes(await bits(key, 'phone-to-mac')), down: await aes(await bits(key, 'mac-to-phone')) };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Seal `bytes` for the other end. `aad` names what it is (a request, or the
|
|
61
|
+
* n-th frame of the answer to one request), so a frame cannot be passed off
|
|
62
|
+
* as another. Out: 12 bytes of nonce, then the sealed bytes.
|
|
63
|
+
*/
|
|
64
|
+
export async function seal(key, bytes, aad) {
|
|
65
|
+
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
|
|
66
|
+
const ct = new Uint8Array(await subtle().encrypt({ name: 'AES-GCM', iv, additionalData: enc.encode(aad) }, key, bytes));
|
|
67
|
+
const out = new Uint8Array(12 + ct.length);
|
|
68
|
+
out.set(iv, 0);
|
|
69
|
+
out.set(ct, 12);
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Open what `seal` made, or throw if a single bit of it was changed. */
|
|
74
|
+
export async function open(key, sealed, aad) {
|
|
75
|
+
if (!(sealed instanceof Uint8Array) || sealed.length < 28) throw new Error('Too short to be sealed.');
|
|
76
|
+
const pt = await subtle().decrypt({ name: 'AES-GCM', iv: sealed.subarray(0, 12), additionalData: enc.encode(aad) }, key, sealed.subarray(12));
|
|
77
|
+
return new Uint8Array(pt);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ------------------------------------------------------------- the wire */
|
|
81
|
+
|
|
82
|
+
// A request, phone to Mac, before it is sealed: four bytes giving the length of
|
|
83
|
+
// a json head, the head, then the body. The head carries a time and a random
|
|
84
|
+
// nonce, so the Mac can turn away one it has already answered.
|
|
85
|
+
export function packRequest(head, body = new Uint8Array(0)) {
|
|
86
|
+
const h = enc.encode(JSON.stringify(head));
|
|
87
|
+
const out = new Uint8Array(4 + h.length + body.length);
|
|
88
|
+
new DataView(out.buffer).setUint32(0, h.length);
|
|
89
|
+
out.set(h, 4);
|
|
90
|
+
out.set(body, 4 + h.length);
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function unpackRequest(bytes) {
|
|
95
|
+
const n = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);
|
|
96
|
+
const head = JSON.parse(new TextDecoder().decode(bytes.subarray(4, 4 + n)));
|
|
97
|
+
return { head, body: bytes.subarray(4 + n) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// An answer, Mac to phone, is a run of frames, each sealed on its own: a head
|
|
101
|
+
// (status and headers), the body in pieces, and an end. The end is sealed too,
|
|
102
|
+
// so an answer cut short by anyone in between is seen to be cut short.
|
|
103
|
+
//
|
|
104
|
+
// WAIT comes before the head and says only "still working". A reply to an
|
|
105
|
+
// agent mid-step is answered when the agent takes it, which can be minutes,
|
|
106
|
+
// and the room gives a Mac 30 seconds to start answering. A phone that asks
|
|
107
|
+
// for these (`wait` in its request's head) skips them; one that does not
|
|
108
|
+
// never gets one.
|
|
109
|
+
export const FRAME = Object.freeze({ HEAD: 0, DATA: 1, END: 2, WAIT: 3 });
|
|
110
|
+
|
|
111
|
+
export const frameAad = (nonce, seq) => `res:${nonce}:${seq}`;
|
|
112
|
+
|
|
113
|
+
// ON THE MAC'S SOCKET TO THE RELAY one byte says what a message is, then eight
|
|
114
|
+
// bytes of request number, then whatever it carries:
|
|
115
|
+
// to the Mac 0x01 a request (the sealed bytes) 0x02 the phone gave up
|
|
116
|
+
// 0x03 a piece of a request too big for one message; the
|
|
117
|
+
// 0x01 that follows carries the last piece
|
|
118
|
+
// from the Mac 0x10 one sealed frame of the answer 0x11 done
|
|
119
|
+
// 0x12 refused: it would not open, or it was a replay
|
|
120
|
+
export const TO_MAC = Object.freeze({ REQUEST: 0x01, CANCEL: 0x02, PART: 0x03 });
|
|
121
|
+
/** The most the relay puts in one socket message to the Mac. Cloudflare's limit is 1 MB. */
|
|
122
|
+
export const PART_BYTES = 512 * 1024;
|
|
123
|
+
export const FROM_MAC = Object.freeze({ FRAME: 0x10, DONE: 0x11, REFUSED: 0x12 });
|
|
124
|
+
export const ID_BYTES = 8;
|
|
125
|
+
|
|
126
|
+
export function tag(type, id, rest = new Uint8Array(0)) {
|
|
127
|
+
const out = new Uint8Array(1 + ID_BYTES + rest.length);
|
|
128
|
+
out[0] = type;
|
|
129
|
+
out.set(id, 1);
|
|
130
|
+
out.set(rest, 1 + ID_BYTES);
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Between the relay and either end each sealed piece goes with its length in
|
|
135
|
+
// front, so one stream of bytes can carry many.
|
|
136
|
+
export function lengthPrefixed(bytes) {
|
|
137
|
+
const out = new Uint8Array(4 + bytes.length);
|
|
138
|
+
new DataView(out.buffer).setUint32(0, bytes.length);
|
|
139
|
+
out.set(bytes, 4);
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Split a stream (or a reader of one) of length-prefixed pieces back into the pieces. */
|
|
144
|
+
export async function* pieces(source) {
|
|
145
|
+
const reader = typeof source.getReader === 'function' ? source.getReader() : source;
|
|
146
|
+
let buf = new Uint8Array(0);
|
|
147
|
+
try {
|
|
148
|
+
for (;;) {
|
|
149
|
+
while (buf.length >= 4) {
|
|
150
|
+
const n = new DataView(buf.buffer, buf.byteOffset, buf.byteLength).getUint32(0);
|
|
151
|
+
if (buf.length < 4 + n) break;
|
|
152
|
+
yield buf.slice(4, 4 + n);
|
|
153
|
+
buf = buf.subarray(4 + n);
|
|
154
|
+
}
|
|
155
|
+
const { value, done } = await reader.read();
|
|
156
|
+
if (done) return;
|
|
157
|
+
const next = new Uint8Array(buf.length + value.length);
|
|
158
|
+
next.set(buf, 0);
|
|
159
|
+
next.set(value, buf.length);
|
|
160
|
+
buf = next;
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
try { reader.releaseLock(); } catch { /* cancelled under us */ }
|
|
164
|
+
}
|
|
165
|
+
}
|
package/shared/repeats.d.mts
CHANGED
|
@@ -28,6 +28,10 @@ export const REPEAT_LABEL_PREFIX: string;
|
|
|
28
28
|
export function repeatLabel(ruleId: string): string;
|
|
29
29
|
export function ruleIdOf(item: { labels?: string[] } | null | undefined): string | null;
|
|
30
30
|
export function isCleanRun(item: { labels?: string[] } | null | undefined): boolean;
|
|
31
|
+
export function runToOpen<T extends { id: string; product: string; labels?: string[]; createdAt: number }>(
|
|
32
|
+
rule: { id: string; product: string; lastOccurrence?: string },
|
|
33
|
+
items: T[],
|
|
34
|
+
): T | null;
|
|
31
35
|
export const DAY_NAMES: string[];
|
|
32
36
|
export function isRepeatRule(rule: unknown): boolean;
|
|
33
37
|
export function ruleLabel(rule: unknown): string;
|
|
@@ -43,3 +47,7 @@ export function foldRepeats(
|
|
|
43
47
|
lines: Array<{ id: string; ts: number; patch: Record<string, unknown> } | null>,
|
|
44
48
|
now?: number,
|
|
45
49
|
): Map<string, RepeatRule>;
|
|
50
|
+
export const PROTOCOL_OPENS: string;
|
|
51
|
+
export const INSTRUCTION_MARK: string;
|
|
52
|
+
/** A repeating run's body without the agent's rules in front of it; any other body whole. */
|
|
53
|
+
export function instructionOf(body: string | null | undefined): string;
|
package/shared/repeats.mjs
CHANGED
|
@@ -15,6 +15,16 @@ const KEY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
|
15
15
|
export const CLEAN_LABEL = 'clean';
|
|
16
16
|
export const REPEAT_LABEL_PREFIX = 'repeat:';
|
|
17
17
|
|
|
18
|
+
// A run's body is the app's rules for the agent (main/repeats.mjs), then the
|
|
19
|
+
// person's instruction after INSTRUCTION_MARK. Screens show the instruction.
|
|
20
|
+
export const PROTOCOL_OPENS = 'THIS IS ONE RUN OF A REPEATING TASK.';
|
|
21
|
+
export const INSTRUCTION_MARK = 'HER INSTRUCTION FOLLOWS.';
|
|
22
|
+
export function instructionOf(body) {
|
|
23
|
+
const text = String(body ?? '');
|
|
24
|
+
const at = text.indexOf(`\n${INSTRUCTION_MARK}\n`);
|
|
25
|
+
return text.startsWith(PROTOCOL_OPENS) && at >= 0 ? text.slice(at + INSTRUCTION_MARK.length + 2).trim() : text;
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
export function repeatLabel(ruleId) {
|
|
19
29
|
return `${REPEAT_LABEL_PREFIX}${ruleId}`;
|
|
20
30
|
}
|
|
@@ -24,14 +34,35 @@ export function ruleIdOf(item) {
|
|
|
24
34
|
return found ? found.slice(REPEAT_LABEL_PREFIX.length) : null;
|
|
25
35
|
}
|
|
26
36
|
|
|
37
|
+
// THE STANDING INSTRUCTION: the words every run after this one is briefed with.
|
|
38
|
+
// `serve` copies this into each occurrence's body under the protocol, and the
|
|
39
|
+
// store tool an agent changes it through reads and writes the same thing, so
|
|
40
|
+
// what the tool shows a run is what the next run gets.
|
|
41
|
+
export function standingInstruction(rule) {
|
|
42
|
+
return rule?.body ?? rule?.title;
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
// A clean run is one the worker EXPLICITLY marked clean. Everything else is news
|
|
28
46
|
// and reaches her, including a run that died without saying anything: this
|
|
29
47
|
// predicate exists to hide, never to show, so it fails open by construction.
|
|
30
48
|
export function isCleanRun(item) {
|
|
31
|
-
|
|
49
|
+
const labels = item?.labels ?? [];
|
|
50
|
+
const aRun = !!ruleIdOf(item) || labels.some((l) => typeof l === 'string' && (l.startsWith('teammate:') || l === 'teammate-offer'));
|
|
51
|
+
return aRun && labels.includes(CLEAN_LABEL);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// THE THREAD AN OPENED REPEATING TASK LANDS ON: its latest run, an ordinary
|
|
55
|
+
// work item, so the Reply box under it messages that run's agent like any
|
|
56
|
+
// other thread (w-5e3f2e047f). The run it last served first; when that one has
|
|
57
|
+
// left the store's window, the newest of its runs still on file. Null before
|
|
58
|
+
// the first run, and never a run of another rule or another project.
|
|
59
|
+
export function runToOpen(rule, items) {
|
|
60
|
+
const mine = (items ?? []).filter((i) => i.product === rule.product && ruleIdOf(i) === rule.id);
|
|
61
|
+
return mine.find((i) => i.id === rule.lastOccurrence)
|
|
62
|
+
?? mine.reduce((newest, i) => (!newest || i.createdAt > newest.createdAt ? i : newest), null);
|
|
32
63
|
}
|
|
33
64
|
|
|
34
|
-
export const DAY_NAMES = ['Sunday',
|
|
65
|
+
export const DAY_NAMES = ['Sunday','Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
35
66
|
|
|
36
67
|
// Three shapes, because they are the three a founder actually asks for:
|
|
37
68
|
// { every: 'day', at } every morning
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// A ROOM'S WALL AND THE ROW MY MAC PUBLISHES FOR IT (w-3d0fa23ffb).
|
|
2
|
+
//
|
|
3
|
+
// Pure. Rows are as the cloud sends them (cloud/supabase/migrations/
|
|
4
|
+
// 20261021000000_rooms.sql): `room_profiles` say what a person lets a card
|
|
5
|
+
// show, `room_presence` is one row per Mac. docs/team/ROOMS-PLAN.md has the
|
|
6
|
+
// rules; the test is tests/a-room-wall-shows-each-person-as-they-chose.
|
|
7
|
+
import { levelFor, localDate, streakOf } from './agent-hours.mjs';
|
|
8
|
+
import { visibilityOf } from './thread-cards.mjs';
|
|
9
|
+
|
|
10
|
+
export const PRESENCE_STALE_MS = 15 * 60_000;
|
|
11
|
+
export const WORKING_ON_MAX = 64;
|
|
12
|
+
const DAY_MS = 86_400_000;
|
|
13
|
+
|
|
14
|
+
export function displayNameOf(name, { full = false } = {}) {
|
|
15
|
+
let s = String(name ?? '').trim();
|
|
16
|
+
if (s.includes('@')) s = s.split('@')[0];
|
|
17
|
+
const words = s.split(/\s+/).filter(Boolean);
|
|
18
|
+
if (words.length < 2 || full) return words.join(' ');
|
|
19
|
+
return `${words[0]} ${words.at(-1)[0].toUpperCase()}.`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const ms = (v) => (v == null ? NaN : typeof v === 'number' ? v : Date.parse(v));
|
|
23
|
+
// A date the publisher wrote in their own zone, read as local midnight here.
|
|
24
|
+
const dayMs = (d) => (typeof d === 'string' ? new Date(`${d}T00:00:00`).getTime() : NaN);
|
|
25
|
+
const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
|
|
26
|
+
|
|
27
|
+
function sumPresence(rows, now) {
|
|
28
|
+
let agents = 0, total = 0, week = 0, today = 0;
|
|
29
|
+
const days = new Set();
|
|
30
|
+
const months = new Map();
|
|
31
|
+
let lead = null;
|
|
32
|
+
for (const r of rows) {
|
|
33
|
+
if (r.gone) continue;
|
|
34
|
+
total += num(r.hours_total);
|
|
35
|
+
if (now < dayMs(r.week_start) + 7 * DAY_MS) week += num(r.hours_week);
|
|
36
|
+
if (now < dayMs(r.day) + DAY_MS) today += num(r.hours_today);
|
|
37
|
+
for (const d of r.active_days ?? []) days.add(String(d).slice(0, 10));
|
|
38
|
+
for (const m of Array.isArray(r.hours_by_month) ? r.hours_by_month : []) {
|
|
39
|
+
if (typeof m?.month === 'string') months.set(m.month, (months.get(m.month) ?? 0) + num(m.hours));
|
|
40
|
+
}
|
|
41
|
+
const fresh = now - ms(r.updated_at) < PRESENCE_STALE_MS;
|
|
42
|
+
if (!fresh || !(num(r.agents_running) > 0)) continue;
|
|
43
|
+
agents += num(r.agents_running);
|
|
44
|
+
if (!lead || (ms(r.working_at) || 0) > (ms(lead.working_at) || 0)) lead = r;
|
|
45
|
+
}
|
|
46
|
+
const byMonth = [...months].sort(([a], [b]) => a.localeCompare(b)).map(([month, hours]) => ({ month, hours }));
|
|
47
|
+
return { agents, total, week, today, streak: streakOf([...days], now), byMonth, lead };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function workingOf({ profile, lead, teammate, cards }) {
|
|
51
|
+
const none = { workingOn: null, workingPrivate: false };
|
|
52
|
+
if (!lead) return none;
|
|
53
|
+
if (profile.show_working === 'none' || lead.working_private) return { workingOn: null, workingPrivate: true };
|
|
54
|
+
if (profile.show_working === 'everyone' && lead.working_on) return { workingOn: lead.working_on, workingPrivate: false };
|
|
55
|
+
// Running, and no name this viewer may read: it says Private thread.
|
|
56
|
+
const hidden = { workingOn: null, workingPrivate: true };
|
|
57
|
+
if (!teammate) return hidden;
|
|
58
|
+
const card = cards
|
|
59
|
+
.filter((c) => c.state === 'running' && c.visible !== false && c.title)
|
|
60
|
+
.sort((a, b) => num(b.updatedAt) - num(a.updatedAt))[0];
|
|
61
|
+
return card ? { workingOn: String(card.title).slice(0, WORKING_ON_MAX), workingPrivate: false } : hidden;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// One card per member with a profile, sorted by name so cards never jump.
|
|
65
|
+
// Invisible people are already hidden from others by the cloud; my own
|
|
66
|
+
// profile still reaches me, so I leave my own wall too. `mine` is what this
|
|
67
|
+
// Mac knows I am working on, which my own card shows whatever others see.
|
|
68
|
+
// `names` is the cloud's room_names for this room, or null on a cloud without
|
|
69
|
+
// it: a member with no card at all is on the wall, named, as not on Rooms yet.
|
|
70
|
+
// In a `listed` room, open to anyone, my own card shows me what strangers see.
|
|
71
|
+
export function wallCards({ members = [], profiles = [], presence = [], teammates = [], teamCards = [], meId = null, now = Date.now(), mine = null, names = null, listed = false }) {
|
|
72
|
+
const inRoom = new Set(members);
|
|
73
|
+
const mates = new Set(teammates);
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const p of profiles) {
|
|
76
|
+
if (!p?.person_id || !inRoom.has(p.person_id)) continue;
|
|
77
|
+
if (now < ms(p.invisible_until)) continue;
|
|
78
|
+
const id = p.person_id;
|
|
79
|
+
const s = sumPresence(presence.filter((r) => r.person_id === id), now);
|
|
80
|
+
const cards = teamCards.filter((c) => c.personId === id);
|
|
81
|
+
out.push({
|
|
82
|
+
id,
|
|
83
|
+
me: id === meId,
|
|
84
|
+
name: p.display_name || '',
|
|
85
|
+
avatarUrl: p.avatar_url ?? null,
|
|
86
|
+
headline: p.headline ?? null,
|
|
87
|
+
bio: p.bio ?? null,
|
|
88
|
+
// My own count is this Mac's, not the cloud's last word on it.
|
|
89
|
+
agents: p.show_agents === false ? null : id === meId && Number.isFinite(mine?.agents) ? mine.agents : s.agents,
|
|
90
|
+
level: p.show_level === false ? null : levelFor(s.total),
|
|
91
|
+
hoursTotal: s.total,
|
|
92
|
+
hoursWeek: s.week,
|
|
93
|
+
hoursToday: s.today,
|
|
94
|
+
streak: s.streak,
|
|
95
|
+
byMonth: s.byMonth,
|
|
96
|
+
teammate: mates.has(id) && id !== meId,
|
|
97
|
+
showActivity: p.show_activity !== false,
|
|
98
|
+
// A list of threads an older Mac still sends is read by nobody.
|
|
99
|
+
activity: null,
|
|
100
|
+
status: p.show_status !== false && p.status_text && !(ms(p.status_until) <= now) ? p.status_text : null,
|
|
101
|
+
...(id === meId && mine ? ownWorking(mine, listed && p.show_working !== 'everyone')
|
|
102
|
+
: workingOf({ profile: p, lead: s.lead, teammate: mates.has(id) && id !== meId, cards })),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
for (const n of names ?? []) {
|
|
106
|
+
if (n.has_card || !inRoom.has(n.person_id) || n.person_id === meId) continue;
|
|
107
|
+
out.push({
|
|
108
|
+
id: n.person_id, me: false, away: true, name: displayNameOf(n.name), avatarUrl: n.avatar_url ?? null, headline: null, bio: null,
|
|
109
|
+
agents: null, level: null, hoursTotal: 0, hoursWeek: 0, hoursToday: 0, streak: 0, byMonth: [],
|
|
110
|
+
teammate: mates.has(n.person_id), showActivity: false, activity: null, status: null, workingOn: null, workingPrivate: false,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return out.sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const ownWorking = (mine, hidden) => (hidden
|
|
117
|
+
? { workingOn: null, workingPrivate: !!mine.workingOn }
|
|
118
|
+
: { workingOn: mine.workingOn ?? null, workingPrivate: false });
|
|
119
|
+
|
|
120
|
+
const shortName = (item) => (String(item?.label ?? '').trim() || String(item?.title ?? '')).slice(0, WORKING_ON_MAX);
|
|
121
|
+
|
|
122
|
+
// The presence row this Mac sends. `running` is the supervisor's running
|
|
123
|
+
// sessions ({itemId, product, startedAt}); `summary` is summarizeRuns'.
|
|
124
|
+
// A thread is named only when its project and its own word leave it to the
|
|
125
|
+
// whole team, and only when I let everyone see it: realtime sends whole rows,
|
|
126
|
+
// so a name on the row reaches everyone in my rooms.
|
|
127
|
+
export function myPresence({ running = [], items = [], products = [], summary, now = Date.now(), showWorking = 'everyone' }) {
|
|
128
|
+
const lead = [...running].sort((a, b) => num(b.startedAt) - num(a.startedAt))[0];
|
|
129
|
+
let working_on = null;
|
|
130
|
+
let working_private = false;
|
|
131
|
+
if (lead && showWorking !== 'none') {
|
|
132
|
+
const item = items.find((i) => i.id === lead.itemId);
|
|
133
|
+
const product = products.find((p) => p.slug === (item?.product ?? lead.product));
|
|
134
|
+
const v = item ? visibilityOf(item, product) : 'private';
|
|
135
|
+
if (v !== undefined && v !== 'team') working_private = true;
|
|
136
|
+
else if (showWorking === 'everyone') working_on = shortName(item) || null;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
agents_running: running.length,
|
|
140
|
+
working_on,
|
|
141
|
+
working_private,
|
|
142
|
+
working_at: lead ? new Date(num(lead.startedAt) || now).toISOString() : null,
|
|
143
|
+
hours_total: round2(summary?.total),
|
|
144
|
+
hours_week: round2(summary?.week),
|
|
145
|
+
week_start: summary?.weekStart ?? localDate(now),
|
|
146
|
+
hours_today: round2(summary?.today),
|
|
147
|
+
day: summary?.day ?? localDate(now),
|
|
148
|
+
streak_days: summary?.streak ?? 0,
|
|
149
|
+
active_days: summary?.activeDays ?? [],
|
|
150
|
+
hours_by_month: summary?.byMonth ?? [],
|
|
151
|
+
// Never a list of threads: it reached everyone in my rooms.
|
|
152
|
+
activity: [],
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const round2 = (v) => Math.round(num(v) * 100) / 100;
|
|
157
|
+
|
|
158
|
+
// A switch turned off keeps that value off the cloud, so nobody can read it
|
|
159
|
+
// from the row, a realtime event or an invite preview.
|
|
160
|
+
export function withoutHidden(row, profile) {
|
|
161
|
+
const out = { ...row };
|
|
162
|
+
if (profile?.show_agents === false) out.agents_running = 0;
|
|
163
|
+
if (profile?.show_level === false) {
|
|
164
|
+
Object.assign(out, { hours_total: 0, hours_week: 0, hours_today: 0, streak_days: 0, active_days: [], hours_by_month: [] });
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// A room's invite link (docs/team/ROOMS-PLAN.md, Joining): the web address
|
|
2
|
+
// people share, and the app's own scheme that page opens.
|
|
3
|
+
import { nameSlug } from './product-name.mjs';
|
|
4
|
+
|
|
5
|
+
export const JOIN_SCHEME = nameSlug;
|
|
6
|
+
export const JOIN_SITE = `https://${nameSlug}.ac/join/`;
|
|
7
|
+
|
|
8
|
+
export const joinLink = (token) => `${JOIN_SITE}${token}`;
|
|
9
|
+
|
|
10
|
+
/** Any link on the app's own scheme: a room invite, or Stripe's way back. */
|
|
11
|
+
export const ourLink = (url) => typeof url === 'string' && url.startsWith(`${JOIN_SCHEME}://`);
|
|
12
|
+
|
|
13
|
+
const TOKEN = /^[A-Za-z0-9_-]{16,64}$/;
|
|
14
|
+
|
|
15
|
+
/** The token in a pasted link or a deep link, or the token itself; null for anything else. */
|
|
16
|
+
export function tokenFromLink(text) {
|
|
17
|
+
const s = String(text ?? '').trim();
|
|
18
|
+
if (TOKEN.test(s)) return s;
|
|
19
|
+
const m = /^(?:https?:\/\/[^/\s]+\/join\/|[a-z][a-z0-9+.-]*:\/\/join\/)([A-Za-z0-9_-]+)\/?(?:[?#].*)?$/i.exec(s);
|
|
20
|
+
return m && TOKEN.test(m[1]) ? m[1] : null;
|
|
21
|
+
}
|
package/shared/runs-on.d.mts
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
export interface RunsOn {
|
|
2
|
-
/** 'claude' or 'codex', or null on a
|
|
2
|
+
/** 'claude' or 'codex', or null on a computer where nothing is signed in. */
|
|
3
3
|
engine?: string | null;
|
|
4
|
-
/**
|
|
5
|
-
* The plan in words, as the tool itself reports it: 'Max 20x', 'Pro'. Null
|
|
6
|
-
* where the file could not be read, and then only the name is drawn. */
|
|
7
|
-
plan?: string | null;
|
|
8
4
|
}
|
|
9
5
|
|
|
10
6
|
/** The word for the plan behind an engine, or null for one we cannot name. */
|
|
11
7
|
export declare function runsOnName(engine?: string | null): string | null;
|
|
12
8
|
|
|
13
|
-
/** The
|
|
14
|
-
export declare function
|
|
15
|
-
|
|
16
|
-
/** The whole sentence, for a hover and for a screen reader. Null for null. */
|
|
17
|
-
export declare function runsOnTitle(facts?: RunsOn): string | null;
|
|
18
|
-
|
|
19
|
-
/** The one line the walk says on a Mac that was already set up. Null for null. */
|
|
20
|
-
export declare function foundOnThisMac(facts?: RunsOn): string | null;
|
|
9
|
+
/** The one line the walk says where it was already set up. Null for null. */
|
|
10
|
+
export declare function foundOnThisComputer(facts?: RunsOn): string | null;
|
package/shared/runs-on.mjs
CHANGED
|
@@ -1,30 +1,39 @@
|
|
|
1
|
-
//
|
|
1
|
+
// THE ONE LINE SAYING IT FOUND AN ACCOUNT ALREADY ON THIS COMPUTER.
|
|
2
2
|
//
|
|
3
|
-
// From a session with a new user, 2026-10-07:
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// From a session with a new user, 2026-10-07, in her words: a new user came in,
|
|
4
|
+
// started running an agent, and was "like, 'Oh my God, where are the tokens
|
|
5
|
+
// coming from? I didn't even connect my account.'"
|
|
6
6
|
//
|
|
7
7
|
// He was right that nothing said. The app finds whichever coding agent is
|
|
8
|
-
// already signed in on the
|
|
8
|
+
// already signed in on the computer and runs every worker on that subscription
|
|
9
9
|
// (main/claude-plan.mjs, main/codex-account.mjs), and the walk is SILENT about
|
|
10
10
|
// it by design: `needsPlan` in renderer/src/plan-setup.ts skips the plan
|
|
11
11
|
// question entirely when one tool is found and signed in, which is the right
|
|
12
|
-
// call and leaves nobody told.
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// call and leaves nobody told.
|
|
13
|
+
//
|
|
14
|
+
// IT IS A ONE-TIME SURPRISE, SO IT GETS A ONE-TIME ANSWER. A row saying this
|
|
15
|
+
// permanently, in the sidebar's foot, is what this file held first; it shipped
|
|
16
|
+
// on 2026-10-07 and came out the same day. Her words: "we have to get rid of
|
|
17
|
+
// this. It should never say that on the sidebar." Somebody asks where the
|
|
18
|
+
// tokens are coming from ONCE, on their first day, so the place for the answer
|
|
19
|
+
// is the walk they are standing in.
|
|
20
|
+
//
|
|
21
|
+
// AND IT NAMES NO PLAN. The row said "Claude · Max 20x" and that was the half
|
|
22
|
+
// she wanted gone most: "very critically, I want to get rid of that Claude Max
|
|
23
|
+
// 20x plan." The question being answered is "did it connect to something of
|
|
24
|
+
// mine", not "what am I paying for", and the second one is already answered
|
|
25
|
+
// properly on each agent's own page in Settings, where somebody goes when they
|
|
26
|
+
// want it.
|
|
15
27
|
//
|
|
16
28
|
// THE PLAN'S VOCABULARY, NOT THE TOOL'S. "Claude" and "ChatGPT" here, never
|
|
17
29
|
// "Claude Code" and "Codex", which is the rule the walk already follows
|
|
18
30
|
// (renderer/src/plan-setup.ts: words people know, "never the names of the tools
|
|
19
|
-
// underneath"). The
|
|
20
|
-
//
|
|
21
|
-
// about which agent is working a row (`engineWordFor`, renderer/src/byline.ts),
|
|
22
|
-
// which is a different question with a different answer.
|
|
31
|
+
// underneath"). The tool names stay where they belong: on a byline about which
|
|
32
|
+
// agent is working a row (`engineWordFor`, renderer/src/byline.ts).
|
|
23
33
|
//
|
|
24
|
-
// NOTHING IS SAID UNTIL SOMETHING IS KNOWN.
|
|
25
|
-
//
|
|
26
|
-
// than "checking…" or a dash.
|
|
27
|
-
// a corner that says nothing: it spends the glance and answers no question.
|
|
34
|
+
// NOTHING IS SAID UNTIL SOMETHING IS KNOWN. Both functions answer null on an
|
|
35
|
+
// engine they cannot name, and the walk draws nothing at all for null rather
|
|
36
|
+
// than "checking…" or a dash.
|
|
28
37
|
|
|
29
38
|
/** The word for the plan behind an engine. Null for an engine we cannot name. */
|
|
30
39
|
export function runsOnName(engine) {
|
|
@@ -34,46 +43,16 @@ export function runsOnName(engine) {
|
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
/**
|
|
37
|
-
* THE LINE
|
|
38
|
-
* plan cannot be read.
|
|
39
|
-
*
|
|
40
|
-
* The plan is the raw label the tool itself reports, already turned into words
|
|
41
|
-
* by whoever read it (`planLabel` in main/claude-plan.mjs for Claude, the
|
|
42
|
-
* named-plan table in main/codex-account.mjs for ChatGPT). It is never
|
|
43
|
-
* invented here: a plan nobody measured, printed confidently, is the app
|
|
44
|
-
* lying about what somebody is paying for.
|
|
45
|
-
*/
|
|
46
|
-
export function runsOnLine({ engine = null, plan = null } = {}) {
|
|
47
|
-
const name = runsOnName(engine);
|
|
48
|
-
if (!name) return null;
|
|
49
|
-
const words = String(plan ?? '').trim();
|
|
50
|
-
return words ? `${name} · ${words}` : name;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* THE WHOLE SENTENCE, for a hover and for whatever reads the screen aloud. The
|
|
55
|
-
* glance surface carries two words and a plan; this is where the rest of the
|
|
56
|
-
* answer lives, and it is the half that tells a new person what they are
|
|
57
|
-
* looking at.
|
|
58
|
-
*/
|
|
59
|
-
export function runsOnTitle({ engine = null, plan = null } = {}) {
|
|
60
|
-
const name = runsOnName(engine);
|
|
61
|
-
if (!name) return null;
|
|
62
|
-
const words = String(plan ?? '').trim();
|
|
63
|
-
const plansentence = words ? ` Your ${words} plan.` : '';
|
|
64
|
-
return `Your agents run on the ${name} account already signed in on this Mac.${plansentence}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* THE ONE LINE DURING THE WALK, on the Mac where there was nothing to ask.
|
|
46
|
+
* THE ONE LINE DURING THE WALK, on the computer where there was nothing to ask.
|
|
69
47
|
*
|
|
70
48
|
* It replaces silence, not a question: somebody with no coding agent set up
|
|
71
49
|
* still gets the plan question and the setup card, and never sees this.
|
|
50
|
+
*
|
|
51
|
+
* It says computer rather than Mac: Agentbox runs on Linux too, and one
|
|
52
|
+
* sentence beats a branch, since "this computer" is true on a Mac as well.
|
|
72
53
|
*/
|
|
73
|
-
export function
|
|
54
|
+
export function foundOnThisComputer({ engine = null } = {}) {
|
|
74
55
|
const name = runsOnName(engine);
|
|
75
56
|
if (!name) return null;
|
|
76
|
-
|
|
77
|
-
if (words) return `Your agents will run on the ${name} ${words} plan already signed in on this Mac.`;
|
|
78
|
-
return `Your agents will run on the ${name} account already signed in on this Mac.`;
|
|
57
|
+
return `Your agents will run on the ${name} account already signed in on this computer.`;
|
|
79
58
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The note the app writes on a row once its ship lands (main/store.mjs), and
|
|
2
|
+
// the one reader that has to know it is the app's and not an agent's
|
|
3
|
+
// (renderer/src/format.ts, offerIsLive).
|
|
4
|
+
export const shippedNote = (sha) => (sha ? `Shipped to main as ${sha}.` : 'Shipped to main.');
|
|
5
|
+
|
|
6
|
+
export const isShippedNote = (text) => /^Shipped to main(?: as [0-9a-f]{7,40})?\.$/.test(String(text ?? '').trim());
|