agentbox-app 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +176 -0
- package/bin/agentbox.mjs +103 -0
- package/briefs/adhd-mode.md +5 -0
- package/briefs/message-rules.md +95 -0
- package/briefs/worker.md +230 -0
- package/briefs/writing-rules.md +336 -0
- package/main/account-discovery.mjs +145 -0
- package/main/account-tooling.mjs +261 -0
- package/main/agent-activity.mjs +56 -0
- package/main/agent-files.mjs +422 -0
- package/main/agent-schedule.mjs +254 -0
- package/main/agent-sessions.mjs +435 -0
- package/main/agents.mjs +1095 -0
- package/main/analytics.mjs +206 -0
- package/main/answer-modes.mjs +206 -0
- package/main/approval-prompt-server.mjs +160 -0
- package/main/approvals.mjs +487 -0
- package/main/artifact-path.mjs +144 -0
- package/main/claude-bin.mjs +311 -0
- package/main/claude-input.mjs +117 -0
- package/main/claude-models.mjs +244 -0
- package/main/claude-plan.mjs +148 -0
- package/main/claude-usage.mjs +149 -0
- package/main/code-change.mjs +383 -0
- package/main/code-file.mjs +116 -0
- package/main/codex-account.mjs +146 -0
- package/main/codex-app-server.mjs +796 -0
- package/main/codex-approvals.mjs +712 -0
- package/main/codex-bin.mjs +261 -0
- package/main/codex-compaction.mjs +49 -0
- package/main/codex-models.mjs +194 -0
- package/main/codex-session.mjs +797 -0
- package/main/codex-threads.mjs +268 -0
- package/main/codex-usage-file.mjs +192 -0
- package/main/codex-usage.mjs +149 -0
- package/main/codex-watch.mjs +35 -0
- package/main/codex.mjs +757 -0
- package/main/config.mjs +345 -0
- package/main/copy-selection.mjs +55 -0
- package/main/crash-report.mjs +158 -0
- package/main/demo.mjs +254 -0
- package/main/dev-window.mjs +103 -0
- package/main/doc-file.mjs +99 -0
- package/main/doc-scheme.mjs +126 -0
- package/main/first-run.mjs +190 -0
- package/main/folders.mjs +91 -0
- package/main/fresh-user.mjs +120 -0
- package/main/git-change.mjs +323 -0
- package/main/img-scheme.mjs +90 -0
- package/main/instruction-settings.mjs +205 -0
- package/main/ipc.mjs +1558 -0
- package/main/live-replies.mjs +77 -0
- package/main/machine.mjs +113 -0
- package/main/main.mjs +972 -0
- package/main/menu.mjs +177 -0
- package/main/notify.mjs +155 -0
- package/main/open-source-file.mjs +8 -0
- package/main/project-identity.mjs +149 -0
- package/main/provider-commands.mjs +32 -0
- package/main/rail-note.mjs +74 -0
- package/main/repeats.mjs +261 -0
- package/main/row-label.mjs +185 -0
- package/main/screen-detail.mjs +44 -0
- package/main/serve.mjs +262 -0
- package/main/settings.mjs +912 -0
- package/main/staleness.mjs +76 -0
- package/main/store/chats.mjs +188 -0
- package/main/store/home.mjs +383 -0
- package/main/store/migrate.mjs +40 -0
- package/main/store/paths.mjs +82 -0
- package/main/store/pending-writes.mjs +122 -0
- package/main/store/project-lock.mjs +200 -0
- package/main/store/project.mjs +523 -0
- package/main/store/queues.mjs +31 -0
- package/main/store/work-items.mjs +299 -0
- package/main/store/write-queue.mjs +14 -0
- package/main/store-modules.mjs +23 -0
- package/main/store.mjs +1269 -0
- package/main/supervisor.mjs +6875 -0
- package/main/task-commands.mjs +161 -0
- package/main/task-folders-offthread.mjs +84 -0
- package/main/task-folders-thread.mjs +13 -0
- package/main/task-folders.mjs +409 -0
- package/main/task-remote-control.mjs +65 -0
- package/main/task-terminals.mjs +169 -0
- package/main/terminal-access.mjs +6 -0
- package/main/updater.mjs +240 -0
- package/main/zoom-keys.mjs +54 -0
- package/main/zoom-readout.mjs +20 -0
- package/mcp/agentbox-mcp.sh +34 -0
- package/mcp/core/account.mjs +77 -0
- package/mcp/core/chat.mjs +61 -0
- package/mcp/core/documents.mjs +238 -0
- package/mcp/core/page.mjs +152 -0
- package/mcp/core/products.mjs +110 -0
- package/mcp/core/skills.mjs +126 -0
- package/mcp/core/work.mjs +377 -0
- package/mcp/stdio.mjs +70 -0
- package/mcp/tools.mjs +257 -0
- package/package.json +186 -0
- package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
- package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
- package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
- package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
- package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
- package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
- package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +299 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
- package/renderer/dist/index.html +13 -0
- package/scripts/after-install.mjs +59 -0
- package/scripts/approval-server.sh +43 -0
- package/scripts/before-publish.mjs +133 -0
- package/shared/agent-import.d.mts +13 -0
- package/shared/agent-import.mjs +252 -0
- package/shared/agents.d.mts +190 -0
- package/shared/agents.mjs +937 -0
- package/shared/analytics-events.mjs +63 -0
- package/shared/answers.d.mts +12 -0
- package/shared/answers.mjs +80 -0
- package/shared/artifact-keys.d.mts +28 -0
- package/shared/artifact-keys.mjs +140 -0
- package/shared/bridge-map.mjs +128 -0
- package/shared/claude-commands.d.mts +20 -0
- package/shared/claude-commands.generated.mjs +149 -0
- package/shared/claude-commands.mjs +131 -0
- package/shared/claude-models.generated.d.mts +15 -0
- package/shared/claude-models.generated.mjs +26 -0
- package/shared/claude-usage.d.mts +16 -0
- package/shared/claude-usage.mjs +172 -0
- package/shared/codex-commands.d.mts +4 -0
- package/shared/codex-commands.mjs +17 -0
- package/shared/codex-import.d.mts +38 -0
- package/shared/codex-import.mjs +232 -0
- package/shared/codex-modes.mjs +159 -0
- package/shared/codex-usage.mjs +180 -0
- package/shared/contracts.mjs +412 -0
- package/shared/crash-scrub.mjs +182 -0
- package/shared/dashboard.mjs +481 -0
- package/shared/dead-run-trace.mjs +127 -0
- package/shared/demo-world.mjs +415 -0
- package/shared/effort-levels.d.mts +15 -0
- package/shared/effort-levels.mjs +76 -0
- package/shared/engines.d.mts +62 -0
- package/shared/engines.mjs +389 -0
- package/shared/first-run-examples.mjs +60 -0
- package/shared/first-run-line.mjs +149 -0
- package/shared/first-run-practice.d.mts +32 -0
- package/shared/first-run-practice.mjs +254 -0
- package/shared/first-run-shape.mjs +100 -0
- package/shared/fresh-user-home.mjs +189 -0
- package/shared/hook-failure.mjs +22 -0
- package/shared/install-id.mjs +39 -0
- package/shared/instruction-defaults.json +5 -0
- package/shared/library-folder.mjs +28 -0
- package/shared/notify-rules.mjs +120 -0
- package/shared/product-name.d.mts +6 -0
- package/shared/product-name.mjs +144 -0
- package/shared/project-folder-check.d.mts +22 -0
- package/shared/project-folder-check.mjs +152 -0
- package/shared/provider-commands.d.mts +8 -0
- package/shared/provider-commands.mjs +58 -0
- package/shared/rank.d.mts +19 -0
- package/shared/rank.mjs +146 -0
- package/shared/recovery.mjs +26 -0
- package/shared/referenced-files.d.mts +11 -0
- package/shared/referenced-files.mjs +122 -0
- package/shared/repeats.d.mts +45 -0
- package/shared/repeats.mjs +193 -0
- package/shared/schemes.mjs +33 -0
- package/shared/settings-terminal.d.mts +1 -0
- package/shared/settings-terminal.mjs +14 -0
- package/shared/side-build.mjs +57 -0
- package/shared/spawn-trouble.d.mts +8 -0
- package/shared/spawn-trouble.mjs +599 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +58 -0
- package/shared/usage.d.mts +53 -0
- package/shared/usage.mjs +319 -0
- package/shared/work-items.mjs +561 -0
- package/shared/work-lines.mjs +360 -0
- package/worker-permissions.json +6 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Which Library folder an artifact lives in. ONE rule, because main and the
|
|
2
|
+
// renderer both answer this question and they were answering it differently.
|
|
3
|
+
//
|
|
4
|
+
// A folder-backed artifact lives at creations/<...>/<slug> on disk, and
|
|
5
|
+
// `creations/` is storage, not a place the founder put anything: left in, it
|
|
6
|
+
// surfaced as a top-level folder called "Creations" holding everything
|
|
7
|
+
// folder-backed, which reads as nonsense when every artifact is one (founder
|
|
8
|
+
// report, 2026-07-24). So the storage root is stripped and whatever is left is
|
|
9
|
+
// the real folder.
|
|
10
|
+
//
|
|
11
|
+
// Main used to skip that strip, which is how the two views disagreed in the
|
|
12
|
+
// worst possible direction: the founder saw nine apps and canvases sitting
|
|
13
|
+
// loose at the top of her Library, while the agent's own list_library showed
|
|
14
|
+
// them tidily filed under "creations/" and therefore never noticed anything to
|
|
15
|
+
// organize.
|
|
16
|
+
export const STORAGE_ROOT = 'creations';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {{ path?: string, dir?: string }} envelope
|
|
20
|
+
* @returns {string} 'designs/round-1', or '' for something at the top level
|
|
21
|
+
*/
|
|
22
|
+
export function libraryFolderOf(envelope) {
|
|
23
|
+
const p = envelope?.path ?? envelope?.dir ?? '';
|
|
24
|
+
const cut = String(p).lastIndexOf('/');
|
|
25
|
+
const folder = cut <= 0 ? '' : String(p).slice(0, cut);
|
|
26
|
+
if (folder === STORAGE_ROOT) return '';
|
|
27
|
+
return folder.startsWith(`${STORAGE_ROOT}/`) ? folder.slice(STORAGE_ROOT.length + 1) : folder;
|
|
28
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// WHEN AGENTBOX IS ALLOWED TO INTERRUPT HER, and what the banner says when it is.
|
|
2
|
+
//
|
|
3
|
+
// Both halves of that sentence are rules, and the second one is the harder one:
|
|
4
|
+
// a notification that fires while she is looking at the row it is about is
|
|
5
|
+
// worse than no notification at all, because it costs her the flow the whole
|
|
6
|
+
// product is built to protect. So the gate is closed by default and opens only
|
|
7
|
+
// when she is provably somewhere else.
|
|
8
|
+
//
|
|
9
|
+
// This is the only copy of the rule. main/notify.mjs is the live wiring around
|
|
10
|
+
// it (the window's focus, the banner's lifetime); everything decidable is here,
|
|
11
|
+
// as plain functions over plain values, because that is the half worth pinning
|
|
12
|
+
// with tests.
|
|
13
|
+
|
|
14
|
+
// HOW LONG A STILL KEYBOARD MEANS SHE HAS GONE, and it is deliberately long.
|
|
15
|
+
// The window can hold focus while she is not at the machine at all: the lid is
|
|
16
|
+
// open, Agentbox is the front app, and she is in another room. That is squarely
|
|
17
|
+
// "in a diff app" as she meant it, so the banner should fire.
|
|
18
|
+
//
|
|
19
|
+
// Five minutes rather than one, because the failure modes are not symmetric.
|
|
20
|
+
// Firing late costs her a few minutes of not knowing. Firing while she is
|
|
21
|
+
// reading a long message with her hands off the keyboard is exactly the
|
|
22
|
+
// interruption she asked us to stop, and she would be right to read it as the
|
|
23
|
+
// feature not working. Reading one card for five unbroken minutes is rare;
|
|
24
|
+
// walking away for five minutes is not.
|
|
25
|
+
export const AWAY_AFTER_MS = 5 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
// She is at the app, so say nothing. Focus alone is not enough (see above) and
|
|
28
|
+
// idleness alone is not either: a still keyboard while she is reading Slack is
|
|
29
|
+
// not a reason to stay quiet.
|
|
30
|
+
export function atTheApp({ focused = false, idleMs = 0 } = {}) {
|
|
31
|
+
return !!focused && idleMs < AWAY_AFTER_MS;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// A title long enough to be a paragraph is normal here; hers run past a
|
|
35
|
+
// hundred characters. macOS will hard-truncate mid-word, so cut on a space and
|
|
36
|
+
// say that it was cut.
|
|
37
|
+
export function clip(text, max = 120) {
|
|
38
|
+
const s = String(text ?? '').replace(/\s+/g, ' ').trim();
|
|
39
|
+
if (s.length <= max) return s;
|
|
40
|
+
const cut = s.slice(0, max);
|
|
41
|
+
const space = cut.lastIndexOf(' ');
|
|
42
|
+
return `${(space > max * 0.6 ? cut.slice(0, space) : cut).replace(/[.,;:]$/, '')}…`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ONE BANNER FOR THE WHOLE TIME SHE IS AWAY. Not one per arrival, and not one
|
|
46
|
+
// per rewrite either, which is what this file used to do.
|
|
47
|
+
//
|
|
48
|
+
// The version before this one already refused to STACK banners: it took the old
|
|
49
|
+
// one down and put a fresh one up. That is one banner on screen, and it is
|
|
50
|
+
// still twenty interruptions, because on macOS there is no way to edit a
|
|
51
|
+
// notification that is already up. Rewriting it means showing another one, with
|
|
52
|
+
// another sound. A forty minute meeting with an agent finishing every two
|
|
53
|
+
// minutes was twenty chimes. That is the thing she is naming, and the count in
|
|
54
|
+
// the old wording is what made it feel necessary to keep re-firing: a number
|
|
55
|
+
// goes stale the second the next agent lands.
|
|
56
|
+
//
|
|
57
|
+
// So: she is told once, in words that stay true however many arrive after, and
|
|
58
|
+
// the rest is waiting on the rows when she comes back. `seen` in
|
|
59
|
+
// main/notify.mjs resets this the moment she has the window, so the next time
|
|
60
|
+
// she walks away she can be told again.
|
|
61
|
+
//
|
|
62
|
+
// THE ONE EXCEPTION, and it is here because an approval card has a clock on it.
|
|
63
|
+
// A finished agent waits as long as it takes. An approval is a worker frozen
|
|
64
|
+
// mid-run with her as the only way through, and it is DENIED AUTOMATICALLY
|
|
65
|
+
// after fifteen minutes (TIMEOUT_MS, main/approval-prompt-server.mjs). If the
|
|
66
|
+
// first banner of the stretch was a finished agent, an approval landing twenty
|
|
67
|
+
// minutes later would die unheard under a strict one-and-done rule. So an
|
|
68
|
+
// approval gets to speak once more, and that is the cap: at most two in one
|
|
69
|
+
// stretch away, never more.
|
|
70
|
+
export function shouldSpeak({ spoke = false, spokeAsk = false, waiting = [] } = {}) {
|
|
71
|
+
const items = (waiting ?? []).filter(Boolean);
|
|
72
|
+
if (!items.length) return false;
|
|
73
|
+
if (!spoke) return true;
|
|
74
|
+
if (spokeAsk) return false;
|
|
75
|
+
return items.some((i) => i.kind === 'approval');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// WHAT THE ONE BANNER SAYS. No number in it, for two reasons that point the
|
|
79
|
+
// same way. The design law rejects counts on a glance surface (STATE.md), and
|
|
80
|
+
// under the rule above a count would be a lie within a minute: it can only ever
|
|
81
|
+
// describe the first second and a half of a stretch that may run for hours.
|
|
82
|
+
//
|
|
83
|
+
// TWO LINES, NEVER THREE, AND NO PROJECT NAME ANYWHERE IN IT.
|
|
84
|
+
//
|
|
85
|
+
// The subtitle field went first, earlier the same day, because she read a lone
|
|
86
|
+
// project name stacked under the title as the title bleeding over. Putting the
|
|
87
|
+
// project into the title instead only moved the problem: a title is the one
|
|
88
|
+
// line macOS will not wrap, so a project called something long pushes "An agent
|
|
89
|
+
// is ready" out of the banner and the fact she needed is the part that gets
|
|
90
|
+
// cut. Both fields are hers to fill with anything, and neither of them is a
|
|
91
|
+
// safe place for a string we do not control the length of.
|
|
92
|
+
//
|
|
93
|
+
// So the title is a fixed sentence and the body is the row's own name, which is
|
|
94
|
+
// where the project is recognisable anyway. Plural without a number covers the
|
|
95
|
+
// rest.
|
|
96
|
+
//
|
|
97
|
+
// `waiting` is everything unseen, oldest first. Returns null when there is
|
|
98
|
+
// nothing to say.
|
|
99
|
+
export function banner(waiting) {
|
|
100
|
+
const items = (waiting ?? []).filter(Boolean);
|
|
101
|
+
if (!items.length) return null;
|
|
102
|
+
|
|
103
|
+
// A frozen worker outranks a finished one, so when both are unseen the
|
|
104
|
+
// banner speaks for the one with a clock on it.
|
|
105
|
+
const asks = items.filter((i) => i.kind === 'approval');
|
|
106
|
+
const lead = asks.length ? asks[0] : items[items.length - 1];
|
|
107
|
+
const many = items.length > 1;
|
|
108
|
+
|
|
109
|
+
const title = asks.length
|
|
110
|
+
? (many ? 'Agents are waiting on you' : 'An agent is waiting on you')
|
|
111
|
+
: (many ? 'Agents are ready' : 'An agent is ready');
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
title,
|
|
115
|
+
body: clip(lead.title),
|
|
116
|
+
// What a click opens. An approval card is already on screen the moment the
|
|
117
|
+
// window comes forward, so that one only needs the window.
|
|
118
|
+
open: lead.kind === 'approval' ? null : (lead.id ?? null),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// THE ONE PLACE THIS APP IS NAMED. Change the two lines under NAME and every
|
|
2
|
+
// screen, every notification, every agent brief and the Dock follow.
|
|
3
|
+
//
|
|
4
|
+
// She is right about the frequency. This app has been Zero, then Astral, then
|
|
5
|
+
// the app, and is agentbox now, and each of the first three renames was a
|
|
6
|
+
// hand-run search and replace over about two hundred literals
|
|
7
|
+
// (scripts/rename-to-powerup.mjs is the third one, kept as history). A sweep
|
|
8
|
+
// cannot be run ahead of time, so between renames the old name sat in the code
|
|
9
|
+
// waiting to be missed, and one of them was: the screenshot on her row shows
|
|
10
|
+
// "Import into the app?" two names later.
|
|
11
|
+
//
|
|
12
|
+
// HOW TO RENAME IT NEXT TIME, the whole procedure:
|
|
13
|
+
//
|
|
14
|
+
// 1. Change NAME below to the new name, spelled exactly as it should read in
|
|
15
|
+
// the MIDDLE of a sentence.
|
|
16
|
+
// 2. Add the name it had before to WAS, at the front of the list.
|
|
17
|
+
// 3. Run `node scripts/product-name.mjs --write`. That is what carries the
|
|
18
|
+
// name into package.json, which Electron reads before any of our code
|
|
19
|
+
// runs and so cannot import this file.
|
|
20
|
+
//
|
|
21
|
+
// Nothing else. `tests/the-app-is-named-in-one-place.test.mjs` fails if step
|
|
22
|
+
// 3 is skipped, and it fails if a new literal of the name is typed into the
|
|
23
|
+
// code by hand instead of read from here.
|
|
24
|
+
//
|
|
25
|
+
// CAPITALISATION IS A RULE HERE, NOT A SPELLING.
|
|
26
|
+
//
|
|
27
|
+
// So NAME is the mid-sentence spelling and `Name` is that same word with its
|
|
28
|
+
// first letter raised. A name that is already an ordinary proper noun, as two
|
|
29
|
+
// of the names in WAS are, comes out identical from both, so the rule costs
|
|
30
|
+
// nothing when it does not apply, and a lowercase name gets it right without
|
|
31
|
+
// anyone having to remember.
|
|
32
|
+
// Pick the one the position calls for. Sentence start, a heading, a title, a
|
|
33
|
+
// menu item and a button label take `Name`; anywhere else in a sentence takes
|
|
34
|
+
// `name`.
|
|
35
|
+
|
|
36
|
+
/** The name, spelled as it reads in the middle of a sentence. */
|
|
37
|
+
export const NAME = 'agentbox';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Every name this app has had, newest first. main/main.mjs walks this to find
|
|
41
|
+
* the data folder an existing install already writes to, because Chromium
|
|
42
|
+
* names that folder after the app and a rename would otherwise hand her an
|
|
43
|
+
* empty one and lose her theme, her zoom and her project order. HISTORY IS
|
|
44
|
+
* NEVER REWRITTEN: add to the front, never edit an entry. */
|
|
45
|
+
export const WAS = Object.freeze(['Powerup', 'Astral', 'Zero']);
|
|
46
|
+
|
|
47
|
+
/** The name at the start of a sentence, in a heading, or on a button. */
|
|
48
|
+
export const Name = NAME.charAt(0).toUpperCase() + NAME.slice(1);
|
|
49
|
+
|
|
50
|
+
/** One name as a slug, so the current one and the old ones are made the same way. */
|
|
51
|
+
const slugOf = (name) => String(name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The name lowercased throughout, for a slug, a folder or a log prefix.
|
|
55
|
+
* NOT for anything she reads. */
|
|
56
|
+
export const nameSlug = slugOf(NAME);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* EVERY SLUG THIS APP HAS ANSWERED TO, current first.
|
|
60
|
+
*
|
|
61
|
+
* The same argument as `envNames` below, one layer up. A slug of ours is not
|
|
62
|
+
* only a folder name: it is the name of the MCP server that IS the store, so it
|
|
63
|
+
* is written into a `--allowedTools` grant in her zero.config.json, and into the
|
|
64
|
+
* config of every Codex thread that has ever run. Both of those outlive a
|
|
65
|
+
* rename, and neither is ours to rewrite: the grant is her file, and a thread
|
|
66
|
+
* started last week is a record.
|
|
67
|
+
*
|
|
68
|
+
* So a rename cannot be allowed to mean "the store stops being the store". The
|
|
69
|
+
* places that ask "is this the store server?" ask it through here, the current
|
|
70
|
+
* name wins, and a name we used to have still answers.
|
|
71
|
+
*
|
|
72
|
+
* THIS IS NOT THE OLD `LEGACY_STORE_SERVERS`. That list held another PRODUCT's
|
|
73
|
+
* name, hardcoded, and was deleted for exactly that reason. Nothing is
|
|
74
|
+
* hardcoded here: this is `WAS`, which the app already keeps for its data
|
|
75
|
+
* folder and its env vars, read one more way.
|
|
76
|
+
*/
|
|
77
|
+
export const WAS_SLUGS = Object.freeze(WAS.map(slugOf));
|
|
78
|
+
|
|
79
|
+
/** The current slug and every slug it has had, current first. */
|
|
80
|
+
export const nameSlugs = Object.freeze([nameSlug, ...WAS_SLUGS]);
|
|
81
|
+
|
|
82
|
+
/** True when `slug` is this app under its current name or any name it has had. */
|
|
83
|
+
export const isOurSlug = (slug) => typeof slug === 'string' && nameSlugs.includes(slug.trim());
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* THE NAME AS AN ENVIRONMENT VARIABLE PREFIX, and every prefix it has had.
|
|
87
|
+
*
|
|
88
|
+
* The app's own env vars carried the name typed out: `ASTRAL_HOME` was still
|
|
89
|
+
* the store root two renames after the app stopped being the app, which is the
|
|
90
|
+
* same leak this whole module exists to close and the one an agent grepping the
|
|
91
|
+
* repo hits first. `envName('HOME')` is what code asks for now.
|
|
92
|
+
*
|
|
93
|
+
* `readEnv` FALLS BACK THROUGH THE OLD NAMES, newest first, for the same reason
|
|
94
|
+
* `WAS` exists at all: a shell, a launchd job or a running process started
|
|
95
|
+
* before the rename still exports the old one, and an app that cannot find its
|
|
96
|
+
* own store root opens empty.
|
|
97
|
+
*/
|
|
98
|
+
export const ENV_PREFIX = NAME.toUpperCase().replace(/[^A-Z0-9]+/g, '_');
|
|
99
|
+
export const WAS_ENV_PREFIXES = Object.freeze(WAS.map((was) => was.toUpperCase().replace(/[^A-Z0-9]+/g, '_')));
|
|
100
|
+
|
|
101
|
+
/** One env var of ours, by its suffix. `envName('HOME')`. */
|
|
102
|
+
export const envName = (suffix) => `${ENV_PREFIX}_${suffix}`;
|
|
103
|
+
|
|
104
|
+
/** Every spelling of one env var of ours, current name first. */
|
|
105
|
+
export const envNames = (suffix) => [ENV_PREFIX, ...WAS_ENV_PREFIXES].map((p) => `${p}_${suffix}`);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The value of one env var of ours, under the current name or any name this app
|
|
109
|
+
* has had. Empty and whitespace-only count as unset, because an exported-but-
|
|
110
|
+
* blank variable is how a shell says nothing rather than how it says "".
|
|
111
|
+
*/
|
|
112
|
+
export function readEnv(suffix, env = process.env) {
|
|
113
|
+
for (const key of envNames(suffix)) {
|
|
114
|
+
const value = env?.[key];
|
|
115
|
+
if (typeof value === 'string' && value.trim()) return value;
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The possessive, because "agentbox's" and "Atlas'" are not the same rule and
|
|
122
|
+
* a template that hardcodes 's is wrong the day the name ends in one. */
|
|
123
|
+
export const possessive = `${NAME}${/s$/i.test(NAME) ? "'" : "'s"}`;
|
|
124
|
+
|
|
125
|
+
/** The possessive at the start of a sentence. */
|
|
126
|
+
export const Possessive = `${Name}${/s$/i.test(NAME) ? "'" : "'s"}`;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* THE NAME INTO TEXT THAT IS NOT CODE: the agent briefs in `briefs/`, which are
|
|
130
|
+
* markdown files a session reads and, for three of the four, files SHE can edit
|
|
131
|
+
* in Settings. They cannot import anything, so they carry `{{name}}` and
|
|
132
|
+
* `{{Name}}` and this fills them in.
|
|
133
|
+
*
|
|
134
|
+
* FILLED WHERE THE BRIEF IS USED, NOT WHERE IT IS READ FOR EDITING. The editor
|
|
135
|
+
* in Settings reads the same files, and substituting on the way in would show
|
|
136
|
+
* her the filled text and then save it back with the name baked into her own
|
|
137
|
+
* copy, which is the one thing this whole change exists to prevent. So
|
|
138
|
+
* `buildBrief` calls this and the settings editor does not.
|
|
139
|
+
*/
|
|
140
|
+
export function fillName(text) {
|
|
141
|
+
return String(text ?? '')
|
|
142
|
+
.replaceAll('{{Name}}', Name)
|
|
143
|
+
.replaceAll('{{name}}', NAME);
|
|
144
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Types for shared/project-folder-check.mjs, which is plain ESM so that main
|
|
2
|
+
// (Node) and the renderer (Vite) can share one copy of the rule.
|
|
3
|
+
|
|
4
|
+
export interface FolderVerdict {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
/** Why it was turned down. For tests and the log; never put on a screen. */
|
|
7
|
+
reason?: 'home' | 'guarded' | 'system' | 'volume';
|
|
8
|
+
/** The sentence to show as it stands, when `ok` is false. */
|
|
9
|
+
say?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const GUARDED: string[];
|
|
13
|
+
|
|
14
|
+
export function checkProjectFolder(
|
|
15
|
+
folder: string | null | undefined,
|
|
16
|
+
opts?: { home?: string },
|
|
17
|
+
): FolderVerdict;
|
|
18
|
+
|
|
19
|
+
export function proposeParent(
|
|
20
|
+
existing?: string[],
|
|
21
|
+
opts?: { home?: string },
|
|
22
|
+
): string;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// THE FOLDERS A PROJECT MAY NOT BE, AND WHY macOS ASKS ABOUT MUSIC.
|
|
2
|
+
//
|
|
3
|
+
// That sentence is macOS, not us. Desktop, Documents, Downloads, Music,
|
|
4
|
+
// Pictures, Movies and everything under ~/Library are guarded by the system's
|
|
5
|
+
// privacy layer, so the FIRST time any process reads one of them the system
|
|
6
|
+
// throws up a panel with our name on it. Agentbox spawns Claude Code with the
|
|
7
|
+
// project folder as its working directory, and a worker that starts in the home
|
|
8
|
+
// folder will list it within its first few tool calls. One folder choice is
|
|
9
|
+
// therefore the difference between no panels at all and a run of them naming
|
|
10
|
+
// every private corner of somebody's Mac, on the second screen they ever see.
|
|
11
|
+
//
|
|
12
|
+
// Nothing stopped that choice. `zero:choose-folder` handed back whatever the
|
|
13
|
+
// Mac's own picker returned, and the picker opens on the home folder, so "just
|
|
14
|
+
// press Open" was a live path to exactly this.
|
|
15
|
+
//
|
|
16
|
+
// SO THIS IS A REFUSAL, NOT A WARNING. A warning on this screen is a question
|
|
17
|
+
// somebody with no way to answer it has to answer anyway. The folder is turned
|
|
18
|
+
// down and the sentence says which folder to pick instead.
|
|
19
|
+
//
|
|
20
|
+
// It is deliberately SHORT. The only folders refused are the ones that are not
|
|
21
|
+
// projects by construction: the home folder itself, the system's own top-level
|
|
22
|
+
// directories, and the seven guarded folders IN THEMSELVES. A project INSIDE
|
|
23
|
+
// any of them is fine and is not asked about — `~/Documents/my-app` is where
|
|
24
|
+
// plenty of people keep their code, it costs one panel the first time and it is
|
|
25
|
+
// genuinely their project. What is refused is pointing at `~/Documents` whole.
|
|
26
|
+
//
|
|
27
|
+
// Pure, so both sides of the bridge and the tests can use one copy: the
|
|
28
|
+
// renderer checks as she picks, and main checks again before it writes.
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The guarded folders, by their name directly under the home folder. The
|
|
32
|
+
* system asks about each of these separately, which is why the panels arrive
|
|
33
|
+
* as a run rather than as one. `Library` is the one that reads as "every app":
|
|
34
|
+
* it holds one directory per application under Application Support. */
|
|
35
|
+
export const GUARDED = [
|
|
36
|
+
'Desktop', 'Documents', 'Downloads', 'Movies', 'Music', 'Pictures', 'Library', 'Public',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Top-level directories that are the Mac and not anybody's work. */
|
|
40
|
+
const SYSTEM = [
|
|
41
|
+
'/', '/Applications', '/Library', '/System', '/Users', '/Volumes',
|
|
42
|
+
'/private', '/tmp', '/var', '/etc', '/usr', '/bin', '/sbin', '/opt', '/net', '/cores',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Trailing slashes off, and `~` left alone: the renderer does not know whose
|
|
47
|
+
* home this is and hands paths through exactly as it got them. */
|
|
48
|
+
function tidy(p) {
|
|
49
|
+
const s = String(p ?? '').trim();
|
|
50
|
+
if (!s) return '';
|
|
51
|
+
return s.length > 1 ? s.replace(/\/+$/, '') : s;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function underHome(p, home) {
|
|
55
|
+
const h = tidy(home);
|
|
56
|
+
if (!h) return null;
|
|
57
|
+
if (p === h) return '';
|
|
58
|
+
return p.startsWith(`${h}/`) ? p.slice(h.length + 1) : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* WHETHER THIS FOLDER CAN BE A PROJECT, and the sentence to show when it
|
|
63
|
+
* cannot. Returns `{ ok: true }` or `{ ok: false, reason, say }`.
|
|
64
|
+
*
|
|
65
|
+
* `say` is written to be shown as it stands, on the setup screen and on the
|
|
66
|
+
* New Project card, so it names the folder that was picked and says what to
|
|
67
|
+
* pick instead. `reason` is for tests and for the note in the log; nothing
|
|
68
|
+
* puts it on a screen.
|
|
69
|
+
*/
|
|
70
|
+
export function checkProjectFolder(folder, { home = '' } = {}) {
|
|
71
|
+
const p = tidy(folder);
|
|
72
|
+
if (!p) return { ok: true };
|
|
73
|
+
|
|
74
|
+
// `~` and `~/` are the home folder written the way our own screens write it.
|
|
75
|
+
const rel = p === '~' || p === '~/' ? '' : (p.startsWith('~/') ? p.slice(2) : underHome(p, home));
|
|
76
|
+
|
|
77
|
+
if (rel === '') {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
reason: 'home',
|
|
81
|
+
say: 'That is your whole home folder. Agents would work across everything on this Mac, and macOS will ask you about Downloads, Music and every app you have. Pick the folder your project’s code is in.',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (rel !== null && GUARDED.includes(rel)) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
reason: 'guarded',
|
|
89
|
+
say: `${rel} holds a lot more than one project, and macOS guards it, so choosing it means a run of permission panels. Pick the folder inside it that your project’s code is in.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (SYSTEM.includes(p)) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
reason: 'system',
|
|
97
|
+
say: `${p} is part of macOS rather than one of your projects. Pick the folder your project’s code is in.`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A volume root: /Volumes/Whatever with nothing after it.
|
|
102
|
+
if (/^\/Volumes\/[^/]+$/.test(p)) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
reason: 'volume',
|
|
106
|
+
say: `${p} is a whole disk rather than one project. Pick the folder your project’s code is in.`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { ok: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* WHERE A NEW PROJECT'S FOLDER IS PROPOSED, when nobody has pointed at one.
|
|
115
|
+
*
|
|
116
|
+
* This used to be `~/Desktop/dev/<slug>` flat, which is one developer's own
|
|
117
|
+
* filing system shipped to everybody, and it is inside Desktop, which is one
|
|
118
|
+
* of the guarded seven above: the very first project anybody made through the
|
|
119
|
+
* card asked macOS for the Desktop before it had drawn anything.
|
|
120
|
+
*
|
|
121
|
+
* So the parent is LEARNED from the projects they already have, and only
|
|
122
|
+
* guessed when there are none. `existing` is the folders of their current
|
|
123
|
+
* projects; the commonest parent among them wins, because somebody with three
|
|
124
|
+
* projects in `~/code` wants the fourth there too.
|
|
125
|
+
*
|
|
126
|
+
* The guess when there is nothing to learn from is `~/dev`, which is not
|
|
127
|
+
* guarded and costs no panel. It is a guess and the card says so by showing
|
|
128
|
+
* the whole path, which is the same thing it has always done.
|
|
129
|
+
*/
|
|
130
|
+
export function proposeParent(existing = [], { home = '~' } = {}) {
|
|
131
|
+
const counts = new Map();
|
|
132
|
+
for (const f of existing) {
|
|
133
|
+
const p = tidy(f);
|
|
134
|
+
if (!p) continue;
|
|
135
|
+
const cut = p.lastIndexOf('/');
|
|
136
|
+
if (cut <= 0) continue;
|
|
137
|
+
const parent = p.slice(0, cut);
|
|
138
|
+
// The PARENT is what gets proposed, so the parent is what is judged. A
|
|
139
|
+
// project sitting directly in ~/Downloads is a real project and is allowed;
|
|
140
|
+
// proposing ~/Downloads as the home of the next one is not.
|
|
141
|
+
if (!checkProjectFolder(parent, { home }).ok) continue;
|
|
142
|
+
counts.set(parent, (counts.get(parent) ?? 0) + 1);
|
|
143
|
+
}
|
|
144
|
+
let best = null;
|
|
145
|
+
let bestN = 0;
|
|
146
|
+
for (const [parent, n] of counts) {
|
|
147
|
+
// Ties go to the shorter path, so a stable answer rather than whichever
|
|
148
|
+
// project happened to be listed first.
|
|
149
|
+
if (n > bestN || (n === bestN && best && parent.length < best.length)) { best = parent; bestN = n; }
|
|
150
|
+
}
|
|
151
|
+
return best ?? `${tidy(home) || '~'}/dev`;
|
|
152
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ClaudeCommand } from './claude-commands.mjs';
|
|
2
|
+
export const CODEX_COMMANDS: ClaudeCommand[];
|
|
3
|
+
export const LOCAL_COMMANDS: ClaudeCommand[];
|
|
4
|
+
export function providerCommands(engine: string): ClaudeCommand[];
|
|
5
|
+
export function providerCommand(text: unknown, engine: string): {name: string; args: string; route: string} | null;
|
|
6
|
+
export function reviewTarget(args?: string): Record<string,string>;
|
|
7
|
+
|
|
8
|
+
export function nativeCommandNames(frame: unknown): string[] | null;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// A command is an operation, never prose asking a model to imitate a CLI.
|
|
2
|
+
import { CLAUDE_COMMANDS, commandWords } from './claude-commands.mjs';
|
|
3
|
+
const command = (name, description, argumentHint = null, aliases = []) => ({ name, description, menuDescription: null, whole: true, argumentHint, aliases });
|
|
4
|
+
export const CODEX_COMMANDS = [
|
|
5
|
+
command('model','Show models or change this task’s model','[model]'),
|
|
6
|
+
command('effort','Show or change reasoning effort','[level]'),
|
|
7
|
+
command('compact','Free up context in this conversation'),
|
|
8
|
+
command('review','Review uncommitted changes, a branch, or specific instructions','[--base branch | --commit sha | instructions]'),
|
|
9
|
+
command('status','Show this task’s configuration and conversation status'),
|
|
10
|
+
command('usage','Show the account’s last reported usage'),
|
|
11
|
+
command('skills','List skills available in this project'),
|
|
12
|
+
command('mcp','Show this conversation’s connected MCP servers'),
|
|
13
|
+
command('goal','View, pause, or clear this conversation’s goal','[pause | clear]'),
|
|
14
|
+
command('diff','Show staged and unstaged changes'),
|
|
15
|
+
command('rename','Rename this task','<name>'),
|
|
16
|
+
command('copy','Copy the last assistant response'),
|
|
17
|
+
command('help','Show available commands and remaining compatibility gaps'),
|
|
18
|
+
];
|
|
19
|
+
export const LOCAL_COMMANDS = CODEX_COMMANDS.filter(c=>['model','effort','diff','rename','help'].includes(c.name));
|
|
20
|
+
// A FORK IS A NEW TASK THAT STARTS WHERE THIS ONE GOT TO (MP-08). It is offered
|
|
21
|
+
// on both engines because both can do it natively: Claude Code resumes under a
|
|
22
|
+
// new id with `--fork-session`, and Codex's own protocol has `thread/fork`. It
|
|
23
|
+
// is refused on Codex for now, in a sentence that says why, because that call
|
|
24
|
+
// has never been made against a real server here.
|
|
25
|
+
const FORK = command('fork','Start a new task from this conversation','<what to try instead>');
|
|
26
|
+
export function providerCommands(engine) {
|
|
27
|
+
// NOT ON THE CODEX MENU, because this menu's rule is that it offers nothing
|
|
28
|
+
// that engine cannot run, and Codex forking is not connected yet. Typed there
|
|
29
|
+
// it gets the ordinary unavailable refusal, which points at the native Codex
|
|
30
|
+
// client, where `codex fork` genuinely works.
|
|
31
|
+
return engine==='codex' ? CODEX_COMMANDS : [command('remote-control','Continue this conversation in Claude on another device',null,['rc']),FORK,...CLAUDE_COMMANDS.map(c=>LOCAL_COMMANDS.find(l=>l.name===c.name)??c),...LOCAL_COMMANDS.filter(l=>!CLAUDE_COMMANDS.some(c=>c.name===l.name))];
|
|
32
|
+
}
|
|
33
|
+
export function providerCommand(text, engine) {
|
|
34
|
+
if(typeof text!=='string')return null;
|
|
35
|
+
const m=text.trim().match(/^\/([a-z][a-z0-9:_-]*)(?:\s+([\s\S]*))?$/i);
|
|
36
|
+
if(!m)return null;
|
|
37
|
+
const word=m[1].toLowerCase(), args=(m[2]??'').trim();
|
|
38
|
+
const cmd=providerCommands(engine).find(c=>commandWords(c).includes(word));
|
|
39
|
+
if(!cmd) return {name:word,args,route:'unavailable'};
|
|
40
|
+
if(cmd.name==='fork')return {name:'fork',args,route:'fork'};
|
|
41
|
+
return {name:cmd.name,args,route:cmd.name==='remote-control'?'remote':engine==='codex'?'codex':LOCAL_COMMANDS.some(c=>c.name===cmd.name)?'local':'claude'};
|
|
42
|
+
}
|
|
43
|
+
export function reviewTarget(args='') {
|
|
44
|
+
args=args.trim();
|
|
45
|
+
if(!args)return {type:'uncommittedChanges'};
|
|
46
|
+
const scope=args.match(/^--(base|commit)\s+([^\s]+)$/);
|
|
47
|
+
if(scope)return scope[1]==='base'?{type:'baseBranch',branch:scope[2]}:{type:'commit',sha:scope[2]};
|
|
48
|
+
if(args.startsWith('--'))throw new Error('Use /review, /review --base branch, /review --commit sha, or /review instructions.');
|
|
49
|
+
return {type:'custom',instructions:args};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Native prompt/skill commands are advertised by the running Claude session,
|
|
53
|
+
// including user and plugin commands. Never guess skill names from filenames.
|
|
54
|
+
export function nativeCommandNames(frame) {
|
|
55
|
+
if(frame?.type!=='system'||frame?.subtype!=='init'||!Array.isArray(frame.slash_commands))return null;
|
|
56
|
+
const blocked=new Set(['add-dir','auto-mode-setup','clear','color','config','import','design-consent','design-revoke','heapdump','exit','stop','__remote-workflow','workflow-launch-exec','usage-credits','extra-usage']);
|
|
57
|
+
return [...new Set(frame.slash_commands.filter(n=>typeof n==='string'&&/^[a-z][a-z0-9:_-]*$/i.test(n)&&!blocked.has(n)))];
|
|
58
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Types for shared/rank.mjs, which is plain ESM so that main (Node) and the
|
|
2
|
+
// renderer (Vite) can share one copy of the ranking rule.
|
|
3
|
+
|
|
4
|
+
export const RANK_STEP: number;
|
|
5
|
+
export function normalizeOrder(slugs: unknown): string[];
|
|
6
|
+
export function productRankScore(order: string[], slug: string): number;
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_PRIORITY: number;
|
|
9
|
+
|
|
10
|
+
export function itemPriority(item: { priority?: number; wrote?: Record<string, { source?: string } | undefined> } | null | undefined): number;
|
|
11
|
+
export const URGENT_PRIORITY: number;
|
|
12
|
+
export function isUrgent(item: { priority?: number; wrote?: Record<string, { source?: string } | undefined> } | null | undefined): boolean;
|
|
13
|
+
|
|
14
|
+
export function orderFromTiers(tiers: Record<string, number> | null | undefined): string[];
|
|
15
|
+
export function landingIndex(
|
|
16
|
+
rects: Array<{ top: number; bottom: number; left: number; width: number }>,
|
|
17
|
+
x: number, y: number,
|
|
18
|
+
): number;
|
|
19
|
+
export function moveProduct(order: string[], slug: string, toIndex: number): string[];
|