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
package/main/updater.mjs
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// THE APP UPDATES ITSELF, so nobody is ever asked to download it twice.
|
|
2
|
+
//
|
|
3
|
+
// There is a signed, notarised build on astral.ac now, so the horse is in
|
|
4
|
+
// front.
|
|
5
|
+
//
|
|
6
|
+
// WHAT IT DOES, in the order it happens. A few seconds after the window is up
|
|
7
|
+
// it asks GitHub whether there is a newer Agentbox. If there is, it downloads it
|
|
8
|
+
// quietly in the background, and when the download finishes it says so in
|
|
9
|
+
// Settings and in ⌘K. Nothing restarts under her. The restart is the button she
|
|
10
|
+
// pressed, which is the half she asked for by name.
|
|
11
|
+
//
|
|
12
|
+
// FOUR THINGS ABOUT THE SHAPE, each load-bearing:
|
|
13
|
+
//
|
|
14
|
+
// 1. IT NEVER OPENS A DIALOG AND NEVER STEALS FOCUS. electron-updater will pop
|
|
15
|
+
// a native box on some paths if you let it; every one of those is off here.
|
|
16
|
+
// An app that interrupts her to tell her it is healthy is the thing her
|
|
17
|
+
// whole inbox exists to stop.
|
|
18
|
+
// 2. IT IS SILENT WHEN IT FAILS. A laptop on a plane, a GitHub outage, a
|
|
19
|
+
// corporate proxy: none of those are her problem and none of them get a
|
|
20
|
+
// banner. The failure is recorded, `state` reports it, and the Settings row
|
|
21
|
+
// is the one place that ever says so, because that is the screen you open
|
|
22
|
+
// when you want to know something.
|
|
23
|
+
// 3. IT DOES NOTHING AT ALL FROM SOURCE. `autoUpdater.isUpdaterActive` is false
|
|
24
|
+
// when `app.isPackaged` is false, so a dev run skips the check rather than
|
|
25
|
+
// erroring against a feed it has no business reading. We report that state
|
|
26
|
+
// as `unsupported` with a reason rather than pretending to be idle.
|
|
27
|
+
// 4. IT INSTALLS ON QUIT TOO. If she never presses the button and just quits,
|
|
28
|
+
// the downloaded update is applied then, so the next launch is current. The
|
|
29
|
+
// button is the fast path, not the only path.
|
|
30
|
+
//
|
|
31
|
+
// WHERE THE UPDATE COMES FROM. `Astral-Agent/astral-releases`, the same public
|
|
32
|
+
// repo astral.ac/download already redirects to, configured in package.json's
|
|
33
|
+
// `build.publish`. electron-updater reads `releases/latest`, then
|
|
34
|
+
// `latest-mac.yml` on that release, and compares its `version` against this
|
|
35
|
+
// build's. It downloads the `.zip`, NOT the `.dmg`: Squirrel. Mac swaps an .app
|
|
36
|
+
// bundle and cannot read a disk image, which is why `build.mac.target` gained a
|
|
37
|
+
// zip beside the dmg and why a release that ships only a dmg fails with
|
|
38
|
+
// ERR_UPDATER_ZIP_FILE_NOT_FOUND. `scripts/publish-download.mjs` is what puts
|
|
39
|
+
// all three files on the release together.
|
|
40
|
+
//
|
|
41
|
+
// AND THE ONE THING THAT IS NOT TRUE ANY MORE. `main/analytics.mjs` opens by
|
|
42
|
+
// calling itself the only place anything leaves this machine. Since this file
|
|
43
|
+
// exists that is no longer so: an update check is a request to github.com. It
|
|
44
|
+
// carries no identity, no key and no account, and it is not routed through
|
|
45
|
+
// PostHog, but it IS traffic, and `legal/privacy.html` should say so in a
|
|
46
|
+
// sentence. That page is in the website repo, not this one.
|
|
47
|
+
|
|
48
|
+
import { createRequire } from 'node:module';
|
|
49
|
+
import { Name } from '../shared/product-name.mjs';
|
|
50
|
+
|
|
51
|
+
const require = createRequire(import.meta.url);
|
|
52
|
+
|
|
53
|
+
// How long after launch the first check happens. Long enough that it is never
|
|
54
|
+
// competing with the window painting or the supervisor waking its sessions up.
|
|
55
|
+
const FIRST_CHECK_MS = 20_000;
|
|
56
|
+
// And then this often, for the app somebody leaves open for days, which is the
|
|
57
|
+
// normal way Agentbox is used.
|
|
58
|
+
const EVERY_MS = 6 * 60 * 60 * 1000;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* PURE, and the whole of what the screens are allowed to know.
|
|
62
|
+
*
|
|
63
|
+
* Kept separate from the class below so the states can be tested without an
|
|
64
|
+
* Electron app, a network, or a packaged build.
|
|
65
|
+
*/
|
|
66
|
+
export function describe(state) {
|
|
67
|
+
const { phase, currentVersion, newVersion, percent, error, checkedAt, readyAt } = state;
|
|
68
|
+
return {
|
|
69
|
+
phase,
|
|
70
|
+
currentVersion,
|
|
71
|
+
newVersion: newVersion ?? null,
|
|
72
|
+
// Whole percent, because a progress number with decimals on it reads as
|
|
73
|
+
// machinery rather than as an answer.
|
|
74
|
+
percent: typeof percent === 'number' ? Math.max(0, Math.min(100, Math.round(percent))) : null,
|
|
75
|
+
error: error ?? null,
|
|
76
|
+
checkedAt: checkedAt ?? null,
|
|
77
|
+
// WHEN THE DOWNLOAD FINISHED, which is the moment the news is true. The
|
|
78
|
+
// inbox row this feeds wears it at its right end like every other row, and
|
|
79
|
+
// every other row's right end answers "how old is this". Built off the
|
|
80
|
+
// clock instead it would read "now" forever, which is the app looking busy
|
|
81
|
+
// about something that has been sitting on the disk since lunchtime.
|
|
82
|
+
readyAt: phase === 'ready' ? (readyAt ?? null) : null,
|
|
83
|
+
// The one question every screen actually asks. A screen should not have to
|
|
84
|
+
// learn which phase strings mean "there is a button to press".
|
|
85
|
+
ready: phase === 'ready',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createUpdater({ app, log = console, onChanged = () => {} }) {
|
|
90
|
+
let state = {
|
|
91
|
+
phase: 'idle',
|
|
92
|
+
currentVersion: app.getVersion(),
|
|
93
|
+
newVersion: null,
|
|
94
|
+
percent: null,
|
|
95
|
+
error: null,
|
|
96
|
+
checkedAt: null,
|
|
97
|
+
readyAt: null,
|
|
98
|
+
};
|
|
99
|
+
let timer = null;
|
|
100
|
+
let stopped = false;
|
|
101
|
+
|
|
102
|
+
const set = (next) => {
|
|
103
|
+
state = { ...state, ...next };
|
|
104
|
+
try { onChanged(describe(state)); } catch {}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Loaded lazily and inside a try, so a build that somehow shipped without the
|
|
108
|
+
// dependency degrades to "no updates" instead of failing to boot at all. The
|
|
109
|
+
// app not starting is a strictly worse outcome than the app not updating.
|
|
110
|
+
let autoUpdater = null;
|
|
111
|
+
try {
|
|
112
|
+
({ autoUpdater } = require('electron-updater'));
|
|
113
|
+
} catch (e) {
|
|
114
|
+
set({ phase: 'unsupported', error: 'This build has no updater in it.' });
|
|
115
|
+
log.warn?.('[updater] electron-updater is not available:', e?.message ?? e);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (autoUpdater) {
|
|
119
|
+
// Quiet by construction. `autoDownload` is the only one of these that is
|
|
120
|
+
// already the default; the rest are here because their defaults interrupt.
|
|
121
|
+
autoUpdater.autoDownload = true;
|
|
122
|
+
autoUpdater.autoInstallOnAppQuit = true;
|
|
123
|
+
autoUpdater.allowPrerelease = false;
|
|
124
|
+
// electron-updater logs to console by default, which is fine, but its
|
|
125
|
+
// `logger` is also what it uses to report errors it swallows. Ours keeps
|
|
126
|
+
// them out of the crash reporter and in the terminal where a build is
|
|
127
|
+
// being watched.
|
|
128
|
+
autoUpdater.logger = {
|
|
129
|
+
info: (...a) => log.info?.('[updater]', ...a),
|
|
130
|
+
warn: (...a) => log.warn?.('[updater]', ...a),
|
|
131
|
+
error: (...a) => log.error?.('[updater]', ...a),
|
|
132
|
+
debug: () => {},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
autoUpdater.on('checking-for-update', () => set({ phase: 'checking', error: null }));
|
|
136
|
+
autoUpdater.on('update-not-available', () => set({
|
|
137
|
+
phase: 'current', newVersion: null, percent: null, error: null, checkedAt: Date.now(),
|
|
138
|
+
}));
|
|
139
|
+
autoUpdater.on('update-available', (info) => set({
|
|
140
|
+
phase: 'downloading', newVersion: info?.version ?? null, percent: 0, error: null, checkedAt: Date.now(),
|
|
141
|
+
}));
|
|
142
|
+
autoUpdater.on('download-progress', (p) => set({ phase: 'downloading', percent: p?.percent ?? null }));
|
|
143
|
+
// STAMPED ONCE, not on every redraw: a second `update-downloaded` for the
|
|
144
|
+
// same version would otherwise make the row young again and move it back to
|
|
145
|
+
// the top of her list for news she has already read.
|
|
146
|
+
autoUpdater.on('update-downloaded', (info) => set({
|
|
147
|
+
phase: 'ready', newVersion: info?.version ?? state.newVersion, percent: 100, error: null,
|
|
148
|
+
readyAt: state.phase === 'ready' && state.readyAt ? state.readyAt : Date.now(),
|
|
149
|
+
}));
|
|
150
|
+
// SILENT ON PURPOSE. See point 2 at the top of this file.
|
|
151
|
+
autoUpdater.on('error', (e) => set({
|
|
152
|
+
phase: 'error',
|
|
153
|
+
// The message only. electron-updater puts a whole stack on some of these
|
|
154
|
+
// and Settings shows this string to a person.
|
|
155
|
+
error: shortError(e),
|
|
156
|
+
checkedAt: Date.now(),
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function check({ manual = false } = {}) {
|
|
161
|
+
if (stopped || !autoUpdater) return describe(state);
|
|
162
|
+
if (!autoUpdater.isUpdaterActive()) {
|
|
163
|
+
// A build run from source. Say which it is rather than reporting a
|
|
164
|
+
// healthy "up to date" that was never checked.
|
|
165
|
+
set({
|
|
166
|
+
phase: 'unsupported',
|
|
167
|
+
error: `${Name} only updates itself when it is installed as an app.`,
|
|
168
|
+
checkedAt: Date.now(),
|
|
169
|
+
});
|
|
170
|
+
return describe(state);
|
|
171
|
+
}
|
|
172
|
+
// Once it is downloaded there is nothing left to check for; checking again
|
|
173
|
+
// would walk the state backwards under a button that says Restart.
|
|
174
|
+
if (state.phase === 'ready') return describe(state);
|
|
175
|
+
if (state.phase === 'checking' || (state.phase === 'downloading' && !manual)) return describe(state);
|
|
176
|
+
try {
|
|
177
|
+
await autoUpdater.checkForUpdates();
|
|
178
|
+
} catch (e) {
|
|
179
|
+
set({ phase: 'error', error: shortError(e), checkedAt: Date.now() });
|
|
180
|
+
}
|
|
181
|
+
return describe(state);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// THE BUTTON. Quit, swap the app, come back up on the new one.
|
|
185
|
+
function install() {
|
|
186
|
+
if (!autoUpdater || state.phase !== 'ready') return false;
|
|
187
|
+
// `isSilent` true so macOS does not put its own progress window over her
|
|
188
|
+
// screen; `isForceRunAfter` true so the app comes back, which is the whole
|
|
189
|
+
// point of pressing a button that says Restart.
|
|
190
|
+
setImmediate(() => autoUpdater.quitAndInstall(true, true));
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function start() {
|
|
195
|
+
if (stopped || timer) return;
|
|
196
|
+
const tick = () => { void check(); };
|
|
197
|
+
setTimeout(tick, FIRST_CHECK_MS).unref?.();
|
|
198
|
+
timer = setInterval(tick, EVERY_MS);
|
|
199
|
+
timer.unref?.();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function stop() {
|
|
203
|
+
stopped = true;
|
|
204
|
+
if (timer) clearInterval(timer);
|
|
205
|
+
timer = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { start, stop, check, install, state: () => describe(state) };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// electron-updater's errors arrive as Error objects whose `message` is
|
|
212
|
+
// occasionally the entire server response. One line, and a sentence a person
|
|
213
|
+
// can read, for the few we can name.
|
|
214
|
+
export function shortError(e) {
|
|
215
|
+
const raw = String(e?.message ?? e ?? 'Something went wrong.').split('\n')[0].trim();
|
|
216
|
+
// AND THE NAME IS ON `code`, NOT IN THE MESSAGE. electron-updater builds
|
|
217
|
+
// these through builder-util-runtime's `newError`, which puts the
|
|
218
|
+
// ERR_UPDATER_ name on the error's `code` and leaves the message as prose
|
|
219
|
+
// with the whole URL in it. Matching the message alone reached none of them.
|
|
220
|
+
// Measured 2026-08-29 in a packaged build against the real GitHub feed: the
|
|
221
|
+
// published v0.1.0 carries only the .dmg, so the check 404s on
|
|
222
|
+
// latest-mac.yml and Settings read "Cannot find latest-mac.yml in the latest
|
|
223
|
+
// release artifacts (https://github.com/Astral-Agent/astral-releases/
|
|
224
|
+
// releases/download/v0.1.0/latest-mac.yml): HttpErr…" to her. The sentence
|
|
225
|
+
// for exactly that failure was already on the line below it.
|
|
226
|
+
const named = `${e?.code ?? ''} ${raw}`;
|
|
227
|
+
if (/ERR_UPDATER_ZIP_FILE_NOT_FOUND/.test(named)) return 'That release has no app in it to install.';
|
|
228
|
+
if (/ERR_UPDATER_CHANNEL_FILE_NOT_FOUND/.test(named)) return 'That release is missing its update file.';
|
|
229
|
+
if (/ERR_UPDATER_NO_PUBLISHED_VERSIONS|LATEST_VERSION_NOT_FOUND/.test(named)) return 'No published version to update to yet.';
|
|
230
|
+
if (/ENOTFOUND|EAI_AGAIN|ENETDOWN|ETIMEDOUT|ECONNREFUSED|ECONNRESET/.test(raw)) return 'Could not reach the internet.';
|
|
231
|
+
// AND THE SAME FAILURE UNDER CHROMIUM'S NAME FOR IT. The line above catches
|
|
232
|
+
// Node's codes, and electron-updater does not always use Node: the check
|
|
233
|
+
// runs through Electron's own `net` module, whose errors read
|
|
234
|
+
// `net::ERR_CONNECTION_REFUSED`. That is not `ECONNREFUSED`, so it fell
|
|
235
|
+
// through to the last line and Settings said `net::ERR_CONNECTION_REFUSED`
|
|
236
|
+
// to her. Measured on 2026-08-29, in a packaged build with the update feed
|
|
237
|
+
// switched off mid-run: that exact string was on the screen.
|
|
238
|
+
if (/^net::ERR_/.test(raw)) return 'Could not reach the internet.';
|
|
239
|
+
return raw.length > 160 ? `${raw.slice(0, 157)}…` : raw;
|
|
240
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Which keystrokes mean zoom. Kept apart from the menu so it imports without
|
|
2
|
+
// Electron and can be tested for real rather than asserted against source text.
|
|
3
|
+
//
|
|
4
|
+
// Her window has been sitting at zoom level 1 (1.2x) since before it was
|
|
5
|
+
// launched, because Chromium stores zoom per page URL in the profile and hands
|
|
6
|
+
// it back on every load. The chords are the only way out of that, so they have
|
|
7
|
+
// to arrive.
|
|
8
|
+
//
|
|
9
|
+
// "Cmd +" is really ⇧⌘= on a US keyboard, which reports key '+' and code
|
|
10
|
+
// 'Equal'. Matching on BOTH key and code is what makes the shifted form, the
|
|
11
|
+
// unshifted form and the numpad all land on the same command.
|
|
12
|
+
|
|
13
|
+
const IN = { Equal: 0.5, NumpadAdd: 0.5, Minus: -0.5, NumpadSubtract: -0.5, Digit0: 0, Numpad0: 0 };
|
|
14
|
+
const KEYS = { '=': 0.5, '+': 0.5, '-': -0.5, _: -0.5, 0: 0 };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The zoom step a before-input-event describes, or null if it is not a zoom
|
|
18
|
+
* chord. 0.5 in, -0.5 out, 0 actual size. The app is a real answer here, so
|
|
19
|
+
* callers must test for null, not for falsiness.
|
|
20
|
+
*/
|
|
21
|
+
export function zoomDeltaFor(input) {
|
|
22
|
+
if (!input || input.type !== 'keyDown') return null;
|
|
23
|
+
if (!(input.meta || input.control)) return null;
|
|
24
|
+
if (input.alt) return null; // ⌥⌘- and friends belong to whoever else wants them
|
|
25
|
+
const byCode = IN[String(input.code ?? '')];
|
|
26
|
+
if (byCode !== undefined) return byCode;
|
|
27
|
+
const byKey = KEYS[String(input.key ?? '')];
|
|
28
|
+
return byKey === undefined ? null : byKey;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// WHICH CONTENTS GETS ZOOMED, and why this is not just `getFocusedWebContents`.
|
|
32
|
+
//
|
|
33
|
+
// That is the CLICK, not the keystroke, and it rules out the accelerator being
|
|
34
|
+
// the only fault. A click had no contents of its own and asked for the focused
|
|
35
|
+
// one, which returns null whenever nothing on screen claims focus, and on macOS
|
|
36
|
+
// the menu itself holds the keyboard while she is choosing from it. Null was a
|
|
37
|
+
// silent no-op, and the silent no-op is the whole complaint.
|
|
38
|
+
//
|
|
39
|
+
// Electron hands a menu click the window it was chosen from, so the click now
|
|
40
|
+
// carries a target and never has to guess. This is the same move `Reload` made
|
|
41
|
+
// on 2026-08-06; zoom was the last item in this menu still guessing.
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The web contents a zoom should act on, first non-destroyed of: the one the
|
|
45
|
+
* caller named (the keystroke's own contents, or the window a menu click came
|
|
46
|
+
* from), then whatever is focused, then the app's own window. Returns null only
|
|
47
|
+
* if there is genuinely nothing on screen.
|
|
48
|
+
*/
|
|
49
|
+
export function zoomTarget({ named = null, focused = null, appWindow = null } = {}) {
|
|
50
|
+
for (const contents of [named, focused, appWindow]) {
|
|
51
|
+
if (contents && !contents.isDestroyed?.()) return contents;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// What the zoom readout says. Kept apart from the menu, like zoom-keys.mjs, so
|
|
2
|
+
// it imports without Electron and the ladder can be tested for real.
|
|
3
|
+
//
|
|
4
|
+
// So this file computes one number and nothing else. Chromium's zoom factor is
|
|
5
|
+
// 1.2 to the power of the level, which is why her ladder reads 76, 83, 91, 100,
|
|
6
|
+
// 110, 120 rather than the round numbers other apps step through. Moving the
|
|
7
|
+
// ladder onto 75/80/90/100/110/125 was offered and she did not ask for it, so
|
|
8
|
+
// the steps stay exactly as they were and only the readout is new.
|
|
9
|
+
|
|
10
|
+
const CHROMIUM_ZOOM_BASE = 1.2;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The percentage to show for a Chromium zoom level. Level 0 is 100%, level 1 is
|
|
14
|
+
* 120%, level -0.5 is 91%. Returns null for a level that is not a number, so a
|
|
15
|
+
* caller can tell "no readout" from a readout of 0.
|
|
16
|
+
*/
|
|
17
|
+
export function zoomPercentFor(level) {
|
|
18
|
+
if (typeof level !== 'number' || !Number.isFinite(level)) return null;
|
|
19
|
+
return Math.round(CHROMIUM_ZOOM_BASE ** level * 100);
|
|
20
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# Launcher for this app's own store server.
|
|
3
|
+
#
|
|
4
|
+
# It exists because node is installed through nvm on this machine and is NOT on
|
|
5
|
+
# a login shell's PATH, so an MCP client spawning `node mcp/stdio.mjs` gets
|
|
6
|
+
# "command not found" and reports the server as failed with nothing useful in
|
|
7
|
+
# it. Hard-coding the current nvm path would work until the next node upgrade
|
|
8
|
+
# silently moved it, so resolve it at launch instead.
|
|
9
|
+
#
|
|
10
|
+
# STDOUT IS THE JSON-RPC STREAM. Anything nvm prints on the way in would be
|
|
11
|
+
# parsed as protocol and corrupt the session, hence the redirects: every setup
|
|
12
|
+
# step here sends its output to stderr or /dev/null, and the only thing that
|
|
13
|
+
# ever writes to stdout is the server itself.
|
|
14
|
+
|
|
15
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
16
|
+
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
|
17
|
+
# shellcheck disable=SC1091
|
|
18
|
+
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" >/dev/null 2>&1
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
22
|
+
# Last resort: the newest version nvm has on disk, without sourcing anything.
|
|
23
|
+
for bin in "$HOME"/.nvm/versions/node/*/bin; do
|
|
24
|
+
[ -x "$bin/node" ] && PATH="$bin:$PATH"
|
|
25
|
+
done
|
|
26
|
+
export PATH
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
30
|
+
echo "[store] cannot find node (looked on PATH and in $HOME/.nvm)" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
exec node "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/stdio.mjs" "$@"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Which founder's store are we operating on, and which product.
|
|
2
|
+
//
|
|
3
|
+
// The app answers this from a live auth session. The MCP server has no session:
|
|
4
|
+
// it is a child process spawned by whatever agent wanted it, so the account is
|
|
5
|
+
// configuration, resolved once at startup.
|
|
6
|
+
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { legacyRoot, setAccountScope, dataRoot, listProjects, sanitizeName } from '../../main/store/project.mjs';
|
|
10
|
+
import { envName } from '../../shared/product-name.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the account whose store this server serves.
|
|
14
|
+
*
|
|
15
|
+
* STORE_ACCOUNT_ID wins. Failing that, if exactly one account exists on disk
|
|
16
|
+
* we use it, because that is unambiguous and asking would be pedantic. If
|
|
17
|
+
* several exist we REFUSE rather than pick: silently operating on the wrong
|
|
18
|
+
* founder's store would be discovered late and by writing to it.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveAccount({ accountId = process.env.STORE_ACCOUNT_ID } = {}) {
|
|
21
|
+
const accountsDir = path.join(legacyRoot(), 'accounts');
|
|
22
|
+
|
|
23
|
+
if (accountId) {
|
|
24
|
+
const dir = path.join(accountsDir, accountId);
|
|
25
|
+
if (!fs.existsSync(dir)) throw new Error(`no such account on disk: ${accountId} (looked in ${accountsDir})`);
|
|
26
|
+
setAccountScope(accountId);
|
|
27
|
+
return { accountId, root: dataRoot() };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let found = [];
|
|
31
|
+
try {
|
|
32
|
+
found = fs.readdirSync(accountsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error(`no accounts found at ${accountsDir}; is ${envName('HOME')} set correctly?`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (found.length === 1) {
|
|
38
|
+
setAccountScope(found[0]);
|
|
39
|
+
return { accountId: found[0], root: dataRoot() };
|
|
40
|
+
}
|
|
41
|
+
if (!found.length) throw new Error(`no accounts found at ${accountsDir}`);
|
|
42
|
+
throw new Error(
|
|
43
|
+
`several accounts on disk (${found.join(', ')}); set STORE_ACCOUNT_ID so this server cannot write to the wrong one`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find a product by id, slug, or name. Agents refer to products the way a
|
|
49
|
+
* person would ("kestrel"), not by generated id, so accept all three and say
|
|
50
|
+
* clearly when a name is ambiguous rather than guessing which one was meant.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveProduct(ref) {
|
|
53
|
+
const wanted = String(ref ?? '').trim();
|
|
54
|
+
if (!wanted) throw new Error('which product? pass its name or id (list_products shows them)');
|
|
55
|
+
const projects = listProjects();
|
|
56
|
+
|
|
57
|
+
const exact = projects.find((p) => p.id === wanted) ?? projects.find((p) => path.basename(p.dir) === wanted);
|
|
58
|
+
if (exact) return exact;
|
|
59
|
+
|
|
60
|
+
const lower = wanted.toLowerCase();
|
|
61
|
+
const byName = projects.filter((p) => (p.name ?? '').toLowerCase() === lower);
|
|
62
|
+
if (byName.length === 1) return byName[0];
|
|
63
|
+
if (byName.length > 1) throw new Error(`several products are called "${wanted}"; use its id instead (${byName.map((p) => p.id).join(', ')})`);
|
|
64
|
+
|
|
65
|
+
// THE NAME IT WAS MADE WITH. A rename changes the label and never the folder,
|
|
66
|
+
// and the folder is that first name slugged, so an agent that still says the
|
|
67
|
+
// old name is pointing at this folder. After the current names, so a name she
|
|
68
|
+
// has since given another project wins.
|
|
69
|
+
const madeAs = projects.find((p) => path.basename(p.dir) === sanitizeName(wanted));
|
|
70
|
+
if (madeAs) return madeAs;
|
|
71
|
+
|
|
72
|
+
const partial = projects.filter((p) => (p.name ?? '').toLowerCase().includes(lower) || path.basename(p.dir).includes(lower));
|
|
73
|
+
if (partial.length === 1) return partial[0];
|
|
74
|
+
if (partial.length > 1) throw new Error(`"${wanted}" matches several products: ${partial.map((p) => p.name ?? p.id).join(', ')}`);
|
|
75
|
+
|
|
76
|
+
throw new Error(`no product called "${wanted}"; list_products shows what exists`);
|
|
77
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Past in-app conversations, read-only.
|
|
2
|
+
//
|
|
3
|
+
// This is continuity, not a feature: a founder and the in-app agent have already
|
|
4
|
+
// decided things in these logs that never made it into a document, and an
|
|
5
|
+
// external agent picking up a product months later should be able to find out
|
|
6
|
+
// why something is the way it is instead of re-deciding it.
|
|
7
|
+
//
|
|
8
|
+
// Strictly read-only, deliberately. The MCP has no business appending to a
|
|
9
|
+
// conversation the app owns and renders live; a message written here would
|
|
10
|
+
// appear in a chat with no session behind it, which reads to the founder as the
|
|
11
|
+
// agent talking to itself.
|
|
12
|
+
|
|
13
|
+
import { listChats, readChatLog } from '../../main/store/chats.mjs';
|
|
14
|
+
import { resolveProduct } from './account.mjs';
|
|
15
|
+
|
|
16
|
+
const DEFAULT_LIMIT = 100;
|
|
17
|
+
const MAX_LIMIT = 500;
|
|
18
|
+
const MAX_TEXT = 4000;
|
|
19
|
+
|
|
20
|
+
export function listProductChats(ref) {
|
|
21
|
+
const project = resolveProduct(ref);
|
|
22
|
+
return listChats(project.dir).map((c) => ({
|
|
23
|
+
id: c.id,
|
|
24
|
+
title: c.title,
|
|
25
|
+
createdAt: c.createdAt ?? null,
|
|
26
|
+
updatedAt: c.updatedAt ?? null,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The tail of one conversation. Tail rather than head because the recent end is
|
|
32
|
+
* almost always what a reader wants, and a long chat would otherwise spend the
|
|
33
|
+
* whole budget on its opening.
|
|
34
|
+
*/
|
|
35
|
+
export function readProductChat(ref, { chatId = null, limit = DEFAULT_LIMIT } = {}) {
|
|
36
|
+
const project = resolveProduct(ref);
|
|
37
|
+
const chats = listChats(project.dir);
|
|
38
|
+
if (!chats.length) return { chatId: null, title: null, messages: [] };
|
|
39
|
+
|
|
40
|
+
const chat = chatId ? chats.find((c) => c.id === chatId) : chats[0]; // newest by default
|
|
41
|
+
if (!chat) throw new Error(`no chat ${chatId} in ${project.name} (list_chats shows what exists)`);
|
|
42
|
+
|
|
43
|
+
const n = Math.min(Math.max(1, Number(limit) || DEFAULT_LIMIT), MAX_LIMIT);
|
|
44
|
+
const log = readChatLog(project.dir, chat.id);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
chatId: chat.id,
|
|
48
|
+
title: chat.title,
|
|
49
|
+
total: log.length,
|
|
50
|
+
messages: log.slice(-n).map((m) => ({
|
|
51
|
+
role: m.role,
|
|
52
|
+
// Truncated per message rather than per response: one enormous message
|
|
53
|
+
// must not crowd out the twenty around it that give it meaning.
|
|
54
|
+
text: typeof m.text === 'string' && m.text.length > MAX_TEXT ? `${m.text.slice(0, MAX_TEXT)}\n[truncated]` : (m.text ?? ''),
|
|
55
|
+
ts: m.ts ?? null,
|
|
56
|
+
// An unanswered question from a past session is a live loose end, so it
|
|
57
|
+
// stays visible rather than being flattened into prose.
|
|
58
|
+
ask: m.ask ? { status: m.ask.status, questions: (m.ask.questions ?? []).map((q) => q.question) } : undefined,
|
|
59
|
+
})),
|
|
60
|
+
};
|
|
61
|
+
}
|