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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// A MESSAGE FROM A TEAMMATE IS SORTED BY HOW URGENT IT IS (2026-10-01): the
|
|
2
|
+
// agent reads each message and gives it a priority, so the reader does not
|
|
3
|
+
// have to.
|
|
4
|
+
//
|
|
5
|
+
// A message is made with priority 0 and nothing ever moved it, so the inbox's
|
|
6
|
+
// priority sort could not tell "the checkout page is down" from "thanks!". Now
|
|
7
|
+
// the Mac the message waits on asks the same small model the namer uses
|
|
8
|
+
// (main/row-label.mjs) for one of the four levels, once per message, and
|
|
9
|
+
// writes it onto the row as an agent. A level the person set by hand wins, by
|
|
10
|
+
// the fold's own rule, and this does not ask again after one.
|
|
11
|
+
import { askSmallModel } from './row-label.mjs';
|
|
12
|
+
|
|
13
|
+
/** The four words, onto the app's own values (renderer/src/priority.ts). */
|
|
14
|
+
export const LEVELS = { urgent: 9, high: 7, medium: 5, low: 2 };
|
|
15
|
+
|
|
16
|
+
const LIVE = new Set(['open', 'claimed', 'blocked']);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* PURE. The newest message on a conversation: its words, who wrote it and
|
|
20
|
+
* when. A reply is the newest when there is one; otherwise the opening body.
|
|
21
|
+
*/
|
|
22
|
+
export function latestMessage(item) {
|
|
23
|
+
const a = item?.wrote?.answer;
|
|
24
|
+
const b = item?.wrote?.body;
|
|
25
|
+
const answered = !!a && !!item?.answer && item.answer !== '(withdrawn)' && (!b || a.ts >= b.ts);
|
|
26
|
+
const w = answered ? a : b;
|
|
27
|
+
const text = String((answered ? item.answer : item?.body) ?? '').trim();
|
|
28
|
+
if (!w || !text) return null;
|
|
29
|
+
return { text, by: w.by ?? null, ts: w.ts ?? 0 };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* PURE. Does this row hold a message for me to sort? Only a conversation that
|
|
34
|
+
* is still going, waiting on me, whose newest message someone else wrote, and
|
|
35
|
+
* which has not been sorted since that message. Never one whose priority the
|
|
36
|
+
* person set by hand: the fold keeps theirs over ours, so asking would only
|
|
37
|
+
* repeat forever.
|
|
38
|
+
*/
|
|
39
|
+
export function wantsPriority(item, product, me) {
|
|
40
|
+
if (!me || !item || item.agent) return false;
|
|
41
|
+
if (!product?.team?.direct) return false;
|
|
42
|
+
if (!LIVE.has(item.status)) return false;
|
|
43
|
+
if (item.assignee !== me) return false;
|
|
44
|
+
const latest = latestMessage(item);
|
|
45
|
+
if (!latest || !latest.by || latest.by === me) return false;
|
|
46
|
+
const set = item.wrote?.priority;
|
|
47
|
+
if (set?.source === 'founder') return false;
|
|
48
|
+
// AND NEVER OVER THE LEVEL THE SENDER PICKED (w-7ba439c883). A message now
|
|
49
|
+
// crosses carrying how urgent the person who wrote it thought it was, and a
|
|
50
|
+
// guess from here would overwrite it: the sender's line is composeItem's, so
|
|
51
|
+
// it claims no authority the fold would defend, and a level written from here
|
|
52
|
+
// counts for nothing anyway (an agent's tag, shared/rank.mjs itemPriority).
|
|
53
|
+
// A row nobody chose a level for is composeItem's 0, so a number above it is
|
|
54
|
+
// exactly "somebody picked one", and `by` says it was not this Mac.
|
|
55
|
+
if (set?.by && set.by !== me && Number.isFinite(item.priority) && item.priority > 0) return false;
|
|
56
|
+
return !(set && set.source !== 'system' && set.ts >= latest.ts);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** PURE. The one question, about the message alone, cut short. */
|
|
60
|
+
export function priorityPrompt(latest) {
|
|
61
|
+
const text = String(latest?.text ?? '').replace(/\r/g, '').trim().slice(0, 1500);
|
|
62
|
+
return [
|
|
63
|
+
'A teammate sent this message to the person you work for. How soon do they',
|
|
64
|
+
'need to read it and answer? Reply with one word: urgent, high, medium or low.',
|
|
65
|
+
'',
|
|
66
|
+
'urgent: something is broken or blocked now, or it is due within hours.',
|
|
67
|
+
'high: it needs an answer today.',
|
|
68
|
+
'medium: an ordinary question or request.',
|
|
69
|
+
'low: a thank you, an FYI or a social note that needs no answer.',
|
|
70
|
+
'',
|
|
71
|
+
'--- message ---',
|
|
72
|
+
text,
|
|
73
|
+
'---',
|
|
74
|
+
].join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** PURE. The model's reply as one of the four words, or '' when it is not one. */
|
|
78
|
+
export function cleanPriority(text) {
|
|
79
|
+
const first = String(text ?? '').split('\n').map((l) => l.trim()).filter(Boolean)[0] ?? '';
|
|
80
|
+
const word = first.replace(/^["'`“”‘’]+|["'`“”‘’.!]+$/g, '').trim().toLowerCase();
|
|
81
|
+
return Object.hasOwn(LEVELS, word) ? word : '';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Ask the small model about one message. '' on every failure, never a throw. */
|
|
85
|
+
export function sortMessage(latest, opts = {}) {
|
|
86
|
+
return askSmallModel(priorityPrompt(latest), cleanPriority, opts);
|
|
87
|
+
}
|
package/main/notify.mjs
CHANGED
|
@@ -104,7 +104,7 @@ export function createNotifier({ window, Notification, powerMonitor, coalesceMs
|
|
|
104
104
|
// two-minute diagnosis.
|
|
105
105
|
n.on('failed', (_e, err) => {
|
|
106
106
|
live = null;
|
|
107
|
-
console.warn(`zero:
|
|
107
|
+
console.warn(`zero: the system refused the notification (${err}). Check ${NAME} in the system notification settings.`);
|
|
108
108
|
});
|
|
109
109
|
live = n;
|
|
110
110
|
try {
|
package/main/rail-note.mjs
CHANGED
|
@@ -27,8 +27,8 @@ import path from 'node:path';
|
|
|
27
27
|
export const NOTE_NAME = 'pinned.md';
|
|
28
28
|
|
|
29
29
|
// Far past anything a sidebar column can hold, and still small enough that a
|
|
30
|
-
// runaway paste cannot fill
|
|
31
|
-
// than truncated: half
|
|
30
|
+
// runaway paste cannot fill the disk. A note that hits this is refused rather
|
|
31
|
+
// than truncated: half a note written back is worse than none.
|
|
32
32
|
export const MAX_NOTE_BYTES = 200_000;
|
|
33
33
|
|
|
34
34
|
/** The file this product's sidebar note lives in, or null if it has no folder. */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {spawn} from 'node:child_process';
|
|
2
|
+
import {createCodexAppServer} from './codex-app-server.mjs';
|
|
3
|
+
import {rememberCodexModels} from './codex-models.mjs';
|
|
4
|
+
import {forget,claudeModels} from './claude-models.mjs';
|
|
5
|
+
import {terminalEnv} from './task-terminals.mjs';
|
|
6
|
+
import {codexLaunchEnv} from './codex-launch-env.mjs';
|
|
7
|
+
export async function refreshAgentModels(engine,config,home){
|
|
8
|
+
if(engine==='claude'){forget();claudeModels({bin:config.claudeBin});return;}
|
|
9
|
+
// Codex is optional and codexBin is null without it: nothing to ask, and spawn(null) only threw into the terminal.
|
|
10
|
+
if(!config.codexBin)return;
|
|
11
|
+
const client=createCodexAppServer({spawn:()=>spawn(config.codexBin,['app-server'],{cwd:config.home,env:codexLaunchEnv(config.codexBin,{...terminalEnv(),CODEX_HOME:home}),stdio:['pipe','pipe','pipe']}),requestTimeoutMs:10000});
|
|
12
|
+
const deadline=setTimeout(()=>client.close('Model refresh timed out'),15000);
|
|
13
|
+
try{
|
|
14
|
+
await client.initialize();let cursor=null;const models=[];const seen=new Set();
|
|
15
|
+
do{const r=await client.request('model/list',{limit:100,includeHidden:true,...(cursor?{cursor}:{})});
|
|
16
|
+
if(!Array.isArray(r.data))throw Error('Model list could not be read.');models.push(...r.data);cursor=r.nextCursor;
|
|
17
|
+
if(cursor&&seen.has(cursor))throw Error('Model list did not finish.');seen.add(cursor);
|
|
18
|
+
}while(cursor);
|
|
19
|
+
if(!models.length)throw Error('No models were returned.');rememberCodexModels(home,models);
|
|
20
|
+
}finally{clearTimeout(deadline);client.close('Model refresh complete');}
|
|
21
|
+
}
|
package/main/repeats.mjs
CHANGED
|
@@ -36,7 +36,7 @@ function describeRule({ every, at, on }) {
|
|
|
36
36
|
// How many periods in a row may be superseded before the founder is told.
|
|
37
37
|
const MISSES_BEFORE_ALERT = 3;
|
|
38
38
|
|
|
39
|
-
// The protocol goes FIRST in an occurrence's body, ahead of
|
|
39
|
+
// The protocol goes FIRST in an occurrence's body, ahead of the user's instruction. An
|
|
40
40
|
// over-long body keeps its prefix and cuts its suffix, so a protocol written
|
|
41
41
|
// last can be truncated away entirely, and a worker that never saw it cannot
|
|
42
42
|
// know that a quiet finish has to be marked.
|
|
@@ -112,11 +112,11 @@ export class Repeats {
|
|
|
112
112
|
title, body, priority = 5, every = 'day', at, on, engine, model,
|
|
113
113
|
}, now = Date.now()) {
|
|
114
114
|
// Refused here rather than stored and ignored later: a rule that cannot be
|
|
115
|
-
// kept has to fail while
|
|
115
|
+
// kept has to fail while the user is still looking at what they typed.
|
|
116
116
|
//
|
|
117
117
|
// `on` is validated AND stored. It was in neither, so a weekly rule was
|
|
118
118
|
// checked without the day that makes it weekly, and no weekly rule ever
|
|
119
|
-
// survived this line
|
|
119
|
+
// survived this line. The refusal says which day is missing now,
|
|
120
120
|
// because "every week at 08:00" reads like a bug in the clock.
|
|
121
121
|
if (!title || !String(title).trim()) throw new Error('a repeating task needs a title');
|
|
122
122
|
if (!isRepeatRule({ every, at, on })) throw new Error(`not a schedule ${NAME} can keep: ${describeRule({ every, at, on })}`);
|
|
@@ -134,7 +134,7 @@ export class Repeats {
|
|
|
134
134
|
patchRule(productDir, id, patch, now = Date.now()) {
|
|
135
135
|
// `on` joins the other two: changing a daily rule to a Thursday one sends
|
|
136
136
|
// every, at AND on, and checking the first two alone refused the change for
|
|
137
|
-
// the same reason a new weekly rule was refused
|
|
137
|
+
// the same reason a new weekly rule was refused.
|
|
138
138
|
if (('at' in patch || 'every' in patch || 'on' in patch)) {
|
|
139
139
|
const current = this.get(productDir, id, now);
|
|
140
140
|
if (!current) throw new Error(`no such repeating task: ${id}`);
|
package/main/row-label.mjs
CHANGED
|
@@ -1,47 +1,46 @@
|
|
|
1
|
-
// THE NAME ON THE ROW, WRITTEN RATHER THAN TAKEN FROM
|
|
1
|
+
// THE NAME ON THE ROW, WRITTEN RATHER THAN TAKEN FROM THE FIRST SENTENCE.
|
|
2
2
|
//
|
|
3
|
-
// A message
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// been", "Can you" or "We". Eleven of those down a column is eleven rows that
|
|
8
|
-
// look like one row.
|
|
3
|
+
// A dictated message keeps its first line as its title, verbatim and forever
|
|
4
|
+
// (renderer/src/message-split.ts). Such titles run long, are often cut off mid
|
|
5
|
+
// sentence with an ellipsis, and many open with "I", "I've been", "Can you" or
|
|
6
|
+
// "We". Eleven of those down a column is eleven rows that look like one row.
|
|
9
7
|
//
|
|
10
8
|
// THERE IS NOTHING TO EXTRACT FROM CLAUDE CODE, which is the first thing this
|
|
11
9
|
// had to find out. Claude Code does generate a short name for a session and it
|
|
12
|
-
// writes it to the terminal tab; it never puts it on disk
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// the
|
|
16
|
-
//
|
|
10
|
+
// writes it to the terminal tab; it never puts it on disk: none of thousands of
|
|
11
|
+
// transcript files measured carried a `summary` row, and main/agent-sessions.mjs
|
|
12
|
+
// found the same. So the name is not read from anywhere. It is written, here, by
|
|
13
|
+
// the same kind of call Claude Code makes: one small fast model, one message,
|
|
14
|
+
// one line back.
|
|
17
15
|
//
|
|
18
|
-
// AND IT IS WRITTEN FROM WHERE THE THREAD
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
16
|
+
// AND IT IS WRITTEN ONCE, FROM WHERE THE THREAD STOOD WHEN IT WAS NAMED.
|
|
17
|
+
// A thread's opening line describes where it started, so the prompt is given
|
|
18
|
+
// the last thing written on the row as well and the name describes the work
|
|
19
|
+
// rather than the first sentence of the ask. It used to be REWRITTEN every time
|
|
20
|
+
// the row's content moved, on the same reasoning, and that is what this file no
|
|
21
|
+
// longer does: a tester lost their threads because the names kept changing
|
|
22
|
+
// under them while agents worked (w-c141733b61, `wantsName` below). A name that
|
|
23
|
+
// follows the work is worth less than a name a person can find again.
|
|
26
24
|
//
|
|
27
25
|
// WHY THE APP DOES THIS AND NOT THE WORKER. It was written into briefs/worker.md
|
|
28
26
|
// first and taken straight back out: the brief is pasted into every session and
|
|
29
27
|
// tests/the-brief-only-says-what-applies.mjs holds it under 6,000 characters,
|
|
30
28
|
// which it was 33 characters short of. A rule every worker must remember is also
|
|
31
|
-
// a rule most of them will not, and the row
|
|
29
|
+
// a rule most of them will not, and the row the user is looking at right now may have
|
|
32
30
|
// no worker on it at all. Doing it here names every row on every product with
|
|
33
31
|
// nothing for anyone to remember.
|
|
34
32
|
//
|
|
35
|
-
// The write goes to `label`, never to `title`. The title
|
|
36
|
-
//
|
|
37
|
-
// the row (shared/work-items.mjs).
|
|
33
|
+
// The write goes to `label`, never to `title`. The title belongs to the user,
|
|
34
|
+
// the fold makes the user outrank us on it, and the user still reads their own
|
|
35
|
+
// words the moment they open the row (shared/work-items.mjs).
|
|
38
36
|
|
|
39
37
|
import { spawn } from 'node:child_process';
|
|
38
|
+
import { codexLaunchEnv } from './codex-launch-env.mjs';
|
|
40
39
|
|
|
41
40
|
/**
|
|
42
41
|
* The model this runs on. Small and fast on purpose: this is a naming call, it
|
|
43
42
|
* happens on a timer, and it must never be worth thinking about. Measured
|
|
44
|
-
* through the CLI
|
|
43
|
+
* through the CLI over ten real rows: 14.1s median a call,
|
|
45
44
|
* nearly all of it the CLI starting a session rather than the model working. */
|
|
46
45
|
export const NAME_MODEL = 'claude-haiku-4-5-20251001';
|
|
47
46
|
|
|
@@ -52,7 +51,7 @@ export const NAME_MODEL = 'claude-haiku-4-5-20251001';
|
|
|
52
51
|
export const NAME_TIMEOUT_MS = 90_000;
|
|
53
52
|
|
|
54
53
|
/**
|
|
55
|
-
* What a name may be. Long enough for
|
|
54
|
+
* What a name may be. Long enough for a seven-word name, short
|
|
56
55
|
* enough that it can never be the thing that makes a row clip: the row draws
|
|
57
56
|
* one line and the summary budget beside it is 112. */
|
|
58
57
|
export const NAME_MAX = 64;
|
|
@@ -63,41 +62,72 @@ export const NAME_MAX = 64;
|
|
|
63
62
|
const LIVE = new Set(['open', 'claimed', 'blocked']);
|
|
64
63
|
|
|
65
64
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
65
|
+
* PURE. Is this title already a name somebody wrote?
|
|
66
|
+
*
|
|
67
|
+
* The one question that decides whether this module speaks at all, and it is
|
|
68
|
+
* answered off the words alone because that is all there is: nothing on a row
|
|
69
|
+
* records whether its title was typed or derived.
|
|
70
|
+
*
|
|
71
|
+
* Three marks, and together they separate the two shapes `splitMessage` makes
|
|
72
|
+
* (renderer/src/message-split.ts). A first line of 120 characters or less is
|
|
73
|
+
* kept VERBATIM and is the name its person wrote. A longer one is clipped into
|
|
74
|
+
* a title by the app, and every clip it can produce is either a whole sentence
|
|
75
|
+
* of at least 25 characters or a hard cut ending in an ellipsis.
|
|
76
|
+
*
|
|
77
|
+
* as short as a name we would write NAME_MAX, so a title already that tight
|
|
78
|
+
* has nothing for us to improve,
|
|
79
|
+
* as few words as a name has the same eight `cleanName` allows,
|
|
80
|
+
* not ending as a sentence does '.', '!', '?' and '…' are how a clipped
|
|
81
|
+
* title ends and not how a name does.
|
|
82
|
+
*
|
|
83
|
+
* It errs towards leaving the words alone, which is the right way to be wrong:
|
|
84
|
+
* the cost is a short typed sentence keeping its own first line, and the cost
|
|
85
|
+
* the other way is somebody losing the thread they named.
|
|
86
|
+
*/
|
|
87
|
+
export function isWrittenName(title) {
|
|
88
|
+
const t = String(title ?? '').trim();
|
|
89
|
+
if (!t) return false;
|
|
90
|
+
if (t.length > NAME_MAX) return false;
|
|
91
|
+
if (t.split(/\s+/).length > 8) return false;
|
|
92
|
+
return !/[.!?…]$/.test(t);
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
/**
|
|
81
96
|
* PURE. Does this row want naming?
|
|
82
97
|
*
|
|
83
|
-
*
|
|
98
|
+
* Four ways to say no, and the last two are the fix for a tester losing their
|
|
99
|
+
* own threads on 2026-10-01: "Add comment to cart.js" became "Improve cart.js
|
|
100
|
+
* header comment" and then "Update cart file comment" inside two minutes.
|
|
101
|
+
*
|
|
84
102
|
* - it is done, so nobody is scanning for it,
|
|
85
103
|
* - it has nothing to name from, which is a row with no words on it yet,
|
|
86
|
-
* -
|
|
104
|
+
* - ITS PERSON ALREADY NAMED IT. A short clear title is the name they will
|
|
105
|
+
* look for, and on a team it is the name they will point someone else at.
|
|
106
|
+
* Improving it is not an improvement.
|
|
107
|
+
* - IT HAS BEEN NAMED. Once, when the thread was made, and then never again.
|
|
108
|
+
*
|
|
109
|
+
* That last line used to read `wroteAt < contentTouchedAt(item)`, so the name
|
|
110
|
+
* followed where the thread stood: an agent writes a note and a result on every
|
|
111
|
+
* run, so every run earned a new name. Following the thread was the intended
|
|
112
|
+
* behaviour and there was a test for it; what it cost in practice is that no
|
|
113
|
+
* thread stayed findable, so the brief for this change overrides both. The idea
|
|
114
|
+
* it was serving survives in the prompt, which still reads the latest thing on
|
|
115
|
+
* the row, so the one name a thread gets describes the work and not just its
|
|
116
|
+
* opening line.
|
|
87
117
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
118
|
+
* The exception is a name the person writes themselves later: a title newer
|
|
119
|
+
* than our label is a rename, so `rowTitle` draws theirs (list-rules.ts) and
|
|
120
|
+
* this stays quiet rather than answering it with another name of ours.
|
|
90
121
|
*/
|
|
91
122
|
export function wantsName(item) {
|
|
92
123
|
if (!item || !LIVE.has(item.status)) return false;
|
|
93
124
|
if (!String(item.title ?? '').trim() && !String(item.body ?? '').trim()) return false;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return wroteAt < contentTouchedAt(item);
|
|
125
|
+
if (isWrittenName(item.title)) return false;
|
|
126
|
+
return !String(item.label ?? '').trim();
|
|
97
127
|
}
|
|
98
128
|
|
|
99
129
|
/**
|
|
100
|
-
* PURE. The one message the naming call gets.
|
|
130
|
+
* PURE. The one message the naming call gets. The user's words first, then the last
|
|
101
131
|
* thing written on the row, which is what makes the name describe the thread
|
|
102
132
|
* rather than its opening line. Truncated hard: a name does not get better for
|
|
103
133
|
* having read six thousand words, and this is spawned on a timer. */
|
|
@@ -148,21 +178,55 @@ export function cleanName(text) {
|
|
|
148
178
|
return bare;
|
|
149
179
|
}
|
|
150
180
|
|
|
181
|
+
/**
|
|
182
|
+
* THE SAME CALL ON A MAC THAT HAS ONLY CODEX. `codex exec` prints the reply
|
|
183
|
+
* alone on stdout and everything else on stderr, so the reading below is the
|
|
184
|
+
* same. No session is kept, none of her Codex config or MCP servers is loaded,
|
|
185
|
+
* and nothing may be written. Measured 2026-10-01, codex-cli 0.160.0: 5.6s. */
|
|
186
|
+
export function codexNameArgs(item) {
|
|
187
|
+
return codexArgs(namePrompt(item));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function codexArgs(prompt) {
|
|
191
|
+
return [
|
|
192
|
+
'exec', '--skip-git-repo-check', '--ephemeral', '--ignore-user-config', '--ignore-rules',
|
|
193
|
+
'-s', 'read-only', '-c', 'model_reasoning_effort=low', prompt,
|
|
194
|
+
];
|
|
195
|
+
}
|
|
196
|
+
|
|
151
197
|
/**
|
|
152
198
|
* Name one row. Resolves to a clean name, or to '' for every failure there is:
|
|
153
199
|
* the binary missing, a non-zero exit, a timeout, an empty reply, a reply that
|
|
154
200
|
* is not a name. Never throws and never rejects, because this is called from a
|
|
155
201
|
* timer whose only correct behaviour on failure is to leave the row alone.
|
|
156
202
|
*/
|
|
157
|
-
export function nameRow(item,
|
|
203
|
+
export function nameRow(item, opts = {}) {
|
|
204
|
+
return askSmallModel(namePrompt(item), cleanName, opts);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* One short question to the small model, on Claude Code or Codex, its reply put
|
|
209
|
+
* through `clean`. The same promise as `nameRow` and for the same reason: ''
|
|
210
|
+
* for every failure, never a throw. Shared with the message sorter
|
|
211
|
+
* (main/message-priority.mjs).
|
|
212
|
+
*
|
|
213
|
+
* `env` is which account the call bills. Without one it inherits the app's own
|
|
214
|
+
* environment, which is always the default Claude account: when that account
|
|
215
|
+
* hit its weekly limit on 2026-10-06 every call died in two seconds and no row
|
|
216
|
+
* was named for a day, while agents ran on the second account. The supervisor
|
|
217
|
+
* passes one per account and walks them (`_askSmall` in main/supervisor.mjs).
|
|
218
|
+
*/
|
|
219
|
+
export function askSmallModel(prompt, clean, { claudeBin, codexBin = null, engine = 'claude', model = NAME_MODEL, timeoutMs = NAME_TIMEOUT_MS, env } = {}) {
|
|
158
220
|
return new Promise((resolve) => {
|
|
159
|
-
|
|
221
|
+
const bin = engine === 'codex' ? codexBin : claudeBin;
|
|
222
|
+
if (!bin) return resolve('');
|
|
160
223
|
let child;
|
|
161
224
|
try {
|
|
162
|
-
child = spawn(
|
|
225
|
+
child = spawn(bin, engine === 'codex' ? codexArgs(prompt) : ['-p', prompt, '--model', model], {
|
|
163
226
|
// Somewhere that is nobody's project. A naming call must not pick up a
|
|
164
227
|
// CLAUDE.md, a settings file or a hook from whatever folder it lands in.
|
|
165
228
|
cwd: '/tmp',
|
|
229
|
+
...(engine === 'codex' ? { env: codexLaunchEnv(bin, env) } : env ? { env } : {}),
|
|
166
230
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
167
231
|
});
|
|
168
232
|
} catch {
|
|
@@ -178,8 +242,8 @@ export function nameRow(item, { claudeBin, model = NAME_MODEL, timeoutMs = NAME_
|
|
|
178
242
|
resolve(value);
|
|
179
243
|
};
|
|
180
244
|
const timer = setTimeout(() => finish(''), timeoutMs);
|
|
181
|
-
child.stdout.on('data', (d) => { out += d.toString(); if (out.length > 4000) finish(
|
|
245
|
+
child.stdout.on('data', (d) => { out += d.toString(); if (out.length > 4000) finish(clean(out)); });
|
|
182
246
|
child.on('error', () => finish(''));
|
|
183
|
-
child.on('close', (code) => finish(code === 0 ?
|
|
247
|
+
child.on('close', (code) => finish(code === 0 ? clean(out) : ''));
|
|
184
248
|
});
|
|
185
249
|
}
|
package/main/runs-on.mjs
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// WHICH ACCOUNT THE AGENTS ARE ACTUALLY RUNNING ON, read off this Mac.
|
|
2
|
+
//
|
|
3
|
+
// w-e217e577e5, 2026-10-07, from a session with a new user: "He didn't realize
|
|
4
|
+
// it auto-connected to Claude/Codex; he wasn't sure how it was even running."
|
|
5
|
+
//
|
|
6
|
+
// Every fact here was already on the disk and already being read, each by the
|
|
7
|
+
// screen that needed it: main/claude-plan.mjs for the Claude plan (the Accounts
|
|
8
|
+
// page), main/codex-account.mjs for the ChatGPT one (the Codex card). What did
|
|
9
|
+
// not exist was ONE answer to "what is powering this", in a shape small enough
|
|
10
|
+
// to ride the snapshot and be drawn in a corner. That is all this is: the two
|
|
11
|
+
// readers, asked about the one account the fleet is spending, and null whenever
|
|
12
|
+
// the answer would be a guess.
|
|
13
|
+
//
|
|
14
|
+
// NULL IS AN ORDINARY ANSWER AND IT MEANS DRAW NOTHING. A Mac mid-setup, a
|
|
15
|
+
// config file being rewritten under us, a login that exists with no plan string
|
|
16
|
+
// in it: the first two are null, the third is the name with no plan. Nothing
|
|
17
|
+
// here ever reports an account that is not there, because the corner it feeds
|
|
18
|
+
// is read by somebody deciding whose money this is spending.
|
|
19
|
+
//
|
|
20
|
+
// IT IS CACHED ON THE LOGIN FILES' OWN TIMES, NOT ON A CLOCK. The snapshot is
|
|
21
|
+
// built every ten seconds and `~/.claude.json` is 148 KB on the machine this was
|
|
22
|
+
// written on, so parsing it per tick is work nobody asked for; it is also a file
|
|
23
|
+
// that genuinely changes, since Claude Code refetches its own profile as it
|
|
24
|
+
// runs. `signInFiles`/`signInStamp` (main/sign-in-files.mjs) already name the
|
|
25
|
+
// files a login writes and read their modification times and nothing else, so
|
|
26
|
+
// the cache key is the newest of those times. A moved time re-reads. A cache
|
|
27
|
+
// that aged out on a timer instead would be either stale after a sign-in or
|
|
28
|
+
// re-parsing for nothing, and the file itself knows which.
|
|
29
|
+
|
|
30
|
+
import fs from 'node:fs';
|
|
31
|
+
import os from 'node:os';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { planFile, planOfAccount } from './claude-plan.mjs';
|
|
34
|
+
import { codexAccount, codexAuthFile } from './codex-account.mjs';
|
|
35
|
+
import { signInFiles, signInStamp } from './sign-in-files.mjs';
|
|
36
|
+
|
|
37
|
+
/** keyed by engine and folder, valued by the stamp it was read at. */
|
|
38
|
+
const remembered = new Map();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* WHAT THE FLEET IS RUNNING ON, or null because nothing readable is signed in.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} facts
|
|
44
|
+
* @param {string} facts.engine Which coding agent the workspace runs on, which
|
|
45
|
+
* the supervisor has already decided (`Supervisor#engineFacts`). Never worked
|
|
46
|
+
* out here: the capability gate lives there, and a corner that guessed could
|
|
47
|
+
* name the subscription the fleet is not spending.
|
|
48
|
+
* @param {string} [facts.claudeProfile] The Claude login, `default` or a folder.
|
|
49
|
+
* @param {string} [facts.codexHome] The CODEX_HOME this workspace runs on.
|
|
50
|
+
* @returns {{ engine: string, plan: string|null }|null} `plan` is the words the
|
|
51
|
+
* tool itself reports, never a label of ours.
|
|
52
|
+
*/
|
|
53
|
+
export function runsOn({ engine = null, home = os.homedir(), claudeProfile = 'default', codexHome = null } = {}) {
|
|
54
|
+
if (engine !== 'claude' && engine !== 'codex') return null;
|
|
55
|
+
const folder = engine === 'codex'
|
|
56
|
+
? (codexHome || path.join(home, '.codex'))
|
|
57
|
+
: (!claudeProfile || claudeProfile === 'default' ? null : claudeProfile);
|
|
58
|
+
// The home is in the key as well as the folder. A default Claude login has no
|
|
59
|
+
// folder of its own, so without it two homes share one entry, which is exactly
|
|
60
|
+
// what a test with a throwaway home does and what a packaged app would do if
|
|
61
|
+
// HOME ever moved under it.
|
|
62
|
+
const key = `${engine}:${home}:${folder ?? 'default'}`;
|
|
63
|
+
const stamp = signInStamp(signInFiles({ engine, folder, home }));
|
|
64
|
+
const seen = remembered.get(key);
|
|
65
|
+
if (seen && seen.stamp === stamp) return seen.value;
|
|
66
|
+
const value = stamp ? read(engine, { home, claudeProfile, folder }) : null;
|
|
67
|
+
remembered.set(key, { stamp, value });
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function read(engine, { home, claudeProfile, folder }) {
|
|
72
|
+
if (engine === 'codex') {
|
|
73
|
+
const id = codexAccount(null, { file: codexAuthFile(folder) });
|
|
74
|
+
return id ? { engine: 'codex', plan: id.plan ?? null } : null;
|
|
75
|
+
}
|
|
76
|
+
// THE LOGIN AND THE PLAN COME OUT OF ONE READ. `readPlan` answers only the
|
|
77
|
+
// second, and "not signed in" and "signed in with no plan string" are two
|
|
78
|
+
// different screens: the first draws nothing, the second draws the name.
|
|
79
|
+
let account = null;
|
|
80
|
+
try {
|
|
81
|
+
account = JSON.parse(fs.readFileSync(planFile(claudeProfile, home), 'utf8'))?.oauthAccount ?? null;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (!account || typeof account !== 'object') return null;
|
|
86
|
+
const plan = planOfAccount(account);
|
|
87
|
+
return { engine: 'claude', plan: plan.known ? plan.label : null };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Forgets every cached reading. For tests, and for a sign-out that lands. */
|
|
91
|
+
export function forgetRunsOn() {
|
|
92
|
+
remembered.clear();
|
|
93
|
+
}
|
package/main/serve.mjs
CHANGED
|
@@ -28,10 +28,17 @@ import path from 'node:path';
|
|
|
28
28
|
import crypto from 'node:crypto';
|
|
29
29
|
import { fileURLToPath } from 'node:url';
|
|
30
30
|
|
|
31
|
+
import { spawn } from 'node:child_process';
|
|
31
32
|
import { loadConfig } from './config.mjs';
|
|
32
33
|
import { Store } from './store.mjs';
|
|
33
34
|
import { Supervisor } from './supervisor.mjs';
|
|
35
|
+
import { carryHerBriefsAcross, joinMessageRules, setAsideShippedMessageRules, unifyAgentInstructions } from './instruction-settings.mjs';
|
|
34
36
|
import { registerIpc } from './ipc.mjs';
|
|
37
|
+
import { reportFromRenderer } from './crash-report.mjs';
|
|
38
|
+
import * as workItemsDisk from './store/work-items.mjs';
|
|
39
|
+
import { createTeamService, teamStateFile } from './team/index.mjs';
|
|
40
|
+
import { loadCloudConfig, supabaseSession, headlessSessionFile } from './team/session.mjs';
|
|
41
|
+
import { appHome, carryMisplacedMachinery, storeRootEnv } from './store/home.mjs';
|
|
35
42
|
|
|
36
43
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
37
44
|
const repoRoot = path.dirname(here);
|
|
@@ -114,6 +121,28 @@ function readVersion() {
|
|
|
114
121
|
}
|
|
115
122
|
}
|
|
116
123
|
|
|
124
|
+
/**
|
|
125
|
+
* The four channels main/main.mjs registers beside its window, not in ipc.mjs.
|
|
126
|
+
*
|
|
127
|
+
* Each is about the desktop window itself (the dock badge, a ⌘R, banners in
|
|
128
|
+
* Notification Center, the window's own crashes), so `registerIpc` never
|
|
129
|
+
* files them and a tab asking got a 404. That was not harmless: a failed
|
|
130
|
+
* `badge` became an uncaught error, the screen reported it on `crash`, that
|
|
131
|
+
* failed too, and the tab sent thousands of requests a second.
|
|
132
|
+
* tests/a-browser-tab-gets-an-answer-on-every-channel.test.mjs.
|
|
133
|
+
*
|
|
134
|
+
* A tab has no dock and no ⌘R of ours to report, and nothing here decides
|
|
135
|
+
* banners yet, so three of them answer with nothing. A crash goes through the
|
|
136
|
+
* same scrubbed path as the desktop's, which writes nothing until crash
|
|
137
|
+
* reports are set up for this process.
|
|
138
|
+
*/
|
|
139
|
+
function answerWhatOnlyTheWindowAnswered(ipcMain) {
|
|
140
|
+
ipcMain.handle('zero:badge', () => null);
|
|
141
|
+
ipcMain.handle('zero:notify', () => null);
|
|
142
|
+
ipcMain.handle('zero:boot-info', () => ({ reloaded: false, builtAt: null, recovered: null }));
|
|
143
|
+
ipcMain.handle('zero:crash', (_e, payload) => { reportFromRenderer(payload ?? {}); return null; });
|
|
144
|
+
}
|
|
145
|
+
|
|
117
146
|
/**
|
|
118
147
|
* Boot the doing half, with no Electron anywhere.
|
|
119
148
|
*
|
|
@@ -124,12 +153,48 @@ function readVersion() {
|
|
|
124
153
|
*/
|
|
125
154
|
export async function bootHeadless({ dataDir = repoRoot, appDir = repoRoot, userDir = dataDir } = {}) {
|
|
126
155
|
const config = loadConfig(dataDir);
|
|
156
|
+
// The browser editor uses the same user files as the desktop. Finish the
|
|
157
|
+
// older migrations first so only user content enters the combined file.
|
|
158
|
+
carryHerBriefsAcross(appDir, userDir);
|
|
159
|
+
joinMessageRules(appDir, userDir);
|
|
160
|
+
setAsideShippedMessageRules(appDir, userDir);
|
|
161
|
+
unifyAgentInstructions(appDir, userDir);
|
|
162
|
+
// THE APP'S OWN HOME IS THE STORE ROOT, BEFORE ANYTHING OPENS THE STORE, as
|
|
163
|
+
// main/main.mjs does for the desktop. The ledgers live under the home the
|
|
164
|
+
// store reads from this variable, and every worker is handed `storeRoot` as
|
|
165
|
+
// that same variable. Without this line the two disagreed: the copy wrote its
|
|
166
|
+
// rows wherever an inherited variable pointed (or a dot-folder in $HOME), each worker
|
|
167
|
+
// looked under `storeRoot`, and every store call answered "no work item".
|
|
168
|
+
// tests/an-agent-in-a-browser-copy-can-reach-its-own-work-item.test.mjs.
|
|
169
|
+
// Rows already written under the old home come along, or they would vanish.
|
|
170
|
+
const inheritedHome = appHome();
|
|
171
|
+
Object.assign(process.env, storeRootEnv(config.storeRoot));
|
|
172
|
+
carryMisplacedMachinery(inheritedHome, config.storeRoot);
|
|
127
173
|
const store = await new Store(config).init();
|
|
128
174
|
const supervisor = new Supervisor(config, store, appDir, dataDir, userDir);
|
|
129
175
|
const window = broadcastingWindow();
|
|
130
176
|
const host = nodeHost();
|
|
131
|
-
|
|
132
|
-
|
|
177
|
+
// THE TEAM, as on the desktop (main/main.mjs), with no Electron: the session
|
|
178
|
+
// file is plain JSON readable only by this user, kept in this copy's own
|
|
179
|
+
// folder and not in the store (headlessSessionFile), and Google sign-in opens
|
|
180
|
+
// in the Mac's own browser.
|
|
181
|
+
const cloudConfig = loadCloudConfig(appDir);
|
|
182
|
+
let ipc = null;
|
|
183
|
+
const team = cloudConfig ? createTeamService({
|
|
184
|
+
session: supabaseSession({
|
|
185
|
+
cloudConfig,
|
|
186
|
+
sessionFile: headlessSessionFile({ userDir, storeRoot: config.storeRoot }),
|
|
187
|
+
openExternal: async (url) => { spawn('open', [url], { stdio: 'ignore', detached: true }).unref(); },
|
|
188
|
+
}),
|
|
189
|
+
store,
|
|
190
|
+
disk: workItemsDisk,
|
|
191
|
+
accountRoot: config.accountRoot,
|
|
192
|
+
stateFile: teamStateFile(config.storeRoot),
|
|
193
|
+
onChange: () => ipc?.push?.(),
|
|
194
|
+
}) : null;
|
|
195
|
+
ipc = registerIpc({ store, supervisor, config, window, host, team });
|
|
196
|
+
answerWhatOnlyTheWindowAnswered(host.ipcMain);
|
|
197
|
+
return { config, store, supervisor, window, host, ipc, team, channels: host.ipcMain.handlers };
|
|
133
198
|
}
|
|
134
199
|
|
|
135
200
|
/** A token in the url, because the port alone is not a door key. */
|
|
@@ -161,10 +226,17 @@ export function createServer({ channels, token, listeners = new Set(), dist = pa
|
|
|
161
226
|
return http.createServer(async (req, res) => {
|
|
162
227
|
const url = new URL(req.url, 'http://127.0.0.1');
|
|
163
228
|
|
|
164
|
-
// The token rides in a header on api calls and in the query on the
|
|
165
|
-
//
|
|
229
|
+
// The token rides in a header on api calls and in the query on the event
|
|
230
|
+
// stream, because a browser cannot put a header on either a navigation or
|
|
231
|
+
// an EventSource. It guards the two doors that reach the store. The built
|
|
232
|
+
// screen is the same public files for everybody, and its own script and
|
|
233
|
+
// style requests carry no token, so asking for one there drew a blank tab.
|
|
234
|
+
// NOR DOES THE PAGE ITSELF ASK: the tab takes the token out of its address
|
|
235
|
+
// bar on load, so a reload asks for `/` bare, and refusing that drew
|
|
236
|
+
// "Wrong or missing token" on every reload (tests/a-browser-tab-survives-a-reload).
|
|
237
|
+
const guarded = url.pathname === '/events' || url.pathname.startsWith('/api/');
|
|
166
238
|
const given = req.headers['x-agentbox-token'] || url.searchParams.get('token');
|
|
167
|
-
if (given !== token) {
|
|
239
|
+
if (guarded && given !== token) {
|
|
168
240
|
res.writeHead(403, { 'content-type': 'text/plain' });
|
|
169
241
|
res.end('Wrong or missing token. Use the url the terminal printed.');
|
|
170
242
|
return;
|