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.
Files changed (302) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +176 -0
  3. package/bin/agentbox.mjs +103 -0
  4. package/briefs/adhd-mode.md +5 -0
  5. package/briefs/message-rules.md +95 -0
  6. package/briefs/worker.md +230 -0
  7. package/briefs/writing-rules.md +336 -0
  8. package/main/account-discovery.mjs +145 -0
  9. package/main/account-tooling.mjs +261 -0
  10. package/main/agent-activity.mjs +56 -0
  11. package/main/agent-files.mjs +422 -0
  12. package/main/agent-schedule.mjs +254 -0
  13. package/main/agent-sessions.mjs +435 -0
  14. package/main/agents.mjs +1095 -0
  15. package/main/analytics.mjs +206 -0
  16. package/main/answer-modes.mjs +206 -0
  17. package/main/approval-prompt-server.mjs +160 -0
  18. package/main/approvals.mjs +487 -0
  19. package/main/artifact-path.mjs +144 -0
  20. package/main/claude-bin.mjs +311 -0
  21. package/main/claude-input.mjs +117 -0
  22. package/main/claude-models.mjs +244 -0
  23. package/main/claude-plan.mjs +148 -0
  24. package/main/claude-usage.mjs +149 -0
  25. package/main/code-change.mjs +383 -0
  26. package/main/code-file.mjs +116 -0
  27. package/main/codex-account.mjs +146 -0
  28. package/main/codex-app-server.mjs +796 -0
  29. package/main/codex-approvals.mjs +712 -0
  30. package/main/codex-bin.mjs +261 -0
  31. package/main/codex-compaction.mjs +49 -0
  32. package/main/codex-models.mjs +194 -0
  33. package/main/codex-session.mjs +797 -0
  34. package/main/codex-threads.mjs +268 -0
  35. package/main/codex-usage-file.mjs +192 -0
  36. package/main/codex-usage.mjs +149 -0
  37. package/main/codex-watch.mjs +35 -0
  38. package/main/codex.mjs +757 -0
  39. package/main/config.mjs +345 -0
  40. package/main/copy-selection.mjs +55 -0
  41. package/main/crash-report.mjs +158 -0
  42. package/main/demo.mjs +254 -0
  43. package/main/dev-window.mjs +103 -0
  44. package/main/doc-file.mjs +99 -0
  45. package/main/doc-scheme.mjs +126 -0
  46. package/main/first-run.mjs +190 -0
  47. package/main/folders.mjs +91 -0
  48. package/main/fresh-user.mjs +120 -0
  49. package/main/git-change.mjs +323 -0
  50. package/main/img-scheme.mjs +90 -0
  51. package/main/instruction-settings.mjs +205 -0
  52. package/main/ipc.mjs +1558 -0
  53. package/main/live-replies.mjs +77 -0
  54. package/main/machine.mjs +113 -0
  55. package/main/main.mjs +972 -0
  56. package/main/menu.mjs +177 -0
  57. package/main/notify.mjs +155 -0
  58. package/main/open-source-file.mjs +8 -0
  59. package/main/project-identity.mjs +149 -0
  60. package/main/provider-commands.mjs +32 -0
  61. package/main/rail-note.mjs +74 -0
  62. package/main/repeats.mjs +261 -0
  63. package/main/row-label.mjs +185 -0
  64. package/main/screen-detail.mjs +44 -0
  65. package/main/serve.mjs +262 -0
  66. package/main/settings.mjs +912 -0
  67. package/main/staleness.mjs +76 -0
  68. package/main/store/chats.mjs +188 -0
  69. package/main/store/home.mjs +383 -0
  70. package/main/store/migrate.mjs +40 -0
  71. package/main/store/paths.mjs +82 -0
  72. package/main/store/pending-writes.mjs +122 -0
  73. package/main/store/project-lock.mjs +200 -0
  74. package/main/store/project.mjs +523 -0
  75. package/main/store/queues.mjs +31 -0
  76. package/main/store/work-items.mjs +299 -0
  77. package/main/store/write-queue.mjs +14 -0
  78. package/main/store-modules.mjs +23 -0
  79. package/main/store.mjs +1269 -0
  80. package/main/supervisor.mjs +6875 -0
  81. package/main/task-commands.mjs +161 -0
  82. package/main/task-folders-offthread.mjs +84 -0
  83. package/main/task-folders-thread.mjs +13 -0
  84. package/main/task-folders.mjs +409 -0
  85. package/main/task-remote-control.mjs +65 -0
  86. package/main/task-terminals.mjs +169 -0
  87. package/main/terminal-access.mjs +6 -0
  88. package/main/updater.mjs +240 -0
  89. package/main/zoom-keys.mjs +54 -0
  90. package/main/zoom-readout.mjs +20 -0
  91. package/mcp/agentbox-mcp.sh +34 -0
  92. package/mcp/core/account.mjs +77 -0
  93. package/mcp/core/chat.mjs +61 -0
  94. package/mcp/core/documents.mjs +238 -0
  95. package/mcp/core/page.mjs +152 -0
  96. package/mcp/core/products.mjs +110 -0
  97. package/mcp/core/skills.mjs +126 -0
  98. package/mcp/core/work.mjs +377 -0
  99. package/mcp/stdio.mjs +70 -0
  100. package/mcp/tools.mjs +257 -0
  101. package/package.json +186 -0
  102. package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
  103. package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
  104. package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
  105. package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
  106. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  107. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  108. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  109. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  110. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  111. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  112. package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
  113. package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
  114. package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
  115. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  116. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  117. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  118. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  119. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  120. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  121. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  122. package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
  123. package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
  124. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  125. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  126. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  127. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  128. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  129. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  130. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  131. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  132. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  133. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  134. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  135. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  136. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  137. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  138. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  139. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  140. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  141. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  142. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  143. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  144. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  145. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  146. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  147. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  148. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  149. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  150. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  151. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  152. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  153. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  154. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  155. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  156. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  157. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  158. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  159. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  160. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  161. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  162. package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
  163. package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
  164. package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
  165. package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
  166. package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
  167. package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
  168. package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
  169. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  170. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  171. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  172. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  173. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  174. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  175. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  176. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  177. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  178. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  179. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  180. package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
  181. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  182. package/renderer/dist/emulator/data/compression/README.md +11 -0
  183. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  184. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  185. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  186. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  187. package/renderer/dist/emulator/data/cores/README.md +22 -0
  188. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  189. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  190. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  191. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  192. package/renderer/dist/emulator/data/cores/package.json +65 -0
  193. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  194. package/renderer/dist/emulator/data/emulator.css +1614 -0
  195. package/renderer/dist/emulator/data/loader.js +170 -0
  196. package/renderer/dist/emulator/data/localization/README.md +63 -0
  197. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  198. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  199. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  200. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  201. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  202. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  203. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  204. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  205. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  206. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  207. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  208. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  209. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  210. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  211. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  212. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  213. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  214. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  215. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  216. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  217. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  218. package/renderer/dist/emulator/data/src/compression.js +142 -0
  219. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  220. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  221. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  222. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  223. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  224. package/renderer/dist/emulator/data/src/storage.js +125 -0
  225. package/renderer/dist/emulator/data/version.json +5 -0
  226. package/renderer/dist/emulator/host.js +299 -0
  227. package/renderer/dist/emulator/index.html +28 -0
  228. package/renderer/dist/emulator/provenance.json +6 -0
  229. package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
  230. package/renderer/dist/index.html +13 -0
  231. package/scripts/after-install.mjs +59 -0
  232. package/scripts/approval-server.sh +43 -0
  233. package/scripts/before-publish.mjs +133 -0
  234. package/shared/agent-import.d.mts +13 -0
  235. package/shared/agent-import.mjs +252 -0
  236. package/shared/agents.d.mts +190 -0
  237. package/shared/agents.mjs +937 -0
  238. package/shared/analytics-events.mjs +63 -0
  239. package/shared/answers.d.mts +12 -0
  240. package/shared/answers.mjs +80 -0
  241. package/shared/artifact-keys.d.mts +28 -0
  242. package/shared/artifact-keys.mjs +140 -0
  243. package/shared/bridge-map.mjs +128 -0
  244. package/shared/claude-commands.d.mts +20 -0
  245. package/shared/claude-commands.generated.mjs +149 -0
  246. package/shared/claude-commands.mjs +131 -0
  247. package/shared/claude-models.generated.d.mts +15 -0
  248. package/shared/claude-models.generated.mjs +26 -0
  249. package/shared/claude-usage.d.mts +16 -0
  250. package/shared/claude-usage.mjs +172 -0
  251. package/shared/codex-commands.d.mts +4 -0
  252. package/shared/codex-commands.mjs +17 -0
  253. package/shared/codex-import.d.mts +38 -0
  254. package/shared/codex-import.mjs +232 -0
  255. package/shared/codex-modes.mjs +159 -0
  256. package/shared/codex-usage.mjs +180 -0
  257. package/shared/contracts.mjs +412 -0
  258. package/shared/crash-scrub.mjs +182 -0
  259. package/shared/dashboard.mjs +481 -0
  260. package/shared/dead-run-trace.mjs +127 -0
  261. package/shared/demo-world.mjs +415 -0
  262. package/shared/effort-levels.d.mts +15 -0
  263. package/shared/effort-levels.mjs +76 -0
  264. package/shared/engines.d.mts +62 -0
  265. package/shared/engines.mjs +389 -0
  266. package/shared/first-run-examples.mjs +60 -0
  267. package/shared/first-run-line.mjs +149 -0
  268. package/shared/first-run-practice.d.mts +32 -0
  269. package/shared/first-run-practice.mjs +254 -0
  270. package/shared/first-run-shape.mjs +100 -0
  271. package/shared/fresh-user-home.mjs +189 -0
  272. package/shared/hook-failure.mjs +22 -0
  273. package/shared/install-id.mjs +39 -0
  274. package/shared/instruction-defaults.json +5 -0
  275. package/shared/library-folder.mjs +28 -0
  276. package/shared/notify-rules.mjs +120 -0
  277. package/shared/product-name.d.mts +6 -0
  278. package/shared/product-name.mjs +144 -0
  279. package/shared/project-folder-check.d.mts +22 -0
  280. package/shared/project-folder-check.mjs +152 -0
  281. package/shared/provider-commands.d.mts +8 -0
  282. package/shared/provider-commands.mjs +58 -0
  283. package/shared/rank.d.mts +19 -0
  284. package/shared/rank.mjs +146 -0
  285. package/shared/recovery.mjs +26 -0
  286. package/shared/referenced-files.d.mts +11 -0
  287. package/shared/referenced-files.mjs +122 -0
  288. package/shared/repeats.d.mts +45 -0
  289. package/shared/repeats.mjs +193 -0
  290. package/shared/schemes.mjs +33 -0
  291. package/shared/settings-terminal.d.mts +1 -0
  292. package/shared/settings-terminal.mjs +14 -0
  293. package/shared/side-build.mjs +57 -0
  294. package/shared/spawn-trouble.d.mts +8 -0
  295. package/shared/spawn-trouble.mjs +599 -0
  296. package/shared/terminal-state.d.mts +1 -0
  297. package/shared/terminal-state.mjs +58 -0
  298. package/shared/usage.d.mts +53 -0
  299. package/shared/usage.mjs +319 -0
  300. package/shared/work-items.mjs +561 -0
  301. package/shared/work-lines.mjs +360 -0
  302. 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
+ }
@@ -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 };