agentbox-app 0.1.6
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/LICENSE +674 -0
- package/README.md +176 -0
- package/bin/agentbox.mjs +103 -0
- package/briefs/adhd-mode.md +5 -0
- package/briefs/message-rules.md +95 -0
- package/briefs/worker.md +230 -0
- package/briefs/writing-rules.md +336 -0
- package/main/account-discovery.mjs +145 -0
- package/main/account-tooling.mjs +261 -0
- package/main/agent-activity.mjs +56 -0
- package/main/agent-files.mjs +422 -0
- package/main/agent-schedule.mjs +254 -0
- package/main/agent-sessions.mjs +435 -0
- package/main/agents.mjs +1095 -0
- package/main/analytics.mjs +206 -0
- package/main/answer-modes.mjs +206 -0
- package/main/approval-prompt-server.mjs +160 -0
- package/main/approvals.mjs +487 -0
- package/main/artifact-path.mjs +144 -0
- package/main/claude-bin.mjs +311 -0
- package/main/claude-input.mjs +117 -0
- package/main/claude-models.mjs +244 -0
- package/main/claude-plan.mjs +148 -0
- package/main/claude-usage.mjs +149 -0
- package/main/code-change.mjs +383 -0
- package/main/code-file.mjs +116 -0
- package/main/codex-account.mjs +146 -0
- package/main/codex-app-server.mjs +796 -0
- package/main/codex-approvals.mjs +712 -0
- package/main/codex-bin.mjs +261 -0
- package/main/codex-compaction.mjs +49 -0
- package/main/codex-models.mjs +194 -0
- package/main/codex-session.mjs +797 -0
- package/main/codex-threads.mjs +268 -0
- package/main/codex-usage-file.mjs +192 -0
- package/main/codex-usage.mjs +149 -0
- package/main/codex-watch.mjs +35 -0
- package/main/codex.mjs +757 -0
- package/main/config.mjs +345 -0
- package/main/copy-selection.mjs +55 -0
- package/main/crash-report.mjs +158 -0
- package/main/demo.mjs +254 -0
- package/main/dev-window.mjs +103 -0
- package/main/doc-file.mjs +99 -0
- package/main/doc-scheme.mjs +126 -0
- package/main/first-run.mjs +190 -0
- package/main/folders.mjs +91 -0
- package/main/fresh-user.mjs +120 -0
- package/main/git-change.mjs +323 -0
- package/main/img-scheme.mjs +90 -0
- package/main/instruction-settings.mjs +205 -0
- package/main/ipc.mjs +1558 -0
- package/main/live-replies.mjs +77 -0
- package/main/machine.mjs +113 -0
- package/main/main.mjs +972 -0
- package/main/menu.mjs +177 -0
- package/main/notify.mjs +155 -0
- package/main/open-source-file.mjs +8 -0
- package/main/project-identity.mjs +149 -0
- package/main/provider-commands.mjs +32 -0
- package/main/rail-note.mjs +74 -0
- package/main/repeats.mjs +261 -0
- package/main/row-label.mjs +185 -0
- package/main/screen-detail.mjs +44 -0
- package/main/serve.mjs +262 -0
- package/main/settings.mjs +912 -0
- package/main/staleness.mjs +76 -0
- package/main/store/chats.mjs +188 -0
- package/main/store/home.mjs +383 -0
- package/main/store/migrate.mjs +40 -0
- package/main/store/paths.mjs +82 -0
- package/main/store/pending-writes.mjs +122 -0
- package/main/store/project-lock.mjs +200 -0
- package/main/store/project.mjs +523 -0
- package/main/store/queues.mjs +31 -0
- package/main/store/work-items.mjs +299 -0
- package/main/store/write-queue.mjs +14 -0
- package/main/store-modules.mjs +23 -0
- package/main/store.mjs +1269 -0
- package/main/supervisor.mjs +6875 -0
- package/main/task-commands.mjs +161 -0
- package/main/task-folders-offthread.mjs +84 -0
- package/main/task-folders-thread.mjs +13 -0
- package/main/task-folders.mjs +409 -0
- package/main/task-remote-control.mjs +65 -0
- package/main/task-terminals.mjs +169 -0
- package/main/terminal-access.mjs +6 -0
- package/main/updater.mjs +240 -0
- package/main/zoom-keys.mjs +54 -0
- package/main/zoom-readout.mjs +20 -0
- package/mcp/agentbox-mcp.sh +34 -0
- package/mcp/core/account.mjs +77 -0
- package/mcp/core/chat.mjs +61 -0
- package/mcp/core/documents.mjs +238 -0
- package/mcp/core/page.mjs +152 -0
- package/mcp/core/products.mjs +110 -0
- package/mcp/core/skills.mjs +126 -0
- package/mcp/core/work.mjs +377 -0
- package/mcp/stdio.mjs +70 -0
- package/mcp/tools.mjs +257 -0
- package/package.json +186 -0
- package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
- package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
- package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
- package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
- package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
- package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
- package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -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 +299 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
- package/renderer/dist/index.html +13 -0
- package/scripts/after-install.mjs +59 -0
- package/scripts/approval-server.sh +43 -0
- package/scripts/before-publish.mjs +133 -0
- package/shared/agent-import.d.mts +13 -0
- package/shared/agent-import.mjs +252 -0
- package/shared/agents.d.mts +190 -0
- package/shared/agents.mjs +937 -0
- package/shared/analytics-events.mjs +63 -0
- package/shared/answers.d.mts +12 -0
- package/shared/answers.mjs +80 -0
- package/shared/artifact-keys.d.mts +28 -0
- package/shared/artifact-keys.mjs +140 -0
- package/shared/bridge-map.mjs +128 -0
- package/shared/claude-commands.d.mts +20 -0
- package/shared/claude-commands.generated.mjs +149 -0
- package/shared/claude-commands.mjs +131 -0
- package/shared/claude-models.generated.d.mts +15 -0
- package/shared/claude-models.generated.mjs +26 -0
- package/shared/claude-usage.d.mts +16 -0
- package/shared/claude-usage.mjs +172 -0
- package/shared/codex-commands.d.mts +4 -0
- package/shared/codex-commands.mjs +17 -0
- package/shared/codex-import.d.mts +38 -0
- package/shared/codex-import.mjs +232 -0
- package/shared/codex-modes.mjs +159 -0
- package/shared/codex-usage.mjs +180 -0
- package/shared/contracts.mjs +412 -0
- package/shared/crash-scrub.mjs +182 -0
- package/shared/dashboard.mjs +481 -0
- package/shared/dead-run-trace.mjs +127 -0
- package/shared/demo-world.mjs +415 -0
- package/shared/effort-levels.d.mts +15 -0
- package/shared/effort-levels.mjs +76 -0
- package/shared/engines.d.mts +62 -0
- package/shared/engines.mjs +389 -0
- package/shared/first-run-examples.mjs +60 -0
- package/shared/first-run-line.mjs +149 -0
- package/shared/first-run-practice.d.mts +32 -0
- package/shared/first-run-practice.mjs +254 -0
- package/shared/first-run-shape.mjs +100 -0
- package/shared/fresh-user-home.mjs +189 -0
- package/shared/hook-failure.mjs +22 -0
- package/shared/install-id.mjs +39 -0
- package/shared/instruction-defaults.json +5 -0
- package/shared/library-folder.mjs +28 -0
- package/shared/notify-rules.mjs +120 -0
- package/shared/product-name.d.mts +6 -0
- package/shared/product-name.mjs +144 -0
- package/shared/project-folder-check.d.mts +22 -0
- package/shared/project-folder-check.mjs +152 -0
- package/shared/provider-commands.d.mts +8 -0
- package/shared/provider-commands.mjs +58 -0
- package/shared/rank.d.mts +19 -0
- package/shared/rank.mjs +146 -0
- package/shared/recovery.mjs +26 -0
- package/shared/referenced-files.d.mts +11 -0
- package/shared/referenced-files.mjs +122 -0
- package/shared/repeats.d.mts +45 -0
- package/shared/repeats.mjs +193 -0
- package/shared/schemes.mjs +33 -0
- package/shared/settings-terminal.d.mts +1 -0
- package/shared/settings-terminal.mjs +14 -0
- package/shared/side-build.mjs +57 -0
- package/shared/spawn-trouble.d.mts +8 -0
- package/shared/spawn-trouble.mjs +599 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +58 -0
- package/shared/usage.d.mts +53 -0
- package/shared/usage.mjs +319 -0
- package/shared/work-items.mjs +561 -0
- package/shared/work-lines.mjs +360 -0
- package/worker-permissions.json +6 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// Shared contracts: the schemas everything meets at.
|
|
2
|
+
// Plain ESM, one import. Imported by main (Node) and renderer (Vite).
|
|
3
|
+
// Validation is deliberately light: enough to make lying structurally hard
|
|
4
|
+
// (EXECUTED without evidence, dangling refs, stale ask responses), no more.
|
|
5
|
+
|
|
6
|
+
import { nameSlug } from './product-name.mjs';
|
|
7
|
+
|
|
8
|
+
export const SCHEMA_VERSION = 2;
|
|
9
|
+
|
|
10
|
+
/* ---------------------------------- IPC ---------------------------------- */
|
|
11
|
+
// Channel names, one place. Renderer subscribes/invokes only through these.
|
|
12
|
+
export const IPC = {
|
|
13
|
+
// renderer -> main
|
|
14
|
+
sendMessage: 'chat:send', // { projectId, text }
|
|
15
|
+
answerAsk: 'ask:answer', // AskResponse
|
|
16
|
+
openProject: 'project:open', // { projectId } -> ProjectSnapshot
|
|
17
|
+
listProjects: 'project:list', // -> [{ id, name, updatedAt }]
|
|
18
|
+
createProject: 'project:create', // { name } -> { id }
|
|
19
|
+
renameProject: 'project:rename', // { projectId, name } -> { ok }
|
|
20
|
+
deleteProject: 'project:delete', // { projectId } -> { ok } | { error } (refuses the open product; erases the folder from disk)
|
|
21
|
+
interruptRun: 'run:interrupt', // { projectId, chatId }
|
|
22
|
+
recoveryResume: 'recovery:resume', // { projectId, chatIds: string[] } -> { ok, resumed: string[] } resume interrupted (cut) chats: for each, ensureSession then kickoff the app-written resume brief (non-persisted, non-echoed). Chats sitting on a pending ask card are skipped (their card is the recovery). Kicking a turn off clears cutAt in the ledger, so the row leaves the Welcome Back sheet as it actually goes working.
|
|
23
|
+
queueAction: 'chat:queue-action', // { projectId, chatId, action: 'cancel'|'sendNow'|'bringBack', id } -> { ok, text?, attachments? } act on an up-next (held) message
|
|
24
|
+
closeCreation: 'creation:close', // { projectId, creationId }
|
|
25
|
+
deleteCreation: 'creation:delete', // { projectId, creationId } -> { ok } (removes the file + catalog entry, not just the tab)
|
|
26
|
+
saveCreationContent: 'creation:save', // { projectId, creationId, content } -> { ok } (founder edits a doc)
|
|
27
|
+
creationContent: 'creation:content', // { projectId, creationId } -> { content } lazy body fetch: project:open ships content only for open tabs and small docs (big closed-tab bodies made boot crawl); the renderer hydrates the rest through here on tab open
|
|
28
|
+
creationPageInlined: 'creation:page-inlined', // { projectId, creationId, page } -> { html } | { error } a folder-design page rendered fully self-contained (relative assets inlined as data URIs) for a token-free srcdoc preview
|
|
29
|
+
createCreation: 'creation:create', // { projectId, title } -> { ok, id } founder makes a markdown doc by hand (the Library's New document)
|
|
30
|
+
addTemplate: 'creation:add-template', // { projectId, template } -> { ok, id, existing? } instantiate a stock template (e.g. 'agents-tray') as a folder-backed creation; opens the existing one if the project already has it
|
|
31
|
+
libraryImport: 'library:import', // { projectId, folder, files: [{ name, mediaType, data }] } -> { ok, imported, refused } founder drops files into the Library; each lands as a kind 'file' creation
|
|
32
|
+
creationFiles: 'creation:files', // { projectId, creationId } -> { tree } | { error } nested file tree (shared/file-tree.mjs buildFileTree) for a folder-backed creation
|
|
33
|
+
creationReadFile: 'creation:read-file', // { projectId, creationId, file } -> { content } | { binary: true } | { error } read one file from a folder-backed creation; binary assets are served instead via creation-file://
|
|
34
|
+
creationSearch: 'creation:search', // { projectId, creationId, query } -> { results: [{ file, line, text }] } | { error } case-insensitive content grep over a folder-backed creation (skips binaries/.env), caps 100 total / 5 per file / 200 chars
|
|
35
|
+
creationSaveFile: 'creation:save-file', // { projectId, creationId, file, content } -> { ok } | { ok: false, error } write + commit one file in a folder-backed creation
|
|
36
|
+
creationSaveImage: 'creation:save-image', // { projectId, creationId, name, mediaType, data(base64) } -> { ok, url, path } | { ok: false, error } save a pasted/dropped image into a doc's assets/ folder, served back over creation-file://
|
|
37
|
+
creationRun: 'creation:run', // { projectId, creationId } -> { ok } start the local runtime for a folder-backed creation
|
|
38
|
+
creationStop: 'creation:stop', // { projectId, creationId } -> { ok } stop the running local runtime
|
|
39
|
+
creationRestore: 'creation:restore', // { projectId, creationId, ref? } -> { ok, restoredTo?, snapshotTag?, warning? } restore a folder-backed creation's files to `ref` (default the "original" tag); dirty work is snapshotted AND tagged first
|
|
40
|
+
setCreationStatus: 'creation:setStatus', // { projectId, creationId, status: 'history'|'exploration'|'current', replacedBy? } -> { ok, creation } | { error } what a creation IS to the company: 'history' = superseded (kept, hidden from default reading), 'exploration' = made and never adopted (kept and SHOWN, but never the live answer to anything), 'current' = the live answer (the absence of a status key). Metadata only, no files move
|
|
41
|
+
saveView: 'view:save', // { projectId, openTabs, activeTab, pinnedTabs, browsers } -> { ok } (which tabs are up; view.json)
|
|
42
|
+
// chats (multiple conversations per product)
|
|
43
|
+
listChats: 'chat:list', // { projectId } -> [{ id, title, updatedAt }]
|
|
44
|
+
createChat: 'chat:create', // { projectId } -> { id, title }
|
|
45
|
+
scrubEmpty: 'chat:scrub-empty', // { projectId, keepIds?, activeId? } -> { removed: string[] } trash abandoned zero-message chats (never the active one or a keepIds chat with unsent draft text); recoverable, never rm
|
|
46
|
+
quickCapture: 'chat:quick-capture', // { projectId?, text, attachments?, model? } -> { id, title, projectId } | { error } create a chat in the current project and send its first message WITHOUT changing the active chat (Cmd+J quick capture); model pins the new chat's brain at birth ('auto'/omitted = the workspace default), so it applies to the first message; emits chat.created + chat.status like a normal send
|
|
47
|
+
|
|
48
|
+
openChat: 'chat:open', // { projectId, chatId } -> { chatId, chat: ChatMessage[] }
|
|
49
|
+
renameChat: 'chat:rename', // { projectId, chatId, title } -> { ok }
|
|
50
|
+
deleteChat: 'chat:delete', // { projectId, chatId } -> { ok } (session closed first, log trashed)
|
|
51
|
+
chatModel: 'chat:model', // { projectId, chatId, model? } -> { model } get/set THIS chat's model; setting also stamps project.lastModel for future chats
|
|
52
|
+
setChatUnread: 'chat:set-unread', // { projectId, chatId, unread } -> { ok } flip a chat's read state WITHOUT opening it: true resurfaces it under Your turn / For you (sidebar "Mark as unread", tray drag), false puts it away (tray drag to Quiet). Goes through the status ledger so it persists and the next feed sync agrees.
|
|
53
|
+
// settings + app
|
|
54
|
+
getSettings: 'settings:get', // -> settings + { dataDir, imapPasswordSet }
|
|
55
|
+
setSettings: 'settings:set', // patch (imapPassword goes to the Keychain, never the file)
|
|
56
|
+
revealData: 'app:reveal-data', // open the configured store root in Finder
|
|
57
|
+
openExternal: 'app:open-external', // { url } -> { ok } open a link in the real browser (doc-editor "Visit"); http/https/mailto only
|
|
58
|
+
// GitHub connector (device flow lives in main; renderer only relays)
|
|
59
|
+
githubConnectStart: 'github:connect-start', // -> { userCode, verificationUri } (opens browser, starts polling)
|
|
60
|
+
githubConnectWait: 'github:connect-wait', // -> { ok, login } | { error } resolves when the poll finishes
|
|
61
|
+
githubStatus: 'github:status', // -> { connected, login? }
|
|
62
|
+
githubDisconnect: 'github:disconnect', // -> { ok } removes the Keychain token
|
|
63
|
+
// Personal Telegram bot connector. Token validation and long polling stay
|
|
64
|
+
// in main; the renderer only drives setup and reads connection state.
|
|
65
|
+
telegramStatus: 'telegram:status', // -> configured/link/listener state
|
|
66
|
+
telegramConnect: 'telegram:connect', // { token } validates, Keychain-saves, and starts linking
|
|
67
|
+
telegramBeginLink: 'telegram:begin-link', // -> a fresh one-tap t.me link
|
|
68
|
+
telegramDisconnect: 'telegram:disconnect', // -> removes token, link, and local routing state
|
|
69
|
+
// Per-product secrets (the founder's OWN product API keys), stored in the
|
|
70
|
+
// macOS Keychain per project, injected into a runnable creation's env at
|
|
71
|
+
// spawn. NO channel ever returns a secret VALUE: list is names-only, set/
|
|
72
|
+
// delete return only { ok }. Values ride set one-way and are never read back.
|
|
73
|
+
secretsList: 'secrets:list', // { projectId } -> { names: string[] }
|
|
74
|
+
secretsSet: 'secrets:set', // { projectId, name, value } -> { ok } | { error }
|
|
75
|
+
secretsDelete: 'secrets:delete', // { projectId, name } -> { ok }
|
|
76
|
+
usageGet: 'usage:get', // -> { projects: { id: { name, models } }, updatedAt } token/cost ledger (best effort, never a gate)
|
|
77
|
+
// Plan and credits surface. Both NEVER throw and NEVER gate; they mirror the
|
|
78
|
+
// billing client's { ok, reason } philosophy so the renderer can render
|
|
79
|
+
// signed-out / unreachable states honestly.
|
|
80
|
+
billingSummary: 'billing:summary', // -> { ok:true, balance, plan, status, menu } | { ok:false, reason, menu } balance (GET /balance) + menu (GET /menu) together
|
|
81
|
+
billingCheckout: 'billing:checkout', // { kind: 'subscription'|'topup' } -> { ok:true } (opens Stripe Checkout in the system browser) | { ok:false, reason }
|
|
82
|
+
billingPortal: 'billing:portal', // -> { ok:true, url } Stripe billing portal (past invoices, card, cancel) | { ok:false, reason }; reason:'no-customer' when they never checked out
|
|
83
|
+
// Cloud (Supabase; auth + sync live in main, tokens in the Keychain)
|
|
84
|
+
authStatus: 'auth:status', // -> { signedIn, email?, isAdmin, gateRequired, sync? }
|
|
85
|
+
// `authSignInGoogle` was here. It opened the system browser and waited for a
|
|
86
|
+
// deep link on the other product's URL scheme to come back.
|
|
87
|
+
authSignInPassword: 'auth:signin-password', // { email, password } -> { ok, error?, code? } signInWithPassword; on ok fans out auth.changed
|
|
88
|
+
authSignUpPassword: 'auth:signup-password', // { email, password } -> { ok, error?, code? } signUp (no email confirmation); on ok fans out auth.changed
|
|
89
|
+
// NOTE: there is deliberately no emailed-code door.
|
|
90
|
+
// `auth:start`/`auth:verify` were removed on 2026-08-06; the product now
|
|
91
|
+
// sends no email.
|
|
92
|
+
authSignOut: 'auth:signout', // -> { ok }
|
|
93
|
+
// Consent-based, move-only migration of PRE-account projects at the legacy
|
|
94
|
+
// root into the signed-in account's space (main/store/account.mjs).
|
|
95
|
+
migrationStatus: 'migration:status', // -> { pending, count, names } does this Mac have legacy projects this account hasn't answered about?
|
|
96
|
+
migrationDecide: 'migration:decide', // { choice: 'bring'|'leave' } -> { ok, moved?, count?, manifestPath?, error? } bring moves each dir (manifest-first, verified, never overwrites/deletes); leave records the dismissal
|
|
97
|
+
// Managed database (Neon) viewer. Every channel takes { projectId, creationId }
|
|
98
|
+
// where creationId is the APP creation that owns the database (the viewer
|
|
99
|
+
// creation's meta.databaseFor). All handlers return { error } on failure.
|
|
100
|
+
dbOverview: 'db:overview', // -> { record, tables, usage, userCount }
|
|
101
|
+
dbRows: 'db:rows', // + { schema, table, limit, offset, orderBy?, orderDir? } -> { columns, rows, total }
|
|
102
|
+
dbMutate: 'db:mutate', // + { action: 'insert'|'update'|'delete', schema, table, values?, where? } -> { ok, rowCount }
|
|
103
|
+
dbQuery: 'db:query', // + { sql } -> { columns, rows, rowCount, truncated, command, durationMs }
|
|
104
|
+
dbUsers: 'db:users', // + { limit?, offset? } -> { enabled, users, total }
|
|
105
|
+
dbUserDelete: 'db:user-delete', // + { userId } -> { ok }
|
|
106
|
+
dbExport: 'db:export', // -> { ok, file, tables, totalRows, bytes } (save dialog, full SQL dump)
|
|
107
|
+
dbConnection: 'db:connection', // -> { pooled, direct, authBaseUrl } (deliberate reveal)
|
|
108
|
+
dbTransfer: 'db:transfer', // -> { ok, url?, expiresAt?, reason? } claim link to the founder's own Neon account
|
|
109
|
+
dbDelete: 'db:delete', // -> { ok } deletes the Neon project (recoverable 7 days server-side) + local record
|
|
110
|
+
// Feedback-pipeline review records (the Agents Tray's founder review surface).
|
|
111
|
+
// Reviews live in per-creation `reviews` records collections; list AGGREGATES
|
|
112
|
+
// every one in the project (the ledger creation differs per project).
|
|
113
|
+
reviewsList: 'reviews:list', // { projectId } -> { reviews: [{ creationId, id, key, ts, data }] } | { error }
|
|
114
|
+
reviewUpdate: 'reviews:update', // { projectId, creationId, key, status: 'pending'|'validated'|'flagged', flagNote? } -> { ok, review } | { error } upserts the SAME record; flag requires a note
|
|
115
|
+
// The dashboard: the company's real state, folded from the append-only
|
|
116
|
+
// ledger at `<projectDir>/dashboard.jsonl` (main/store/dashboard.mjs) with
|
|
117
|
+
// PostHog-sourced metrics resolved against the founder's own analytics.
|
|
118
|
+
dashboardGet: 'dashboard:get', // { projectId } -> { dashboard } | { error } folded picture, metrics resolved; never throws on an analytics failure (the metric stays honestly absent)
|
|
119
|
+
// Asked when the founder clicks a number, never on page load. `sources: null`
|
|
120
|
+
// means this product is not on managed analytics, which is the renderer's
|
|
121
|
+
// signal that the number cannot be clicked into at all.
|
|
122
|
+
dashboardSources: 'dashboard:sources', // { projectId, event, days } -> { sources: { event, days, rows: [{ source, campaign, people, hits }], absent } | null } | { error }
|
|
123
|
+
dashboardPatch: 'dashboard:patch', // { projectId, patch, source: 'founder'|'agent' } -> { ok, dashboard } | { error } append one partial patch; a founder patch is not overruled by a later agent patch to the same field
|
|
124
|
+
// main -> renderer (single event channel, payload = StreamEvent)
|
|
125
|
+
//
|
|
126
|
+
// The channel's own name, not the other product's. Both ends read it from
|
|
127
|
+
// this constant and nothing else in the repo spells it out, so the string was
|
|
128
|
+
// free to change; it was verified unreferenced before it did.
|
|
129
|
+
stream: `${nameSlug}:stream`,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// StreamEvent.type values (main -> renderer). One channel, discriminated union.
|
|
133
|
+
export const STREAM = {
|
|
134
|
+
appNotify: 'app.notify', // { title, body } cloud brain ping -> the web client shows a browser Notification when permitted (desktop uses native notifications main-side and never emits this)
|
|
135
|
+
chatDelta: 'chat.delta', // { messageId, text } incremental agent text
|
|
136
|
+
chatMessage: 'chat.message', // ChatMessage completed (agent or user echo)
|
|
137
|
+
runStatus: 'run.status', // { label, state: 'working'|'idle'|'error'|'needs-cleanup', detail?, stalled? } stalled: the turn is alive but the transport is failing and retrying (the founder must never read a retry loop as progress)
|
|
138
|
+
creationOpened: 'creation.opened', // Creation envelope
|
|
139
|
+
creationUpdated: 'creation.updated', // Creation envelope (same id, new content)
|
|
140
|
+
creationDeleted: 'creation.deleted', // { creationId } (founder deleted it; drop from catalog + any open tab)
|
|
141
|
+
creationRuntime: 'creation.runtime', // { projectId, creationId, status: 'idle'|'starting'|'running'|'stopping'|'error', port?, previewUrl?, logTail? } dev-server runtime status for a folder-backed app creation (project-wide, not chat-scoped); previewUrl only in cloud, where the port rides a public subdomain
|
|
142
|
+
askRequest: 'ask.request', // AskRequest
|
|
143
|
+
askResolved: 'ask.resolved', // { requestId }
|
|
144
|
+
runFrame: 'run.frame', // { runId, seq, file } screenshot FILE REF, never blobs
|
|
145
|
+
runFrameLive: 'run.frame.live', // { runId, seq, src, w, h } screencast frame DESCRIPTOR; src is a
|
|
146
|
+
// a live:// URL into main's in-memory cache (display-only,
|
|
147
|
+
// latest wins, never evidence). run.frame stays the evidence still.
|
|
148
|
+
runCursor: 'run.cursor', // { runId, seq, x, y, click } where the persona's hands are, in the
|
|
149
|
+
// frame's CSS pixels; the live view glides a pointer there
|
|
150
|
+
runEvent: 'run.event', // RunEvent (for live narration / transcript)
|
|
151
|
+
sessionError: 'session.error', // { message, kind: 'rate-limit'|'crash'|'auth'|'other' }
|
|
152
|
+
receipts: 'chat.receipts', // { messageId, receipts: string[] }
|
|
153
|
+
chatRenamed: 'chat.renamed', // { chatId, title } (auto-title from the first message)
|
|
154
|
+
projectRenamed: 'project.renamed', // { projectId, name } the agent named an idea-stage project (rename_project); the sidebar/top bar update live
|
|
155
|
+
chatCreated: 'chat.created', // { chat: { id, title, updatedAt } } the agent made a new chat; the sidebar adds it live. NOT chat-scoped, so it folds whatever chat is active.
|
|
156
|
+
chatMessageRemoved: 'chat.message-removed', // { id } drop a message from the log/dock (an up-next message the founder cancelled or pulled back)
|
|
157
|
+
chatModel: 'chat.model', // { model } the agent switched THIS conversation's model; the chip reflects it live
|
|
158
|
+
chatWorkers: 'chat.workers', // { op: 'start'|'end', worker: { id, label, model } } a delegated worker session (e.g. a build session) started or finished on this conversation; the model picker's "Working now" roster reflects who is actually running, on which model
|
|
159
|
+
compaction: 'chat.compaction', // { state: 'running'|'done'|'failed' } dedicated, persistent compaction indicator (distinct from run.status so agent work never clobbers it)
|
|
160
|
+
browserOpen: 'browser-open', // { url } a renderer link escape (http/https) main routes into the in-app browser: open, or focus a tab already at that url. GLOBAL, not chat-scoped, so it folds regardless of the active project/chat.
|
|
161
|
+
chatStatus: 'chat.status', // { projectId, chatId, status: { working, workingSince, askPending, askSince, doneAt, doneDetail, readAt, cutAt, cutKind } } per-chat sidebar status ledger (main/status-ledger.mjs). cutAt (epoch ms) + cutKind ('sleep'|'restart') carry the INTERRUPTED state: cutAt set with no new turn since == Your turn, unread, like a pending question. DELIBERATELY NOT in CHAT_SCOPED_EVENTS: the sidebar must reflect BACKGROUND chats too, so it folds into a per-chat map regardless of the active chat (the project gate in foldStream still applies). Persisted in main (with a rolling backup); the project:open snapshot re-seeds the renderer map on startup.
|
|
162
|
+
recoveryWake: 'recovery.wake', // { projectId, wokeAt, chats: [{ id, title, cutAt, cutKind, ranForMs, lastFinished, askPending }] } ONE signal on system resume (powerMonitor) carrying the OPEN project's interrupted chats, so the renderer can raise the Welcome Back sheet. ranForMs = cutAt - workingSince (null if unknown); lastFinished = the last tool trail line before the cut (null if none); askPending marks a row cut mid-question (offer Open, not Resume). GLOBAL, not chat-scoped. Ordinary chat.status events also flow for each entry, so the sidebar is correct even if the sheet never shows.
|
|
163
|
+
authChanged: 'auth.changed', // { signedIn, email? } main finished a sign-in (Google deep link) or sign-out; the gate lifts without polling. GLOBAL, not chat-scoped.
|
|
164
|
+
billingChanged: 'billing.changed', // { summary } main refreshed the billing summary after a finished checkout; the Plan and usage room updates without a manual reload. GLOBAL, not chat-scoped. Nothing emits or listens for this today.
|
|
165
|
+
dashboardUpdated: 'dashboard.updated', // { projectId } an agent (in ANY conversation) appended to the dashboard ledger; the page refetches. Carries no payload on purpose: the fold plus the analytics resolution happen in main, and several chats may write at once, so the renderer asks for the current truth rather than folding a patch it may have raced. NOT chat-scoped (the project gate in foldStream still applies).
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Stream events scoped to ONE conversation: the renderer folds these only when
|
|
169
|
+
// event.chatId matches the active chat. Everything else (creations, run frames)
|
|
170
|
+
// is project-wide.
|
|
171
|
+
export const CHAT_SCOPED_EVENTS = [
|
|
172
|
+
'chat.delta', 'chat.message', 'chat.message-removed', 'chat.receipts', 'ask.request', 'ask.resolved',
|
|
173
|
+
'run.status', 'session.error', 'chat.model', 'chat.workers', 'chat.compaction',
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
/* ------------------------------ chat + asks ------------------------------ */
|
|
177
|
+
// ChatMessage: { id, role: 'user'|'agent', text, ts, receipts?: string[] }
|
|
178
|
+
|
|
179
|
+
export function makeAskRegistry() {
|
|
180
|
+
// Idempotent ask_user round-trips: one pending request at a time per session,
|
|
181
|
+
// stable requestId, stale/duplicate answers rejected, timeout expires cleanly.
|
|
182
|
+
const pending = new Map(); // requestId -> { resolve, timer }
|
|
183
|
+
return {
|
|
184
|
+
register(request, resolve, onTimeout) {
|
|
185
|
+
if (!request?.requestId) throw new Error('ask request needs requestId');
|
|
186
|
+
if (pending.has(request.requestId)) return false; // duplicate register = no-op
|
|
187
|
+
const timer = request.timeoutMs
|
|
188
|
+
? setTimeout(() => { pending.delete(request.requestId); onTimeout?.(); }, request.timeoutMs)
|
|
189
|
+
: null;
|
|
190
|
+
pending.set(request.requestId, { resolve, timer });
|
|
191
|
+
return true;
|
|
192
|
+
},
|
|
193
|
+
answer(response) {
|
|
194
|
+
const entry = pending.get(response?.requestId);
|
|
195
|
+
if (!entry) return { ok: false, reason: 'stale-or-unknown' };
|
|
196
|
+
clearTimeout(entry.timer);
|
|
197
|
+
pending.delete(response.requestId);
|
|
198
|
+
entry.resolve(response);
|
|
199
|
+
return { ok: true };
|
|
200
|
+
},
|
|
201
|
+
cancelAll(reason = 'cancelled') {
|
|
202
|
+
for (const [id, entry] of pending) {
|
|
203
|
+
clearTimeout(entry.timer);
|
|
204
|
+
entry.resolve({ requestId: id, cancelled: true, reason });
|
|
205
|
+
}
|
|
206
|
+
pending.clear();
|
|
207
|
+
},
|
|
208
|
+
has(requestId) { return pending.has(requestId); },
|
|
209
|
+
get size() { return pending.size; },
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* -------------------------------- creations ------------------------------- */
|
|
214
|
+
export const CREATION_KINDS = [
|
|
215
|
+
'markdown', // the workhorse: reports, ICP docs, plans (rendered as doc)
|
|
216
|
+
'findings', // findings report (evidence-first order, trust spine)
|
|
217
|
+
'live-run', // live browser view of a persona driving the product
|
|
218
|
+
'persona', // persona profile
|
|
219
|
+
'transcript', // interview / session transcript
|
|
220
|
+
'design', // rendered HTML in a sandboxed iframe
|
|
221
|
+
'canvas', // agent-authored HTML on an infinite pannable/zoomable canvas
|
|
222
|
+
// (camera + ground only; everything on the board is the HTML)
|
|
223
|
+
'app', // folder-backed runnable app (dir + entry, has a local runtime;
|
|
224
|
+
// the ONE creation for a product: code, database views, and
|
|
225
|
+
// the DATABASE.md/DEPLOYMENT.md receipts all live here)
|
|
226
|
+
'file', // founder-imported asset (video, image, audio, anything):
|
|
227
|
+
// bytes live at envelope.path, the viewer renders by
|
|
228
|
+
// extension via creation-file://, content is never read
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
// The kinds the AGENT may mint via open_creation. The other two stay in
|
|
232
|
+
// CREATION_KINDS only because internal code paths build them: live-run by
|
|
233
|
+
// start_test_run, file by founder drag-drop.
|
|
234
|
+
export const AGENT_CREATION_KINDS = [
|
|
235
|
+
// NOTE: 'findings' was retired 2026-07-23. A test run's report is now a
|
|
236
|
+
// plain markdown doc (fewer primitives is the goal), so the agent never
|
|
237
|
+
// mints a bespoke report type. 'findings' stays in CREATION_KINDS only so
|
|
238
|
+
// legacy reports already on disk remain valid; nothing creates new ones and
|
|
239
|
+
// there is no special renderer (a legacy findings creation renders as
|
|
240
|
+
// markdown, converted on the fly).
|
|
241
|
+
'markdown', 'persona', 'transcript', 'design', 'canvas', 'app',
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
// The kinds whose content IS text and must reach disk as a string. A doc saved
|
|
245
|
+
// as an editor's JSON tree instead renders as nothing at all: the viewer tests
|
|
246
|
+
// `typeof content === 'string'`, an object fails it, and the fallback renderer
|
|
247
|
+
// has no branch for that shape, so the founder is shown an empty panel beside a
|
|
248
|
+
// chat that says the document is open (reported twice, on two different
|
|
249
|
+
// documents, 2026-07-30 and 2026-07-31).
|
|
250
|
+
export const TEXT_CREATION_KINDS = ['markdown', 'persona', 'transcript', 'design', 'canvas'];
|
|
251
|
+
|
|
252
|
+
const BLOCK_NODES = new Set([
|
|
253
|
+
'doc', 'paragraph', 'heading', 'blockquote', 'listItem', 'bulletList',
|
|
254
|
+
'orderedList', 'codeBlock', 'horizontalRule', 'tableRow',
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
function gatherText(node, out) {
|
|
258
|
+
if (!node || typeof node !== 'object') return;
|
|
259
|
+
if (Array.isArray(node)) { for (const n of node) gatherText(n, out); return; }
|
|
260
|
+
if (typeof node.text === 'string') { out.push(node.text); return; }
|
|
261
|
+
if (Array.isArray(node.content)) {
|
|
262
|
+
for (const n of node.content) gatherText(n, out);
|
|
263
|
+
if (BLOCK_NODES.has(node.type)) out.push('\n\n');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The text of a creation whose content should be a string, whatever shape it
|
|
269
|
+
* actually arrived in. Used at BOTH ends on purpose: the save path so a
|
|
270
|
+
* malformed write never lands, and the viewer so the documents already on disk
|
|
271
|
+
* are readable without anyone rewriting them.
|
|
272
|
+
*
|
|
273
|
+
* It never returns empty for content that exists. Falling back to the raw JSON
|
|
274
|
+
* is ugly, and it is the point: a founder can see what is there, copy it, and
|
|
275
|
+
* ask the agent to rewrite it. A blank panel tells them their work is gone.
|
|
276
|
+
*/
|
|
277
|
+
export function creationText(content) {
|
|
278
|
+
if (typeof content === 'string') return content;
|
|
279
|
+
if (content == null) return '';
|
|
280
|
+
if (typeof content !== 'object') return String(content);
|
|
281
|
+
for (const key of ['markdown', 'body', 'text']) {
|
|
282
|
+
if (typeof content[key] === 'string') return content[key];
|
|
283
|
+
}
|
|
284
|
+
const parts = [];
|
|
285
|
+
gatherText(content, parts);
|
|
286
|
+
const text = parts.join('').replace(/\n{3,}/g, '\n\n').trim();
|
|
287
|
+
if (text) return text;
|
|
288
|
+
return ['```json', JSON.stringify(content, null, 2), '```'].join('\n');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Creation envelope: { id, kind, title, ts, schemaVersion, meta?, and exactly
|
|
292
|
+
// one of: path (project-relative single file) or dir (project-relative
|
|
293
|
+
// folder, requires entry: the file to run/open within that folder) }
|
|
294
|
+
// Content lives on disk, not the envelope.
|
|
295
|
+
export function validateCreation(c) {
|
|
296
|
+
const errors = [];
|
|
297
|
+
if (!c?.id) errors.push('creation.id required');
|
|
298
|
+
if (!CREATION_KINDS.includes(c?.kind)) errors.push(`creation.kind must be one of ${CREATION_KINDS.join('|')}`);
|
|
299
|
+
if (!c?.title) errors.push('creation.title required');
|
|
300
|
+
const hasPath = typeof c?.path === 'string' && c.path.length > 0;
|
|
301
|
+
const hasDir = typeof c?.dir === 'string' && c.dir.length > 0;
|
|
302
|
+
if (hasPath === hasDir) errors.push('creation needs exactly one of path (single file) or dir (folder-backed)');
|
|
303
|
+
if (hasDir && !c?.entry) errors.push('folder-backed creation requires entry');
|
|
304
|
+
return errors;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/* -------------------------------- findings -------------------------------- */
|
|
308
|
+
export const PROVENANCE = ['EXECUTED', 'INFERRED'];
|
|
309
|
+
export const SEVERITIES = ['critical', 'major', 'minor', 'note'];
|
|
310
|
+
|
|
311
|
+
// Finding: { id, title, provenance, severity, category, trigger, expected,
|
|
312
|
+
// actual, quote?, quoteCite?, repro: string[],
|
|
313
|
+
// evidenceRefs: [{ type:'event', runId, seq } | { type:'screenshot', runId, file }] }
|
|
314
|
+
//
|
|
315
|
+
// The structural honesty rule (Codex round 2): EXECUTED requires evidence that
|
|
316
|
+
// RESOLVES. resolver = { eventSeqs(runId): Set<number>, screenshots(runId): Set<string> }
|
|
317
|
+
export function validateFinding(f, resolver) {
|
|
318
|
+
const errors = [];
|
|
319
|
+
if (!f?.id) errors.push('finding.id required');
|
|
320
|
+
if (!f?.title) errors.push('finding.title required');
|
|
321
|
+
if (!PROVENANCE.includes(f?.provenance)) errors.push('finding.provenance must be EXECUTED|INFERRED');
|
|
322
|
+
if (!SEVERITIES.includes(f?.severity)) errors.push('finding.severity invalid');
|
|
323
|
+
if (f?.provenance === 'EXECUTED') {
|
|
324
|
+
if (!Array.isArray(f.evidenceRefs) || f.evidenceRefs.length === 0) {
|
|
325
|
+
errors.push(`finding ${f.id}: EXECUTED requires non-empty evidenceRefs`);
|
|
326
|
+
} else if (resolver) {
|
|
327
|
+
for (const ref of f.evidenceRefs) {
|
|
328
|
+
if (ref.type === 'event') {
|
|
329
|
+
if (!resolver.eventSeqs?.(ref.runId)?.has(ref.seq)) {
|
|
330
|
+
errors.push(`finding ${f.id}: dangling event ref ${ref.runId}#${ref.seq}`);
|
|
331
|
+
}
|
|
332
|
+
} else if (ref.type === 'screenshot') {
|
|
333
|
+
if (!resolver.screenshots?.(ref.runId)?.has(ref.file)) {
|
|
334
|
+
errors.push(`finding ${f.id}: dangling screenshot ref ${ref.runId}/${ref.file}`);
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
errors.push(`finding ${f.id}: unknown evidenceRef type ${ref?.type}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (!Array.isArray(f.repro) || f.repro.length === 0) {
|
|
342
|
+
errors.push(`finding ${f.id}: EXECUTED requires repro steps`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return errors;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Report: { schemaVersion, title, kicker, receipts: { sessions, minutes, findings },
|
|
349
|
+
// intro, findings: Finding[], coverage?, wtp? (QUARANTINED, never a headline),
|
|
350
|
+
// dissent?, theDo: { title, body } } Reports END in a DO.
|
|
351
|
+
export function validateReport(r, resolver) {
|
|
352
|
+
const errors = [];
|
|
353
|
+
if (!r?.title) errors.push('report.title required');
|
|
354
|
+
if (!r?.theDo?.title) errors.push('report must end in a DO (theDo.title required)');
|
|
355
|
+
if (!Array.isArray(r?.findings) || r.findings.length === 0) errors.push('report.findings required');
|
|
356
|
+
for (const f of r?.findings ?? []) errors.push(...validateFinding(f, resolver));
|
|
357
|
+
return errors;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* ---------------------------------- runs ---------------------------------- */
|
|
361
|
+
export const BROWSER_MODES = ['playwright', 'patchright'];
|
|
362
|
+
|
|
363
|
+
// RunConfig: { runId, productDir, entryUrl, personaIds: string[],
|
|
364
|
+
// skillVersions: {}, browserMode, model, startedAt, schemaVersion }
|
|
365
|
+
export function validateRunConfig(c) {
|
|
366
|
+
const errors = [];
|
|
367
|
+
if (!c?.runId) errors.push('runConfig.runId required');
|
|
368
|
+
if (!c?.entryUrl) errors.push('runConfig.entryUrl required');
|
|
369
|
+
if (!BROWSER_MODES.includes(c?.browserMode)) errors.push('runConfig.browserMode invalid');
|
|
370
|
+
if (!Array.isArray(c?.personaIds) || c.personaIds.length === 0) errors.push('runConfig.personaIds required');
|
|
371
|
+
return errors;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// RunEvent (events.jsonl lines): { seq, ts, type, ...payload }
|
|
375
|
+
export const RUN_EVENT_TYPES = [
|
|
376
|
+
'action', // { tool, args, durationMs, result? } a browser/tool action
|
|
377
|
+
'screenshot', // { file } file ref relative to the run dir
|
|
378
|
+
'heartbeat', // { state } liveness for the quiet chip
|
|
379
|
+
'hygiene', // HygieneEntry created/updated
|
|
380
|
+
'tool', // { name, durationMs } non-browser tool timing
|
|
381
|
+
'note', // { text } agent narration
|
|
382
|
+
'usage', // { model, inputTokens?, outputTokens?, costUsd? } best effort,
|
|
383
|
+
// partial under subscription auth; NEVER a dogfood gate
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
// HygieneEntry: { id, kind: 'account'|'row'|'webhook'|'email'|'other', identifier,
|
|
387
|
+
// createdAt, cleanup: 'auto'|'manual', status: 'pending'|'cleaned'|'dismissed',
|
|
388
|
+
// reason? } A run with any status:'pending' entry is 'needs-cleanup', which is
|
|
389
|
+
// a COMPLETION GATE: the UI keeps the run open until each entry is cleaned or
|
|
390
|
+
// dismissed with a persisted reason.
|
|
391
|
+
export function runNeedsCleanup(hygieneEntries) {
|
|
392
|
+
return (hygieneEntries ?? []).some((h) => h.status === 'pending');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Payment rail rule (concrete, v1): never enter card/payment fields; any payment
|
|
396
|
+
// URL/form pauses the run and asks the user. Exported so the browser driver and
|
|
397
|
+
// the user-test skill reference ONE rule.
|
|
398
|
+
export const PAYMENT_RULE =
|
|
399
|
+
'NEVER enter card or payment details. On any payment page, checkout form, or ' +
|
|
400
|
+
'card field: stop, record a hygiene note, and ask the user how to proceed.';
|
|
401
|
+
|
|
402
|
+
/* ------------------------- diff-rerun noise honesty ------------------------ */
|
|
403
|
+
// v1 makes NO numeric delta claims by default. Count-level comparisons are
|
|
404
|
+
// UNCALIBRATED unless >= 2 prior same-config baseline runs exist.
|
|
405
|
+
export function noiseBand(baselineFindingCounts) {
|
|
406
|
+
if (!Array.isArray(baselineFindingCounts) || baselineFindingCounts.length < 2) {
|
|
407
|
+
return { calibrated: false, label: 'UNCALIBRATED' };
|
|
408
|
+
}
|
|
409
|
+
const min = Math.min(...baselineFindingCounts);
|
|
410
|
+
const max = Math.max(...baselineFindingCounts);
|
|
411
|
+
return { calibrated: true, min, max, label: `${min}-${max} across ${baselineFindingCounts.length} baseline runs` };
|
|
412
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// A crash report is scrubbed BEFORE it leaves, never after (the founder,
|
|
2
|
+
// 2026-08-19, her yes on). The order matters and it is the whole design: a
|
|
3
|
+
// reporter that ships a raw error and filters at the far end has already put a
|
|
4
|
+
// stranger's home directory, username and client project name on someone else's
|
|
5
|
+
// server, and no retention policy takes that back.
|
|
6
|
+
//
|
|
7
|
+
// What a real error carries, measured 2026-08-19 (reports/analytics-evaluation.html
|
|
8
|
+
// in the Agentbox store): a plain ENOENT puts the user's home dir, their username
|
|
9
|
+
// and their project folder name in THREE places at once — `err.message`,
|
|
10
|
+
// `err.path`, and the first line of `err.stack`. the app's own code adds a fourth:
|
|
11
|
+
// of 46 `throw new Error` sites, several interpolate a user string straight into
|
|
12
|
+
// the message (`no such product: ${slug}`, where the slug is the name of the
|
|
13
|
+
// project someone is working on).
|
|
14
|
+
//
|
|
15
|
+
// So this file is the only place that decides what a report may contain, and it
|
|
16
|
+
// works by ALLOWLIST: a payload is built field by field out of things we know
|
|
17
|
+
// are safe, never by copying an error and deleting the bad parts. Deleting is
|
|
18
|
+
// how the fourth place gets missed.
|
|
19
|
+
//
|
|
20
|
+
// The four rules, in the founder's terms:
|
|
21
|
+
// 1. The home directory becomes `~`, everywhere, in every string.
|
|
22
|
+
// 2. Below a folder root the names go and the DEPTH stays. `~/<home+4>` says
|
|
23
|
+
// "four levels under home" and names none of them.
|
|
24
|
+
// 3. `err.path`, `err.dest` and the rest of the syscall's arguments are gone
|
|
25
|
+
// outright. The syscall NAME (`open`) and the code (`ENOENT`) survive,
|
|
26
|
+
// because they debug and they carry nothing.
|
|
27
|
+
// 4. A stack keeps only the frames inside the app bundle. Everything else is
|
|
28
|
+
// counted, not sent.
|
|
29
|
+
// And one line under all of them: NO TASK TEXT, EVER. In Agentbox a task's title
|
|
30
|
+
// and body ARE the user's prompt, so any payload carrying one turns the page's
|
|
31
|
+
// promise about prompts into a lie rather than an imprecision.
|
|
32
|
+
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
|
|
35
|
+
// Anything that looks like a filesystem path: a `/` or `~/` start, then
|
|
36
|
+
// everything up to whitespace or a closing quote/bracket. Deliberately greedy
|
|
37
|
+
// about what counts as a path, because a missed one is a leak and a false
|
|
38
|
+
// positive costs a word of debug detail.
|
|
39
|
+
const PATH_RE = /(?:file:\/\/)?(?:~|\/)[^\s'"`)\]},;]*/g;
|
|
40
|
+
|
|
41
|
+
// A redacted token always carries `<`, which is how later passes tell "already
|
|
42
|
+
// handled" from "still raw" without re-parsing.
|
|
43
|
+
const MARK = '<';
|
|
44
|
+
|
|
45
|
+
function depthOf(rest) {
|
|
46
|
+
return rest.split('/').filter(Boolean).length;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A path inside our own bundle, which `scrubPath` has already made relative to
|
|
50
|
+
// the bundle root and so cannot contain anything of theirs. It is the same
|
|
51
|
+
// string in every report from every Mac, which is exactly why it may be set
|
|
52
|
+
// aside before we go looking for the person: it carries no information about
|
|
53
|
+
// them. EXPORTED because the check below and the tests both have to set the
|
|
54
|
+
// same thing aside, and two definitions of "ours" is how they drift apart.
|
|
55
|
+
export function withoutOurOwnPaths(text) {
|
|
56
|
+
return String(text).replace(/app:[^\s'"`)\]},;]*/g, ' ');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// One path token in, one safe token out.
|
|
60
|
+
export function scrubPath(raw, ctx = {}) {
|
|
61
|
+
const home = ctx.home ? path.resolve(ctx.home) : null;
|
|
62
|
+
const appDir = ctx.appDir ? path.resolve(ctx.appDir) : null;
|
|
63
|
+
let p = raw.startsWith('file://') ? raw.slice('file://'.length) : raw;
|
|
64
|
+
// A trailing `:12:5` from a stack frame is line and column, not path.
|
|
65
|
+
let tail = '';
|
|
66
|
+
const lineCol = p.match(/(:\d+(?::\d+)?)$/);
|
|
67
|
+
if (lineCol) { tail = lineCol[1]; p = p.slice(0, -tail.length); }
|
|
68
|
+
if (home && (p === '~' || p.startsWith('~/'))) p = path.join(home, p.slice(1));
|
|
69
|
+
|
|
70
|
+
// Inside the app bundle is OUR code, which is the one thing a crash report
|
|
71
|
+
// exists to tell us. It is also the only path with no user in it.
|
|
72
|
+
if (appDir && (p === appDir || p.startsWith(appDir + path.sep))) {
|
|
73
|
+
const rel = path.relative(appDir, p).split(path.sep).join('/');
|
|
74
|
+
return `app:${rel || '.'}${tail}`;
|
|
75
|
+
}
|
|
76
|
+
if (home && (p === home || p.startsWith(home + path.sep))) {
|
|
77
|
+
const d = depthOf(p.slice(home.length));
|
|
78
|
+
return d ? `<~+${d}>${tail}` : '<~>';
|
|
79
|
+
}
|
|
80
|
+
if (p.startsWith('/')) return `<abs+${depthOf(p)}>${tail}`;
|
|
81
|
+
return `<path>${tail}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Every string that leaves goes through this, including ones we believe are
|
|
85
|
+
// safe. Belief is what the ENOENT measurement disproved.
|
|
86
|
+
export function scrubText(text, ctx = {}) {
|
|
87
|
+
if (typeof text !== 'string' || !text) return text;
|
|
88
|
+
// The trailing value goes FIRST, before the path pass: the shape the audit
|
|
89
|
+
// found in our own throw sites is a RELATIVE path (`no such file: ${rel}`),
|
|
90
|
+
// and the path pass would keep its first segment (`clients<abs+2>`) because a
|
|
91
|
+
// relative path has no leading slash to match on.
|
|
92
|
+
let out = text.replace(/(: )([^\s:'"]{1,200})$/, (m, sep, token) => (token.includes(MARK) ? m : `${sep}<value>`));
|
|
93
|
+
out = out.replace(PATH_RE, (m) => scrubPath(m, ctx));
|
|
94
|
+
// A username reaches a message on its own too (`os.userInfo.username`),
|
|
95
|
+
// with no path around it to catch it.
|
|
96
|
+
const user = ctx.username || (ctx.home ? path.basename(ctx.home) : null);
|
|
97
|
+
if (user && user.length > 2) out = out.split(user).join('<user>');
|
|
98
|
+
// Quoted runs are where interpolated user strings hide when they are not
|
|
99
|
+
// paths. Anything already redacted keeps its shape; the rest goes.
|
|
100
|
+
out = out.replace(/'([^']{1,400})'|"([^"]{1,400})"/g, (m, a, b) => {
|
|
101
|
+
const inner = a ?? b ?? '';
|
|
102
|
+
return inner.includes(MARK) ? m : "'<str>'";
|
|
103
|
+
});
|
|
104
|
+
return out.length > 300 ? out.slice(0, 300) + '…' : out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const FRAME_RE = /^\s*at\s+(?:(.+?)\s+\()?([^()]+?)(?::(\d+):(\d+))?\)?\s*$/;
|
|
108
|
+
|
|
109
|
+
// Rule 4. Frames inside the bundle are kept whole (function name included:
|
|
110
|
+
// it is our identifier, not theirs); everything else is counted and dropped.
|
|
111
|
+
export function scrubStack(stack, ctx = {}) {
|
|
112
|
+
const frames = [];
|
|
113
|
+
let dropped = 0;
|
|
114
|
+
for (const line of String(stack || '').split('\n')) {
|
|
115
|
+
const m = line.match(FRAME_RE);
|
|
116
|
+
if (!m) continue;
|
|
117
|
+
const [, fn, file, ln, col] = m;
|
|
118
|
+
const safe = scrubPath(file.trim(), ctx);
|
|
119
|
+
if (safe.startsWith('app:')) {
|
|
120
|
+
frames.push({ fn: fn ? fn.trim().slice(0, 80) : null, at: safe, line: ln ? Number(ln) : null, col: col ? Number(col) : null });
|
|
121
|
+
} else {
|
|
122
|
+
dropped += 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { frames: frames.slice(0, 20), dropped };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Rule 3, as an allowlist rather than a delete-list. `err.path`, `err.dest`,
|
|
129
|
+
// `err.address`, `err.port`, `err.hostname`, `err.config`, `err.request` and
|
|
130
|
+
// whatever the next library invents are absent because nothing copies them,
|
|
131
|
+
// not because someone remembered them.
|
|
132
|
+
export function scrubError(err, ctx = {}) {
|
|
133
|
+
const e = err && typeof err === 'object' ? err : { message: String(err) };
|
|
134
|
+
const { frames, dropped } = scrubStack(e.stack, ctx);
|
|
135
|
+
return {
|
|
136
|
+
name: typeof e.name === 'string' ? e.name.slice(0, 60) : 'Error',
|
|
137
|
+
// A code is a constant (`ENOENT`, `EPERM`, `ERR_MODULE_NOT_FOUND`) and a
|
|
138
|
+
// syscall is a verb (`open`). Neither has ever held a user's word.
|
|
139
|
+
code: typeof e.code === 'string' || typeof e.code === 'number' ? String(e.code).slice(0, 40) : null,
|
|
140
|
+
syscall: typeof e.syscall === 'string' ? e.syscall.slice(0, 40) : null,
|
|
141
|
+
message: scrubText(typeof e.message === 'string' ? e.message : String(e), ctx),
|
|
142
|
+
site: frames[0] ? `${frames[0].at}:${frames[0].line ?? 0}` : null,
|
|
143
|
+
frames,
|
|
144
|
+
framesDropped: dropped,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The last gate, and the reason it exists: every rule above is a claim, and a
|
|
149
|
+
// claim about a payload is worth what a test of the payload says it is worth.
|
|
150
|
+
// This walks the finished report and answers "is there anything private left in
|
|
151
|
+
// here", and the writer refuses to keep a report that fails. Scrub before it
|
|
152
|
+
// leaves means there is no second chance further down.
|
|
153
|
+
export function findPrivate(value, ctx = {}) {
|
|
154
|
+
const home = ctx.home ? path.resolve(ctx.home) : null;
|
|
155
|
+
const user = ctx.username || (home ? path.basename(home) : null);
|
|
156
|
+
const hits = [];
|
|
157
|
+
const walk = (v, at) => {
|
|
158
|
+
if (typeof v === 'string') {
|
|
159
|
+
// A path inside our own bundle is ours and is the point of the report, so
|
|
160
|
+
// it comes out of the string before ANY of the three scans rather than
|
|
161
|
+
// being special-cased inside them. It used to come out before the path
|
|
162
|
+
// scan only, and that cost us every report on a whole class of Mac: a
|
|
163
|
+
// username that happens to be a substring of one of our own file paths
|
|
164
|
+
// reads as a leak, the report is thrown away, and `kind` becomes
|
|
165
|
+
// `redacted`. Measured on a GitHub macOS runner, 2026-08-25, where the
|
|
166
|
+
// account is called `runner` and the bundle ships
|
|
167
|
+
// `node_modules/@vitest/runner`. `dev`, `test`, `app` and `node` are the
|
|
168
|
+
// same shape and are ordinary names for a Mac account. `scrubPath` has
|
|
169
|
+
// already made these relative to the bundle, so nothing of theirs can be
|
|
170
|
+
// in one.
|
|
171
|
+
const rest = withoutOurOwnPaths(v);
|
|
172
|
+
if (home && rest.includes(home)) hits.push(`${at}: home directory`);
|
|
173
|
+
if (user && user.length > 2 && rest.includes(user)) hits.push(`${at}: username`);
|
|
174
|
+
for (const m of rest.match(PATH_RE) || []) {
|
|
175
|
+
if (m.trim().split('/').filter(Boolean).length >= 2) hits.push(`${at}: path ${m.trim()}`);
|
|
176
|
+
}
|
|
177
|
+
} else if (Array.isArray(v)) v.forEach((x, i) => walk(x, `${at}[${i}]`));
|
|
178
|
+
else if (v && typeof v === 'object') for (const [k, x] of Object.entries(v)) walk(x, at ? `${at}.${k}` : k);
|
|
179
|
+
};
|
|
180
|
+
walk(value, '');
|
|
181
|
+
return hits;
|
|
182
|
+
}
|