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,190 @@
|
|
|
1
|
+
// THE FIRST TASK'S ANSWER, READ OFF THE DISK.
|
|
2
|
+
//
|
|
3
|
+
// This is the whole of what replaces the session. It opens the readme in the
|
|
4
|
+
// folder she pointed at, or the description in a package.json beside it, and
|
|
5
|
+
// hands back one line. There is no queue, no model and no network in it, which
|
|
6
|
+
// is the point: the sentence on screen says the answer comes back in a few
|
|
7
|
+
// seconds, and this is the only version of the task that can keep that promise.
|
|
8
|
+
//
|
|
9
|
+
// The judgement about what makes a good line is in shared/first-run-line.mjs,
|
|
10
|
+
// where the tests are. This file only reads.
|
|
11
|
+
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { execFileSync } from 'node:child_process';
|
|
16
|
+
import { firstRunAnswer } from '../shared/first-run-line.mjs';
|
|
17
|
+
import { DEPTH, SKIP, projectSentence } from '../shared/first-run-shape.mjs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The names a readme is actually called, in the order worth trying. Case is
|
|
21
|
+
* not assumed: a folder listing is cheap and `readme.md` is common. */
|
|
22
|
+
const README = ['README.md', 'README', 'README.txt', 'README.markdown', 'readme.md'];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* As much of a readme as could possibly hold its first sentence. A readme is
|
|
26
|
+
* sometimes a book, and the whole of one has no business in memory here. */
|
|
27
|
+
const READ_BYTES = 64 * 1024;
|
|
28
|
+
|
|
29
|
+
function readHead(file) {
|
|
30
|
+
let fd = null;
|
|
31
|
+
try {
|
|
32
|
+
fd = fs.openSync(file, 'r');
|
|
33
|
+
const buf = Buffer.alloc(READ_BYTES);
|
|
34
|
+
const n = fs.readSync(fd, buf, 0, READ_BYTES, 0);
|
|
35
|
+
return buf.slice(0, n).toString('utf8');
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
} finally {
|
|
39
|
+
if (fd != null) { try { fs.closeSync(fd); } catch { /* nothing to do */ } }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The readme at the top of a folder, whatever it is called. Only the top
|
|
45
|
+
* level, which is the bound she took as recommended on round four: "Read the
|
|
46
|
+
* readme and the top level of this folder, and nothing deeper than that." */
|
|
47
|
+
export function readReadme(folder) {
|
|
48
|
+
if (!folder) return null;
|
|
49
|
+
for (const name of README) {
|
|
50
|
+
const file = path.join(folder, name);
|
|
51
|
+
if (fs.existsSync(file)) return readHead(file);
|
|
52
|
+
}
|
|
53
|
+
// A folder whose readme is spelled some other way. One listing, top level.
|
|
54
|
+
let entries = [];
|
|
55
|
+
try { entries = fs.readdirSync(folder); } catch { return null; }
|
|
56
|
+
const hit = entries.find((e) => /^readme(\.|$)/i.test(e));
|
|
57
|
+
return hit ? readHead(path.join(folder, hit)) : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The one field in a manifest that says what a thing is. package.json is the
|
|
62
|
+
* common case by a distance; the other two cost two lines each. */
|
|
63
|
+
export function readDescription(folder) {
|
|
64
|
+
if (!folder) return null;
|
|
65
|
+
const pkg = path.join(folder, 'package.json');
|
|
66
|
+
if (fs.existsSync(pkg)) {
|
|
67
|
+
try {
|
|
68
|
+
const d = JSON.parse(fs.readFileSync(pkg, 'utf8')).description;
|
|
69
|
+
if (typeof d === 'string' && d.trim()) return d.trim();
|
|
70
|
+
} catch { /* a package.json that does not parse is not a description */ }
|
|
71
|
+
}
|
|
72
|
+
for (const name of ['pyproject.toml', 'Cargo.toml']) {
|
|
73
|
+
const file = path.join(folder, name);
|
|
74
|
+
if (!fs.existsSync(file)) continue;
|
|
75
|
+
const m = readHead(file)?.match(/^\s*description\s*=\s*["']([^"'\n]+)["']/m);
|
|
76
|
+
if (m) return m[1].trim();
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* THE DESCRIPTION, when the folder has one written down. Never throws: a
|
|
83
|
+
* folder deleted between the name screen and this one still gets a sentence,
|
|
84
|
+
* because the walk is not allowed to stop on it.
|
|
85
|
+
*
|
|
86
|
+
* This is NOT the task's answer any more, and says why: measured over the 29
|
|
87
|
+
* project folders in ~/Desktop/dev on 2026-08-21, only 16 of them have a line
|
|
88
|
+
* anywhere that says what the project is. It is what goes UNDER THE PROJECT'S
|
|
89
|
+
* NAME IN THE SIDEBAR, which is the job it was wanted for. */
|
|
90
|
+
export function describeFor({ folder, name, shortFolder }) {
|
|
91
|
+
try {
|
|
92
|
+
return firstRunAnswer({
|
|
93
|
+
readme: readReadme(folder),
|
|
94
|
+
description: readDescription(folder),
|
|
95
|
+
name,
|
|
96
|
+
folder: shortFolder ?? folder,
|
|
97
|
+
});
|
|
98
|
+
} catch {
|
|
99
|
+
return firstRunAnswer({ readme: null, description: null, name, folder: shortFolder ?? folder });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* THE GUARDED FOLDERS AS FULL PATHS, for the walk to refuse to enter.
|
|
105
|
+
*
|
|
106
|
+
* The picker refuses these as a project (shared/project-folder-check.mjs) and
|
|
107
|
+
* that is the door a tester actually walked through. This is the second door.
|
|
108
|
+
* The walk is three levels deep, so ANY caller that hands it the home folder
|
|
109
|
+
* makes it read ~/Desktop, ~/Downloads, ~/Music, ~/Pictures, ~/Movies,
|
|
110
|
+
* ~/Documents and ~/Library, and macOS answers each one with a consent panel
|
|
111
|
+
* carrying our name. Measured on 2026-08-25 against a real home folder: 4,001
|
|
112
|
+
* listings, every one of the seven touched, and 137 apps under Application
|
|
113
|
+
* Support plus 706 under Containers read by name. All of that to say "20,000
|
|
114
|
+
* files across 7,053 folders".
|
|
115
|
+
*
|
|
116
|
+
* Matched as WHOLE PATHS directly under the home folder, never by name. A
|
|
117
|
+
* project called `Documents` inside somebody's code folder is their project;
|
|
118
|
+
* `~/Documents` is not. A project INSIDE a guarded folder still counts
|
|
119
|
+
* normally, because the walk starts below the guard. */
|
|
120
|
+
function guardedPaths(home) {
|
|
121
|
+
const h = String(home ?? '').replace(/\/+$/, '');
|
|
122
|
+
if (!h) return new Set();
|
|
123
|
+
return new Set(
|
|
124
|
+
['Desktop', 'Documents', 'Downloads', 'Movies', 'Music', 'Pictures', 'Library', 'Public']
|
|
125
|
+
.map((n) => path.join(h, n)),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* WHAT IS ACTUALLY IN THE FOLDER. Three levels deep, skipping everything that
|
|
131
|
+
* is not the person's own work, never entering a folder macOS guards, and
|
|
132
|
+
* stopping at a hard cap so a folder with a million files in it cannot turn a
|
|
133
|
+
* two second promise into a long one. */
|
|
134
|
+
export function countFolder(folder, { cap = 20_000, home = os.homedir() } = {}) {
|
|
135
|
+
const out = { files: 0, dirs: 0, ext: Object.create(null), newest: 0, capped: false };
|
|
136
|
+
const guarded = guardedPaths(home);
|
|
137
|
+
const walk = (dir, depth) => {
|
|
138
|
+
if (out.files >= cap) { out.capped = true; return; }
|
|
139
|
+
let entries = [];
|
|
140
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
141
|
+
for (const e of entries) {
|
|
142
|
+
if (out.files >= cap) { out.capped = true; return; }
|
|
143
|
+
if (e.name.startsWith('.') || SKIP.has(e.name)) continue;
|
|
144
|
+
const p = path.join(dir, e.name);
|
|
145
|
+
// Counted as a folder, never opened. The number stays honest and macOS
|
|
146
|
+
// is never asked.
|
|
147
|
+
if (guarded.has(p)) { out.dirs += 1; continue; }
|
|
148
|
+
if (e.isDirectory()) { out.dirs += 1; if (depth > 0) walk(p, depth - 1); continue; }
|
|
149
|
+
out.files += 1;
|
|
150
|
+
const ext = path.extname(e.name);
|
|
151
|
+
if (ext) out.ext[ext] = (out.ext[ext] ?? 0) + 1;
|
|
152
|
+
try { const m = fs.statSync(p).mtimeMs; if (m > out.newest) out.newest = m; } catch { /* a file that vanished mid-walk is not a failure */ }
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
walk(folder, DEPTH);
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Git's own words for when the folder last changed, or null when it is not a
|
|
161
|
+
* repo. Its answer is better than a file's timestamp, which a checkout or a
|
|
162
|
+
* build can move without anybody having written anything. */
|
|
163
|
+
export function lastCommit(folder) {
|
|
164
|
+
try {
|
|
165
|
+
// GIT_DIR beats `-C`, so a shell that exported one (a git hook does, and so
|
|
166
|
+
// does anything launched from inside one) would have every folder answer
|
|
167
|
+
// with THAT repository's last commit, repo or not. Ask about the folder.
|
|
168
|
+
const env = { ...process.env };
|
|
169
|
+
for (const key of Object.keys(env)) if (key.startsWith('GIT_')) delete env[key];
|
|
170
|
+
const out = execFileSync('git', ['-C', folder, 'log', '-1', '--format=%cr'], {
|
|
171
|
+
stdio: ['ignore', 'pipe', 'ignore'], timeout: 2_000, env,
|
|
172
|
+
}).toString().trim();
|
|
173
|
+
return out || null;
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* THE FIRST TASK'S ANSWER.The sentence is ours and every number in it is read
|
|
181
|
+
* off the folder chosen two screens earlier, so it is true about any project
|
|
182
|
+
* and cannot be a guess about what that project is. Never throws. */
|
|
183
|
+
export function answerFor({ folder, name }) {
|
|
184
|
+
try {
|
|
185
|
+
const count = countFolder(folder);
|
|
186
|
+
return projectSentence({ ...count, name, lastCommit: lastCommit(folder) });
|
|
187
|
+
} catch {
|
|
188
|
+
return projectSentence({ name, files: 0, dirs: 0, ext: {} });
|
|
189
|
+
}
|
|
190
|
+
}
|
package/main/folders.mjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// WALKING THE DISK FOR A SCREEN THAT CANNOT OPEN A MAC DIALOG.
|
|
2
|
+
//
|
|
3
|
+
// THE MISUNDERSTANDING THIS FILE EXISTS TO CORRECT. A browser tab cannot be
|
|
4
|
+
// handed a real path by the operating system: the file input gives a page a
|
|
5
|
+
// blob and a bare filename, on purpose, because a web page is not trusted with
|
|
6
|
+
// somebody's disk. That is true and it is not going to change.
|
|
7
|
+
//
|
|
8
|
+
// It is also not the problem. The thing that needs to read the disk is not the
|
|
9
|
+
// tab, it is the process the terminal started, and that process is ordinary
|
|
10
|
+
// Node running as the person who typed the command. It has exactly the same
|
|
11
|
+
// access the desktop app has. So the tab never asks the operating system for a
|
|
12
|
+
// path. It asks the app what is in a folder, draws the answer, and sends back
|
|
13
|
+
// the folder she clicked. The picker is ours instead of the Mac's, and the
|
|
14
|
+
// answer is a real absolute path either way.
|
|
15
|
+
//
|
|
16
|
+
// This is how every editor in a browser does it, and it is the only part of
|
|
17
|
+
// the desktop app that needed replacing rather than reusing.
|
|
18
|
+
//
|
|
19
|
+
// WHAT IS DELIBERATELY NOT HERE. No serving of file CONTENTS, and no walking
|
|
20
|
+
// upward past the root. This lists directory names so somebody can point at
|
|
21
|
+
// one. The folder it ends up pointing at still goes through
|
|
22
|
+
// checkProjectFolder, the same as a path the Mac's own dialog returned.
|
|
23
|
+
|
|
24
|
+
import fs from 'node:fs';
|
|
25
|
+
import os from 'node:os';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
|
|
28
|
+
/** Where the picker opens when nothing has been chosen yet. */
|
|
29
|
+
export const startingPoint = (home = os.homedir()) => home;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve what a screen typed or clicked into an absolute path.
|
|
33
|
+
*
|
|
34
|
+
* `~` is how our own screens write the home folder, and somebody typing a path
|
|
35
|
+
* by hand writes it that way too.
|
|
36
|
+
*/
|
|
37
|
+
export function absolute(at, home = os.homedir()) {
|
|
38
|
+
const s = String(at ?? '').trim();
|
|
39
|
+
if (!s) return startingPoint(home);
|
|
40
|
+
if (s === '~') return home;
|
|
41
|
+
if (s.startsWith('~/')) return path.join(home, s.slice(2));
|
|
42
|
+
return path.resolve(s);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The folders inside one folder, and the way back up.
|
|
47
|
+
*
|
|
48
|
+
* Returns `{ at, parent, home, folders }`, or `{ at, unreadable }` when the
|
|
49
|
+
* folder is gone or the system will not open it. Unreadable is an ordinary
|
|
50
|
+
* answer rather than a throw: somebody navigating a disk lands on a folder
|
|
51
|
+
* they cannot open fairly often, and the screen should say so and stay put.
|
|
52
|
+
*/
|
|
53
|
+
export function listFolders({ at, home = os.homedir(), showHidden = false } = {}) {
|
|
54
|
+
const here = absolute(at, home);
|
|
55
|
+
const parent = path.dirname(here);
|
|
56
|
+
|
|
57
|
+
let entries;
|
|
58
|
+
try {
|
|
59
|
+
entries = fs.readdirSync(here, { withFileTypes: true });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return {
|
|
62
|
+
at: here,
|
|
63
|
+
parent: parent === here ? null : parent,
|
|
64
|
+
home,
|
|
65
|
+
folders: [],
|
|
66
|
+
unreadable: err.code === 'EACCES' || err.code === 'EPERM'
|
|
67
|
+
? 'The system will not let this be opened.'
|
|
68
|
+
: 'That folder is not there any more.',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const folders = entries
|
|
73
|
+
.filter((entry) => {
|
|
74
|
+
// A symlink to a directory is a directory as far as somebody choosing a
|
|
75
|
+
// project folder is concerned, and plenty of real checkouts are one.
|
|
76
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) return false;
|
|
77
|
+
if (!showHidden && entry.name.startsWith('.')) return false;
|
|
78
|
+
return true;
|
|
79
|
+
})
|
|
80
|
+
.map((entry) => ({ name: entry.name, path: path.join(here, entry.name) }))
|
|
81
|
+
.filter((folder) => {
|
|
82
|
+
if (!folder.path) return false;
|
|
83
|
+
// A symlink that points at a file, or at nothing, is not a folder to walk
|
|
84
|
+
// into. statSync follows it; a broken one throws and is dropped.
|
|
85
|
+
try { return fs.statSync(folder.path).isDirectory(); } catch { return false; }
|
|
86
|
+
})
|
|
87
|
+
// The way a person reads a folder listing, not the way the disk returns it.
|
|
88
|
+
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
|
|
89
|
+
|
|
90
|
+
return { at: here, parent: parent === here ? null : parent, home, folders };
|
|
91
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// OPEN A SECOND AGENTBOX AS SOMEBODY WHO HAS NEVER OPENED AGENTBOX.
|
|
2
|
+
//
|
|
3
|
+
// Two halves, and this file is the first one. The second half — walking the
|
|
4
|
+
// onboarding again inside the app she already has — is `restartFirstRun` in
|
|
5
|
+
// `renderer/src/onboarding.ts`, and it is the small one: it forgets that the
|
|
6
|
+
// walk was finished and forgets nothing else. Being a NEW USER is bigger than
|
|
7
|
+
// that. A new user has no projects, no settings, no theme, no store and no
|
|
8
|
+
// config file, and there was no way to be one on a downloaded app except to
|
|
9
|
+
// throw the install away, because `npm run fresh` needs a repo beside it and a
|
|
10
|
+
// terminal in front of it and a downloaded app has neither.
|
|
11
|
+
//
|
|
12
|
+
// So this launches THE RUNNING APP AGAIN, inside a throwaway home. The recipe
|
|
13
|
+
// is `shared/fresh-user-home.mjs`, which is the one `scripts/fresh-user.mjs`
|
|
14
|
+
// has used since 2026-08-20, so there is one place it is written down.
|
|
15
|
+
//
|
|
16
|
+
// NOTHING OF HERS IS TOUCHED. Nothing is deleted, nothing is copied out, and
|
|
17
|
+
// her own Agentbox keeps running with every agent in it: the single-instance
|
|
18
|
+
// lock is keyed on userData, a throwaway home is a different userData, so the
|
|
19
|
+
// two take separate locks and stand side by side. That is the property the
|
|
20
|
+
// whole thing rests on, and it is why this may not be written as a reset.
|
|
21
|
+
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { spawn } from 'node:child_process';
|
|
25
|
+
import { HOMES, existingHomes, prepareHome, makeHome, freshEnv } from '../shared/fresh-user-home.mjs';
|
|
26
|
+
import { NAME } from '../shared/product-name.mjs';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* What to run, and what to hand it.
|
|
30
|
+
*
|
|
31
|
+
* PACKAGED IS THE CASE THAT MATTERS, because the whole ask is about a
|
|
32
|
+
* downloaded app: `process.execPath` is `Astral.app/Contents/MacOS/Astral`
|
|
33
|
+
* and it takes no arguments.
|
|
34
|
+
*
|
|
35
|
+
* UNPACKAGED IT IS ELECTRON, and Electron on its own opens its default window
|
|
36
|
+
* rather than this app, so the app directory has to be handed back to it.
|
|
37
|
+
* `process.argv[1]` is that directory when Agentbox is run from source. This
|
|
38
|
+
* half exists so the row can be driven in a test and in a dev window rather
|
|
39
|
+
* than only in a build, which is the difference between a feature that is
|
|
40
|
+
* checked and one that is hoped for. */
|
|
41
|
+
export function relaunchCommand({ execPath = process.execPath, argv = process.argv, packaged = true } = {}) {
|
|
42
|
+
if (packaged) return { bin: execPath, args: [] };
|
|
43
|
+
const appPath = argv[1];
|
|
44
|
+
return { bin: execPath, args: appPath ? [appPath] : [] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Open one. Returns what happened, in the words the app will show her: this
|
|
49
|
+
* is reported to the window rather than thrown, because a row in ⌘K that
|
|
50
|
+
* fails silently is worse than one that says why.
|
|
51
|
+
*
|
|
52
|
+
* `withAgents` defaults ON. A new user of THIS product already has Claude
|
|
53
|
+
* Code, and the walk's last card offers to bring their agents over, so a
|
|
54
|
+
* fresh copy that cannot see any agent files tests a screen no real user of
|
|
55
|
+
* hers will ever get. Measured 2026-08-22: plain `npm run fresh` hides
|
|
56
|
+
* `~/.claude`, and the walk's agent beat then says there is nothing to bring.
|
|
57
|
+
* It cannot break inbox zero: the walk shows only its own three examples. */
|
|
58
|
+
export function openFreshUser({
|
|
59
|
+
withAgents = true,
|
|
60
|
+
execPath = process.execPath,
|
|
61
|
+
argv = process.argv,
|
|
62
|
+
packaged = true,
|
|
63
|
+
spawnFn = spawn,
|
|
64
|
+
now,
|
|
65
|
+
} = {}) {
|
|
66
|
+
let prepared;
|
|
67
|
+
try {
|
|
68
|
+
prepared = prepareHome(makeHome(now), { withAgents });
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return { ok: false, error: `Could not make a throwaway home under ${HOMES}: ${err?.message ?? err}` };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { bin, args } = relaunchCommand({ execPath, argv, packaged });
|
|
74
|
+
if (!bin || !fs.existsSync(bin)) {
|
|
75
|
+
return { ok: false, error: `Cannot find this app's own program at ${bin}.`, home: prepared.home };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let child;
|
|
79
|
+
try {
|
|
80
|
+
child = spawnFn(bin, args, {
|
|
81
|
+
detached: true,
|
|
82
|
+
stdio: 'ignore',
|
|
83
|
+
env: freshEnv(prepared.home),
|
|
84
|
+
});
|
|
85
|
+
child.unref?.();
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return { ok: false, error: `Could not start a second ${NAME}: ${err?.message ?? err}`, home: prepared.home };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// THE NOTES ARE THE HONEST PART. Both of these are things a fresh copy can
|
|
91
|
+
// be missing without looking broken, and a test that quietly cannot run an
|
|
92
|
+
// agent is a test that lies. They are said rather than assumed.
|
|
93
|
+
const notes = [];
|
|
94
|
+
if (!prepared.claudeBin) notes.push('Claude Code is not at ~/.local/bin/claude, so agents will not run in it.');
|
|
95
|
+
if (!prepared.keychain) notes.push('There is no ~/Library/Keychains here, so Claude Code will say it is not logged in.');
|
|
96
|
+
if (withAgents && !prepared.agents) notes.push('There is no ~/.claude on this Mac, so it will have no agents to offer.');
|
|
97
|
+
|
|
98
|
+
return { ok: true, home: prepared.home, agents: prepared.agents, notes, pid: child?.pid ?? null };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The leftovers, so the app can say how many there are rather than growing a
|
|
103
|
+
* folder under /tmp nobody ever looks at. */
|
|
104
|
+
export function freshUserHomes() {
|
|
105
|
+
return { root: HOMES, homes: existingHomes() };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Delete every throwaway home. Only ever the /tmp folder this file made:
|
|
110
|
+
* `HOMES` is not configurable, and that is on purpose. */
|
|
111
|
+
export function clearFreshUserHomes() {
|
|
112
|
+
try {
|
|
113
|
+
fs.rmSync(HOMES, { recursive: true, force: true });
|
|
114
|
+
return { ok: true };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export { HOMES, path as _path };
|