@soederpop/luca 0.0.2

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 (358) hide show
  1. package/CLAUDE.md +71 -0
  2. package/README.md +78 -0
  3. package/bun.lock +2928 -0
  4. package/bunfig.toml +3 -0
  5. package/commands/audit-docs.ts +740 -0
  6. package/commands/build-scaffolds.ts +154 -0
  7. package/commands/generate-api-docs.ts +114 -0
  8. package/commands/update-introspection.ts +67 -0
  9. package/docs/CLI.md +335 -0
  10. package/docs/README.md +88 -0
  11. package/docs/TABLE-OF-CONTENTS.md +157 -0
  12. package/docs/apis/clients/elevenlabs.md +84 -0
  13. package/docs/apis/clients/graph.md +56 -0
  14. package/docs/apis/clients/openai.md +69 -0
  15. package/docs/apis/clients/rest.md +41 -0
  16. package/docs/apis/clients/websocket.md +107 -0
  17. package/docs/apis/features/agi/assistant.md +471 -0
  18. package/docs/apis/features/agi/assistants-manager.md +154 -0
  19. package/docs/apis/features/agi/claude-code.md +602 -0
  20. package/docs/apis/features/agi/conversation-history.md +352 -0
  21. package/docs/apis/features/agi/conversation.md +333 -0
  22. package/docs/apis/features/agi/docs-reader.md +121 -0
  23. package/docs/apis/features/agi/openai-codex.md +318 -0
  24. package/docs/apis/features/agi/openapi.md +138 -0
  25. package/docs/apis/features/agi/semantic-search.md +387 -0
  26. package/docs/apis/features/agi/skills-library.md +216 -0
  27. package/docs/apis/features/node/container-link.md +133 -0
  28. package/docs/apis/features/node/content-db.md +313 -0
  29. package/docs/apis/features/node/disk-cache.md +379 -0
  30. package/docs/apis/features/node/dns.md +651 -0
  31. package/docs/apis/features/node/docker.md +705 -0
  32. package/docs/apis/features/node/downloader.md +81 -0
  33. package/docs/apis/features/node/esbuild.md +59 -0
  34. package/docs/apis/features/node/file-manager.md +182 -0
  35. package/docs/apis/features/node/fs.md +581 -0
  36. package/docs/apis/features/node/git.md +330 -0
  37. package/docs/apis/features/node/google-auth.md +174 -0
  38. package/docs/apis/features/node/google-calendar.md +187 -0
  39. package/docs/apis/features/node/google-docs.md +151 -0
  40. package/docs/apis/features/node/google-drive.md +225 -0
  41. package/docs/apis/features/node/google-sheets.md +179 -0
  42. package/docs/apis/features/node/grep.md +290 -0
  43. package/docs/apis/features/node/helpers.md +135 -0
  44. package/docs/apis/features/node/ink.md +334 -0
  45. package/docs/apis/features/node/ipc-socket.md +260 -0
  46. package/docs/apis/features/node/json-tree.md +86 -0
  47. package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
  48. package/docs/apis/features/node/networking.md +281 -0
  49. package/docs/apis/features/node/nlp.md +133 -0
  50. package/docs/apis/features/node/opener.md +97 -0
  51. package/docs/apis/features/node/os.md +118 -0
  52. package/docs/apis/features/node/package-finder.md +402 -0
  53. package/docs/apis/features/node/postgres.md +212 -0
  54. package/docs/apis/features/node/proc.md +430 -0
  55. package/docs/apis/features/node/process-manager.md +210 -0
  56. package/docs/apis/features/node/python.md +278 -0
  57. package/docs/apis/features/node/repl.md +88 -0
  58. package/docs/apis/features/node/runpod.md +673 -0
  59. package/docs/apis/features/node/secure-shell.md +169 -0
  60. package/docs/apis/features/node/semantic-search.md +401 -0
  61. package/docs/apis/features/node/sqlite.md +211 -0
  62. package/docs/apis/features/node/telegram.md +254 -0
  63. package/docs/apis/features/node/tts.md +118 -0
  64. package/docs/apis/features/node/ui.md +703 -0
  65. package/docs/apis/features/node/vault.md +64 -0
  66. package/docs/apis/features/node/vm.md +84 -0
  67. package/docs/apis/features/node/window-manager.md +337 -0
  68. package/docs/apis/features/node/yaml-tree.md +85 -0
  69. package/docs/apis/features/node/yaml.md +176 -0
  70. package/docs/apis/features/web/asset-loader.md +47 -0
  71. package/docs/apis/features/web/container-link.md +133 -0
  72. package/docs/apis/features/web/esbuild.md +59 -0
  73. package/docs/apis/features/web/helpers.md +135 -0
  74. package/docs/apis/features/web/network.md +30 -0
  75. package/docs/apis/features/web/speech.md +55 -0
  76. package/docs/apis/features/web/vault.md +64 -0
  77. package/docs/apis/features/web/vm.md +84 -0
  78. package/docs/apis/features/web/voice.md +67 -0
  79. package/docs/apis/servers/express.md +127 -0
  80. package/docs/apis/servers/mcp.md +213 -0
  81. package/docs/apis/servers/websocket.md +99 -0
  82. package/docs/documentation-audit.md +134 -0
  83. package/docs/examples/content-db.md +77 -0
  84. package/docs/examples/disk-cache.md +83 -0
  85. package/docs/examples/docker.md +101 -0
  86. package/docs/examples/downloader.md +70 -0
  87. package/docs/examples/esbuild.md +80 -0
  88. package/docs/examples/file-manager.md +82 -0
  89. package/docs/examples/fs.md +83 -0
  90. package/docs/examples/git.md +85 -0
  91. package/docs/examples/google-auth.md +88 -0
  92. package/docs/examples/google-calendar.md +94 -0
  93. package/docs/examples/google-docs.md +82 -0
  94. package/docs/examples/google-drive.md +96 -0
  95. package/docs/examples/google-sheets.md +95 -0
  96. package/docs/examples/grep.md +85 -0
  97. package/docs/examples/ink-blocks.md +75 -0
  98. package/docs/examples/ink-renderer.md +41 -0
  99. package/docs/examples/ink.md +103 -0
  100. package/docs/examples/ipc-socket.md +103 -0
  101. package/docs/examples/json-tree.md +91 -0
  102. package/docs/examples/launcher-app-command-listener.md +120 -0
  103. package/docs/examples/networking.md +58 -0
  104. package/docs/examples/nlp.md +91 -0
  105. package/docs/examples/opener.md +78 -0
  106. package/docs/examples/os.md +72 -0
  107. package/docs/examples/package-finder.md +89 -0
  108. package/docs/examples/port-exposer.md +89 -0
  109. package/docs/examples/postgres.md +91 -0
  110. package/docs/examples/proc.md +81 -0
  111. package/docs/examples/process-manager.md +79 -0
  112. package/docs/examples/python.md +91 -0
  113. package/docs/examples/repl.md +93 -0
  114. package/docs/examples/runpod.md +119 -0
  115. package/docs/examples/secure-shell.md +92 -0
  116. package/docs/examples/sqlite.md +86 -0
  117. package/docs/examples/telegram.md +77 -0
  118. package/docs/examples/tts.md +86 -0
  119. package/docs/examples/ui.md +80 -0
  120. package/docs/examples/vault.md +70 -0
  121. package/docs/examples/vm.md +86 -0
  122. package/docs/examples/window-manager.md +125 -0
  123. package/docs/examples/yaml-tree.md +93 -0
  124. package/docs/examples/yaml.md +104 -0
  125. package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
  126. package/docs/ideas/container-use-api.md +9 -0
  127. package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
  128. package/docs/ideas/feature-stacks.md +22 -0
  129. package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
  130. package/docs/ideas/mcp-design.md +9 -0
  131. package/docs/ideas/web-container-debugging-feature.md +13 -0
  132. package/docs/introspection-audit.md +49 -0
  133. package/docs/introspection.md +154 -0
  134. package/docs/mcp/readme.md +162 -0
  135. package/docs/models.ts +38 -0
  136. package/docs/philosophy.md +85 -0
  137. package/docs/principles.md +7 -0
  138. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
  139. package/docs/prompts/mcp-test-easy-command.md +27 -0
  140. package/docs/reports/assistant-bugs.md +38 -0
  141. package/docs/reports/attach-pattern-usage.md +18 -0
  142. package/docs/reports/code-audit-results.md +391 -0
  143. package/docs/reports/introspection-audit-tasks.md +378 -0
  144. package/docs/reports/luca-mcp-improvements.md +128 -0
  145. package/docs/scaffolds/client.md +140 -0
  146. package/docs/scaffolds/command.md +106 -0
  147. package/docs/scaffolds/endpoint.md +176 -0
  148. package/docs/scaffolds/feature.md +148 -0
  149. package/docs/scaffolds/server.md +187 -0
  150. package/docs/tasks/web-container-helper-discovery.md +71 -0
  151. package/docs/todos.md +1 -0
  152. package/docs/tutorials/01-getting-started.md +106 -0
  153. package/docs/tutorials/02-container.md +210 -0
  154. package/docs/tutorials/03-scripts.md +194 -0
  155. package/docs/tutorials/04-features-overview.md +196 -0
  156. package/docs/tutorials/05-state-and-events.md +171 -0
  157. package/docs/tutorials/06-servers.md +157 -0
  158. package/docs/tutorials/07-endpoints.md +198 -0
  159. package/docs/tutorials/08-commands.md +171 -0
  160. package/docs/tutorials/09-clients.md +162 -0
  161. package/docs/tutorials/10-creating-features.md +198 -0
  162. package/docs/tutorials/11-contentbase.md +191 -0
  163. package/docs/tutorials/12-assistants.md +215 -0
  164. package/docs/tutorials/13-introspection.md +147 -0
  165. package/docs/tutorials/14-type-system.md +174 -0
  166. package/docs/tutorials/15-project-patterns.md +222 -0
  167. package/docs/tutorials/16-google-features.md +534 -0
  168. package/docs/tutorials/17-tui-blocks.md +530 -0
  169. package/docs/tutorials/18-semantic-search.md +334 -0
  170. package/index.ts +1 -0
  171. package/luca.console.ts +9 -0
  172. package/main.py +6 -0
  173. package/package.json +154 -0
  174. package/pyproject.toml +7 -0
  175. package/scripts/animations/chrome-glitch.ts +55 -0
  176. package/scripts/animations/index.ts +16 -0
  177. package/scripts/animations/neon-pulse.ts +64 -0
  178. package/scripts/animations/types.ts +6 -0
  179. package/scripts/build-web.ts +28 -0
  180. package/scripts/examples/ask-luca-expert.ts +42 -0
  181. package/scripts/examples/assistant-questions.ts +12 -0
  182. package/scripts/examples/excalidraw-expert.ts +75 -0
  183. package/scripts/examples/expert-chat.ts +0 -0
  184. package/scripts/examples/file-manager.ts +14 -0
  185. package/scripts/examples/ideas.ts +12 -0
  186. package/scripts/examples/interactive-chat.ts +20 -0
  187. package/scripts/examples/openai-tool-calls.ts +113 -0
  188. package/scripts/examples/opening-a-web-browser.ts +5 -0
  189. package/scripts/examples/telegram-bot.ts +79 -0
  190. package/scripts/examples/telegram-ink-ui.ts +302 -0
  191. package/scripts/examples/using-assistant-with-mcp.ts +560 -0
  192. package/scripts/examples/using-claude-code.ts +10 -0
  193. package/scripts/examples/using-contentdb.ts +35 -0
  194. package/scripts/examples/using-conversations.ts +35 -0
  195. package/scripts/examples/using-disk-cache.ts +10 -0
  196. package/scripts/examples/using-docker-shell.ts +75 -0
  197. package/scripts/examples/using-elevenlabs.ts +25 -0
  198. package/scripts/examples/using-google-calendar.ts +57 -0
  199. package/scripts/examples/using-google-docs.ts +74 -0
  200. package/scripts/examples/using-google-drive.ts +74 -0
  201. package/scripts/examples/using-google-sheets.ts +89 -0
  202. package/scripts/examples/using-nlp.ts +55 -0
  203. package/scripts/examples/using-ollama.ts +10 -0
  204. package/scripts/examples/using-openai-codex.ts +23 -0
  205. package/scripts/examples/using-postgres.ts +55 -0
  206. package/scripts/examples/using-runpod.ts +32 -0
  207. package/scripts/examples/using-tts.ts +40 -0
  208. package/scripts/examples/vm-loading-esm-modules.ts +16 -0
  209. package/scripts/scaffold.ts +391 -0
  210. package/scripts/scratch.ts +15 -0
  211. package/scripts/test-command-listener.ts +123 -0
  212. package/scripts/test-window-manager-lifecycle.ts +86 -0
  213. package/scripts/test-window-manager.ts +43 -0
  214. package/scripts/update-introspection-data.ts +58 -0
  215. package/src/agi/README.md +14 -0
  216. package/src/agi/container.server.ts +114 -0
  217. package/src/agi/endpoints/ask.ts +60 -0
  218. package/src/agi/endpoints/conversations/[id].ts +45 -0
  219. package/src/agi/endpoints/conversations.ts +31 -0
  220. package/src/agi/endpoints/experts.ts +37 -0
  221. package/src/agi/features/assistant.ts +767 -0
  222. package/src/agi/features/assistants-manager.ts +260 -0
  223. package/src/agi/features/claude-code.ts +1111 -0
  224. package/src/agi/features/conversation-history.ts +497 -0
  225. package/src/agi/features/conversation.ts +799 -0
  226. package/src/agi/features/openai-codex.ts +631 -0
  227. package/src/agi/features/openapi.ts +438 -0
  228. package/src/agi/features/skills-library.ts +425 -0
  229. package/src/agi/index.ts +6 -0
  230. package/src/agi/lib/token-counter.ts +122 -0
  231. package/src/browser.ts +25 -0
  232. package/src/bus.ts +100 -0
  233. package/src/cli/cli.ts +70 -0
  234. package/src/client.ts +461 -0
  235. package/src/clients/civitai/index.ts +541 -0
  236. package/src/clients/client-template.ts +41 -0
  237. package/src/clients/comfyui/index.ts +597 -0
  238. package/src/clients/elevenlabs/index.ts +291 -0
  239. package/src/clients/openai/index.ts +451 -0
  240. package/src/clients/supabase/index.ts +366 -0
  241. package/src/command.ts +164 -0
  242. package/src/commands/chat.ts +182 -0
  243. package/src/commands/console.ts +192 -0
  244. package/src/commands/describe.ts +433 -0
  245. package/src/commands/eval.ts +116 -0
  246. package/src/commands/help.ts +214 -0
  247. package/src/commands/index.ts +14 -0
  248. package/src/commands/mcp.ts +64 -0
  249. package/src/commands/prompt.ts +807 -0
  250. package/src/commands/run.ts +257 -0
  251. package/src/commands/sandbox-mcp.ts +439 -0
  252. package/src/commands/scaffold.ts +79 -0
  253. package/src/commands/serve.ts +172 -0
  254. package/src/container.ts +781 -0
  255. package/src/endpoint.ts +340 -0
  256. package/src/feature.ts +75 -0
  257. package/src/hash-object.ts +97 -0
  258. package/src/helper.ts +543 -0
  259. package/src/introspection/generated.agi.ts +23388 -0
  260. package/src/introspection/generated.node.ts +18899 -0
  261. package/src/introspection/generated.web.ts +2021 -0
  262. package/src/introspection/index.ts +256 -0
  263. package/src/introspection/scan.ts +912 -0
  264. package/src/node/container.ts +354 -0
  265. package/src/node/feature.ts +13 -0
  266. package/src/node/features/container-link.ts +558 -0
  267. package/src/node/features/content-db.ts +475 -0
  268. package/src/node/features/disk-cache.ts +382 -0
  269. package/src/node/features/dns.ts +655 -0
  270. package/src/node/features/docker.ts +912 -0
  271. package/src/node/features/downloader.ts +92 -0
  272. package/src/node/features/esbuild.ts +68 -0
  273. package/src/node/features/file-manager.ts +357 -0
  274. package/src/node/features/fs.ts +534 -0
  275. package/src/node/features/git.ts +492 -0
  276. package/src/node/features/google-auth.ts +502 -0
  277. package/src/node/features/google-calendar.ts +300 -0
  278. package/src/node/features/google-docs.ts +404 -0
  279. package/src/node/features/google-drive.ts +339 -0
  280. package/src/node/features/google-sheets.ts +279 -0
  281. package/src/node/features/grep.ts +406 -0
  282. package/src/node/features/helpers.ts +374 -0
  283. package/src/node/features/ink.ts +490 -0
  284. package/src/node/features/ipc-socket.ts +459 -0
  285. package/src/node/features/json-tree.ts +188 -0
  286. package/src/node/features/launcher-app-command-listener.ts +388 -0
  287. package/src/node/features/networking.ts +925 -0
  288. package/src/node/features/nlp.ts +211 -0
  289. package/src/node/features/opener.ts +166 -0
  290. package/src/node/features/os.ts +157 -0
  291. package/src/node/features/package-finder.ts +539 -0
  292. package/src/node/features/port-exposer.ts +342 -0
  293. package/src/node/features/postgres.ts +273 -0
  294. package/src/node/features/proc.ts +502 -0
  295. package/src/node/features/process-manager.ts +542 -0
  296. package/src/node/features/python.ts +444 -0
  297. package/src/node/features/repl.ts +194 -0
  298. package/src/node/features/runpod.ts +802 -0
  299. package/src/node/features/secure-shell.ts +248 -0
  300. package/src/node/features/semantic-search.ts +924 -0
  301. package/src/node/features/sqlite.ts +289 -0
  302. package/src/node/features/telegram.ts +342 -0
  303. package/src/node/features/tts.ts +184 -0
  304. package/src/node/features/ui.ts +857 -0
  305. package/src/node/features/vault.ts +164 -0
  306. package/src/node/features/vm.ts +312 -0
  307. package/src/node/features/window-manager.ts +804 -0
  308. package/src/node/features/yaml-tree.ts +149 -0
  309. package/src/node/features/yaml.ts +132 -0
  310. package/src/node.ts +70 -0
  311. package/src/react/index.ts +175 -0
  312. package/src/registry.ts +199 -0
  313. package/src/scaffolds/generated.ts +1613 -0
  314. package/src/scaffolds/template.ts +37 -0
  315. package/src/schemas/base.ts +255 -0
  316. package/src/server.ts +135 -0
  317. package/src/servers/express.ts +209 -0
  318. package/src/servers/mcp.ts +805 -0
  319. package/src/servers/socket.ts +120 -0
  320. package/src/state.ts +101 -0
  321. package/src/web/clients/socket.ts +82 -0
  322. package/src/web/container.ts +74 -0
  323. package/src/web/extension.ts +30 -0
  324. package/src/web/feature.ts +12 -0
  325. package/src/web/features/asset-loader.ts +64 -0
  326. package/src/web/features/container-link.ts +385 -0
  327. package/src/web/features/esbuild.ts +79 -0
  328. package/src/web/features/helpers.ts +267 -0
  329. package/src/web/features/network.ts +61 -0
  330. package/src/web/features/speech.ts +87 -0
  331. package/src/web/features/vault.ts +189 -0
  332. package/src/web/features/vm.ts +78 -0
  333. package/src/web/features/voice-recognition.ts +129 -0
  334. package/src/web/shims/isomorphic-vm.ts +149 -0
  335. package/test/bus.test.ts +134 -0
  336. package/test/clients-servers.test.ts +216 -0
  337. package/test/container-link.test.ts +274 -0
  338. package/test/features.test.ts +160 -0
  339. package/test/integration.test.ts +787 -0
  340. package/test/node-container.test.ts +121 -0
  341. package/test/rate-limit.test.ts +272 -0
  342. package/test/semantic-search.test.ts +550 -0
  343. package/test/state.test.ts +121 -0
  344. package/test-integration/assistant.test.ts +138 -0
  345. package/test-integration/assistants-manager.test.ts +123 -0
  346. package/test-integration/claude-code.test.ts +98 -0
  347. package/test-integration/conversation-history.test.ts +205 -0
  348. package/test-integration/conversation.test.ts +137 -0
  349. package/test-integration/elevenlabs.test.ts +55 -0
  350. package/test-integration/google-services.test.ts +80 -0
  351. package/test-integration/helpers.ts +89 -0
  352. package/test-integration/openai-codex.test.ts +93 -0
  353. package/test-integration/runpod.test.ts +58 -0
  354. package/test-integration/server-endpoints.test.ts +97 -0
  355. package/test-integration/skills-library.test.ts +157 -0
  356. package/test-integration/telegram.test.ts +46 -0
  357. package/tsconfig.json +58 -0
  358. package/uv.lock +8 -0
@@ -0,0 +1,740 @@
1
+ import { z } from 'zod'
2
+ import type { ContainerContext } from '@soederpop/luca'
3
+ import { CommandOptionsSchema } from '@soederpop/luca/schemas'
4
+ import * as readline from 'readline'
5
+ import * as fs from 'fs'
6
+ import { spawn } from 'child_process'
7
+ import * as path from 'path'
8
+
9
+ export const argsSchema = CommandOptionsSchema.extend({
10
+ reset: z.boolean().default(false).describe('Reset all audit progress'),
11
+ module: z.string().optional().describe('Jump directly to a specific module by shortcut'),
12
+ })
13
+
14
+ // ── Types ────────────────────────────────────────────────────────────────────
15
+
16
+ type ItemStatus = 'good' | 'needs_work' | 'skipped' | 'pending'
17
+
18
+ type ModuleProgress = {
19
+ status: 'pending' | 'in_progress' | 'completed'
20
+ items: Record<string, ItemStatus>
21
+ lastItem?: string
22
+ }
23
+
24
+ type AuditProgress = {
25
+ modules: Record<string, ModuleProgress>
26
+ lastModule?: string
27
+ }
28
+
29
+ type AuditItem = {
30
+ id: string
31
+ kind: 'class_jsdoc' | 'static_description' | 'method_jsdoc' | 'method_param' | 'getter_jsdoc' | 'option_describe' | 'state_describe' | 'event_describe'
32
+ label: string
33
+ currentValue: string
34
+ line: number
35
+ filePath: string
36
+ }
37
+
38
+ type ModuleInfo = {
39
+ shortcut: string
40
+ registryName: string
41
+ filePath: string
42
+ mtime: number
43
+ }
44
+
45
+ // ── Helpers ──────────────────────────────────────────────────────────────────
46
+
47
+ const COLORS = {
48
+ reset: '\x1b[0m',
49
+ bold: '\x1b[1m',
50
+ dim: '\x1b[2m',
51
+ red: '\x1b[31m',
52
+ green: '\x1b[32m',
53
+ yellow: '\x1b[33m',
54
+ blue: '\x1b[34m',
55
+ magenta: '\x1b[35m',
56
+ cyan: '\x1b[36m',
57
+ white: '\x1b[37m',
58
+ bgRed: '\x1b[41m',
59
+ bgGreen: '\x1b[42m',
60
+ bgYellow: '\x1b[43m',
61
+ }
62
+
63
+ function c(color: keyof typeof COLORS, text: string): string {
64
+ return `${COLORS[color]}${text}${COLORS.reset}`
65
+ }
66
+
67
+ function statusIcon(status: ItemStatus): string {
68
+ switch (status) {
69
+ case 'good': return c('green', '✓')
70
+ case 'needs_work': return c('yellow', '~')
71
+ case 'skipped': return c('dim', '–')
72
+ case 'pending': return c('dim', '○')
73
+ }
74
+ }
75
+
76
+ function moduleStatusIcon(status: ModuleProgress['status']): string {
77
+ switch (status) {
78
+ case 'completed': return c('green', '✓')
79
+ case 'in_progress': return c('yellow', '◐')
80
+ case 'pending': return c('dim', '○')
81
+ }
82
+ }
83
+
84
+ function ask(rl: readline.Interface, question: string): Promise<string> {
85
+ return new Promise((resolve) => {
86
+ rl.question(question, (answer) => resolve(answer.trim().toLowerCase()))
87
+ })
88
+ }
89
+
90
+ function clearScreen() {
91
+ process.stdout.write('\x1b[2J\x1b[H')
92
+ }
93
+
94
+ // ── Source file discovery ────────────────────────────────────────────────────
95
+
96
+ function discoverModules(container: any, fm: any): ModuleInfo[] {
97
+ const registries = [
98
+ { name: 'features', paths: ['src/node/features/*.ts', 'src/web/features/*.ts'] },
99
+ { name: 'clients', paths: ['src/node/clients/*.ts', 'src/web/clients/*.ts', 'src/clients/*.ts'] },
100
+ { name: 'servers', paths: ['src/node/servers/*.ts', 'src/servers/*.ts'] },
101
+ ]
102
+
103
+ const modules: ModuleInfo[] = []
104
+ const seen = new Set<string>()
105
+
106
+ for (const reg of registries) {
107
+ const available: string[] = container[reg.name]?.available ?? []
108
+
109
+ for (const pattern of reg.paths) {
110
+ const files = fm.match(pattern) as string[]
111
+ for (const relPath of files) {
112
+ const absPath = path.resolve(container.cwd, relPath)
113
+ let content: string
114
+ try {
115
+ content = fs.readFileSync(absPath, 'utf-8')
116
+ } catch {
117
+ continue
118
+ }
119
+
120
+ // Find which shortcut this file registers via the register() call
121
+ // Pattern: .register('shortcut', Class) or .register("shortcut", Class)
122
+ for (const shortcut of available) {
123
+ const fullShortcut = `${reg.name}.${shortcut}`
124
+ if (seen.has(fullShortcut)) continue
125
+
126
+ // Look specifically for the register call or the static shortcut declaration
127
+ const registerPattern = new RegExp(
128
+ `\\.register\\(\\s*['"\`]${shortcut}['"\`]` +
129
+ `|shortcut\\s*=\\s*['"\`]${fullShortcut}['"\`]`
130
+ )
131
+
132
+ if (registerPattern.test(content)) {
133
+ let mtime = 0
134
+ try {
135
+ mtime = fs.statSync(absPath).mtimeMs
136
+ } catch {}
137
+
138
+ modules.push({
139
+ shortcut,
140
+ registryName: reg.name,
141
+ filePath: absPath,
142
+ mtime,
143
+ })
144
+ seen.add(fullShortcut)
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ // Sort by mtime descending (most recently edited first)
152
+ modules.sort((a, b) => b.mtime - a.mtime)
153
+ return modules
154
+ }
155
+
156
+ // ── Source file parsing for audit items ───────────────────────────────────────
157
+
158
+ function findClassEnd(lines: string[], classStartLine: number): number {
159
+ // Find matching closing brace for the class. classStartLine is 0-indexed.
160
+ let braceDepth = 0
161
+ let foundOpen = false
162
+ for (let i = classStartLine; i < lines.length; i++) {
163
+ for (const ch of lines[i]!) {
164
+ if (ch === '{') { braceDepth++; foundOpen = true }
165
+ if (ch === '}') braceDepth--
166
+ if (foundOpen && braceDepth === 0) return i
167
+ }
168
+ }
169
+ return lines.length - 1
170
+ }
171
+
172
+ function extractAuditItems(filePath: string, shortcut: string, registryName: string): AuditItem[] {
173
+ const content = fs.readFileSync(filePath, 'utf-8')
174
+ const lines = content.split('\n')
175
+ const items: AuditItem[] = []
176
+ const prefix = `${registryName}:${shortcut}`
177
+ const fullShortcut = `${registryName}.${shortcut}`
178
+
179
+ // Find the class that declares this shortcut
180
+ let classLine = -1 // 0-indexed
181
+ let className = ''
182
+ for (let i = 0; i < lines.length; i++) {
183
+ const match = lines[i]!.match(/^export\s+class\s+(\w+)\s+extends\s+\w*(?:Feature|Client|Server|Helper)/)
184
+ if (match) {
185
+ // Check if this class declares the shortcut we're looking for
186
+ // by scanning its static shortcut property within the next ~15 lines
187
+ for (let j = i + 1; j < Math.min(i + 15, lines.length); j++) {
188
+ if (
189
+ lines[j]!.includes(`"${shortcut}"`) ||
190
+ lines[j]!.includes(`'${shortcut}'`) ||
191
+ lines[j]!.includes(`"${fullShortcut}"`) ||
192
+ lines[j]!.includes(`'${fullShortcut}'`)
193
+ ) {
194
+ classLine = i
195
+ className = match[1]!
196
+ break
197
+ }
198
+ }
199
+ if (classLine >= 0) break
200
+ }
201
+ }
202
+
203
+ if (classLine === -1) return items
204
+
205
+ const classEnd = findClassEnd(lines, classLine)
206
+
207
+ // 1. Class JSDoc
208
+ const classJsdoc = findJSDocAbove(lines, classLine)
209
+ items.push({
210
+ id: `${prefix}:class_jsdoc`,
211
+ kind: 'class_jsdoc',
212
+ label: `${className} class JSDoc`,
213
+ currentValue: classJsdoc.text,
214
+ line: classJsdoc.line > 0 ? classJsdoc.line : classLine + 1,
215
+ filePath,
216
+ })
217
+
218
+ // 2. Static description
219
+ for (let i = classLine; i <= classEnd; i++) {
220
+ const descMatch = lines[i]!.match(/static\s+(?:override\s+)?description\s*[=:]\s*["'`](.*)["'`]/)
221
+ if (descMatch) {
222
+ items.push({
223
+ id: `${prefix}:static_description`,
224
+ kind: 'static_description',
225
+ label: `static description`,
226
+ currentValue: descMatch[1] || '',
227
+ line: i + 1,
228
+ filePath,
229
+ })
230
+ break
231
+ }
232
+ }
233
+
234
+ // 3. Options schema .describe() calls
235
+ findSchemaDescribes(lines, filePath, prefix, 'option', 'Options').forEach(item => items.push(item))
236
+
237
+ // 4. State schema .describe() calls
238
+ findSchemaDescribes(lines, filePath, prefix, 'state', 'State').forEach(item => items.push(item))
239
+
240
+ // 5. Events schema .describe() calls
241
+ findSchemaDescribes(lines, filePath, prefix, 'event', 'Events').forEach(item => items.push(item))
242
+
243
+ // 6. Getters within class bounds
244
+ for (let i = classLine + 1; i <= classEnd; i++) {
245
+ const getterMatch = lines[i]!.match(/^\s+(?:override\s+)?get\s+(\w+)\s*\(/)
246
+ if (getterMatch) {
247
+ const name = getterMatch[1]!
248
+ if (['initialState', 'container', 'options', 'context', 'cacheKey', 'isEnabled', 'shortcut'].includes(name)) continue
249
+ const jsdoc = findJSDocAbove(lines, i)
250
+ items.push({
251
+ id: `${prefix}:getter:${name}`,
252
+ kind: 'getter_jsdoc',
253
+ label: `get ${name}()`,
254
+ currentValue: jsdoc.text,
255
+ line: jsdoc.line > 0 ? jsdoc.line : i + 1,
256
+ filePath,
257
+ })
258
+ }
259
+ }
260
+
261
+ // 7. Methods within class bounds
262
+ for (let i = classLine + 1; i <= classEnd; i++) {
263
+ const line = lines[i]!
264
+ // Match method declarations (with or without async)
265
+ const methodMatch = line.match(/^\s+(?:async\s+)?(\w+)\s*\(/)
266
+ if (methodMatch) {
267
+ const name = methodMatch[1]!
268
+ if (name === 'constructor' || name.startsWith('_') || name === 'afterInitialize' || name === 'enable') continue
269
+ if (line.includes('private ') || line.includes('static ')) continue
270
+ if (line.match(/^\s+(?:override\s+)?(?:get|set)\s+/)) continue
271
+
272
+ const jsdoc = findJSDocAbove(lines, i)
273
+ items.push({
274
+ id: `${prefix}:method:${name}`,
275
+ kind: 'method_jsdoc',
276
+ label: `${name}()`,
277
+ currentValue: jsdoc.text,
278
+ line: jsdoc.line > 0 ? jsdoc.line : i + 1,
279
+ filePath,
280
+ })
281
+
282
+ // Extract method params and their docs
283
+ const paramNames = extractParamNames(lines, i)
284
+ for (const pName of paramNames) {
285
+ const paramDesc = jsdoc.text ? extractParamDescription(jsdoc.text, pName) : ''
286
+ items.push({
287
+ id: `${prefix}:method:${name}:param:${pName}`,
288
+ kind: 'method_param',
289
+ label: ` @param ${pName}`,
290
+ currentValue: paramDesc,
291
+ line: jsdoc.line > 0 ? jsdoc.line : i + 1,
292
+ filePath,
293
+ })
294
+ }
295
+ }
296
+ }
297
+
298
+ return items
299
+ }
300
+
301
+ function findJSDocAbove(lines: string[], lineIdx: number): { text: string; line: number } {
302
+ // Walk backwards from lineIdx looking for a JSDoc block ending with */
303
+ let i = lineIdx - 1
304
+ // Skip blank lines
305
+ while (i >= 0 && lines[i]!.trim() === '') i--
306
+
307
+ if (i < 0 || !lines[i]!.trim().endsWith('*/')) {
308
+ return { text: '', line: 0 }
309
+ }
310
+
311
+ // Found end of JSDoc, walk backwards to find /**
312
+ const endLine = i
313
+ while (i >= 0 && !lines[i]!.includes('/**')) i--
314
+
315
+ if (i < 0) return { text: '', line: 0 }
316
+
317
+ const startLine = i
318
+ const jsdocLines = lines.slice(startLine, endLine + 1)
319
+ const text = jsdocLines.join('\n')
320
+ return { text, line: startLine + 1 } // 1-indexed
321
+ }
322
+
323
+ function findSchemaDescribes(
324
+ lines: string[],
325
+ filePath: string,
326
+ prefix: string,
327
+ kind: 'option' | 'state' | 'event',
328
+ schemaKeyword: string,
329
+ ): AuditItem[] {
330
+ const items: AuditItem[] = []
331
+ const auditKind = `${kind}_describe` as AuditItem['kind']
332
+
333
+ // Look for schema definitions like FooOptionsSchema = ... .extend({
334
+ // and then find individual .describe() calls within
335
+ for (let i = 0; i < lines.length; i++) {
336
+ const line = lines[i]!
337
+ if (!line.includes(schemaKeyword) || !line.includes('Schema')) continue
338
+ if (!line.includes('extend') && !line.includes('object')) continue
339
+
340
+ // Scan forward within the schema block for property .describe() calls
341
+ let braceDepth = 0
342
+ let inSchema = false
343
+ for (let j = i; j < lines.length; j++) {
344
+ const sline = lines[j]!
345
+ for (const ch of sline) {
346
+ if (ch === '{') { braceDepth++; inSchema = true }
347
+ if (ch === '}') braceDepth--
348
+ }
349
+
350
+ // Look for: propertyName: z.something().describe('...')
351
+ const propMatch = sline.match(/^\s+(\w+)\s*:\s*z\./)
352
+ if (propMatch && inSchema) {
353
+ const propName = propMatch[1]!
354
+ const describeMatch = sline.match(/\.describe\(\s*['"`](.*)['"`]\s*\)/)
355
+ const descValue = describeMatch ? describeMatch[1] || '' : ''
356
+ items.push({
357
+ id: `${prefix}:${kind}:${propName}`,
358
+ kind: auditKind,
359
+ label: `${kind} schema: ${propName}`,
360
+ currentValue: descValue ? `.describe('${descValue}')` : '',
361
+ line: j + 1,
362
+ filePath,
363
+ })
364
+ }
365
+
366
+ if (inSchema && braceDepth === 0) break
367
+ }
368
+ break // only process first matching schema
369
+ }
370
+
371
+ return items
372
+ }
373
+
374
+ function extractParamNames(lines: string[], methodLine: number): string[] {
375
+ // Read the method signature (possibly multi-line) and extract parameter names
376
+ const params: string[] = []
377
+ let parenDepth = 0
378
+ let started = false
379
+
380
+ for (let i = methodLine; i < Math.min(methodLine + 15, lines.length); i++) {
381
+ const line = lines[i]!
382
+ for (let j = 0; j < line.length; j++) {
383
+ if (line[j] === '(') { parenDepth++; started = true }
384
+ if (line[j] === ')') parenDepth--
385
+ if (started && parenDepth === 0) {
386
+ // Extract param names from the collected signature
387
+ const sigStart = lines[methodLine]!.indexOf('(')
388
+ let sig = ''
389
+ for (let k = methodLine; k <= i; k++) {
390
+ sig += lines[k]! + '\n'
391
+ }
392
+ const insideParens = sig.substring(sig.indexOf('(') + 1, sig.lastIndexOf(')'))
393
+ // Split by commas (respecting nested generics/objects)
394
+ const paramStrings = splitParams(insideParens)
395
+ for (const ps of paramStrings) {
396
+ const nameMatch = ps.trim().match(/^(\w+)/)
397
+ if (nameMatch && nameMatch[1]) {
398
+ params.push(nameMatch[1])
399
+ }
400
+ }
401
+ return params
402
+ }
403
+ }
404
+ }
405
+ return params
406
+ }
407
+
408
+ function splitParams(sig: string): string[] {
409
+ const results: string[] = []
410
+ let depth = 0
411
+ let current = ''
412
+ for (const ch of sig) {
413
+ if (ch === '<' || ch === '{' || ch === '(') depth++
414
+ if (ch === '>' || ch === '}' || ch === ')') depth--
415
+ if (ch === ',' && depth === 0) {
416
+ results.push(current)
417
+ current = ''
418
+ } else {
419
+ current += ch
420
+ }
421
+ }
422
+ if (current.trim()) results.push(current)
423
+ return results
424
+ }
425
+
426
+ function extractParamDescription(jsdoc: string, paramName: string): string {
427
+ const regex = new RegExp(`@param\\s+(?:\\{[^}]*\\}\\s+)?\\[?${paramName}[^\\n]*`, 'g')
428
+ const match = jsdoc.match(regex)
429
+ if (!match) return ''
430
+ return match[0]!
431
+ }
432
+
433
+ // ── Display ──────────────────────────────────────────────────────────────────
434
+
435
+ function printHeader(title: string) {
436
+ const line = '─'.repeat(60)
437
+ console.log(`\n${c('cyan', line)}`)
438
+ console.log(c('bold', ` ${title}`))
439
+ console.log(`${c('cyan', line)}\n`)
440
+ }
441
+
442
+ function printModuleList(modules: ModuleInfo[], progress: AuditProgress) {
443
+ printHeader('Audit Modules (sorted by last edit)')
444
+
445
+ for (let i = 0; i < modules.length; i++) {
446
+ const mod = modules[i]!
447
+ const key = `${mod.registryName}.${mod.shortcut}`
448
+ const mp = progress.modules[key]
449
+ const icon = mp ? moduleStatusIcon(mp.status) : c('dim', '○')
450
+ const date = new Date(mod.mtime).toLocaleDateString()
451
+
452
+ let itemCounts = ''
453
+ if (mp) {
454
+ const total = Object.keys(mp.items).length
455
+ const good = Object.values(mp.items).filter(s => s === 'good').length
456
+ const needs = Object.values(mp.items).filter(s => s === 'needs_work').length
457
+ if (total > 0) {
458
+ itemCounts = c('dim', ` (${good}/${total} good${needs > 0 ? `, ${needs} needs work` : ''})`)
459
+ }
460
+ }
461
+
462
+ console.log(` ${icon} ${c('bold', `${i + 1}.`)} ${c('white', key)} ${c('dim', date)}${itemCounts}`)
463
+ }
464
+
465
+ console.log()
466
+ }
467
+
468
+ function printAuditItem(item: AuditItem, status: ItemStatus, index: number, total: number) {
469
+ const relPath = item.filePath.replace(process.cwd() + '/', '')
470
+ console.log(c('dim', ` ${relPath}:${item.line}`))
471
+ console.log(c('bold', ` [${index + 1}/${total}] ${item.label}`))
472
+ console.log()
473
+
474
+ if (!item.currentValue) {
475
+ console.log(` ${c('red', 'MISSING')} - No documentation found`)
476
+ } else {
477
+ // Display the current value, indented
478
+ const displayLines = item.currentValue.split('\n')
479
+ for (const dl of displayLines) {
480
+ console.log(` ${c('dim', '│')} ${dl}`)
481
+ }
482
+ }
483
+ console.log()
484
+ }
485
+
486
+ // ── Main ─────────────────────────────────────────────────────────────────────
487
+
488
+ async function auditDocs(options: z.infer<typeof argsSchema>, context: ContainerContext) {
489
+ const { container } = context as any
490
+
491
+ const cache = container.feature('diskCache')
492
+ const fm = container.feature('fileManager')
493
+ await fm.start({ exclude: ['node_modules', 'dist', '.cache', '.git'] })
494
+
495
+ // Reset if requested
496
+ if (options.reset) {
497
+ await cache.rm('audit-docs:progress')
498
+ console.log(c('green', 'Audit progress has been reset.'))
499
+ }
500
+
501
+ // Load progress
502
+ let progress: AuditProgress
503
+ if (await cache.has('audit-docs:progress')) {
504
+ progress = await cache.get('audit-docs:progress', true) as AuditProgress
505
+ } else {
506
+ progress = { modules: {} }
507
+ }
508
+
509
+ // Discover all modules
510
+ const modules = discoverModules(container, fm)
511
+
512
+ if (modules.length === 0) {
513
+ console.log(c('red', 'No helper modules found.'))
514
+ return
515
+ }
516
+
517
+ const rl = readline.createInterface({
518
+ input: process.stdin,
519
+ output: process.stdout,
520
+ })
521
+
522
+ const saveProgress = async () => {
523
+ await cache.set('audit-docs:progress', JSON.stringify(progress))
524
+ }
525
+
526
+ try {
527
+ // If --module flag given, jump straight to it
528
+ if (options.module) {
529
+ const mod = modules.find(m =>
530
+ m.shortcut === options.module ||
531
+ `${m.registryName}.${m.shortcut}` === options.module
532
+ )
533
+ if (!mod) {
534
+ console.log(c('red', `Module "${options.module}" not found.`))
535
+ console.log('Available:', modules.map(m => `${m.registryName}.${m.shortcut}`).join(', '))
536
+ return
537
+ }
538
+ await auditModule(mod, progress, rl, saveProgress)
539
+ return
540
+ }
541
+
542
+ // Main menu loop
543
+ while (true) {
544
+ clearScreen()
545
+ printModuleList(modules, progress)
546
+
547
+ const completedCount = Object.values(progress.modules).filter(m => m.status === 'completed').length
548
+ console.log(c('dim', ` Progress: ${completedCount}/${modules.length} modules completed`))
549
+ console.log()
550
+ console.log(c('dim', ' Enter a number to audit, ') + c('cyan', 'r') + c('dim', ' to resume last, ') + c('cyan', 'q') + c('dim', ' to quit'))
551
+
552
+ const answer = await ask(rl, ' > ')
553
+
554
+ if (answer === 'q' || answer === 'quit') {
555
+ break
556
+ }
557
+
558
+ if (answer === 'r' || answer === 'resume') {
559
+ // Resume last in-progress module
560
+ const lastKey = progress.lastModule
561
+ if (lastKey) {
562
+ const mod = modules.find(m => `${m.registryName}.${m.shortcut}` === lastKey)
563
+ if (mod) {
564
+ await auditModule(mod, progress, rl, saveProgress)
565
+ continue
566
+ }
567
+ }
568
+ console.log(c('yellow', ' No module to resume.'))
569
+ await ask(rl, ' Press enter to continue...')
570
+ continue
571
+ }
572
+
573
+ const num = parseInt(answer, 10)
574
+ if (num >= 1 && num <= modules.length) {
575
+ await auditModule(modules[num - 1]!, progress, rl, saveProgress)
576
+ continue
577
+ }
578
+
579
+ console.log(c('red', ' Invalid input.'))
580
+ await ask(rl, ' Press enter to continue...')
581
+ }
582
+ } finally {
583
+ rl.close()
584
+ await saveProgress()
585
+ console.log(c('dim', '\nProgress saved.\n'))
586
+ }
587
+ }
588
+
589
+ async function auditModule(
590
+ mod: ModuleInfo,
591
+ progress: AuditProgress,
592
+ rl: readline.Interface,
593
+ saveProgress: () => Promise<void>,
594
+ ) {
595
+ const key = `${mod.registryName}.${mod.shortcut}`
596
+ progress.lastModule = key
597
+
598
+ // Initialize module progress if needed
599
+ if (!progress.modules[key]) {
600
+ progress.modules[key] = { status: 'pending', items: {} }
601
+ }
602
+ const mp = progress.modules[key]!
603
+ mp.status = 'in_progress'
604
+ await saveProgress()
605
+
606
+ // Extract audit items from source
607
+ let items = extractAuditItems(mod.filePath, mod.shortcut, mod.registryName)
608
+
609
+ if (items.length === 0) {
610
+ console.log(c('yellow', `\n No auditable items found in ${mod.filePath}`))
611
+ mp.status = 'completed'
612
+ await saveProgress()
613
+ await ask(rl, ' Press enter to continue...')
614
+ return
615
+ }
616
+
617
+ // Initialize item statuses
618
+ for (const item of items) {
619
+ if (!mp.items[item.id]) {
620
+ mp.items[item.id] = 'pending'
621
+ }
622
+ }
623
+
624
+ // Find where to resume (first non-good item, or where we left off)
625
+ let startIdx = 0
626
+ if (mp.lastItem) {
627
+ const lastIdx = items.findIndex(it => it.id === mp.lastItem)
628
+ if (lastIdx >= 0) startIdx = lastIdx
629
+ }
630
+
631
+ for (let i = startIdx; i < items.length; i++) {
632
+ const item = items[i]!
633
+ const status = mp.items[item.id] || 'pending'
634
+
635
+ // Skip already-approved items
636
+ if (status === 'good') continue
637
+
638
+ clearScreen()
639
+ printHeader(`${key}`)
640
+ printAuditItem(item, status, i, items.length)
641
+
642
+ // Show quick status bar
643
+ const good = Object.values(mp.items).filter(s => s === 'good').length
644
+ const total = Object.keys(mp.items).length
645
+ console.log(c('dim', ` ${good}/${total} items approved`))
646
+ console.log()
647
+ console.log(` ${c('green', 'y')}=good ${c('yellow', 'e')}=edit in cursor ${c('dim', 's')}=skip ${c('cyan', 'n')}=next module ${c('red', 'q')}=quit`)
648
+
649
+ const answer = await ask(rl, ' > ')
650
+
651
+ if (answer === 'q' || answer === 'quit') {
652
+ mp.lastItem = item.id
653
+ await saveProgress()
654
+ return
655
+ }
656
+
657
+ if (answer === 'n' || answer === 'next') {
658
+ mp.lastItem = item.id
659
+ await saveProgress()
660
+ return
661
+ }
662
+
663
+ if (answer === 'y' || answer === 'yes' || answer === 'g' || answer === 'good') {
664
+ mp.items[item.id] = 'good'
665
+ mp.lastItem = item.id
666
+ await saveProgress()
667
+ continue
668
+ }
669
+
670
+ if (answer === 's' || answer === 'skip') {
671
+ mp.items[item.id] = 'skipped'
672
+ mp.lastItem = item.id
673
+ await saveProgress()
674
+ continue
675
+ }
676
+
677
+ if (answer === 'e' || answer === 'edit' || answer === '') {
678
+ mp.items[item.id] = 'needs_work'
679
+
680
+ // Open Cursor editor at the exact line and wait for file close
681
+ await new Promise<void>((resolve) => {
682
+ const child = spawn('cursor', ['--wait', '--goto', `${item.filePath}:${item.line}`], {
683
+ stdio: 'inherit',
684
+ })
685
+ child.on('close', () => resolve())
686
+ child.on('error', (err) => {
687
+ console.log(c('red', ` Failed to open cursor: ${err.message}`))
688
+ console.log(c('dim', ' Make sure "cursor" is in your PATH (Shell Command: Install from Cursor)'))
689
+ resolve()
690
+ })
691
+ })
692
+
693
+ // After vim exits, re-extract this item to show updated value
694
+ items = extractAuditItems(mod.filePath, mod.shortcut, mod.registryName)
695
+ const refreshed = items.find(it => it.id === item.id)
696
+
697
+ if (refreshed) {
698
+ clearScreen()
699
+ printHeader(`${key} (refreshed)`)
700
+ printAuditItem(refreshed, 'needs_work', i, items.length)
701
+ console.log(` ${c('green', 'y')}=good now ${c('yellow', 'e')}=edit again ${c('dim', 's')}=skip`)
702
+
703
+ const answer2 = await ask(rl, ' > ')
704
+ if (answer2 === 'y' || answer2 === 'yes' || answer2 === 'good') {
705
+ mp.items[item.id] = 'good'
706
+ } else if (answer2 === 'e' || answer2 === 'edit') {
707
+ // Re-edit: go back one step
708
+ i--
709
+ } else if (answer2 === 'q') {
710
+ mp.lastItem = item.id
711
+ await saveProgress()
712
+ return
713
+ }
714
+ }
715
+
716
+ mp.lastItem = item.id
717
+ await saveProgress()
718
+ continue
719
+ }
720
+
721
+ // Default: treat as skip
722
+ mp.lastItem = item.id
723
+ await saveProgress()
724
+ }
725
+
726
+ // Check if all items are handled
727
+ const allDone = Object.values(mp.items).every(s => s === 'good' || s === 'skipped')
728
+ if (allDone) {
729
+ mp.status = 'completed'
730
+ console.log(c('green', `\n ✓ Module ${key} audit complete!`))
731
+ }
732
+ await saveProgress()
733
+ await ask(rl, ' Press enter to continue...')
734
+ }
735
+
736
+ export default {
737
+ description: 'Begin or resume an audit of the various helper documentation and descriptions.',
738
+ argsSchema,
739
+ handler: auditDocs,
740
+ }