agentbox-app 0.1.7 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +220 -58
- package/bin/agentbox.mjs +18 -3
- package/briefs/message-rules.md +24 -3
- package/briefs/worker.md +63 -49
- package/briefs/writing-rules.md +90 -131
- package/main/account-discovery.mjs +21 -20
- package/main/account-tooling.mjs +11 -11
- package/main/agent-activity.mjs +45 -5
- package/main/agent-schedule.mjs +14 -14
- package/main/agent-sessions.mjs +94 -10
- package/main/agent-updates.mjs +116 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/approvals.mjs +14 -5
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +32 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +25 -13
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +18 -0
- package/main/codex-launch-env.mjs +31 -0
- package/main/codex-memory-gate.mjs +260 -0
- package/main/codex-models.mjs +12 -7
- package/main/codex-threads.mjs +46 -23
- package/main/codex-usage-file.mjs +7 -7
- package/main/codex-usage.mjs +4 -4
- package/main/codex-watch.mjs +12 -12
- package/main/config.mjs +82 -17
- package/main/demo.mjs +17 -14
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +204 -0
- package/main/feedback.mjs +55 -0
- package/main/folder-pictures.mjs +48 -0
- package/main/frame-scrollbars.mjs +90 -0
- package/main/fresh-user.mjs +7 -4
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +9 -3
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +134 -60
- package/main/ipc.mjs +335 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +48 -1
- package/main/main.mjs +138 -15
- package/main/memory-gate-history.mjs +254 -0
- package/main/memory-gate-server.mjs +461 -0
- package/main/memory-gate.mjs +206 -0
- package/main/message-priority.mjs +87 -0
- package/main/notify.mjs +1 -1
- package/main/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +21 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +115 -51
- package/main/runs-on.mjs +93 -0
- package/main/serve.mjs +77 -5
- package/main/settings.mjs +245 -26
- package/main/ship-queue.mjs +279 -0
- package/main/sign-in-files.mjs +39 -0
- package/main/source-updater.mjs +298 -0
- package/main/store/home.mjs +84 -7
- package/main/store/project.mjs +15 -19
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +245 -13
- package/main/supervisor.mjs +1634 -417
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +94 -16
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +14 -2
- package/main/team/index.mjs +440 -0
- package/main/team/memory-cloud.mjs +247 -0
- package/main/team/projects.mjs +162 -0
- package/main/team/session.mjs +225 -0
- package/main/team/sign-in.mjs +106 -0
- package/main/team/supabase-backend.mjs +231 -0
- package/main/team/sync.mjs +163 -0
- package/main/updater.mjs +15 -5
- package/main/write-before-reload.mjs +50 -0
- package/mcp/core/page.mjs +10 -12
- package/mcp/core/work.mjs +123 -26
- package/mcp/tools.mjs +13 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
- package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/after-install.mjs +14 -0
- package/scripts/approval-server.sh +7 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +39 -16
- package/shared/agents.d.mts +26 -8
- package/shared/agents.mjs +100 -108
- package/shared/analytics-events.mjs +19 -2
- package/shared/answers.mjs +12 -0
- package/shared/bridge-map.d.mts +12 -0
- package/shared/bridge-map.mjs +38 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +6 -6
- package/shared/claude-usage.mjs +6 -6
- package/shared/codex-import.mjs +7 -7
- package/shared/codex-modes.d.mts +50 -0
- package/shared/codex-modes.mjs +1 -1
- package/shared/codex-usage.mjs +7 -8
- package/shared/demo-world.mjs +6 -6
- package/shared/engines.mjs +27 -10
- package/shared/feedback.d.mts +14 -0
- package/shared/feedback.mjs +44 -0
- package/shared/first-run-examples.mjs +4 -4
- package/shared/first-run-practice.d.mts +16 -0
- package/shared/first-run-practice.mjs +176 -70
- package/shared/fresh-user-home.mjs +34 -4
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +9 -3
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- package/shared/runs-on.d.mts +20 -0
- package/shared/runs-on.mjs +79 -0
- package/shared/schemes.d.mts +11 -0
- package/shared/schemes.mjs +1 -1
- package/shared/shortcuts.d.mts +41 -0
- package/shared/shortcuts.mjs +236 -0
- package/shared/spawn-trouble.d.mts +2 -0
- package/shared/spawn-trouble.mjs +40 -19
- package/shared/team-rules.d.mts +17 -0
- package/shared/team-rules.mjs +230 -0
- package/shared/team-status.d.mts +11 -0
- package/shared/team-status.mjs +90 -0
- package/shared/terminal-links.d.mts +2 -0
- package/shared/terminal-links.mjs +55 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +16 -0
- package/shared/thread-cards.d.mts +19 -0
- package/shared/thread-cards.mjs +254 -0
- package/shared/usage.d.mts +1 -1
- package/shared/usage.mjs +6 -7
- package/shared/work-items.mjs +258 -28
- package/shared/work-lines.mjs +393 -16
- package/worker-permissions.json +1 -1
- 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/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-B7hhfHrT.js +0 -684
- package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
- 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/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/emulator/audio-rules.mjs +0 -74
- package/renderer/dist/emulator/data/compression/README.md +0 -11
- package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
- package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
- package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +0 -22
- 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 +0 -65
- package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
- package/renderer/dist/emulator/data/emulator.css +0 -1614
- package/renderer/dist/emulator/data/loader.js +0 -170
- package/renderer/dist/emulator/data/localization/README.md +0 -63
- package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
- package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
- package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
- package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
- package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
- package/renderer/dist/emulator/data/localization/en-US.json +0 -339
- package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
- package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
- package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
- package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
- package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
- package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
- package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
- package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
- package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
- package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
- package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
- package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
- package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
- package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
- package/renderer/dist/emulator/data/src/GameManager.js +0 -464
- package/renderer/dist/emulator/data/src/compression.js +0 -142
- package/renderer/dist/emulator/data/src/emulator.js +0 -6158
- package/renderer/dist/emulator/data/src/gamepad.js +0 -167
- package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
- package/renderer/dist/emulator/data/src/shaders.js +0 -236
- package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
- package/renderer/dist/emulator/data/src/storage.js +0 -125
- package/renderer/dist/emulator/data/version.json +0 -5
- package/renderer/dist/emulator/host.js +0 -299
- package/renderer/dist/emulator/index.html +0 -28
- package/renderer/dist/emulator/provenance.json +0 -6
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// THE ACCOUNTS AGENTBOX RUNS ON ARE THE ONES ON THE DISK, not the ones somebody
|
|
2
2
|
// remembered to type into a config file.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// Picture someone with two subscriptions: a second account signed in through
|
|
5
|
+
// its own CLAUDE_CONFIG_DIR, and a first one that has hit its WEEKLY limit.
|
|
6
|
+
// The fleet stops for days, with a rested account on the same disk the whole
|
|
7
|
+
// time. The register of accounts was the
|
|
8
8
|
// hand-written `authProfiles` key in zero.config.json, which nothing in the app
|
|
9
9
|
// writes and nothing in the app checks against reality, so an account set up
|
|
10
10
|
// the ordinary way (a new CLAUDE_CONFIG_DIR, a /login) was invisible BY
|
|
@@ -18,24 +18,25 @@
|
|
|
18
18
|
// - A folder under the home directory named `.claude` or `.claude-<anything>`
|
|
19
19
|
// that holds a login is an account.
|
|
20
20
|
// - IT ONLY COUNTS IF ITS SUBSCRIPTION IS NEW. Two folders holding one
|
|
21
|
-
// account is
|
|
22
|
-
//
|
|
23
|
-
// round to an account that is already at its limit. The test is on
|
|
21
|
+
// account is a real setup (~/.claude and ~/.claude-second both signed in
|
|
22
|
+
// as the same person), and counting it twice sends two thirds of every
|
|
23
|
+
// spawn round to an account that is already at its limit. The test is on
|
|
24
24
|
// `accountUuid`, the id Claude Code stores for the subscription itself,
|
|
25
|
-
// never on the email
|
|
26
|
-
// - NOTHING
|
|
27
|
-
// decision; this is only a discovery. Discovered accounts go after
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
25
|
+
// never on the email, because one email can hold more than one subscription.
|
|
26
|
+
// - NOTHING THE USER CONFIGURED IS EVER DROPPED OR REORDERED. The config is a
|
|
27
|
+
// decision; this is only a discovery. Discovered accounts go after the
|
|
28
|
+
// configured ones.
|
|
29
|
+
// - A folder with no login is not an account. Folders such as
|
|
30
|
+
// ~/.claude-swap-backup or ~/.claude-worktrees are exactly that, and
|
|
31
|
+
// neither is a place to send work.
|
|
31
32
|
//
|
|
32
|
-
// WHY THERE IS NO REFRESH BUTTON
|
|
33
|
+
// WHY THERE IS NO REFRESH BUTTON. It is not costly.
|
|
33
34
|
// The directory listing is cached on the home folder's own mtime, so the
|
|
34
35
|
// readdir happens once and then only when a folder appears or disappears; the
|
|
35
36
|
// login inside each candidate is read through `accountIdentity`, which is
|
|
36
|
-
// already cached on that file's mtime. Measured on
|
|
37
|
-
//
|
|
38
|
-
//
|
|
37
|
+
// already cached on that file's mtime. Measured on a home folder of 200
|
|
38
|
+
// entries, 9 .claude-shaped, 5 of them directories, 3 holding a login, a
|
|
39
|
+
// warm call costs 0.029 ms, and it is asked on the supervisor's
|
|
39
40
|
// 15-second tick. A button would only be a thing to remember to press.
|
|
40
41
|
//
|
|
41
42
|
// THE DELIBERATE LIMIT: one level, under the home directory only. A profile
|
|
@@ -103,9 +104,9 @@ export function discoverProfiles({ home = os.homedir() } = {}) {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
/**
|
|
106
|
-
* THE PROFILES THE FLEET ACTUALLY RUNS ON:
|
|
107
|
-
*
|
|
108
|
-
* running on.
|
|
107
|
+
* THE PROFILES THE FLEET ACTUALLY RUNS ON: the configured ones first, exactly
|
|
108
|
+
* as written, then any subscription found on the disk that none of the
|
|
109
|
+
* configured ones is already running on.
|
|
109
110
|
*
|
|
110
111
|
* Profiles are the strings the rest of the app has always used: the literal
|
|
111
112
|
* 'default' for ~/.claude (which is what spawning with no CLAUDE_CONFIG_DIR
|
package/main/account-tooling.mjs
CHANGED
|
@@ -182,16 +182,15 @@ function list(names) {
|
|
|
182
182
|
//
|
|
183
183
|
// The Accounts page named its two rows `default` and `.claude-second`, which
|
|
184
184
|
// are folders, not people. So there was no way to tell from the screen which
|
|
185
|
-
// subscription either row was running on, and
|
|
186
|
-
// second
|
|
187
|
-
// what it
|
|
188
|
-
// browser
|
|
189
|
-
// that one back.
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
// reported: "Idk which is logged in nothing changed in settings".
|
|
185
|
+
// subscription either row was running on, and that can cost a person their
|
|
186
|
+
// second account. Say the page reports the second account signed out. They do
|
|
187
|
+
// exactly what it tells them to do, run the command and type /login, and the
|
|
188
|
+
// browser is already signed in as the account the first row uses, so the login
|
|
189
|
+
// hands that one back. The other account that had been in that folder is gone,
|
|
190
|
+
// replaced by the same account the first row was already using; the config
|
|
191
|
+
// backups the CLI itself writes show the swap. Nothing on the page changes
|
|
192
|
+
// when it happens, because nothing on the page had ever said whose account it
|
|
193
|
+
// was: "Idk which is logged in nothing changed in settings".
|
|
195
194
|
//
|
|
196
195
|
// One line of the answer, read from the file Claude Code keeps its own account
|
|
197
196
|
// in. Null when the folder holds no login, which reads on the page as the one
|
|
@@ -199,7 +198,8 @@ function list(names) {
|
|
|
199
198
|
const identityCache = new Map();
|
|
200
199
|
|
|
201
200
|
export function accountIdentity(dir, { home = os.homedir() } = {}) {
|
|
202
|
-
|
|
201
|
+
let file = path.join(dir || defaultClaudeHome(home), '.claude.json');
|
|
202
|
+
if (!dir && !fs.existsSync(file)) file = path.join(home, '.claude.json');
|
|
203
203
|
// Cached on the file's own mtime rather than on a timer. That file is ~120 KB
|
|
204
204
|
// on this machine and the settings page re-reads on every poll, so parsing it
|
|
205
205
|
// each time would be real work for an answer that changes about twice a
|
package/main/agent-activity.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Ephemeral, local status from matched tool start/end events. A persisted trace
|
|
2
2
|
// is history and cannot prove a command is still running. Nothing here executes
|
|
3
3
|
// commands, records reasoning, or changes worker permissions.
|
|
4
|
-
import { plainCommand, shortPath } from '../shared/work-lines.mjs';
|
|
4
|
+
import { commandWork, isBookkeeping, plainCommand, shortPath } from '../shared/work-lines.mjs';
|
|
5
5
|
const clean = value => typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
6
6
|
function add(session, id, label, detail) {
|
|
7
7
|
if (!id || !label) return false;
|
|
@@ -11,12 +11,43 @@ function add(session, id, label, detail) {
|
|
|
11
11
|
return true;
|
|
12
12
|
}
|
|
13
13
|
function finish(session, id) { return session.activeTools?.delete(id) ?? false; }
|
|
14
|
-
|
|
14
|
+
// A turn ending clears what it was doing, but not a background job: that is
|
|
15
|
+
// still running, and the session is kept open waiting for it
|
|
16
|
+
// (main/claude-input.mjs), so the row keeps saying what it waits on.
|
|
17
|
+
function clear(session) {
|
|
18
|
+
let changed = false;
|
|
19
|
+
for (const [id, entry] of session.activeTools ?? []) {
|
|
20
|
+
if (entry.background) continue;
|
|
21
|
+
session.activeTools.delete(id);
|
|
22
|
+
changed = true;
|
|
23
|
+
}
|
|
24
|
+
return changed;
|
|
25
|
+
}
|
|
26
|
+
function backgroundJobs(session, tasks) {
|
|
27
|
+
const pending = session.activeTools ??= new Map();
|
|
28
|
+
const live = new Set(tasks.map(t => `bg:${t?.task_id}`));
|
|
29
|
+
let changed = false;
|
|
30
|
+
for (const [id, entry] of pending) if (entry.background && !live.has(id)) { pending.delete(id); changed = true; }
|
|
31
|
+
for (const task of tasks) {
|
|
32
|
+
const description = clean(task?.description);
|
|
33
|
+
const plain = commandWork(description);
|
|
34
|
+
const label = plain ? `${plain.doing} ${plain.subject}`.trim() : `Waiting on: ${description || 'a background job'}`;
|
|
35
|
+
if (add(session, `bg:${task?.task_id}`, label, description)) { pending.get(`bg:${task?.task_id}`).background = true; changed = true; }
|
|
36
|
+
}
|
|
37
|
+
return changed;
|
|
38
|
+
}
|
|
15
39
|
export function currentActivity(session) { return [...(session.activeTools?.values() ?? [])]; }
|
|
16
40
|
function toolLabel(name, input) {
|
|
17
41
|
const command = clean(input.command);
|
|
18
42
|
const file = clean(input.file_path ?? input.path);
|
|
19
|
-
if (name === 'Bash')
|
|
43
|
+
if (name === 'Bash') {
|
|
44
|
+
// WHAT IT IS DOING, IN WORDS, when the whole command is recognised. The
|
|
45
|
+
// line under a running row is the same event as the line in the thread
|
|
46
|
+
// above it, read in the present tense (`doing`, shared/work-lines.mjs).
|
|
47
|
+
const plain = commandWork(command);
|
|
48
|
+
if (plain) return [`${plain.doing} ${plain.subject}`.trim(), input.command ?? ''];
|
|
49
|
+
return ['Running ' + (plainCommand(command) || 'command'), input.command ?? ''];
|
|
50
|
+
}
|
|
20
51
|
if (['Read','Write','Edit','MultiEdit'].includes(name)) return [`${name === 'Read' ? 'Reading' : name === 'Write' ? 'Writing' : 'Editing'} ${shortPath(file) || 'file'}`, file];
|
|
21
52
|
if (['Grep','Glob','WebSearch'].includes(name)) return [`Searching ${clean(input.pattern ?? input.query) || 'files'}`, input.pattern ?? input.query ?? ''];
|
|
22
53
|
if (name === 'WebFetch') return ['Reading web page', input.url ?? ''];
|
|
@@ -28,9 +59,13 @@ export function claudeActivity(session, line) {
|
|
|
28
59
|
const obj = JSON.parse(line);
|
|
29
60
|
if (obj.parent_tool_use_id) return false;
|
|
30
61
|
if (obj.type === 'result') return clear(session);
|
|
62
|
+
if (obj.type === 'system' && obj.subtype === 'background_tasks_changed' && Array.isArray(obj.tasks)) return backgroundJobs(session, obj.tasks);
|
|
31
63
|
let changed = false;
|
|
32
64
|
for (const part of Array.isArray(obj.message?.content) ? obj.message.content : []) {
|
|
33
65
|
if (obj.type === 'assistant' && part.type === 'tool_use') {
|
|
66
|
+
// The store's own records never become the word on the row: taking the
|
|
67
|
+
// item is not what the session is doing (w-0bd0d8b2ef).
|
|
68
|
+
if (isBookkeeping(part.name)) continue;
|
|
34
69
|
const [label, detail] = toolLabel(part.name, part.input ?? {});
|
|
35
70
|
changed = add(session, part.id, label, detail) || changed;
|
|
36
71
|
}
|
|
@@ -47,9 +82,14 @@ export function codexActivity(session, method, params) {
|
|
|
47
82
|
if (method !== 'item/started') return false;
|
|
48
83
|
if (item.status && !['inProgress', 'in_progress', 'running'].includes(item.status)) return false;
|
|
49
84
|
switch (item.type) {
|
|
50
|
-
case 'commandExecution':
|
|
85
|
+
case 'commandExecution': {
|
|
86
|
+
const plain = commandWork(clean(item.command));
|
|
87
|
+
const label = plain ? `${plain.doing} ${plain.subject}`.trim() : `Running ${plainCommand(clean(item.command)) || 'command'}`;
|
|
88
|
+
return add(session, item.id, label, item.command);
|
|
89
|
+
}
|
|
51
90
|
case 'fileChange': return add(session, item.id, 'Editing files', (Array.isArray(item.changes) ? item.changes : []).map(c => c?.path ?? '').join('\n'));
|
|
52
|
-
case 'mcpToolCall': return
|
|
91
|
+
case 'mcpToolCall': return isBookkeeping(clean(item.tool)) ? false
|
|
92
|
+
: add(session, item.id, `Using ${clean(item.tool).replace(/_/g, ' ') || 'tool'}`, `${item.server ?? ''} · ${item.tool ?? ''}`);
|
|
53
93
|
case 'webSearch': return add(session, item.id, 'Searching the web', item.query ?? '');
|
|
54
94
|
default: return false;
|
|
55
95
|
}
|
package/main/agent-schedule.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// WHEN
|
|
1
|
+
// WHEN THE USER ASKED TO SEE AN AGENT AGAIN, AND WHEN THEY ASKED NOT TO. One small
|
|
2
2
|
// durable map, and the only state the app keeps about a session it did not start.
|
|
3
3
|
//
|
|
4
4
|
// Three facts per agent and they are different facts. `runAt` is a moment she
|
|
@@ -41,9 +41,9 @@ import { agentKey } from '../shared/agents.mjs';
|
|
|
41
41
|
|
|
42
42
|
const num = (v) => (Number.isFinite(Number(v)) ? Math.max(0, Math.trunc(Number(v))) : 0);
|
|
43
43
|
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
44
|
+
// THE USER'S SIDE OF A THREAD IS SMALL, and this file is read on every draw. A
|
|
45
|
+
// real session has a handful of human turns, typically a sentence or two each.
|
|
46
|
+
// The card shows three. Eight, at 600
|
|
47
47
|
// characters, is far more than anything draws and small enough that a session
|
|
48
48
|
// left running for a month cannot grow the file without bound.
|
|
49
49
|
const KEEP_SPOKE = 8;
|
|
@@ -116,8 +116,8 @@ export class AgentSchedule {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// An entry that says none of "later", "seen", "I spoke to it" or "here is what
|
|
119
|
-
//
|
|
120
|
-
// same list as it was:
|
|
119
|
+
// the user said" is not an entry. The last of those four is why this is not the
|
|
120
|
+
// same list as it was: the user's words outlive the reply that carried them, so an
|
|
121
121
|
// entry holding only `spoke` is still the record of a conversation.
|
|
122
122
|
_keep(key, entry, now) {
|
|
123
123
|
if (entry.runAt > now || entry.doneThrough > 0 || entry.repliedAt > 0 || entry.spoke?.length) this.byKey[key] = entry;
|
|
@@ -155,8 +155,8 @@ export class AgentSchedule {
|
|
|
155
155
|
// undid the close, saying a session was working on something she had
|
|
156
156
|
// already finished with.
|
|
157
157
|
//
|
|
158
|
-
// BUT IT DOES NOT UNSAY WHAT
|
|
159
|
-
// and `spoke` is the ONLY record anywhere that
|
|
158
|
+
// BUT IT DOES NOT UNSAY WHAT THE USER SAID. This used to empty `spoke` as well,
|
|
159
|
+
// and `spoke` is the ONLY record anywhere that the user ever spoke to a session:
|
|
160
160
|
// Claude Code stamps an injected message `isMeta` and the transcript reader
|
|
161
161
|
// drops it, correctly, so nothing else on the machine knows. Measured on
|
|
162
162
|
// session-46: four of her messages reached that session and the file holds
|
|
@@ -200,12 +200,12 @@ export class AgentSchedule {
|
|
|
200
200
|
*
|
|
201
201
|
* It is here because it is the only place that knows. Claude Code stamps a
|
|
202
202
|
* message injected over its socket `isMeta: true` and wraps it in "Another
|
|
203
|
-
* Claude session sent a message…
|
|
204
|
-
*
|
|
205
|
-
* what
|
|
203
|
+
* Claude session sent a message…", so the transcript reader drops it, and
|
|
204
|
+
* without this record messages sent from Agentbox left the row saying only
|
|
205
|
+
* what the session was started with days earlier.
|
|
206
206
|
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
207
|
+
* Another of the user's sessions pinging this one is a real peer, and drawing
|
|
208
|
+
* it as the user's words would be a lie on the one card built to be trusted. So the record
|
|
209
209
|
* is written at the moment of delivery, by the only process that knows it
|
|
210
210
|
* delivered it.
|
|
211
211
|
*/
|
|
@@ -236,7 +236,7 @@ export class AgentSchedule {
|
|
|
236
236
|
if (entry.runAt > now) { out.runAt = entry.runAt; out.runAtSetAt = entry.setAt; }
|
|
237
237
|
if (entry.doneThrough > 0) out.doneThrough = entry.doneThrough;
|
|
238
238
|
if (entry.repliedAt > 0) out.repliedAt = entry.repliedAt;
|
|
239
|
-
// the card quotes what
|
|
239
|
+
// the card quotes what the user said last, and a session that has since
|
|
240
240
|
// stopped is exactly when she most needs to see it.
|
|
241
241
|
if (entry.spoke?.length) out.spoke = entry.spoke;
|
|
242
242
|
return out;
|
package/main/agent-sessions.mjs
CHANGED
|
@@ -58,8 +58,12 @@ import path from 'node:path';
|
|
|
58
58
|
* the last three days and 9 in the last seven, in two folders she works in.
|
|
59
59
|
* Three days is a card with tonight's test on it and almost nothing else; the
|
|
60
60
|
* filters have already done the work that "a few" was standing in for, so the
|
|
61
|
-
* window can afford to be the generous reading.
|
|
62
|
-
|
|
61
|
+
* window can afford to be the generous reading.
|
|
62
|
+
*
|
|
63
|
+
* Ten since the launch list (w-db6f5e331e, 2026-10-05): "showing your recently
|
|
64
|
+
* active agents over the last 10 days". Codex reads the same number
|
|
65
|
+
* (main/codex-threads.mjs), so the two halves of the card agree. */
|
|
66
|
+
export const RECENT_DAYS = 10;
|
|
63
67
|
|
|
64
68
|
/**
|
|
65
69
|
* How much of a transcript is read to find its first human turn. The first
|
|
@@ -67,6 +71,15 @@ export const RECENT_DAYS = 7;
|
|
|
67
71
|
* the largest in her store on 2026-08-29 is 13MB, and there are hundreds. */
|
|
68
72
|
const HEAD_BYTES = 96 * 1024;
|
|
69
73
|
|
|
74
|
+
/**
|
|
75
|
+
* How far past the head one cut-off first message may be followed
|
|
76
|
+
* (w-24edc04548, 2026-10-05). A screenshot pasted into the first message is
|
|
77
|
+
* written into that row as base64, so the row outgrows the head: three of her
|
|
78
|
+
* ten days' terminal threads opened with rows of 240KB, 294KB and 486KB, were
|
|
79
|
+
* cut, lost their name and fell off the card. Eight megabytes is the Codex
|
|
80
|
+
* reader's own ceiling (main/codex-threads.mjs) and several screenshots. */
|
|
81
|
+
const FIRST_TURN_BYTES = 8 * 1024 * 1024;
|
|
82
|
+
|
|
70
83
|
/**
|
|
71
84
|
* A stop on how many transcripts are OPENED, for the same reason the folder
|
|
72
85
|
* scan in agent-files.mjs has one: opening a card may not turn into a disk
|
|
@@ -80,6 +93,10 @@ const HEAD_BYTES = 96 * 1024;
|
|
|
80
93
|
* whether tonight's thread survived was down to where its folder sorted. A
|
|
81
94
|
* cap that can drop the newest thread on the machine is not a safety rail. */
|
|
82
95
|
const MAX_READS = 1200;
|
|
96
|
+
// How long the conversation read may take, in ms (w-ec62ab6b38). The card that
|
|
97
|
+
// uses it says "Looking" meanwhile; three seconds reaches two thirds of her
|
|
98
|
+
// week's 9,073 transcripts and all of a normal Mac's.
|
|
99
|
+
const READ_BUDGET_MS = 3000;
|
|
83
100
|
|
|
84
101
|
/* ------------------------- WHAT IS NOT HERS TO IMPORT ---------------------- */
|
|
85
102
|
|
|
@@ -106,6 +123,21 @@ export function isScratchFolder(folder, home = os.homedir()) {
|
|
|
106
123
|
|| /^\/var\/folders(\/|$)/.test(p);
|
|
107
124
|
}
|
|
108
125
|
|
|
126
|
+
/**
|
|
127
|
+
* THE SAME RULE, READ OFF THE FOLDER CLAUDE CODE FILED THE TRANSCRIPT IN, so
|
|
128
|
+
* it can be applied before the file is opened (w-24edc04548, 2026-10-05).
|
|
129
|
+
* Claude Code names that folder after the working folder with every other
|
|
130
|
+
* character turned into `-`, so `/private/tmp/w-1` is `-private-tmp-w-1`.
|
|
131
|
+
* Of her 12,272 transcripts touched in ten days about 12,000 were in temp
|
|
132
|
+
* folders, and opening each one only to refuse it spent the card's three
|
|
133
|
+
* seconds before it reached her oldest threads. */
|
|
134
|
+
export function isScratchProjectDir(name, home = os.homedir()) {
|
|
135
|
+
const n = String(name ?? '');
|
|
136
|
+
const here = path.resolve(String(home ?? '')).replace(/[^a-zA-Z0-9]/g, '-');
|
|
137
|
+
if (here.length > 1 && (n === here || n.startsWith(`${here}-`))) return false;
|
|
138
|
+
return /^-(private-)?(tmp|var-folders)(-|$)/.test(n);
|
|
139
|
+
}
|
|
140
|
+
|
|
109
141
|
/**
|
|
110
142
|
* THE ONES A PERSON STARTED, which is the whole filter.
|
|
111
143
|
*
|
|
@@ -171,7 +203,9 @@ export function threadTitle(texts, max = 72) {
|
|
|
171
203
|
function titleFromOne(text, max) {
|
|
172
204
|
const raw = String(text ?? '').replace(/\r/g, '');
|
|
173
205
|
for (const line of raw.split('\n')) {
|
|
174
|
-
|
|
206
|
+
// `[Image #1]` and `[Pasted text #1 +40 lines]` are Claude Code's stand-ins
|
|
207
|
+
// for what was pasted, not words she typed.
|
|
208
|
+
const t = line.replace(/\[(?:Image|Pasted text) #\d+[^\]]*\]/g, '').trim();
|
|
175
209
|
if (!t) continue;
|
|
176
210
|
if (NOT_TYPED.some((re) => re.test(t))) return '';
|
|
177
211
|
const clean = t
|
|
@@ -209,7 +243,7 @@ export function shortFolder(folder, home = os.homedir()) {
|
|
|
209
243
|
* every message row and the first human turn is the first message, so all
|
|
210
244
|
* three are at the top, and the alternative is reading gigabytes to fill in a
|
|
211
245
|
* list. Her largest transcript is 13MB and there are hundreds. */
|
|
212
|
-
export function readTranscriptHead(file, bytes = HEAD_BYTES) {
|
|
246
|
+
export function readTranscriptHead(file, bytes = HEAD_BYTES, most = FIRST_TURN_BYTES) {
|
|
213
247
|
let fd = null;
|
|
214
248
|
try {
|
|
215
249
|
fd = fs.openSync(file, 'r');
|
|
@@ -218,22 +252,37 @@ export function readTranscriptHead(file, bytes = HEAD_BYTES) {
|
|
|
218
252
|
const text = buf.slice(0, n).toString('utf8');
|
|
219
253
|
// The last line of a fixed read is usually cut in half. Dropping it costs
|
|
220
254
|
// nothing, because what is wanted is at the top.
|
|
221
|
-
const lines = text.split('\n')
|
|
255
|
+
const lines = text.split('\n');
|
|
256
|
+
const cut = lines.pop();
|
|
222
257
|
let cwd = null;
|
|
223
258
|
let entrypoint = null;
|
|
224
259
|
const said = [];
|
|
225
|
-
|
|
226
|
-
if (!line.trim()) continue;
|
|
260
|
+
const take = (line) => {
|
|
227
261
|
let row;
|
|
228
|
-
try { row = JSON.parse(line); } catch {
|
|
262
|
+
try { row = JSON.parse(line); } catch { return; }
|
|
229
263
|
if (!cwd && typeof row.cwd === 'string' && row.cwd) cwd = row.cwd;
|
|
230
264
|
if (!entrypoint && typeof row.entrypoint === 'string' && row.entrypoint) entrypoint = row.entrypoint;
|
|
231
265
|
if (row.type === 'user' && said.length < 5) {
|
|
232
266
|
const t = messageText(row);
|
|
233
267
|
if (t.trim()) said.push(t);
|
|
234
268
|
}
|
|
269
|
+
};
|
|
270
|
+
for (const line of lines) {
|
|
271
|
+
if (!line.trim()) continue;
|
|
272
|
+
take(line);
|
|
235
273
|
if (cwd && entrypoint && said.length >= 5) break;
|
|
236
274
|
}
|
|
275
|
+
// UNLESS THE HALF LINE IS HER FIRST MESSAGE, cut because a screenshot was
|
|
276
|
+
// pasted into it. Then it is read to its end, once, up to `most`. Only a
|
|
277
|
+
// cut row that names itself a user message near its start qualifies, and
|
|
278
|
+
// only when nothing she said was found and the session is not already
|
|
279
|
+
// known to be the app's own, so the thousands of worker transcripts are
|
|
280
|
+
// never read past their head.
|
|
281
|
+
if (!said.length && n === bytes && cut && /"type":"user"/.test(cut.slice(0, 2048))
|
|
282
|
+
&& (!entrypoint || startedByHand(entrypoint))) {
|
|
283
|
+
const rest = restOfLine(fd, n, most - n);
|
|
284
|
+
if (rest) take(Buffer.concat([buf.subarray(buf.lastIndexOf(0x0a, n - 1) + 1, n), rest]).toString('utf8'));
|
|
285
|
+
}
|
|
237
286
|
return { cwd, entrypoint, said };
|
|
238
287
|
} catch {
|
|
239
288
|
return { cwd: null, entrypoint: null, said: [] };
|
|
@@ -242,6 +291,25 @@ export function readTranscriptHead(file, bytes = HEAD_BYTES) {
|
|
|
242
291
|
}
|
|
243
292
|
}
|
|
244
293
|
|
|
294
|
+
/**
|
|
295
|
+
* The bytes from `from` up to the next newline, or null if there is none
|
|
296
|
+
* within `budget` bytes: a line that long is given up on, not read whole. A
|
|
297
|
+
* file that ends without a newline ends the line too. */
|
|
298
|
+
function restOfLine(fd, from, budget) {
|
|
299
|
+
const parts = [];
|
|
300
|
+
let at = from;
|
|
301
|
+
const chunk = Buffer.alloc(256 * 1024);
|
|
302
|
+
while (at - from < budget) {
|
|
303
|
+
const n = fs.readSync(fd, chunk, 0, Math.min(chunk.length, budget - (at - from)), at);
|
|
304
|
+
if (n <= 0) return Buffer.concat(parts);
|
|
305
|
+
const nl = chunk.subarray(0, n).indexOf(0x0a);
|
|
306
|
+
if (nl >= 0) { parts.push(Buffer.from(chunk.subarray(0, nl))); return Buffer.concat(parts); }
|
|
307
|
+
parts.push(Buffer.from(chunk.subarray(0, n)));
|
|
308
|
+
at += n;
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
245
313
|
/**
|
|
246
314
|
* The words out of one transcript row, whichever of the two shapes it is in.
|
|
247
315
|
* Content is a string on the plain ones and a list of blocks on the rest. */
|
|
@@ -278,11 +346,27 @@ function readTerminalThreads({ home, cutoff, max, skipIds = new Set() }) {
|
|
|
278
346
|
try { when = fs.statSync(file).mtimeMs; } catch { continue; }
|
|
279
347
|
if (when < cutoff) { skipped.old += 1; continue; }
|
|
280
348
|
if (isSubagentPath(file.slice(root.length))) { skipped.subagent += 1; continue; }
|
|
349
|
+
if (isScratchProjectDir(path.relative(root, file).split(path.sep)[0], home)) { skipped.scratch += 1; continue; }
|
|
281
350
|
recent.push({ file, when });
|
|
282
351
|
}
|
|
283
352
|
recent.sort((a, b) => b.when - a.when);
|
|
284
|
-
|
|
285
|
-
|
|
353
|
+
// THE CAP COUNTS WHAT IS KEPT, NOT WHAT IS OPENED (w-ec62ab6b38, 2026-09-28).
|
|
354
|
+
// On a busy machine the last seven days held 9,073 transcripts, the newest
|
|
355
|
+
// 1,200 were opened, and every one of those was started by agentbox itself,
|
|
356
|
+
// so a conversation started by hand was never looked at and the import card
|
|
357
|
+
// said there was nothing, as if the app were not looking in Claude Code at
|
|
358
|
+
// all. Somebody running dozens of agents fills this folder with ours. So it
|
|
359
|
+
// opens newest first until it has `max` of the person's own, or until
|
|
360
|
+
// READ_BUDGET_MS has gone, whichever is first. Reading all 9,073 heads took
|
|
361
|
+
// 5.0s there.
|
|
362
|
+
const started = Date.now();
|
|
363
|
+
let opened = 0;
|
|
364
|
+
for (const { file, when } of recent) {
|
|
365
|
+
if (out.length >= max || Date.now() - started > READ_BUDGET_MS) {
|
|
366
|
+
skipped.unread = recent.length - opened;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
opened += 1;
|
|
286
370
|
const id = path.basename(file, '.jsonl');
|
|
287
371
|
// THE DESKTOP APP WRITES A THREAD TWICE, once as a transcript here and once
|
|
288
372
|
// as its own record, joined by `cliSessionId`. Measured 2026-08-29 on the
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Release discovery is read-only. An updater is a dedicated PTY command,
|
|
2
|
+
// started only by the app's Update now action, never by a poll or terminal open.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {execFile} from 'node:child_process';
|
|
7
|
+
import {promisify} from 'node:util';
|
|
8
|
+
import {TaskTerminals,terminalEnv} from './task-terminals.mjs';
|
|
9
|
+
import {codexLaunchEnv} from './codex-launch-env.mjs';
|
|
10
|
+
const exec=promisify(execFile);
|
|
11
|
+
const valid=v=>typeof v==='string'&&/^\d+\.\d+\.\d+$/.test(v);
|
|
12
|
+
export function newerVersion(installed,latest){
|
|
13
|
+
if(!valid(installed)||!valid(latest))return false;
|
|
14
|
+
const a=installed.split('.').map(Number),b=latest.split('.').map(Number);
|
|
15
|
+
for(let i=0;i<3;i++){if(b[i]!==a[i])return b[i]>a[i];}return false;
|
|
16
|
+
}
|
|
17
|
+
export function updatePlan({engine,bin,real,home=os.homedir(),channel='latest'}){
|
|
18
|
+
if(!['claude','codex'].includes(engine)||!path.isAbsolute(bin||'')||!real)return null;
|
|
19
|
+
const name=engine==='claude'?'Claude Code':'Codex';
|
|
20
|
+
const brew=real.match(/^(\/opt\/homebrew|\/usr\/local)\/Caskroom\/(claude-code(?:@latest)?|codex)\//);
|
|
21
|
+
if(brew&&((engine==='claude'&&brew[2].startsWith('claude-code'))||(engine==='codex'&&brew[2]==='codex')))
|
|
22
|
+
return {name,file:`${brew[1]}/bin/brew`,args:['upgrade','--cask',brew[2]],feed:`https://formulae.brew.sh/api/cask/${brew[2]}.json`,format:'brew'};
|
|
23
|
+
const pkg=engine==='claude'?'@anthropic-ai/claude-code':'@openai/codex';
|
|
24
|
+
const tail=`/lib/node_modules/${pkg}/`;
|
|
25
|
+
const at=real.indexOf(tail);
|
|
26
|
+
if(at>0){const prefix=real.slice(0,at);return {name,file:path.join(prefix,'bin/npm'),args:['install','--global','--prefix',prefix,`${pkg}@${channel}`],env:{PATH:`${prefix}/bin:${process.env.PATH||''}`},feed:`https://registry.npmjs.org/${pkg}/${channel}`,format:'npm'};}
|
|
27
|
+
if(engine==='claude'&&real.startsWith(path.join(home,'.local/share/claude/versions')+'/'))return {name,file:bin,args:['update'],feed:`https://downloads.claude.ai/claude-code-releases/${channel}`,format:'text'};
|
|
28
|
+
const standalone=real.match(/^(.*)\/packages\/standalone\/(?:releases|current)\//);
|
|
29
|
+
if(engine==='codex'&&standalone)return {name,file:'/bin/bash',args:['-o','pipefail','-c','/usr/bin/curl -fsSL --connect-timeout 10 --max-time 60 https://chatgpt.com/codex/install.sh | /bin/sh'],env:{CODEX_HOME:standalone[1],CODEX_INSTALL_DIR:path.dirname(bin)},feed:'https://releases.openai.com/codex/channels/latest',format:'codex'};
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const json=file=>{try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return {}}};
|
|
33
|
+
export async function installedAgent(engine,config){
|
|
34
|
+
const bin=engine==='codex'?config.codexBin:config.claudeBin;
|
|
35
|
+
if(!bin)throw Error('This agent is not installed.');
|
|
36
|
+
const home=config.home||os.homedir();
|
|
37
|
+
const settings=engine==='claude'?json(path.join(process.env.CLAUDE_CONFIG_DIR||path.join(home,'.claude'),'settings.json')):{};
|
|
38
|
+
const cleanEnv=terminalEnv();
|
|
39
|
+
const env=engine==='codex'?codexLaunchEnv(bin,cleanEnv):cleanEnv;
|
|
40
|
+
// Version inspection must not itself auto-update anything.
|
|
41
|
+
const {stdout}=await exec(bin,['--version'],{timeout:8000,maxBuffer:16384,env:{...env,DISABLE_AUTOUPDATER:'1'}});
|
|
42
|
+
const version=stdout.match(/\b\d+\.\d+\.\d+(?:-[\w.-]+)?\b/)?.[0];
|
|
43
|
+
const channel=settings.autoUpdatesChannel==='stable'?'stable':'latest';
|
|
44
|
+
return {engine,bin,real:fs.realpathSync(bin),home,version,channel,disabled:settings.env?.DISABLE_UPDATES==='1'||process.env.DISABLE_UPDATES==='1'};
|
|
45
|
+
}
|
|
46
|
+
export async function latestRelease(plan){
|
|
47
|
+
const r=await fetch(plan.feed,{signal:AbortSignal.timeout(10000),headers:{Accept:plan.format==='text'?'text/plain':'application/json'}});
|
|
48
|
+
if(!r.ok)throw Error('Release check failed.');
|
|
49
|
+
const body=await r.text();if(body.length>2_000_000)throw Error('Release response too large.');
|
|
50
|
+
const data=plan.format==='text'?null:JSON.parse(body);
|
|
51
|
+
const version=plan.format==='text'?body.trim():plan.format==='codex'?String(data.version||data.tag_name||'').replace(/^(rust-v|v)/,''):data.version;
|
|
52
|
+
if(!valid(version))throw Error('Release version could not be read.');return version;
|
|
53
|
+
}
|
|
54
|
+
export class AgentUpdates{
|
|
55
|
+
constructor({installation,latest=latestRelease,refresh=async()=>{},terminals,now=()=>Date.now()}={}){
|
|
56
|
+
this.installation=installation;this.latest=latest;this.refresh=refresh;this.now=now;this.entries=new Map();this.checks=new Map();this.starts=new Map();this.finishes=new Map();this.plans=new Map();
|
|
57
|
+
this.terminals=terminals||new TaskTerminals({sweepMs:0});
|
|
58
|
+
this.terminals.resolve=engine=>{const p=this.plans.get(engine);if(!p)throw Error('Click Update now first.');return {cwd:p.home,command:{file:p.file,args:p.args,env:p.env},notice:`Updating ${p.name}.`}};
|
|
59
|
+
}
|
|
60
|
+
status(engine){return this.entries.get(engine)||{engine,state:'unknown'};}
|
|
61
|
+
check(engine,force=false){
|
|
62
|
+
if(!['claude','codex'].includes(engine))return Promise.reject(Error('Unknown agent.'));
|
|
63
|
+
if(this.starts.has(engine)||this.status(engine).state==='running')return Promise.resolve(this.status(engine));
|
|
64
|
+
if(this.checks.has(engine))return this.checks.get(engine);
|
|
65
|
+
const saved=this.status(engine);if(!force&&saved.checkedAt&&this.now()-saved.checkedAt<3600000)return Promise.resolve(saved);
|
|
66
|
+
const work=(async()=>{let result;try{
|
|
67
|
+
const install=await this.installation(engine);const plan=install.disabled?null:updatePlan(install);
|
|
68
|
+
if(!plan)result={engine,state:'unsupported',message:'Update this installation using its own installer.'};
|
|
69
|
+
else{const latest=await this.latest(plan);result={engine,state:newerVersion(install.version,latest)?'available':'current',installed:install.version,latest};if(!valid(install.version))throw Error('Installed version could not be read.');}
|
|
70
|
+
}catch{result={engine,state:'unknown',message:'Could not check for updates. Your current models are still available.'};}
|
|
71
|
+
result.checkedAt=this.now();if(this.starts.has(engine)||this.status(engine).state==='running')return this.status(engine);this.entries.set(engine,result);return result;})().finally(()=>this.checks.delete(engine));
|
|
72
|
+
this.checks.set(engine,work);return work;
|
|
73
|
+
}
|
|
74
|
+
start(engine){
|
|
75
|
+
if(this.starts.has(engine))return this.starts.get(engine);
|
|
76
|
+
if(this.status(engine).state==='running')return Promise.resolve(this.status(engine));
|
|
77
|
+
// Validate again at click time: an installation may have changed since
|
|
78
|
+
// the picker opened. Never trust a command or a release passed by the UI.
|
|
79
|
+
const work=(async()=>{
|
|
80
|
+
const install=await this.installation(engine);const plan=install.disabled?null:updatePlan(install);
|
|
81
|
+
if(!plan)throw Error('This installation must be updated using its own installer.');
|
|
82
|
+
const target=await this.latest(plan);
|
|
83
|
+
if(!valid(install.version))throw Error('Installed version could not be read.');
|
|
84
|
+
if(!newerVersion(install.version,target)){
|
|
85
|
+
await this.refresh(engine);const current={engine,state:'current',installed:install.version,latest:target,checkedAt:this.now(),message:'Up to date. Available models refreshed.'};this.entries.set(engine,current);return current;
|
|
86
|
+
}
|
|
87
|
+
this.plans.set(engine,{...plan,home:install.home});this.terminals.close(engine);
|
|
88
|
+
this.terminals.open(engine);
|
|
89
|
+
const running={engine,state:'running',installed:install.version,latest:target};this.entries.set(engine,running);
|
|
90
|
+
const finished=this.terminals.get(engine).exitPromise.then(async()=>{
|
|
91
|
+
try{
|
|
92
|
+
if(this.terminals.get(engine).exitCode!==0)throw Error('Update did not finish. You can retry.');
|
|
93
|
+
const after=await this.installation(engine);
|
|
94
|
+
if(!valid(after.version)||newerVersion(after.version,target))throw Error('The installed version is still older. Check the terminal output before retrying.');
|
|
95
|
+
await this.refresh(engine);
|
|
96
|
+
this.entries.set(engine,{engine,state:'current',installed:after.version,latest:target,checkedAt:this.now(),message:'Updated. Available models refreshed.'});
|
|
97
|
+
}catch(error){this.entries.set(engine,{...running,state:'failed',message:error.message});}
|
|
98
|
+
});this.finishes.set(engine,finished);return running;
|
|
99
|
+
})().catch(error=>{this.entries.set(engine,{engine,state:'failed',message:error.message});throw error;}).finally(()=>this.starts.delete(engine));
|
|
100
|
+
this.starts.set(engine,work);return work;
|
|
101
|
+
}
|
|
102
|
+
settled(engine){return this.finishes.get(engine)||Promise.resolve();}
|
|
103
|
+
terminal(engine,p){
|
|
104
|
+
// Opening the viewer is never an install action, including after it ended.
|
|
105
|
+
if(!this.plans.has(engine))throw Error('Click Update now first.');
|
|
106
|
+
switch(p.action){
|
|
107
|
+
case 'open':return this.terminals.read(engine,0);
|
|
108
|
+
case 'read':return this.terminals.read(engine,p.offset);
|
|
109
|
+
case 'write':return this.terminals.write(engine,p.data);
|
|
110
|
+
case 'resize':return this.terminals.resize(engine,p.cols,p.rows);
|
|
111
|
+
case 'close':return this.terminals.close(engine);
|
|
112
|
+
default:throw Error('Unknown terminal action.');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
shutdown(){return this.terminals.shutdown();}
|
|
116
|
+
}
|