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
package/main/config.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import os from 'node:os';
|
|
|
11
11
|
import crypto from 'node:crypto';
|
|
12
12
|
import { resolveClaudeBin, candidatePaths } from './claude-bin.mjs';
|
|
13
13
|
import { resolveCodexBin } from './codex-bin.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import { defaultStoreRoot } from './store/home.mjs';
|
|
15
15
|
import { readPlans, slotsForPlans } from './claude-plan.mjs';
|
|
16
16
|
import { machineSlots } from './machine.mjs';
|
|
17
17
|
import { effectiveProfiles } from './account-discovery.mjs';
|
|
@@ -34,14 +34,6 @@ const CONFIG_NAME = 'zero.config.json';
|
|
|
34
34
|
export const DEFAULT_SESSIONS_AT_ONCE = 3;
|
|
35
35
|
|
|
36
36
|
const DEFAULTS = {
|
|
37
|
-
// Where the inbox's own files live. WHICH FOLDER THIS SHOULD BE ON A
|
|
38
|
-
// STRANGER'S MAC IS NOT DECIDED HERE: it is the open question on.
|
|
39
|
-
//
|
|
40
|
-
// IT IS THE APP'S OWN NAME SINCE 2026-09-22, where it used to be an older
|
|
41
|
-
// product's. A real store is always named in the config, so this default is
|
|
42
|
-
// only ever the answer for an install carrying none, and renaming it moved
|
|
43
|
-
// no files: the folder it used to point at is untouched and still on disk.
|
|
44
|
-
storeRoot: path.join(os.homedir(), Name),
|
|
45
37
|
// ONE ACCOUNT FOLDER PER INSTALL, AND NOBODY ELSE'S.
|
|
46
38
|
//
|
|
47
39
|
// This was a literal id for most of the app's life, which meant every copy
|
|
@@ -59,10 +51,30 @@ const DEFAULTS = {
|
|
|
59
51
|
// How many sessions run at once. The queue absorbs the rest; when the plan
|
|
60
52
|
// window caps out, a longer queue is the visible, benign failure mode.
|
|
61
53
|
//
|
|
62
|
-
// THIS IS THE NUMBER FOR A MAX SUBSCRIPTION,
|
|
63
|
-
//
|
|
54
|
+
// THIS IS THE NUMBER FOR A MAX SUBSCRIPTION, the biggest plan. When nobody
|
|
55
|
+
// has set one, loadConfig below reads the plan
|
|
64
56
|
// Claude Code is signed in on and lowers this for a smaller one.
|
|
65
57
|
maxConcurrentSessions: DEFAULT_SESSIONS_AT_ONCE,
|
|
58
|
+
// LET AGENTBOX DECIDE HOW MANY RUN (w-e5225b62ba). On unless somebody has set
|
|
59
|
+
// a number themselves, which `loadConfig` works out below from whether one was
|
|
60
|
+
// ever written down. With it on, `maxConcurrentSessions` is not read at all:
|
|
61
|
+
// `Supervisor#_slotsPerAccount` asks main/machine.mjs for what this Mac
|
|
62
|
+
// carries and shares that out across the live accounts. `agentsNudge` is how
|
|
63
|
+
// many times "Something felt slow" has been pressed, and only ever subtracts.
|
|
64
|
+
agentsAuto: true,
|
|
65
|
+
agentsNudge: 0,
|
|
66
|
+
// HOLD HEAVY WORK WHEN MEMORY IS SHORT (w-3958c3753d). Off until somebody
|
|
67
|
+
// turns it on from the Agents page. With it on, every shell command a worker
|
|
68
|
+
// runs asks main/memory-gate-server.mjs first, and heavy ones wait their turn
|
|
69
|
+
// while the Mac is short of memory. `memoryGateSlots` is how many heavy
|
|
70
|
+
// commands may run at once; null is Auto, one per 8 GB.
|
|
71
|
+
memoryGate: false,
|
|
72
|
+
memoryGateSlots: null,
|
|
73
|
+
// STOP WHAT FINISHED AGENTS LEAVE RUNNING (main/leftovers.mjs). Its own
|
|
74
|
+
// switch, off until somebody turns it on; `cleanupLeftoversSince` is the
|
|
75
|
+
// moment they did, so nothing is stopped sooner than the idle time after it.
|
|
76
|
+
cleanupLeftovers: false,
|
|
77
|
+
cleanupLeftoversSince: null,
|
|
66
78
|
// What spawned sessions are allowed to do. Left unset on purpose, because the
|
|
67
79
|
// right default now depends on whether a store MCP server is present: see
|
|
68
80
|
// Supervisor#sessionArgsFor. Setting it here still wins over that, which is
|
|
@@ -80,11 +92,9 @@ const DEFAULTS = {
|
|
|
80
92
|
// PERSONAL PROJECTS AND THE DRIVE WERE BOTH DECLARED HERE AND BOTH ARE GONE
|
|
81
93
|
// (w-d19d6d387c, 2026-09-22). A personal project ran its sessions on the
|
|
82
94
|
// founder's message and nothing else, with no brief and no store; the drive
|
|
83
|
-
// asked "what next?" by itself on a project that had gone quiet.
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// that self-drives the product entirely. That should never have existed
|
|
87
|
-
// anyway, especially because we're open sourcing it soon."
|
|
95
|
+
// asked "what next?" by itself on a project that had gone quiet. There is no
|
|
96
|
+
// case where a project should run without a brief and a store, and code that
|
|
97
|
+
// drives a product on its own does not belong in an open-source app.
|
|
88
98
|
//
|
|
89
99
|
// A config still carrying `personalProducts` or `driveEnabled` is not an
|
|
90
100
|
// error; nothing reads either key now.
|
|
@@ -142,6 +152,19 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
|
|
|
142
152
|
else console.warn(`zero: could not read ${file}: ${err.message}`);
|
|
143
153
|
}
|
|
144
154
|
const config = { ...DEFAULTS, ...overrides, freshInstall: fresh };
|
|
155
|
+
// Where the inbox's own files live, when the file names no store. A path in
|
|
156
|
+
// the file always wins, including on Linux. The default is computed here, not
|
|
157
|
+
// at import: on Linux it is the XDG data directory, and on macOS a folder of
|
|
158
|
+
// the app's name. An import-time path cannot see the `home` a test passes,
|
|
159
|
+
// and it cannot see a blanked XDG_DATA_HOME.
|
|
160
|
+
//
|
|
161
|
+
// A stand-in home does not inherit this machine's XDG_DATA_HOME. That
|
|
162
|
+
// variable is an absolute path in the real home, and a throwaway asked with
|
|
163
|
+
// `{ home }` would otherwise open the real store.
|
|
164
|
+
if (typeof overrides.storeRoot !== 'string' || !overrides.storeRoot.trim()) {
|
|
165
|
+
const real = path.resolve(home) === path.resolve(os.homedir());
|
|
166
|
+
config.storeRoot = defaultStoreRoot({ home, env: real ? process.env : {} });
|
|
167
|
+
}
|
|
145
168
|
// An older alias for `storeRoot` was read here until 2026-09-22. A config
|
|
146
169
|
// still carrying only the old key falls through to the default now, which is
|
|
147
170
|
// why the key was renamed in zero.config.json in the same step and not after
|
|
@@ -150,11 +173,20 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
|
|
|
150
173
|
// The account id, minted once per install. Written back immediately, because
|
|
151
174
|
// a generated id that is not saved is a different id next launch and the
|
|
152
175
|
// store would move out from under everything.
|
|
176
|
+
//
|
|
177
|
+
// AND A NEW INSTALL MAY CHOOSE CODEX FROM ITS FIRST LAUNCH (w-db6f5e331e).
|
|
178
|
+
// `engineChoice` is the moment from which a row may run on Codex; it exists
|
|
179
|
+
// so August rows in one old store never move engine unasked, and it could
|
|
180
|
+
// only be written by hand, so a new person with both engines never got
|
|
181
|
+
// Codex at all. A Mac with no config file has no old rows, so its first
|
|
182
|
+
// launch is that moment. An install with a config is left as it was.
|
|
183
|
+
const firstLaunch = fresh && !config.engineChoice ? new Date().toISOString() : null;
|
|
184
|
+
if (firstLaunch) config.engineChoice = firstLaunch;
|
|
153
185
|
if (typeof config.accountId !== 'string' || !config.accountId) {
|
|
154
186
|
config.accountId = crypto.randomUUID();
|
|
155
187
|
try {
|
|
156
188
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
157
|
-
fs.writeFileSync(file, `${JSON.stringify({ ...overrides, accountId: config.accountId }, null, 2)}\n`);
|
|
189
|
+
fs.writeFileSync(file, `${JSON.stringify({ ...overrides, accountId: config.accountId, ...(firstLaunch ? { engineChoice: firstLaunch } : {}) }, null, 2)}\n`);
|
|
158
190
|
} catch (err) {
|
|
159
191
|
console.warn(`zero: could not write the new account id to ${file}: ${err.message}`);
|
|
160
192
|
}
|
|
@@ -205,6 +237,11 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
|
|
|
205
237
|
config.codexFound = codex.found;
|
|
206
238
|
config.codexCertain = codex.certain !== false;
|
|
207
239
|
config.codexFrom = codex.from;
|
|
240
|
+
// AND AN OLDER INSTALL WITH CODEX ON IT OPENS THE CHOICE TOO (w-d5d632e503).
|
|
241
|
+
// Only a Mac with no config file did, so a person who installed before
|
|
242
|
+
// that, or who added Codex later, never saw Codex anywhere. Written at the
|
|
243
|
+
// end, once `appDir` is on the config to save through.
|
|
244
|
+
const codexOpened = codexChoiceToOpen(config, codex.found);
|
|
208
245
|
// HOW MANY AGENTS AT ONCE, WHEN NOBODY HAS SAID, IS THE PLAN AND THE MACHINE
|
|
209
246
|
// TOGETHER. Somebody on a smaller plan gets several sessions started for them,
|
|
210
247
|
// they all spend the same allowance, and what they see is agents that are slow
|
|
@@ -237,6 +274,16 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
|
|
|
237
274
|
// already in zero.config.json is untouched, for the same reason the plan cap
|
|
238
275
|
// leaves it alone.
|
|
239
276
|
config.machineSlots = machineSlots().slots;
|
|
277
|
+
// WHO IS DECIDING THE NUMBER, read off whether one was ever written down
|
|
278
|
+
// (w-e5225b62ba). A number in zero.config.json is somebody's own instruction
|
|
279
|
+
// about this Mac and keeps being obeyed; an install that never touched the
|
|
280
|
+
// stepper was already having its number filled in here, and Automatic is that
|
|
281
|
+
// same thing said out loud and kept up to date. An explicit `agentsAuto`
|
|
282
|
+
// outranks the guess, because turning Automatic back on after setting a number
|
|
283
|
+
// has to be possible.
|
|
284
|
+
if (typeof overrides.agentsAuto !== 'boolean') {
|
|
285
|
+
config.agentsAuto = typeof overrides.maxConcurrentSessions !== 'number';
|
|
286
|
+
}
|
|
240
287
|
if (typeof overrides.maxConcurrentSessions !== 'number') {
|
|
241
288
|
const plans = readPlans(effectiveProfiles(config.authProfiles, { home }), { home });
|
|
242
289
|
const slots = slotsForPlans(plans, config.machineSlots);
|
|
@@ -254,9 +301,27 @@ export function loadConfig(appDir, { home = os.homedir() } = {}) {
|
|
|
254
301
|
// Where the file is, so anything that edits a setting can write it back
|
|
255
302
|
// without being handed appDir a second time.
|
|
256
303
|
config.appDir = appDir;
|
|
304
|
+
if (codexOpened) {
|
|
305
|
+
try {
|
|
306
|
+
saveConfig(config, { engineChoice: codexOpened });
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.warn(`zero: could not open the Codex choice in ${file}: ${err.message}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
257
311
|
return config;
|
|
258
312
|
}
|
|
259
313
|
|
|
314
|
+
/**
|
|
315
|
+
* THE MOMENT TO OPEN THE CODEX CHOICE AT, OR NULL. Now, when Codex is found
|
|
316
|
+
* and the key was never written. NOW is what keeps a row marked `codex` in
|
|
317
|
+
* August on Claude Code: it was written before this moment, so it is stale
|
|
318
|
+
* (`engineChoiceOnRowIsStale`). A moment already written is kept, and
|
|
319
|
+
* `"engineChoice": false` written by hand keeps the choice shut. */
|
|
320
|
+
export function codexChoiceToOpen(config, codexFound) {
|
|
321
|
+
if (!codexFound || config.engineChoice != null) return null;
|
|
322
|
+
return new Date().toISOString();
|
|
323
|
+
}
|
|
324
|
+
|
|
260
325
|
export function configFile(appDir) {
|
|
261
326
|
return path.join(appDir, CONFIG_NAME);
|
|
262
327
|
}
|
package/main/demo.mjs
CHANGED
|
@@ -34,9 +34,8 @@ import path from 'node:path';
|
|
|
34
34
|
import { spawn } from 'node:child_process';
|
|
35
35
|
import { DEMO_DOCS, DEMO_ENV, DEMO_PRODUCTS, demoLedgerLines } from '../shared/demo-world.mjs';
|
|
36
36
|
import { prepareHome } from '../shared/fresh-user-home.mjs';
|
|
37
|
-
import { Name } from '../shared/product-name.mjs';
|
|
38
37
|
import { relaunchCommand } from './fresh-user.mjs';
|
|
39
|
-
import { encodeProjectPath, storeRootEnv } from './store/home.mjs';
|
|
38
|
+
import { defaultStoreRoot, encodeProjectPath, storeRootEnv } from './store/home.mjs';
|
|
40
39
|
import { NAME, nameSlug } from '../shared/product-name.mjs';
|
|
41
40
|
|
|
42
41
|
/**
|
|
@@ -87,22 +86,21 @@ export function clearDemoHomes() {
|
|
|
87
86
|
/**
|
|
88
87
|
* Where the demo copy will keep its store, and where it will keep its own
|
|
89
88
|
* records. Both are derived from the throwaway home rather than configured,
|
|
90
|
-
* because the copy derives them the same way from the same home
|
|
91
|
-
*
|
|
92
|
-
*
|
|
89
|
+
* because the copy derives them the same way from the same home. Both are
|
|
90
|
+
* `defaultStoreRoot` for that home with no inherited data directory: on
|
|
91
|
+
* macOS a folder of the app's name, on Linux the XDG path under the
|
|
92
|
+
* throwaway. Passing the machine's XDG_DATA_HOME would seed the demo into
|
|
93
|
+
* the real store.
|
|
93
94
|
*
|
|
94
|
-
* THE FOLDER IS
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* at the old folder: the demo copy would have written its store to one place
|
|
98
|
-
* and looked for it in another. Two literals that have to agree is the defect;
|
|
99
|
-
* one import is the fix. */
|
|
95
|
+
* THE FOLDER IS THAT FUNCTION, not a second spelling of it. A literal next
|
|
96
|
+
* to the default is how the demo once wrote its store in one place and
|
|
97
|
+
* looked for it in another. */
|
|
100
98
|
export function demoPaths(home) {
|
|
101
|
-
const storeRoot =
|
|
99
|
+
const storeRoot = defaultStoreRoot({ home, env: {} });
|
|
102
100
|
return {
|
|
103
101
|
storeRoot,
|
|
104
102
|
accountRoot: path.join(storeRoot, 'accounts', DEMO_ACCOUNT),
|
|
105
|
-
appHome:
|
|
103
|
+
appHome: storeRoot,
|
|
106
104
|
};
|
|
107
105
|
}
|
|
108
106
|
|
|
@@ -192,7 +190,12 @@ export function seedDemoStore(home, { now = Date.now() } = {}) {
|
|
|
192
190
|
* standing in an invented store. */
|
|
193
191
|
export function demoEnv(home, env = process.env) {
|
|
194
192
|
const { appHome } = demoPaths(home);
|
|
195
|
-
|
|
193
|
+
const next = { ...env, HOME: home, CFFIXED_USER_HOME: home, ...storeRootEnv(appHome), [DEMO_ENV]: '1' };
|
|
194
|
+
// The copy's store is defaultStoreRoot of ITS home. The parent's
|
|
195
|
+
// XDG_DATA_HOME is the real data directory, and leaving it set points the
|
|
196
|
+
// demo at that directory. The other base directories go for the same reason.
|
|
197
|
+
for (const key of ['XDG_DATA_HOME', 'XDG_CONFIG_HOME', 'XDG_STATE_HOME', 'XDG_CACHE_HOME']) delete next[key];
|
|
198
|
+
return next;
|
|
196
199
|
}
|
|
197
200
|
|
|
198
201
|
/**
|
package/main/dev-window.mjs
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
// A HOT-RELOADING WINDOW MUST NOT BE THE ONE
|
|
1
|
+
// A HOT-RELOADING WINDOW MUST NOT BE THE ONE THE USER WORKS IN.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// checked out or merged
|
|
3
|
+
// A window an agent starts against a vite dev server can end up running on the
|
|
4
|
+
// real store. Vite rebuilds that window whenever the source under it changes,
|
|
5
|
+
// and the source under it is the checkout every agent shares, which can be
|
|
6
|
+
// checked out or merged several times in a few minutes.
|
|
7
7
|
//
|
|
8
|
-
// The
|
|
8
|
+
// The change that hurts is one to App.tsx that adds, removes or moves a
|
|
9
9
|
// hook, which is what a branch switch does to it. React Fast Refresh keeps a
|
|
10
10
|
// component's state only while its hooks match, so App is rebuilt from nothing:
|
|
11
11
|
// `snap` is null again, App.tsx draws `<div className="boot">the app</div>`, and
|
|
12
|
-
// every child of App is thrown away and remade.
|
|
13
|
-
//
|
|
12
|
+
// every child of App is thrown away and remade. The window drops back to the
|
|
13
|
+
// loading page, and an open artifact closes and reopens. The
|
|
14
14
|
// page never navigates, which is why nothing in the dev server's log looked bad
|
|
15
15
|
// enough to be it. Six triggers were measured against a real window and exactly
|
|
16
16
|
// one was guilty: scripts/what-throws-her-out-of-agentbox.mjs.
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// WHAT IS STILL ALLOWED, because a guard that stops the work is a guard somebody
|
|
26
26
|
// deletes:
|
|
27
27
|
//
|
|
28
|
-
// Fixtures. Nothing in a fixtures window is
|
|
28
|
+
// Fixtures. Nothing in a fixtures window is real, so there is nothing to be
|
|
29
29
|
// thrown out of, and this is what screenshot and proof scripts use.
|
|
30
30
|
//
|
|
31
31
|
// A throwaway store. A script that points HOME at a temp directory has no
|
package/main/doc-scheme.mjs
CHANGED
|
@@ -124,3 +124,5 @@ export class DocGrants {
|
|
|
124
124
|
// main/zoom-keys.mjs) scales the window, and the pane is an ordinary iframe in
|
|
125
125
|
// that window, so an open file follows it like everything else. Do not put a
|
|
126
126
|
// listener back into a served page: nothing is injected into a file she reads.
|
|
127
|
+
// The one thing added to a page is its scrollbar's look, from outside and after
|
|
128
|
+
// it is served, and frame-scrollbars.mjs says why that is not this.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// SETTING UP SOMEBODY'S PLAN FOR THEM (w-9f6975906c, picked 2026-10-05).
|
|
2
|
+
//
|
|
3
|
+
// A person with a Claude or ChatGPT plan and no Claude Code or Codex used to
|
|
4
|
+
// reach a card with a link to an install page. Now the walk asks which plan
|
|
5
|
+
// they pay for and this does the rest: install the tool if it is missing, start
|
|
6
|
+
// its own sign-in, and wait until the tool itself says it is signed in. The one
|
|
7
|
+
// thing left for them is approving in their browser.
|
|
8
|
+
//
|
|
9
|
+
// EVERY COMMAND HERE IS THE TOOL'S OWN, and that is a rule, not a style.
|
|
10
|
+
// Anthropic does not allow third party apps to offer claude.ai login unless
|
|
11
|
+
// approved, so Agentbox never draws a sign-in of its own: it runs Claude Code's
|
|
12
|
+
// `auth login`, which opens Anthropic's page, and Codex's `login`, which opens
|
|
13
|
+
// OpenAI's. Measured 2026-10-05 into throwaway home folders with stdin closed:
|
|
14
|
+
// both installers finish with no prompts (Claude Code 24.6 s, Codex 9.7 s) and
|
|
15
|
+
// land in `~/.local/bin`, the first place each finder looks; `claude auth login
|
|
16
|
+
// --claudeai` runs `open <url>` with a localhost callback, so approving in the
|
|
17
|
+
// browser finishes it with nothing to paste.
|
|
18
|
+
//
|
|
19
|
+
// NO API KEY REACHES ANYTHING RUN HERE. An inherited ANTHROPIC_API_KEY makes
|
|
20
|
+
// Claude Code report itself signed in on the key and bill it, which is the
|
|
21
|
+
// opposite of setting up their plan; `terminalEnv` is the scrub the built-in
|
|
22
|
+
// terminal already uses.
|
|
23
|
+
|
|
24
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
25
|
+
import { terminalEnv } from './task-terminals.mjs';
|
|
26
|
+
import { codexLaunchEnv } from './codex-launch-env.mjs';
|
|
27
|
+
|
|
28
|
+
const INSTALLERS = {
|
|
29
|
+
claude: { shell: '/bin/bash', url: 'https://claude.ai/install.sh', pipe: 'bash' },
|
|
30
|
+
codex: { shell: '/bin/sh', url: 'https://chatgpt.com/codex/install.sh', pipe: 'sh' },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function installCommand(engine) {
|
|
34
|
+
const i = INSTALLERS[engine];
|
|
35
|
+
return { file: i.shell, args: ['-c', `curl -fsSL ${i.url} | ${i.pipe}`] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The plan, never the API console: `--claudeai` is Claude Code's own name for it. */
|
|
39
|
+
export function signInCommand(engine, bin) {
|
|
40
|
+
return engine === 'claude'
|
|
41
|
+
? { file: bin, args: ['auth', 'login', '--claudeai'] }
|
|
42
|
+
: { file: bin, args: ['login'] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Exit 0 is signed in and 1 is not, for both (measured 2026-10-05). */
|
|
46
|
+
export function statusCommand(engine, bin) {
|
|
47
|
+
return engine === 'claude'
|
|
48
|
+
? { file: bin, args: ['auth', 'status', '--json'] }
|
|
49
|
+
: { file: bin, args: ['login', 'status'] };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const LOG_LIMIT = 64 * 1024;
|
|
53
|
+
const STATUS_TIMEOUT = 15_000;
|
|
54
|
+
const POLL = 2_000;
|
|
55
|
+
const ANSI = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
56
|
+
|
|
57
|
+
function realSpawn(file, args, { env, onData, keepStdin = false } = {}) {
|
|
58
|
+
let child;
|
|
59
|
+
try {
|
|
60
|
+
child = nodeSpawn(file, args, { env, stdio: [keepStdin ? 'pipe' : 'ignore', 'pipe', 'pipe'] });
|
|
61
|
+
} catch (error) {
|
|
62
|
+
onData?.(`${error.message}\n`);
|
|
63
|
+
return { exit: Promise.resolve(127), kill() {} };
|
|
64
|
+
}
|
|
65
|
+
const feed = (d) => onData?.(String(d));
|
|
66
|
+
child.stdout?.on('data', feed);
|
|
67
|
+
child.stderr?.on('data', feed);
|
|
68
|
+
const exit = new Promise((resolve) => {
|
|
69
|
+
child.on('exit', (code) => resolve(code ?? 1));
|
|
70
|
+
child.on('error', (error) => { onData?.(`${error.message}\n`); resolve(127); });
|
|
71
|
+
});
|
|
72
|
+
return { exit, kill: () => { try { child.kill('SIGTERM'); } catch { /* already gone */ } } };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* One setup per tool, held for the life of the app.
|
|
79
|
+
*
|
|
80
|
+
* `find(engine)` is the finder, looking again from scratch (the caller passes
|
|
81
|
+
* `recheckClaude` / `recheckCodex`, which also move the config so the rest of
|
|
82
|
+
* the app runs on what was just installed). `spawn` and `wait` are seams for
|
|
83
|
+
* the tests; nothing in the app passes them.
|
|
84
|
+
*/
|
|
85
|
+
export function createEngineSetup({ find, spawn = realSpawn, wait = sleep, env = process.env, poll = POLL, onSignIn = () => {} } = {}) {
|
|
86
|
+
const runEnv = terminalEnv(env);
|
|
87
|
+
const toolEnv = (engine, bin) => engine === 'codex' ? codexLaunchEnv(bin, runEnv) : runEnv;
|
|
88
|
+
const jobs = new Map();
|
|
89
|
+
|
|
90
|
+
const fresh = (engine) => ({ engine, phase: 'idle', error: null, log: '', running: null, login: null, run: 0 });
|
|
91
|
+
const job = (engine) => { if (!jobs.has(engine)) jobs.set(engine, fresh(engine)); return jobs.get(engine); };
|
|
92
|
+
const say = (j, text) => { j.log = (j.log + String(text).replace(ANSI, '')).slice(-LOG_LIMIT); };
|
|
93
|
+
const lastLine = (text) => text.split('\n').map((l) => l.trim()).filter(Boolean).pop() ?? '';
|
|
94
|
+
|
|
95
|
+
async function signedIn(j, bin) {
|
|
96
|
+
const { file, args } = statusCommand(j.engine, bin);
|
|
97
|
+
let output = '';
|
|
98
|
+
const p = spawn(file, args, { env: toolEnv(j.engine, bin), onData: (d) => { output = (output + String(d).replace(ANSI, '')).slice(-LOG_LIMIT); } });
|
|
99
|
+
const TIMED_OUT = Symbol('no answer');
|
|
100
|
+
const code = await Promise.race([p.exit, wait(STATUS_TIMEOUT).then(() => { p.kill(); return TIMED_OUT; })]);
|
|
101
|
+
// TOLD TO THE APP, which routes on it (w-d5d632e503): a Claude Code that
|
|
102
|
+
// is signed out beside a Codex that is signed in is a Mac that runs on
|
|
103
|
+
// Codex. Only a real yes or no; a check that never answered says nothing.
|
|
104
|
+
if (code === 0 || code === 1) onSignIn(j.engine, code === 0);
|
|
105
|
+
if (code === 0 || code === 1) return code === 0;
|
|
106
|
+
if (output) say(j, output);
|
|
107
|
+
const name = j.engine === 'codex' ? 'Codex' : 'Claude Code';
|
|
108
|
+
throw new Error(code === TIMED_OUT
|
|
109
|
+
? `${name} did not answer the sign-in check. Try again.`
|
|
110
|
+
: `${name} could not check sign-in: ${lastLine(output) || `exit ${code}`}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function startLogin(j, bin) {
|
|
114
|
+
j.login?.kill();
|
|
115
|
+
const { file, args } = signInCommand(j.engine, bin);
|
|
116
|
+
const login = spawn(file, args, { env: toolEnv(j.engine, bin), keepStdin: true, onData: (d) => say(j, d) });
|
|
117
|
+
j.login = login;
|
|
118
|
+
return login;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function go(j, run) {
|
|
122
|
+
const live = () => j.run === run;
|
|
123
|
+
let hit = find(j.engine);
|
|
124
|
+
if (!hit?.found) {
|
|
125
|
+
j.phase = 'installing';
|
|
126
|
+
const { file, args } = installCommand(j.engine);
|
|
127
|
+
const p = spawn(file, args, { env: runEnv, onData: (d) => say(j, d) });
|
|
128
|
+
j.running = p;
|
|
129
|
+
const code = await p.exit;
|
|
130
|
+
j.running = null;
|
|
131
|
+
if (!live()) return;
|
|
132
|
+
if (code !== 0) return fail(j, `The installer stopped: ${lastLine(j.log) || `exit ${code}`}`);
|
|
133
|
+
hit = find(j.engine);
|
|
134
|
+
if (!hit?.found) return fail(j, 'The installer finished, but the app still cannot find it.');
|
|
135
|
+
}
|
|
136
|
+
j.bin = hit.path;
|
|
137
|
+
j.phase = 'checking';
|
|
138
|
+
if (await signedIn(j, hit.path)) return live() && ready(j);
|
|
139
|
+
if (!live()) return;
|
|
140
|
+
j.phase = 'signing-in';
|
|
141
|
+
let login = startLogin(j, hit.path);
|
|
142
|
+
let ended = false;
|
|
143
|
+
const watchLogin = (loginProcess) => loginProcess.exit.then(() => { if (loginProcess === j.login) ended = true; });
|
|
144
|
+
watchLogin(login);
|
|
145
|
+
for (;;) {
|
|
146
|
+
await wait(poll);
|
|
147
|
+
if (!live()) return;
|
|
148
|
+
if (await signedIn(j, hit.path)) { if (live()) ready(j); return; }
|
|
149
|
+
if (j.login !== login) {
|
|
150
|
+
login = j.login;
|
|
151
|
+
ended = false;
|
|
152
|
+
watchLogin(login);
|
|
153
|
+
}
|
|
154
|
+
if (ended) return fail(j, `The sign-in was closed before it finished.${lastLine(j.log) ? ` ${lastLine(j.log)}` : ''}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ready(j) { j.login?.kill(); j.login = null; j.phase = 'ready'; j.error = null; }
|
|
159
|
+
function fail(j, error) { j.login?.kill(); j.login = null; j.phase = 'failed'; j.error = error; }
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
start(engine) {
|
|
163
|
+
const j = job(engine);
|
|
164
|
+
if (['installing', 'checking', 'signing-in'].includes(j.phase)) return this.status(engine);
|
|
165
|
+
j.run += 1; j.phase = 'checking'; j.error = null; j.log = '';
|
|
166
|
+
const run = j.run;
|
|
167
|
+
go(j, run).catch((e) => { if (j.run === run) fail(j, String(e?.message ?? e)); });
|
|
168
|
+
return this.status(engine);
|
|
169
|
+
},
|
|
170
|
+
/** Kill the sign-in that is waiting and start a new one, which opens the page again. */
|
|
171
|
+
signInAgain(engine) {
|
|
172
|
+
const j = job(engine);
|
|
173
|
+
if (j.phase === 'signing-in' && j.bin) startLogin(j, j.bin);
|
|
174
|
+
return this.status(engine);
|
|
175
|
+
},
|
|
176
|
+
cancel(engine) {
|
|
177
|
+
const j = job(engine);
|
|
178
|
+
j.run += 1;
|
|
179
|
+
j.running?.kill(); j.login?.kill();
|
|
180
|
+
j.running = null; j.login = null;
|
|
181
|
+
if (j.phase !== 'ready') j.phase = 'idle';
|
|
182
|
+
return this.status(engine);
|
|
183
|
+
},
|
|
184
|
+
status(engine) {
|
|
185
|
+
const j = job(engine);
|
|
186
|
+
return { engine, phase: j.phase, error: j.error, log: j.log };
|
|
187
|
+
},
|
|
188
|
+
/**
|
|
189
|
+
* WHETHER THIS TOOL CAN RUN AN AGENT RIGHT NOW: found, and signed in by its
|
|
190
|
+
* own account. The walk asks before it asks anything, so a Mac that is
|
|
191
|
+
* already set up never sees the plan question. */
|
|
192
|
+
async readiness(engine) {
|
|
193
|
+
const hit = find(engine);
|
|
194
|
+
if (!hit?.found) return { engine, found: false, signedIn: false };
|
|
195
|
+
try { return { engine, found: true, signedIn: await signedIn({ engine, log: '' }, hit.path) }; }
|
|
196
|
+
catch (error) { return { engine, found: true, signedIn: false, error: error.message }; }
|
|
197
|
+
},
|
|
198
|
+
/** The same question against a path already found, so asking it at
|
|
199
|
+
* launch does not search the Mac again. */
|
|
200
|
+
signedInNow(engine, bin) {
|
|
201
|
+
return signedIn({ engine, log: '' }, bin).catch(() => false);
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// SENDING FEEDBACK TO THE AGENTBOX TEAM (w-1b574413db, 2026-10-04).
|
|
2
|
+
//
|
|
3
|
+
// The card hands the main process what you wrote and what you attached, and
|
|
4
|
+
// this file posts it to one server function. That function holds who it goes
|
|
5
|
+
// to as a secret (`FEEDBACK_TO`, cloud/supabase/functions/feedback), so the
|
|
6
|
+
// address is in no file of this public repository and no install can show it.
|
|
7
|
+
//
|
|
8
|
+
// WHERE IT GOES IS BAKED IN, like the analytics key (main/analytics.mjs): the
|
|
9
|
+
// release writes `bakedFeedbackUrl` into the packaged package.json, a copy run
|
|
10
|
+
// from source carries none, and a `feedbackUrl` in zero.config.json outranks
|
|
11
|
+
// both. A copy with nowhere to send says so in words rather than pretending.
|
|
12
|
+
//
|
|
13
|
+
// It is not behind the diagnostics switch. That switch is about what Agentbox
|
|
14
|
+
// sends on its own; this is something a person typed and pressed Send on.
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import { checkFeedback } from '../shared/feedback.mjs';
|
|
17
|
+
import { Name, readEnv } from '../shared/product-name.mjs';
|
|
18
|
+
|
|
19
|
+
function bakedUrl() {
|
|
20
|
+
try {
|
|
21
|
+
return createRequire(import.meta.url)('../package.json')?.bakedFeedbackUrl ?? null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const BAKED = bakedUrl();
|
|
28
|
+
const clean = (v) => (typeof v === 'string' && v.trim() ? v.trim() : null);
|
|
29
|
+
|
|
30
|
+
export function feedbackUrl(config, baked = BAKED, env = process.env) {
|
|
31
|
+
return clean(config?.feedbackUrl) ?? clean(readEnv('FEEDBACK_URL', env)) ?? clean(baked);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Answers { ok: true } or { ok: false, error } with the error as a sentence.
|
|
35
|
+
// It never throws, so the card always has something to say.
|
|
36
|
+
export async function sendFeedback({ url, payload, fetchImpl = globalThis.fetch, timeoutMs = 60_000 }) {
|
|
37
|
+
if (!url) return { ok: false, error: `Feedback is not set up in this copy of ${Name}.` };
|
|
38
|
+
// Checked again here, off the bytes that would actually travel, since the
|
|
39
|
+
// renderer is not the last word on what leaves this machine.
|
|
40
|
+
const sized = (payload?.files ?? []).map((f) => ({ size: Math.floor((String(f?.data ?? '').length * 3) / 4) }));
|
|
41
|
+
const check = checkFeedback({ text: payload?.text, files: sized });
|
|
42
|
+
if (!check.ok) return { ok: false, error: check.reason };
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetchImpl(url, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'content-type': 'application/json' },
|
|
47
|
+
body: JSON.stringify(payload),
|
|
48
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
49
|
+
});
|
|
50
|
+
if (res?.ok) return { ok: true };
|
|
51
|
+
return { ok: false, error: 'It did not send. Try again in a moment.' };
|
|
52
|
+
} catch {
|
|
53
|
+
return { ok: false, error: 'It did not send. Check your connection and try again.' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// THE PICTURES IN A FOLDER A MESSAGE NAMES, newest first.
|
|
2
|
+
//
|
|
3
|
+
// A worker that drew several versions names their folder on a line of its own,
|
|
4
|
+
// and until 2026-10-04 that line was grey text. Focus now draws the pictures
|
|
5
|
+
// under it (FolderPreview), and this is the one thing the window cannot do for
|
|
6
|
+
// itself: look inside a folder.
|
|
7
|
+
//
|
|
8
|
+
// Two rules, the same two the picture scheme enforces (./img-scheme.mjs), so a
|
|
9
|
+
// folder line is never a way to list anything else: the folder is inside this
|
|
10
|
+
// product's own folders or the account root, and only pictures come back. One
|
|
11
|
+
// level only, because a design folder keeps old rounds in folders of their own.
|
|
12
|
+
// tests/a-named-folder-shows-its-pictures.test.mjs holds every edge.
|
|
13
|
+
import fsDefault from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { resolveArtifact } from './artifact-path.mjs';
|
|
16
|
+
import { servable } from './img-scheme.mjs';
|
|
17
|
+
|
|
18
|
+
const PICTURE = /\.(png|jpe?g|gif|webp|avif)$/i;
|
|
19
|
+
|
|
20
|
+
export function folderPictures({ src, product, products = [], accountRoot, storeRoot, limit = 60, fs = fsDefault }) {
|
|
21
|
+
if (typeof src !== 'string' || !src.trim()) return { ok: false, error: 'No folder was named.' };
|
|
22
|
+
const named = src.replace(/\/+$/, '');
|
|
23
|
+
const found = resolveArtifact({ src: named, product, products, accountRoot, storeRoot, fs });
|
|
24
|
+
if (!found.ok) return { ok: false, error: found.error };
|
|
25
|
+
const own = products.find((p) => p.slug === product) ?? null;
|
|
26
|
+
const roots = [accountRoot, own?.dir, own?.repoPath].filter(Boolean);
|
|
27
|
+
let entries;
|
|
28
|
+
try {
|
|
29
|
+
if (!fs.statSync(found.path).isDirectory()) return { ok: false, error: `${src} is not a folder.` };
|
|
30
|
+
entries = fs.readdirSync(found.path, { withFileTypes: true });
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return { ok: false, error: `${src} could not be read (${e.message}).` };
|
|
33
|
+
}
|
|
34
|
+
const pictures = [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry.isFile() || !PICTURE.test(entry.name)) continue;
|
|
37
|
+
const file = path.join(found.path, entry.name);
|
|
38
|
+
if (!servable(file, roots)) continue;
|
|
39
|
+
let mtime = 0;
|
|
40
|
+
try { mtime = fs.statSync(file).mtimeMs; } catch { continue; }
|
|
41
|
+
pictures.push({ file, mtime });
|
|
42
|
+
}
|
|
43
|
+
if (!pictures.length && !roots.some((r) => found.path === r || found.path.startsWith(r + path.sep))) {
|
|
44
|
+
return { ok: false, error: `${src} is not in this project.` };
|
|
45
|
+
}
|
|
46
|
+
pictures.sort((a, b) => b.mtime - a.mtime || a.file.localeCompare(b.file));
|
|
47
|
+
return { ok: true, folder: found.path, pictures: pictures.slice(0, limit).map((p) => p.file), total: pictures.length };
|
|
48
|
+
}
|