agentbox-app 0.1.7 → 0.1.11
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 +209 -55
- package/bin/agentbox.mjs +6 -0
- package/briefs/message-rules.md +13 -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 +9 -10
- 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 +114 -0
- package/main/agents.mjs +52 -46
- package/main/analytics.mjs +27 -0
- package/main/approval-prompt-server.mjs +2 -2
- package/main/artifact-path.mjs +11 -0
- package/main/claude-bin.mjs +31 -5
- package/main/claude-input.mjs +45 -3
- package/main/claude-plan.mjs +11 -11
- package/main/code-change.mjs +3 -3
- package/main/codex-account.mjs +1 -1
- package/main/codex-bin.mjs +17 -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 +68 -8
- package/main/dev-window.mjs +9 -9
- package/main/doc-scheme.mjs +2 -0
- package/main/engine-setup.mjs +193 -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 +6 -3
- package/main/git-change-offthread.mjs +70 -0
- package/main/git-change-thread.mjs +11 -0
- package/main/git-change.mjs +1 -1
- package/main/img-scheme.mjs +70 -1
- package/main/instruction-settings.mjs +92 -54
- package/main/ipc.mjs +316 -42
- package/main/leftovers.mjs +518 -0
- package/main/live-replies.mjs +43 -5
- package/main/machine.mjs +47 -0
- package/main/main.mjs +119 -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/rail-note.mjs +2 -2
- package/main/refresh-agent-models.mjs +20 -0
- package/main/repeats.mjs +4 -4
- package/main/row-label.mjs +114 -51
- package/main/serve.mjs +67 -5
- package/main/settings.mjs +223 -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 +47 -2
- package/main/store/work-items.mjs +150 -6
- package/main/store.mjs +235 -9
- package/main/supervisor.mjs +1444 -406
- package/main/task-commands.mjs +8 -10
- package/main/task-folders.mjs +74 -13
- package/main/task-remote-control.mjs +3 -3
- package/main/task-terminals.mjs +2 -1
- 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 +9 -11
- package/mcp/core/work.mjs +91 -25
- package/mcp/tools.mjs +12 -6
- package/package.json +21 -9
- package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
- package/renderer/dist/assets/index-B-2Az952.css +32 -0
- package/renderer/dist/assets/index-D6qncgai.js +1168 -0
- package/renderer/dist/index.html +2 -2
- package/scripts/memory-gate-hook.sh +59 -0
- package/shared/agent-import.mjs +38 -15
- 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 +37 -1
- package/shared/claude-commands.generated.mjs +2 -2
- package/shared/claude-models.generated.mjs +4 -4
- 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-usage.mjs +7 -8
- package/shared/demo-world.mjs +5 -5
- 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 +22 -3
- package/shared/instruction-defaults.json +2 -2
- package/shared/product-name.mjs +4 -2
- package/shared/project-folder-check.mjs +6 -0
- package/shared/rank.d.mts +1 -0
- package/shared/rank.mjs +22 -0
- package/shared/referenced-files.mjs +1 -1
- 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 +39 -18
- 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 +255 -27
- 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/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,20 @@
|
|
|
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
|
+
export async function refreshAgentModels(engine,config,home){
|
|
7
|
+
if(engine==='claude'){forget();claudeModels({bin:config.claudeBin});return;}
|
|
8
|
+
// Codex is optional and codexBin is null without it: nothing to ask, and spawn(null) only threw into the terminal.
|
|
9
|
+
if(!config.codexBin)return;
|
|
10
|
+
const client=createCodexAppServer({spawn:()=>spawn(config.codexBin,['app-server'],{cwd:config.home,env:{...terminalEnv(),CODEX_HOME:home},stdio:['pipe','pipe','pipe']}),requestTimeoutMs:10000});
|
|
11
|
+
const deadline=setTimeout(()=>client.close('Model refresh timed out'),15000);
|
|
12
|
+
try{
|
|
13
|
+
await client.initialize();let cursor=null;const models=[];const seen=new Set();
|
|
14
|
+
do{const r=await client.request('model/list',{limit:100,includeHidden:true,...(cursor?{cursor}:{})});
|
|
15
|
+
if(!Array.isArray(r.data))throw Error('Model list could not be read.');models.push(...r.data);cursor=r.nextCursor;
|
|
16
|
+
if(cursor&&seen.has(cursor))throw Error('Model list did not finish.');seen.add(cursor);
|
|
17
|
+
}while(cursor);
|
|
18
|
+
if(!models.length)throw Error('No models were returned.');rememberCodexModels(home,models);
|
|
19
|
+
}finally{clearTimeout(deadline);client.close('Model refresh complete');}
|
|
20
|
+
}
|
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,45 @@
|
|
|
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';
|
|
40
38
|
|
|
41
39
|
/**
|
|
42
40
|
* The model this runs on. Small and fast on purpose: this is a naming call, it
|
|
43
41
|
* happens on a timer, and it must never be worth thinking about. Measured
|
|
44
|
-
* through the CLI
|
|
42
|
+
* through the CLI over ten real rows: 14.1s median a call,
|
|
45
43
|
* nearly all of it the CLI starting a session rather than the model working. */
|
|
46
44
|
export const NAME_MODEL = 'claude-haiku-4-5-20251001';
|
|
47
45
|
|
|
@@ -52,7 +50,7 @@ export const NAME_MODEL = 'claude-haiku-4-5-20251001';
|
|
|
52
50
|
export const NAME_TIMEOUT_MS = 90_000;
|
|
53
51
|
|
|
54
52
|
/**
|
|
55
|
-
* What a name may be. Long enough for
|
|
53
|
+
* What a name may be. Long enough for a seven-word name, short
|
|
56
54
|
* enough that it can never be the thing that makes a row clip: the row draws
|
|
57
55
|
* one line and the summary budget beside it is 112. */
|
|
58
56
|
export const NAME_MAX = 64;
|
|
@@ -63,41 +61,72 @@ export const NAME_MAX = 64;
|
|
|
63
61
|
const LIVE = new Set(['open', 'claimed', 'blocked']);
|
|
64
62
|
|
|
65
63
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
64
|
+
* PURE. Is this title already a name somebody wrote?
|
|
65
|
+
*
|
|
66
|
+
* The one question that decides whether this module speaks at all, and it is
|
|
67
|
+
* answered off the words alone because that is all there is: nothing on a row
|
|
68
|
+
* records whether its title was typed or derived.
|
|
69
|
+
*
|
|
70
|
+
* Three marks, and together they separate the two shapes `splitMessage` makes
|
|
71
|
+
* (renderer/src/message-split.ts). A first line of 120 characters or less is
|
|
72
|
+
* kept VERBATIM and is the name its person wrote. A longer one is clipped into
|
|
73
|
+
* a title by the app, and every clip it can produce is either a whole sentence
|
|
74
|
+
* of at least 25 characters or a hard cut ending in an ellipsis.
|
|
75
|
+
*
|
|
76
|
+
* as short as a name we would write NAME_MAX, so a title already that tight
|
|
77
|
+
* has nothing for us to improve,
|
|
78
|
+
* as few words as a name has the same eight `cleanName` allows,
|
|
79
|
+
* not ending as a sentence does '.', '!', '?' and '…' are how a clipped
|
|
80
|
+
* title ends and not how a name does.
|
|
81
|
+
*
|
|
82
|
+
* It errs towards leaving the words alone, which is the right way to be wrong:
|
|
83
|
+
* the cost is a short typed sentence keeping its own first line, and the cost
|
|
84
|
+
* the other way is somebody losing the thread they named.
|
|
85
|
+
*/
|
|
86
|
+
export function isWrittenName(title) {
|
|
87
|
+
const t = String(title ?? '').trim();
|
|
88
|
+
if (!t) return false;
|
|
89
|
+
if (t.length > NAME_MAX) return false;
|
|
90
|
+
if (t.split(/\s+/).length > 8) return false;
|
|
91
|
+
return !/[.!?…]$/.test(t);
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
/**
|
|
81
95
|
* PURE. Does this row want naming?
|
|
82
96
|
*
|
|
83
|
-
*
|
|
97
|
+
* Four ways to say no, and the last two are the fix for a tester losing their
|
|
98
|
+
* own threads on 2026-10-01: "Add comment to cart.js" became "Improve cart.js
|
|
99
|
+
* header comment" and then "Update cart file comment" inside two minutes.
|
|
100
|
+
*
|
|
84
101
|
* - it is done, so nobody is scanning for it,
|
|
85
102
|
* - it has nothing to name from, which is a row with no words on it yet,
|
|
86
|
-
* -
|
|
103
|
+
* - ITS PERSON ALREADY NAMED IT. A short clear title is the name they will
|
|
104
|
+
* look for, and on a team it is the name they will point someone else at.
|
|
105
|
+
* Improving it is not an improvement.
|
|
106
|
+
* - IT HAS BEEN NAMED. Once, when the thread was made, and then never again.
|
|
107
|
+
*
|
|
108
|
+
* That last line used to read `wroteAt < contentTouchedAt(item)`, so the name
|
|
109
|
+
* followed where the thread stood: an agent writes a note and a result on every
|
|
110
|
+
* run, so every run earned a new name. Following the thread was the intended
|
|
111
|
+
* behaviour and there was a test for it; what it cost in practice is that no
|
|
112
|
+
* thread stayed findable, so the brief for this change overrides both. The idea
|
|
113
|
+
* it was serving survives in the prompt, which still reads the latest thing on
|
|
114
|
+
* the row, so the one name a thread gets describes the work and not just its
|
|
115
|
+
* opening line.
|
|
87
116
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
117
|
+
* The exception is a name the person writes themselves later: a title newer
|
|
118
|
+
* than our label is a rename, so `rowTitle` draws theirs (list-rules.ts) and
|
|
119
|
+
* this stays quiet rather than answering it with another name of ours.
|
|
90
120
|
*/
|
|
91
121
|
export function wantsName(item) {
|
|
92
122
|
if (!item || !LIVE.has(item.status)) return false;
|
|
93
123
|
if (!String(item.title ?? '').trim() && !String(item.body ?? '').trim()) return false;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return wroteAt < contentTouchedAt(item);
|
|
124
|
+
if (isWrittenName(item.title)) return false;
|
|
125
|
+
return !String(item.label ?? '').trim();
|
|
97
126
|
}
|
|
98
127
|
|
|
99
128
|
/**
|
|
100
|
-
* PURE. The one message the naming call gets.
|
|
129
|
+
* PURE. The one message the naming call gets. The user's words first, then the last
|
|
101
130
|
* thing written on the row, which is what makes the name describe the thread
|
|
102
131
|
* rather than its opening line. Truncated hard: a name does not get better for
|
|
103
132
|
* having read six thousand words, and this is spawned on a timer. */
|
|
@@ -148,21 +177,55 @@ export function cleanName(text) {
|
|
|
148
177
|
return bare;
|
|
149
178
|
}
|
|
150
179
|
|
|
180
|
+
/**
|
|
181
|
+
* THE SAME CALL ON A MAC THAT HAS ONLY CODEX. `codex exec` prints the reply
|
|
182
|
+
* alone on stdout and everything else on stderr, so the reading below is the
|
|
183
|
+
* same. No session is kept, none of her Codex config or MCP servers is loaded,
|
|
184
|
+
* and nothing may be written. Measured 2026-10-01, codex-cli 0.160.0: 5.6s. */
|
|
185
|
+
export function codexNameArgs(item) {
|
|
186
|
+
return codexArgs(namePrompt(item));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function codexArgs(prompt) {
|
|
190
|
+
return [
|
|
191
|
+
'exec', '--skip-git-repo-check', '--ephemeral', '--ignore-user-config', '--ignore-rules',
|
|
192
|
+
'-s', 'read-only', '-c', 'model_reasoning_effort=low', prompt,
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
|
|
151
196
|
/**
|
|
152
197
|
* Name one row. Resolves to a clean name, or to '' for every failure there is:
|
|
153
198
|
* the binary missing, a non-zero exit, a timeout, an empty reply, a reply that
|
|
154
199
|
* is not a name. Never throws and never rejects, because this is called from a
|
|
155
200
|
* timer whose only correct behaviour on failure is to leave the row alone.
|
|
156
201
|
*/
|
|
157
|
-
export function nameRow(item,
|
|
202
|
+
export function nameRow(item, opts = {}) {
|
|
203
|
+
return askSmallModel(namePrompt(item), cleanName, opts);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* One short question to the small model, on Claude Code or Codex, its reply put
|
|
208
|
+
* through `clean`. The same promise as `nameRow` and for the same reason: ''
|
|
209
|
+
* for every failure, never a throw. Shared with the message sorter
|
|
210
|
+
* (main/message-priority.mjs).
|
|
211
|
+
*
|
|
212
|
+
* `env` is which account the call bills. Without one it inherits the app's own
|
|
213
|
+
* environment, which is always the default Claude account: when that account
|
|
214
|
+
* hit its weekly limit on 2026-10-06 every call died in two seconds and no row
|
|
215
|
+
* was named for a day, while agents ran on the second account. The supervisor
|
|
216
|
+
* passes one per account and walks them (`_askSmall` in main/supervisor.mjs).
|
|
217
|
+
*/
|
|
218
|
+
export function askSmallModel(prompt, clean, { claudeBin, codexBin = null, engine = 'claude', model = NAME_MODEL, timeoutMs = NAME_TIMEOUT_MS, env } = {}) {
|
|
158
219
|
return new Promise((resolve) => {
|
|
159
|
-
|
|
220
|
+
const bin = engine === 'codex' ? codexBin : claudeBin;
|
|
221
|
+
if (!bin) return resolve('');
|
|
160
222
|
let child;
|
|
161
223
|
try {
|
|
162
|
-
child = spawn(
|
|
224
|
+
child = spawn(bin, engine === 'codex' ? codexArgs(prompt) : ['-p', prompt, '--model', model], {
|
|
163
225
|
// Somewhere that is nobody's project. A naming call must not pick up a
|
|
164
226
|
// CLAUDE.md, a settings file or a hook from whatever folder it lands in.
|
|
165
227
|
cwd: '/tmp',
|
|
228
|
+
...(env ? { env } : {}),
|
|
166
229
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
167
230
|
});
|
|
168
231
|
} catch {
|
|
@@ -178,8 +241,8 @@ export function nameRow(item, { claudeBin, model = NAME_MODEL, timeoutMs = NAME_
|
|
|
178
241
|
resolve(value);
|
|
179
242
|
};
|
|
180
243
|
const timer = setTimeout(() => finish(''), timeoutMs);
|
|
181
|
-
child.stdout.on('data', (d) => { out += d.toString(); if (out.length > 4000) finish(
|
|
244
|
+
child.stdout.on('data', (d) => { out += d.toString(); if (out.length > 4000) finish(clean(out)); });
|
|
182
245
|
child.on('error', () => finish(''));
|
|
183
|
-
child.on('close', (code) => finish(code === 0 ?
|
|
246
|
+
child.on('close', (code) => finish(code === 0 ? clean(out) : ''));
|
|
184
247
|
});
|
|
185
248
|
}
|
package/main/serve.mjs
CHANGED
|
@@ -28,10 +28,16 @@ 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';
|
|
34
35
|
import { registerIpc } from './ipc.mjs';
|
|
36
|
+
import { reportFromRenderer } from './crash-report.mjs';
|
|
37
|
+
import * as workItemsDisk from './store/work-items.mjs';
|
|
38
|
+
import { createTeamService, teamStateFile } from './team/index.mjs';
|
|
39
|
+
import { loadCloudConfig, supabaseSession, headlessSessionFile } from './team/session.mjs';
|
|
40
|
+
import { appHome, carryMisplacedMachinery, storeRootEnv } from './store/home.mjs';
|
|
35
41
|
|
|
36
42
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
37
43
|
const repoRoot = path.dirname(here);
|
|
@@ -114,6 +120,28 @@ function readVersion() {
|
|
|
114
120
|
}
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The four channels main/main.mjs registers beside its window, not in ipc.mjs.
|
|
125
|
+
*
|
|
126
|
+
* Each is about the desktop window itself (the dock badge, a ⌘R, banners in
|
|
127
|
+
* Notification Center, the window's own crashes), so `registerIpc` never
|
|
128
|
+
* files them and a tab asking got a 404. That was not harmless: a failed
|
|
129
|
+
* `badge` became an uncaught error, the screen reported it on `crash`, that
|
|
130
|
+
* failed too, and the tab sent thousands of requests a second.
|
|
131
|
+
* tests/a-browser-tab-gets-an-answer-on-every-channel.test.mjs.
|
|
132
|
+
*
|
|
133
|
+
* A tab has no dock and no ⌘R of ours to report, and nothing here decides
|
|
134
|
+
* banners yet, so three of them answer with nothing. A crash goes through the
|
|
135
|
+
* same scrubbed path as the desktop's, which writes nothing until crash
|
|
136
|
+
* reports are set up for this process.
|
|
137
|
+
*/
|
|
138
|
+
function answerWhatOnlyTheWindowAnswered(ipcMain) {
|
|
139
|
+
ipcMain.handle('zero:badge', () => null);
|
|
140
|
+
ipcMain.handle('zero:notify', () => null);
|
|
141
|
+
ipcMain.handle('zero:boot-info', () => ({ reloaded: false, builtAt: null, recovered: null }));
|
|
142
|
+
ipcMain.handle('zero:crash', (_e, payload) => { reportFromRenderer(payload ?? {}); return null; });
|
|
143
|
+
}
|
|
144
|
+
|
|
117
145
|
/**
|
|
118
146
|
* Boot the doing half, with no Electron anywhere.
|
|
119
147
|
*
|
|
@@ -124,12 +152,42 @@ function readVersion() {
|
|
|
124
152
|
*/
|
|
125
153
|
export async function bootHeadless({ dataDir = repoRoot, appDir = repoRoot, userDir = dataDir } = {}) {
|
|
126
154
|
const config = loadConfig(dataDir);
|
|
155
|
+
// THE APP'S OWN HOME IS THE STORE ROOT, BEFORE ANYTHING OPENS THE STORE, as
|
|
156
|
+
// main/main.mjs does for the desktop. The ledgers live under the home the
|
|
157
|
+
// store reads from this variable, and every worker is handed `storeRoot` as
|
|
158
|
+
// that same variable. Without this line the two disagreed: the copy wrote its
|
|
159
|
+
// rows wherever an inherited variable pointed (or `~/.agentbox`), each worker
|
|
160
|
+
// looked under `storeRoot`, and every store call answered "no work item".
|
|
161
|
+
// tests/an-agent-in-a-browser-copy-can-reach-its-own-work-item.test.mjs.
|
|
162
|
+
// Rows already written under the old home come along, or they would vanish.
|
|
163
|
+
const inheritedHome = appHome();
|
|
164
|
+
Object.assign(process.env, storeRootEnv(config.storeRoot));
|
|
165
|
+
carryMisplacedMachinery(inheritedHome, config.storeRoot);
|
|
127
166
|
const store = await new Store(config).init();
|
|
128
167
|
const supervisor = new Supervisor(config, store, appDir, dataDir, userDir);
|
|
129
168
|
const window = broadcastingWindow();
|
|
130
169
|
const host = nodeHost();
|
|
131
|
-
|
|
132
|
-
|
|
170
|
+
// THE TEAM, as on the desktop (main/main.mjs), with no Electron: the session
|
|
171
|
+
// file is plain JSON readable only by this user, kept in this copy's own
|
|
172
|
+
// folder and not in the store (headlessSessionFile), and Google sign-in opens
|
|
173
|
+
// in the Mac's own browser.
|
|
174
|
+
const cloudConfig = loadCloudConfig(appDir);
|
|
175
|
+
let ipc = null;
|
|
176
|
+
const team = cloudConfig ? createTeamService({
|
|
177
|
+
session: supabaseSession({
|
|
178
|
+
cloudConfig,
|
|
179
|
+
sessionFile: headlessSessionFile({ userDir, storeRoot: config.storeRoot }),
|
|
180
|
+
openExternal: async (url) => { spawn('open', [url], { stdio: 'ignore', detached: true }).unref(); },
|
|
181
|
+
}),
|
|
182
|
+
store,
|
|
183
|
+
disk: workItemsDisk,
|
|
184
|
+
accountRoot: config.accountRoot,
|
|
185
|
+
stateFile: teamStateFile(config.storeRoot),
|
|
186
|
+
onChange: () => ipc?.push?.(),
|
|
187
|
+
}) : null;
|
|
188
|
+
ipc = registerIpc({ store, supervisor, config, window, host, team });
|
|
189
|
+
answerWhatOnlyTheWindowAnswered(host.ipcMain);
|
|
190
|
+
return { config, store, supervisor, window, host, ipc, team, channels: host.ipcMain.handlers };
|
|
133
191
|
}
|
|
134
192
|
|
|
135
193
|
/** A token in the url, because the port alone is not a door key. */
|
|
@@ -161,10 +219,14 @@ export function createServer({ channels, token, listeners = new Set(), dist = pa
|
|
|
161
219
|
return http.createServer(async (req, res) => {
|
|
162
220
|
const url = new URL(req.url, 'http://127.0.0.1');
|
|
163
221
|
|
|
164
|
-
// The token rides in a header on api calls and in the query on the
|
|
165
|
-
//
|
|
222
|
+
// The token rides in a header on api calls and in the query on the event
|
|
223
|
+
// stream, because a browser cannot put a header on either a navigation or
|
|
224
|
+
// an EventSource. It guards the two doors that reach the store. The built
|
|
225
|
+
// screen is the same public files for everybody, and its own script and
|
|
226
|
+
// style requests carry no token, so asking for one there drew a blank tab.
|
|
227
|
+
const guarded = url.pathname === '/events' || url.pathname.startsWith('/api/');
|
|
166
228
|
const given = req.headers['x-agentbox-token'] || url.searchParams.get('token');
|
|
167
|
-
if (given !== token) {
|
|
229
|
+
if (guarded && given !== token) {
|
|
168
230
|
res.writeHead(403, { 'content-type': 'text/plain' });
|
|
169
231
|
res.end('Wrong or missing token. Use the url the terminal printed.');
|
|
170
232
|
return;
|