@nghyane/arcane 0.1.0

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 (738) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/README.md +12 -0
  3. package/examples/README.md +21 -0
  4. package/examples/custom-tools/README.md +109 -0
  5. package/examples/custom-tools/hello/index.ts +20 -0
  6. package/examples/custom-tools/todo/index.ts +206 -0
  7. package/examples/extensions/README.md +143 -0
  8. package/examples/extensions/api-demo.ts +89 -0
  9. package/examples/extensions/chalk-logger.ts +25 -0
  10. package/examples/extensions/hello.ts +32 -0
  11. package/examples/extensions/pirate.ts +43 -0
  12. package/examples/extensions/plan-mode.ts +550 -0
  13. package/examples/extensions/reload-runtime.ts +37 -0
  14. package/examples/extensions/todo.ts +296 -0
  15. package/examples/extensions/tools.ts +144 -0
  16. package/examples/extensions/with-deps/index.ts +35 -0
  17. package/examples/extensions/with-deps/package-lock.json +31 -0
  18. package/examples/extensions/with-deps/package.json +16 -0
  19. package/examples/hooks/README.md +56 -0
  20. package/examples/hooks/auto-commit-on-exit.ts +48 -0
  21. package/examples/hooks/confirm-destructive.ts +58 -0
  22. package/examples/hooks/custom-compaction.ts +116 -0
  23. package/examples/hooks/dirty-repo-guard.ts +51 -0
  24. package/examples/hooks/file-trigger.ts +40 -0
  25. package/examples/hooks/git-checkpoint.ts +52 -0
  26. package/examples/hooks/handoff.ts +150 -0
  27. package/examples/hooks/permission-gate.ts +33 -0
  28. package/examples/hooks/protected-paths.ts +29 -0
  29. package/examples/hooks/qna.ts +119 -0
  30. package/examples/hooks/status-line.ts +39 -0
  31. package/examples/sdk/01-minimal.ts +21 -0
  32. package/examples/sdk/02-custom-model.ts +49 -0
  33. package/examples/sdk/03-custom-prompt.ts +43 -0
  34. package/examples/sdk/04-skills.ts +43 -0
  35. package/examples/sdk/06-extensions.ts +80 -0
  36. package/examples/sdk/06-hooks.ts +61 -0
  37. package/examples/sdk/07-context-files.ts +35 -0
  38. package/examples/sdk/08-prompt-templates.ts +36 -0
  39. package/examples/sdk/08-slash-commands.ts +41 -0
  40. package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
  41. package/examples/sdk/11-sessions.ts +47 -0
  42. package/examples/sdk/README.md +150 -0
  43. package/package.json +464 -0
  44. package/scripts/format-prompts.ts +184 -0
  45. package/scripts/generate-docs-index.ts +40 -0
  46. package/scripts/generate-template.ts +32 -0
  47. package/src/bun-imports.d.ts +22 -0
  48. package/src/capability/context-file.ts +39 -0
  49. package/src/capability/extension-module.ts +33 -0
  50. package/src/capability/extension.ts +47 -0
  51. package/src/capability/fs.ts +89 -0
  52. package/src/capability/hook.ts +39 -0
  53. package/src/capability/index.ts +432 -0
  54. package/src/capability/instruction.ts +36 -0
  55. package/src/capability/mcp.ts +60 -0
  56. package/src/capability/prompt.ts +34 -0
  57. package/src/capability/rule.ts +223 -0
  58. package/src/capability/settings.ts +34 -0
  59. package/src/capability/skill.ts +48 -0
  60. package/src/capability/slash-command.ts +39 -0
  61. package/src/capability/ssh.ts +41 -0
  62. package/src/capability/system-prompt.ts +34 -0
  63. package/src/capability/tool.ts +37 -0
  64. package/src/capability/types.ts +156 -0
  65. package/src/cli/args.ts +259 -0
  66. package/src/cli/config-cli.ts +357 -0
  67. package/src/cli/file-processor.ts +124 -0
  68. package/src/cli/grep-cli.ts +152 -0
  69. package/src/cli/jupyter-cli.ts +106 -0
  70. package/src/cli/list-models.ts +103 -0
  71. package/src/cli/plugin-cli.ts +661 -0
  72. package/src/cli/session-picker.ts +42 -0
  73. package/src/cli/setup-cli.ts +376 -0
  74. package/src/cli/shell-cli.ts +174 -0
  75. package/src/cli/ssh-cli.ts +179 -0
  76. package/src/cli/stats-cli.ts +197 -0
  77. package/src/cli/update-cli.ts +286 -0
  78. package/src/cli/web-search-cli.ts +143 -0
  79. package/src/cli.ts +65 -0
  80. package/src/commands/commit.ts +36 -0
  81. package/src/commands/config.ts +51 -0
  82. package/src/commands/grep.ts +41 -0
  83. package/src/commands/jupyter.ts +32 -0
  84. package/src/commands/launch.ts +139 -0
  85. package/src/commands/plugin.ts +70 -0
  86. package/src/commands/setup.ts +42 -0
  87. package/src/commands/shell.ts +29 -0
  88. package/src/commands/ssh.ts +60 -0
  89. package/src/commands/stats.ts +29 -0
  90. package/src/commands/update.ts +21 -0
  91. package/src/commands/web-search.ts +42 -0
  92. package/src/commit/agentic/agent.ts +311 -0
  93. package/src/commit/agentic/fallback.ts +96 -0
  94. package/src/commit/agentic/index.ts +359 -0
  95. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  96. package/src/commit/agentic/prompts/session-user.md +25 -0
  97. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  98. package/src/commit/agentic/prompts/system.md +38 -0
  99. package/src/commit/agentic/state.ts +69 -0
  100. package/src/commit/agentic/tools/analyze-file.ts +118 -0
  101. package/src/commit/agentic/tools/git-file-diff.ts +194 -0
  102. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  103. package/src/commit/agentic/tools/git-overview.ts +84 -0
  104. package/src/commit/agentic/tools/index.ts +56 -0
  105. package/src/commit/agentic/tools/propose-changelog.ts +128 -0
  106. package/src/commit/agentic/tools/propose-commit.ts +154 -0
  107. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  108. package/src/commit/agentic/tools/split-commit.ts +280 -0
  109. package/src/commit/agentic/topo-sort.ts +44 -0
  110. package/src/commit/agentic/trivial.ts +51 -0
  111. package/src/commit/agentic/validation.ts +200 -0
  112. package/src/commit/analysis/conventional.ts +165 -0
  113. package/src/commit/analysis/index.ts +4 -0
  114. package/src/commit/analysis/scope.ts +242 -0
  115. package/src/commit/analysis/summary.ts +112 -0
  116. package/src/commit/analysis/validation.ts +66 -0
  117. package/src/commit/changelog/detect.ts +37 -0
  118. package/src/commit/changelog/generate.ts +110 -0
  119. package/src/commit/changelog/index.ts +234 -0
  120. package/src/commit/changelog/parse.ts +44 -0
  121. package/src/commit/cli.ts +93 -0
  122. package/src/commit/git/diff.ts +148 -0
  123. package/src/commit/git/errors.ts +9 -0
  124. package/src/commit/git/index.ts +211 -0
  125. package/src/commit/git/operations.ts +54 -0
  126. package/src/commit/index.ts +5 -0
  127. package/src/commit/map-reduce/index.ts +64 -0
  128. package/src/commit/map-reduce/map-phase.ts +178 -0
  129. package/src/commit/map-reduce/reduce-phase.ts +145 -0
  130. package/src/commit/map-reduce/utils.ts +9 -0
  131. package/src/commit/message.ts +11 -0
  132. package/src/commit/model-selection.ts +69 -0
  133. package/src/commit/pipeline.ts +243 -0
  134. package/src/commit/prompts/analysis-system.md +148 -0
  135. package/src/commit/prompts/analysis-user.md +38 -0
  136. package/src/commit/prompts/changelog-system.md +50 -0
  137. package/src/commit/prompts/changelog-user.md +18 -0
  138. package/src/commit/prompts/file-observer-system.md +24 -0
  139. package/src/commit/prompts/file-observer-user.md +8 -0
  140. package/src/commit/prompts/reduce-system.md +50 -0
  141. package/src/commit/prompts/reduce-user.md +17 -0
  142. package/src/commit/prompts/summary-retry.md +3 -0
  143. package/src/commit/prompts/summary-system.md +38 -0
  144. package/src/commit/prompts/summary-user.md +13 -0
  145. package/src/commit/prompts/types-description.md +2 -0
  146. package/src/commit/types.ts +109 -0
  147. package/src/commit/utils/exclusions.ts +42 -0
  148. package/src/config/file-lock.ts +121 -0
  149. package/src/config/keybindings.ts +280 -0
  150. package/src/config/model-registry.ts +1140 -0
  151. package/src/config/model-resolver.ts +812 -0
  152. package/src/config/prompt-templates.ts +526 -0
  153. package/src/config/resolve-config-value.ts +92 -0
  154. package/src/config/settings-schema.ts +1236 -0
  155. package/src/config/settings.ts +706 -0
  156. package/src/config.ts +414 -0
  157. package/src/cursor.ts +239 -0
  158. package/src/debug/index.ts +431 -0
  159. package/src/debug/log-formatting.ts +60 -0
  160. package/src/debug/log-viewer.ts +903 -0
  161. package/src/debug/profiler.ts +158 -0
  162. package/src/debug/report-bundle.ts +366 -0
  163. package/src/debug/system-info.ts +112 -0
  164. package/src/discovery/agents-md.ts +68 -0
  165. package/src/discovery/agents.ts +199 -0
  166. package/src/discovery/builtin.ts +815 -0
  167. package/src/discovery/claude-plugins.ts +205 -0
  168. package/src/discovery/claude.ts +506 -0
  169. package/src/discovery/cline.ts +83 -0
  170. package/src/discovery/codex.ts +532 -0
  171. package/src/discovery/cursor.ts +218 -0
  172. package/src/discovery/gemini.ts +395 -0
  173. package/src/discovery/github.ts +117 -0
  174. package/src/discovery/helpers.ts +698 -0
  175. package/src/discovery/index.ts +89 -0
  176. package/src/discovery/mcp-json.ts +156 -0
  177. package/src/discovery/opencode.ts +394 -0
  178. package/src/discovery/ssh.ts +160 -0
  179. package/src/discovery/vscode.ts +103 -0
  180. package/src/discovery/windsurf.ts +145 -0
  181. package/src/exa/company.ts +57 -0
  182. package/src/exa/index.ts +62 -0
  183. package/src/exa/linkedin.ts +57 -0
  184. package/src/exa/mcp-client.ts +289 -0
  185. package/src/exa/render.ts +244 -0
  186. package/src/exa/researcher.ts +89 -0
  187. package/src/exa/search.ts +330 -0
  188. package/src/exa/types.ts +166 -0
  189. package/src/exa/websets.ts +247 -0
  190. package/src/exec/bash-executor.ts +184 -0
  191. package/src/exec/exec.ts +53 -0
  192. package/src/export/custom-share.ts +65 -0
  193. package/src/export/html/index.ts +162 -0
  194. package/src/export/html/template.css +889 -0
  195. package/src/export/html/template.generated.ts +2 -0
  196. package/src/export/html/template.html +45 -0
  197. package/src/export/html/template.js +1329 -0
  198. package/src/export/html/template.macro.ts +24 -0
  199. package/src/export/html/vendor/highlight.min.js +1213 -0
  200. package/src/export/html/vendor/marked.min.js +6 -0
  201. package/src/export/ttsr.ts +434 -0
  202. package/src/extensibility/custom-commands/bundled/review/index.ts +433 -0
  203. package/src/extensibility/custom-commands/index.ts +15 -0
  204. package/src/extensibility/custom-commands/loader.ts +231 -0
  205. package/src/extensibility/custom-commands/types.ts +111 -0
  206. package/src/extensibility/custom-tools/index.ts +22 -0
  207. package/src/extensibility/custom-tools/loader.ts +235 -0
  208. package/src/extensibility/custom-tools/types.ts +226 -0
  209. package/src/extensibility/custom-tools/wrapper.ts +45 -0
  210. package/src/extensibility/extensions/index.ts +136 -0
  211. package/src/extensibility/extensions/loader.ts +520 -0
  212. package/src/extensibility/extensions/runner.ts +774 -0
  213. package/src/extensibility/extensions/types.ts +1293 -0
  214. package/src/extensibility/extensions/wrapper.ts +188 -0
  215. package/src/extensibility/hooks/index.ts +16 -0
  216. package/src/extensibility/hooks/loader.ts +273 -0
  217. package/src/extensibility/hooks/runner.ts +441 -0
  218. package/src/extensibility/hooks/tool-wrapper.ts +106 -0
  219. package/src/extensibility/hooks/types.ts +817 -0
  220. package/src/extensibility/plugins/doctor.ts +65 -0
  221. package/src/extensibility/plugins/git-url.ts +281 -0
  222. package/src/extensibility/plugins/index.ts +33 -0
  223. package/src/extensibility/plugins/installer.ts +192 -0
  224. package/src/extensibility/plugins/loader.ts +338 -0
  225. package/src/extensibility/plugins/manager.ts +716 -0
  226. package/src/extensibility/plugins/parser.ts +105 -0
  227. package/src/extensibility/plugins/types.ts +190 -0
  228. package/src/extensibility/skills.ts +385 -0
  229. package/src/extensibility/slash-commands.ts +287 -0
  230. package/src/extensibility/tool-proxy.ts +25 -0
  231. package/src/index.ts +275 -0
  232. package/src/internal-urls/agent-protocol.ts +136 -0
  233. package/src/internal-urls/artifact-protocol.ts +97 -0
  234. package/src/internal-urls/docs-index.generated.ts +54 -0
  235. package/src/internal-urls/docs-protocol.ts +84 -0
  236. package/src/internal-urls/index.ts +31 -0
  237. package/src/internal-urls/json-query.ts +126 -0
  238. package/src/internal-urls/memory-protocol.ts +133 -0
  239. package/src/internal-urls/router.ts +70 -0
  240. package/src/internal-urls/rule-protocol.ts +55 -0
  241. package/src/internal-urls/skill-protocol.ts +111 -0
  242. package/src/internal-urls/types.ts +52 -0
  243. package/src/ipy/executor.ts +556 -0
  244. package/src/ipy/gateway-coordinator.ts +426 -0
  245. package/src/ipy/kernel.ts +892 -0
  246. package/src/ipy/modules.ts +109 -0
  247. package/src/ipy/prelude.py +831 -0
  248. package/src/ipy/prelude.ts +3 -0
  249. package/src/ipy/runtime.ts +222 -0
  250. package/src/lsp/client.ts +867 -0
  251. package/src/lsp/clients/biome-client.ts +202 -0
  252. package/src/lsp/clients/index.ts +50 -0
  253. package/src/lsp/clients/lsp-linter-client.ts +93 -0
  254. package/src/lsp/clients/swiftlint-client.ts +120 -0
  255. package/src/lsp/config.ts +397 -0
  256. package/src/lsp/defaults.json +464 -0
  257. package/src/lsp/edits.ts +109 -0
  258. package/src/lsp/index.ts +1268 -0
  259. package/src/lsp/lspmux.ts +250 -0
  260. package/src/lsp/render.ts +689 -0
  261. package/src/lsp/types.ts +414 -0
  262. package/src/lsp/utils.ts +549 -0
  263. package/src/main.ts +773 -0
  264. package/src/mcp/client.ts +239 -0
  265. package/src/mcp/config-writer.ts +215 -0
  266. package/src/mcp/config.ts +363 -0
  267. package/src/mcp/index.ts +55 -0
  268. package/src/mcp/json-rpc.ts +84 -0
  269. package/src/mcp/loader.ts +124 -0
  270. package/src/mcp/manager.ts +490 -0
  271. package/src/mcp/oauth-discovery.ts +274 -0
  272. package/src/mcp/oauth-flow.ts +229 -0
  273. package/src/mcp/render.ts +123 -0
  274. package/src/mcp/tool-bridge.ts +372 -0
  275. package/src/mcp/tool-cache.ts +121 -0
  276. package/src/mcp/transports/http.ts +332 -0
  277. package/src/mcp/transports/index.ts +6 -0
  278. package/src/mcp/transports/stdio.ts +281 -0
  279. package/src/mcp/types.ts +248 -0
  280. package/src/memories/index.ts +1099 -0
  281. package/src/memories/storage.ts +563 -0
  282. package/src/modes/components/agent-dashboard.ts +1130 -0
  283. package/src/modes/components/assistant-message.ts +144 -0
  284. package/src/modes/components/bash-execution.ts +218 -0
  285. package/src/modes/components/bordered-loader.ts +41 -0
  286. package/src/modes/components/branch-summary-message.ts +45 -0
  287. package/src/modes/components/codemode-group.ts +369 -0
  288. package/src/modes/components/compaction-summary-message.ts +51 -0
  289. package/src/modes/components/countdown-timer.ts +46 -0
  290. package/src/modes/components/custom-editor.ts +181 -0
  291. package/src/modes/components/custom-message.ts +91 -0
  292. package/src/modes/components/diff.ts +186 -0
  293. package/src/modes/components/dynamic-border.ts +25 -0
  294. package/src/modes/components/extensions/extension-dashboard.ts +325 -0
  295. package/src/modes/components/extensions/extension-list.ts +484 -0
  296. package/src/modes/components/extensions/index.ts +9 -0
  297. package/src/modes/components/extensions/inspector-panel.ts +321 -0
  298. package/src/modes/components/extensions/state-manager.ts +586 -0
  299. package/src/modes/components/extensions/types.ts +191 -0
  300. package/src/modes/components/footer.ts +315 -0
  301. package/src/modes/components/history-search.ts +157 -0
  302. package/src/modes/components/hook-editor.ts +101 -0
  303. package/src/modes/components/hook-input.ts +72 -0
  304. package/src/modes/components/hook-message.ts +100 -0
  305. package/src/modes/components/hook-selector.ts +155 -0
  306. package/src/modes/components/index.ts +41 -0
  307. package/src/modes/components/keybinding-hints.ts +65 -0
  308. package/src/modes/components/login-dialog.ts +164 -0
  309. package/src/modes/components/mcp-add-wizard.ts +1295 -0
  310. package/src/modes/components/model-selector.ts +625 -0
  311. package/src/modes/components/oauth-selector.ts +210 -0
  312. package/src/modes/components/plugin-settings.ts +477 -0
  313. package/src/modes/components/python-execution.ts +196 -0
  314. package/src/modes/components/queue-mode-selector.ts +56 -0
  315. package/src/modes/components/read-tool-group.ts +119 -0
  316. package/src/modes/components/session-selector.ts +242 -0
  317. package/src/modes/components/settings-defs.ts +340 -0
  318. package/src/modes/components/settings-selector.ts +529 -0
  319. package/src/modes/components/show-images-selector.ts +45 -0
  320. package/src/modes/components/skill-message.ts +90 -0
  321. package/src/modes/components/status-line/index.ts +4 -0
  322. package/src/modes/components/status-line/presets.ts +94 -0
  323. package/src/modes/components/status-line/segments.ts +352 -0
  324. package/src/modes/components/status-line/separators.ts +55 -0
  325. package/src/modes/components/status-line/types.ts +75 -0
  326. package/src/modes/components/status-line-segment-editor.ts +354 -0
  327. package/src/modes/components/status-line.ts +421 -0
  328. package/src/modes/components/theme-selector.ts +63 -0
  329. package/src/modes/components/thinking-selector.ts +64 -0
  330. package/src/modes/components/todo-display.ts +115 -0
  331. package/src/modes/components/todo-reminder.ts +40 -0
  332. package/src/modes/components/tool-execution.ts +703 -0
  333. package/src/modes/components/tree-selector.ts +904 -0
  334. package/src/modes/components/ttsr-notification.ts +80 -0
  335. package/src/modes/components/user-message-selector.ts +146 -0
  336. package/src/modes/components/user-message.ts +22 -0
  337. package/src/modes/components/visual-truncate.ts +63 -0
  338. package/src/modes/components/welcome.ts +247 -0
  339. package/src/modes/controllers/command-controller.ts +1120 -0
  340. package/src/modes/controllers/event-controller.ts +479 -0
  341. package/src/modes/controllers/extension-ui-controller.ts +778 -0
  342. package/src/modes/controllers/input-controller.ts +671 -0
  343. package/src/modes/controllers/mcp-command-controller.ts +1315 -0
  344. package/src/modes/controllers/selector-controller.ts +712 -0
  345. package/src/modes/controllers/ssh-command-controller.ts +452 -0
  346. package/src/modes/index.ts +15 -0
  347. package/src/modes/interactive-mode.ts +1027 -0
  348. package/src/modes/print-mode.ts +191 -0
  349. package/src/modes/rpc/rpc-client.ts +583 -0
  350. package/src/modes/rpc/rpc-mode.ts +700 -0
  351. package/src/modes/rpc/rpc-types.ts +236 -0
  352. package/src/modes/theme/dark.json +95 -0
  353. package/src/modes/theme/defaults/alabaster.json +93 -0
  354. package/src/modes/theme/defaults/amethyst.json +96 -0
  355. package/src/modes/theme/defaults/anthracite.json +93 -0
  356. package/src/modes/theme/defaults/basalt.json +91 -0
  357. package/src/modes/theme/defaults/birch.json +95 -0
  358. package/src/modes/theme/defaults/dark-abyss.json +91 -0
  359. package/src/modes/theme/defaults/dark-arctic.json +104 -0
  360. package/src/modes/theme/defaults/dark-aurora.json +95 -0
  361. package/src/modes/theme/defaults/dark-catppuccin.json +107 -0
  362. package/src/modes/theme/defaults/dark-cavern.json +91 -0
  363. package/src/modes/theme/defaults/dark-copper.json +95 -0
  364. package/src/modes/theme/defaults/dark-cosmos.json +90 -0
  365. package/src/modes/theme/defaults/dark-cyberpunk.json +102 -0
  366. package/src/modes/theme/defaults/dark-dracula.json +98 -0
  367. package/src/modes/theme/defaults/dark-eclipse.json +91 -0
  368. package/src/modes/theme/defaults/dark-ember.json +95 -0
  369. package/src/modes/theme/defaults/dark-equinox.json +90 -0
  370. package/src/modes/theme/defaults/dark-forest.json +96 -0
  371. package/src/modes/theme/defaults/dark-github.json +105 -0
  372. package/src/modes/theme/defaults/dark-gruvbox.json +112 -0
  373. package/src/modes/theme/defaults/dark-lavender.json +95 -0
  374. package/src/modes/theme/defaults/dark-lunar.json +89 -0
  375. package/src/modes/theme/defaults/dark-midnight.json +95 -0
  376. package/src/modes/theme/defaults/dark-monochrome.json +94 -0
  377. package/src/modes/theme/defaults/dark-monokai.json +98 -0
  378. package/src/modes/theme/defaults/dark-nebula.json +90 -0
  379. package/src/modes/theme/defaults/dark-nord.json +97 -0
  380. package/src/modes/theme/defaults/dark-ocean.json +101 -0
  381. package/src/modes/theme/defaults/dark-one.json +100 -0
  382. package/src/modes/theme/defaults/dark-rainforest.json +91 -0
  383. package/src/modes/theme/defaults/dark-reef.json +91 -0
  384. package/src/modes/theme/defaults/dark-retro.json +92 -0
  385. package/src/modes/theme/defaults/dark-rose-pine.json +96 -0
  386. package/src/modes/theme/defaults/dark-sakura.json +95 -0
  387. package/src/modes/theme/defaults/dark-slate.json +95 -0
  388. package/src/modes/theme/defaults/dark-solarized.json +97 -0
  389. package/src/modes/theme/defaults/dark-solstice.json +90 -0
  390. package/src/modes/theme/defaults/dark-starfall.json +91 -0
  391. package/src/modes/theme/defaults/dark-sunset.json +99 -0
  392. package/src/modes/theme/defaults/dark-swamp.json +90 -0
  393. package/src/modes/theme/defaults/dark-synthwave.json +103 -0
  394. package/src/modes/theme/defaults/dark-taiga.json +91 -0
  395. package/src/modes/theme/defaults/dark-terminal.json +95 -0
  396. package/src/modes/theme/defaults/dark-tokyo-night.json +101 -0
  397. package/src/modes/theme/defaults/dark-tundra.json +91 -0
  398. package/src/modes/theme/defaults/dark-twilight.json +91 -0
  399. package/src/modes/theme/defaults/dark-volcanic.json +91 -0
  400. package/src/modes/theme/defaults/graphite.json +92 -0
  401. package/src/modes/theme/defaults/index.ts +195 -0
  402. package/src/modes/theme/defaults/light-arctic.json +107 -0
  403. package/src/modes/theme/defaults/light-aurora-day.json +91 -0
  404. package/src/modes/theme/defaults/light-canyon.json +91 -0
  405. package/src/modes/theme/defaults/light-catppuccin.json +106 -0
  406. package/src/modes/theme/defaults/light-cirrus.json +90 -0
  407. package/src/modes/theme/defaults/light-coral.json +95 -0
  408. package/src/modes/theme/defaults/light-cyberpunk.json +96 -0
  409. package/src/modes/theme/defaults/light-dawn.json +90 -0
  410. package/src/modes/theme/defaults/light-dunes.json +91 -0
  411. package/src/modes/theme/defaults/light-eucalyptus.json +95 -0
  412. package/src/modes/theme/defaults/light-forest.json +100 -0
  413. package/src/modes/theme/defaults/light-frost.json +95 -0
  414. package/src/modes/theme/defaults/light-github.json +115 -0
  415. package/src/modes/theme/defaults/light-glacier.json +91 -0
  416. package/src/modes/theme/defaults/light-gruvbox.json +108 -0
  417. package/src/modes/theme/defaults/light-haze.json +90 -0
  418. package/src/modes/theme/defaults/light-honeycomb.json +95 -0
  419. package/src/modes/theme/defaults/light-lagoon.json +91 -0
  420. package/src/modes/theme/defaults/light-lavender.json +95 -0
  421. package/src/modes/theme/defaults/light-meadow.json +91 -0
  422. package/src/modes/theme/defaults/light-mint.json +95 -0
  423. package/src/modes/theme/defaults/light-monochrome.json +101 -0
  424. package/src/modes/theme/defaults/light-ocean.json +99 -0
  425. package/src/modes/theme/defaults/light-one.json +99 -0
  426. package/src/modes/theme/defaults/light-opal.json +91 -0
  427. package/src/modes/theme/defaults/light-orchard.json +91 -0
  428. package/src/modes/theme/defaults/light-paper.json +95 -0
  429. package/src/modes/theme/defaults/light-prism.json +90 -0
  430. package/src/modes/theme/defaults/light-retro.json +98 -0
  431. package/src/modes/theme/defaults/light-sand.json +95 -0
  432. package/src/modes/theme/defaults/light-savanna.json +91 -0
  433. package/src/modes/theme/defaults/light-solarized.json +102 -0
  434. package/src/modes/theme/defaults/light-soleil.json +90 -0
  435. package/src/modes/theme/defaults/light-sunset.json +99 -0
  436. package/src/modes/theme/defaults/light-synthwave.json +98 -0
  437. package/src/modes/theme/defaults/light-tokyo-night.json +111 -0
  438. package/src/modes/theme/defaults/light-wetland.json +91 -0
  439. package/src/modes/theme/defaults/light-zenith.json +89 -0
  440. package/src/modes/theme/defaults/limestone.json +94 -0
  441. package/src/modes/theme/defaults/mahogany.json +97 -0
  442. package/src/modes/theme/defaults/marble.json +93 -0
  443. package/src/modes/theme/defaults/obsidian.json +91 -0
  444. package/src/modes/theme/defaults/onyx.json +91 -0
  445. package/src/modes/theme/defaults/pearl.json +93 -0
  446. package/src/modes/theme/defaults/porcelain.json +91 -0
  447. package/src/modes/theme/defaults/quartz.json +96 -0
  448. package/src/modes/theme/defaults/sandstone.json +95 -0
  449. package/src/modes/theme/defaults/titanium.json +90 -0
  450. package/src/modes/theme/light.json +93 -0
  451. package/src/modes/theme/mermaid-cache.ts +111 -0
  452. package/src/modes/theme/theme-schema.json +429 -0
  453. package/src/modes/theme/theme.ts +2333 -0
  454. package/src/modes/types.ts +216 -0
  455. package/src/modes/utils/ui-helpers.ts +529 -0
  456. package/src/patch/applicator.ts +1482 -0
  457. package/src/patch/diff.ts +425 -0
  458. package/src/patch/fuzzy.ts +784 -0
  459. package/src/patch/hashline.ts +972 -0
  460. package/src/patch/index.ts +964 -0
  461. package/src/patch/normalize.ts +397 -0
  462. package/src/patch/normative.ts +72 -0
  463. package/src/patch/parser.ts +532 -0
  464. package/src/patch/shared.ts +400 -0
  465. package/src/patch/types.ts +292 -0
  466. package/src/priority.json +35 -0
  467. package/src/prompts/agents/explore.md +48 -0
  468. package/src/prompts/agents/frontmatter.md +9 -0
  469. package/src/prompts/agents/init.md +36 -0
  470. package/src/prompts/agents/librarian.md +53 -0
  471. package/src/prompts/agents/oracle.md +51 -0
  472. package/src/prompts/agents/reviewer.md +70 -0
  473. package/src/prompts/agents/task.md +14 -0
  474. package/src/prompts/compaction/branch-summary-context.md +5 -0
  475. package/src/prompts/compaction/branch-summary-preamble.md +2 -0
  476. package/src/prompts/compaction/branch-summary.md +30 -0
  477. package/src/prompts/compaction/compaction-short-summary.md +9 -0
  478. package/src/prompts/compaction/compaction-summary-context.md +5 -0
  479. package/src/prompts/compaction/compaction-summary.md +38 -0
  480. package/src/prompts/compaction/compaction-turn-prefix.md +17 -0
  481. package/src/prompts/compaction/compaction-update-summary.md +45 -0
  482. package/src/prompts/memories/consolidation.md +30 -0
  483. package/src/prompts/memories/read_path.md +11 -0
  484. package/src/prompts/memories/stage_one_input.md +6 -0
  485. package/src/prompts/memories/stage_one_system.md +21 -0
  486. package/src/prompts/review-request.md +64 -0
  487. package/src/prompts/system/agent-creation-architect.md +65 -0
  488. package/src/prompts/system/agent-creation-user.md +6 -0
  489. package/src/prompts/system/custom-system-prompt.md +68 -0
  490. package/src/prompts/system/file-operations.md +10 -0
  491. package/src/prompts/system/subagent-submit-reminder.md +11 -0
  492. package/src/prompts/system/subagent-system-prompt.md +31 -0
  493. package/src/prompts/system/subagent-user-prompt.md +8 -0
  494. package/src/prompts/system/summarization-system.md +3 -0
  495. package/src/prompts/system/system-prompt.md +300 -0
  496. package/src/prompts/system/title-system.md +2 -0
  497. package/src/prompts/system/ttsr-interrupt.md +7 -0
  498. package/src/prompts/system/web-search.md +28 -0
  499. package/src/prompts/tools/ask.md +44 -0
  500. package/src/prompts/tools/bash.md +24 -0
  501. package/src/prompts/tools/browser.md +33 -0
  502. package/src/prompts/tools/calculator.md +12 -0
  503. package/src/prompts/tools/explore.md +29 -0
  504. package/src/prompts/tools/fetch.md +16 -0
  505. package/src/prompts/tools/find.md +18 -0
  506. package/src/prompts/tools/gemini-image.md +23 -0
  507. package/src/prompts/tools/grep.md +28 -0
  508. package/src/prompts/tools/hashline.md +232 -0
  509. package/src/prompts/tools/librarian.md +24 -0
  510. package/src/prompts/tools/lsp.md +28 -0
  511. package/src/prompts/tools/oracle.md +26 -0
  512. package/src/prompts/tools/patch.md +74 -0
  513. package/src/prompts/tools/python.md +66 -0
  514. package/src/prompts/tools/read.md +36 -0
  515. package/src/prompts/tools/replace.md +38 -0
  516. package/src/prompts/tools/reviewer.md +41 -0
  517. package/src/prompts/tools/ssh.md +51 -0
  518. package/src/prompts/tools/task-summary.md +28 -0
  519. package/src/prompts/tools/task.md +275 -0
  520. package/src/prompts/tools/todo-write.md +65 -0
  521. package/src/prompts/tools/undo-edit.md +7 -0
  522. package/src/prompts/tools/web-search.md +19 -0
  523. package/src/prompts/tools/write.md +18 -0
  524. package/src/sdk.ts +1287 -0
  525. package/src/secrets/index.ts +116 -0
  526. package/src/secrets/obfuscator.ts +269 -0
  527. package/src/secrets/regex.ts +21 -0
  528. package/src/session/agent-session.ts +4669 -0
  529. package/src/session/agent-storage.ts +621 -0
  530. package/src/session/artifacts.ts +132 -0
  531. package/src/session/auth-storage.ts +1433 -0
  532. package/src/session/blob-store.ts +103 -0
  533. package/src/session/compaction/branch-summarization.ts +315 -0
  534. package/src/session/compaction/compaction.ts +864 -0
  535. package/src/session/compaction/index.ts +7 -0
  536. package/src/session/compaction/pruning.ts +91 -0
  537. package/src/session/compaction/utils.ts +171 -0
  538. package/src/session/history-storage.ts +170 -0
  539. package/src/session/messages.ts +317 -0
  540. package/src/session/session-manager.ts +2276 -0
  541. package/src/session/session-storage.ts +342 -0
  542. package/src/session/streaming-output.ts +565 -0
  543. package/src/slash-commands/builtin-registry.ts +439 -0
  544. package/src/ssh/config-writer.ts +183 -0
  545. package/src/ssh/connection-manager.ts +444 -0
  546. package/src/ssh/ssh-executor.ts +127 -0
  547. package/src/ssh/sshfs-mount.ts +135 -0
  548. package/src/stt/downloader.ts +71 -0
  549. package/src/stt/index.ts +3 -0
  550. package/src/stt/recorder.ts +351 -0
  551. package/src/stt/setup.ts +52 -0
  552. package/src/stt/stt-controller.ts +160 -0
  553. package/src/stt/transcribe.py +70 -0
  554. package/src/stt/transcriber.ts +91 -0
  555. package/src/system-prompt.ts +685 -0
  556. package/src/task/agents.ts +155 -0
  557. package/src/task/batch.ts +102 -0
  558. package/src/task/commands.ts +134 -0
  559. package/src/task/discovery.ts +126 -0
  560. package/src/task/executor.ts +908 -0
  561. package/src/task/index.ts +223 -0
  562. package/src/task/output-manager.ts +107 -0
  563. package/src/task/parallel.ts +84 -0
  564. package/src/task/render.ts +326 -0
  565. package/src/task/subprocess-tool-registry.ts +88 -0
  566. package/src/task/template.ts +32 -0
  567. package/src/task/types.ts +144 -0
  568. package/src/tools/ask.ts +523 -0
  569. package/src/tools/bash-interactive.ts +419 -0
  570. package/src/tools/bash-interceptor.ts +105 -0
  571. package/src/tools/bash-normalize.ts +107 -0
  572. package/src/tools/bash-skill-urls.ts +177 -0
  573. package/src/tools/bash.ts +347 -0
  574. package/src/tools/browser.ts +1374 -0
  575. package/src/tools/calculator.ts +537 -0
  576. package/src/tools/context.ts +39 -0
  577. package/src/tools/explore.ts +23 -0
  578. package/src/tools/fetch.ts +1091 -0
  579. package/src/tools/find.ts +540 -0
  580. package/src/tools/fs-cache-invalidation.ts +28 -0
  581. package/src/tools/gemini-image.ts +907 -0
  582. package/src/tools/grep.ts +489 -0
  583. package/src/tools/index.ts +337 -0
  584. package/src/tools/json-tree.ts +231 -0
  585. package/src/tools/jtd-to-json-schema.ts +247 -0
  586. package/src/tools/jtd-to-typescript.ts +198 -0
  587. package/src/tools/librarian.ts +33 -0
  588. package/src/tools/list-limit.ts +40 -0
  589. package/src/tools/notebook.ts +287 -0
  590. package/src/tools/oracle.ts +40 -0
  591. package/src/tools/output-meta.ts +459 -0
  592. package/src/tools/output-utils.ts +63 -0
  593. package/src/tools/path-utils.ts +116 -0
  594. package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
  595. package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
  596. package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
  597. package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
  598. package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
  599. package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
  600. package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
  601. package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
  602. package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
  603. package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
  604. package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
  605. package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
  606. package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
  607. package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
  608. package/src/tools/python.ts +1118 -0
  609. package/src/tools/read.ts +1193 -0
  610. package/src/tools/render-utils.ts +680 -0
  611. package/src/tools/renderers.ts +60 -0
  612. package/src/tools/reviewer-tool.ts +41 -0
  613. package/src/tools/ssh.ts +326 -0
  614. package/src/tools/subagent-tool.ts +169 -0
  615. package/src/tools/submit-result.ts +152 -0
  616. package/src/tools/todo-write.ts +255 -0
  617. package/src/tools/tool-errors.ts +92 -0
  618. package/src/tools/tool-result.ts +86 -0
  619. package/src/tools/undo-edit.ts +145 -0
  620. package/src/tools/undo-history.ts +22 -0
  621. package/src/tools/write.ts +274 -0
  622. package/src/tui/code-cell.ts +108 -0
  623. package/src/tui/file-list.ts +47 -0
  624. package/src/tui/index.ts +11 -0
  625. package/src/tui/output-block.ts +144 -0
  626. package/src/tui/status-line.ts +39 -0
  627. package/src/tui/tree-list.ts +53 -0
  628. package/src/tui/types.ts +16 -0
  629. package/src/tui/utils.ts +116 -0
  630. package/src/utils/changelog.ts +98 -0
  631. package/src/utils/event-bus.ts +33 -0
  632. package/src/utils/external-editor.ts +59 -0
  633. package/src/utils/file-display-mode.ts +36 -0
  634. package/src/utils/file-mentions.ts +384 -0
  635. package/src/utils/frontmatter.ts +101 -0
  636. package/src/utils/fuzzy.ts +108 -0
  637. package/src/utils/ignore-files.ts +119 -0
  638. package/src/utils/image-convert.ts +27 -0
  639. package/src/utils/image-resize.ts +236 -0
  640. package/src/utils/mime.ts +30 -0
  641. package/src/utils/open.ts +20 -0
  642. package/src/utils/shell-snapshot.ts +199 -0
  643. package/src/utils/timings.ts +26 -0
  644. package/src/utils/title-generator.ts +167 -0
  645. package/src/utils/tools-manager.ts +362 -0
  646. package/src/web/scrapers/artifacthub.ts +215 -0
  647. package/src/web/scrapers/arxiv.ts +88 -0
  648. package/src/web/scrapers/aur.ts +175 -0
  649. package/src/web/scrapers/biorxiv.ts +141 -0
  650. package/src/web/scrapers/bluesky.ts +284 -0
  651. package/src/web/scrapers/brew.ts +177 -0
  652. package/src/web/scrapers/cheatsh.ts +78 -0
  653. package/src/web/scrapers/chocolatey.ts +158 -0
  654. package/src/web/scrapers/choosealicense.ts +110 -0
  655. package/src/web/scrapers/cisa-kev.ts +100 -0
  656. package/src/web/scrapers/clojars.ts +180 -0
  657. package/src/web/scrapers/coingecko.ts +184 -0
  658. package/src/web/scrapers/crates-io.ts +128 -0
  659. package/src/web/scrapers/crossref.ts +149 -0
  660. package/src/web/scrapers/devto.ts +177 -0
  661. package/src/web/scrapers/discogs.ts +307 -0
  662. package/src/web/scrapers/discourse.ts +221 -0
  663. package/src/web/scrapers/dockerhub.ts +160 -0
  664. package/src/web/scrapers/fdroid.ts +158 -0
  665. package/src/web/scrapers/firefox-addons.ts +214 -0
  666. package/src/web/scrapers/flathub.ts +239 -0
  667. package/src/web/scrapers/github-gist.ts +68 -0
  668. package/src/web/scrapers/github.ts +490 -0
  669. package/src/web/scrapers/gitlab.ts +456 -0
  670. package/src/web/scrapers/go-pkg.ts +275 -0
  671. package/src/web/scrapers/hackage.ts +94 -0
  672. package/src/web/scrapers/hackernews.ts +208 -0
  673. package/src/web/scrapers/hex.ts +121 -0
  674. package/src/web/scrapers/huggingface.ts +385 -0
  675. package/src/web/scrapers/iacr.ts +86 -0
  676. package/src/web/scrapers/index.ts +249 -0
  677. package/src/web/scrapers/jetbrains-marketplace.ts +169 -0
  678. package/src/web/scrapers/lemmy.ts +220 -0
  679. package/src/web/scrapers/lobsters.ts +186 -0
  680. package/src/web/scrapers/mastodon.ts +310 -0
  681. package/src/web/scrapers/maven.ts +152 -0
  682. package/src/web/scrapers/mdn.ts +172 -0
  683. package/src/web/scrapers/metacpan.ts +253 -0
  684. package/src/web/scrapers/musicbrainz.ts +272 -0
  685. package/src/web/scrapers/npm.ts +114 -0
  686. package/src/web/scrapers/nuget.ts +205 -0
  687. package/src/web/scrapers/nvd.ts +243 -0
  688. package/src/web/scrapers/ollama.ts +265 -0
  689. package/src/web/scrapers/open-vsx.ts +119 -0
  690. package/src/web/scrapers/opencorporates.ts +275 -0
  691. package/src/web/scrapers/openlibrary.ts +319 -0
  692. package/src/web/scrapers/orcid.ts +298 -0
  693. package/src/web/scrapers/osv.ts +192 -0
  694. package/src/web/scrapers/packagist.ts +174 -0
  695. package/src/web/scrapers/pub-dev.ts +185 -0
  696. package/src/web/scrapers/pubmed.ts +177 -0
  697. package/src/web/scrapers/pypi.ts +129 -0
  698. package/src/web/scrapers/rawg.ts +124 -0
  699. package/src/web/scrapers/readthedocs.ts +125 -0
  700. package/src/web/scrapers/reddit.ts +104 -0
  701. package/src/web/scrapers/repology.ts +262 -0
  702. package/src/web/scrapers/rfc.ts +209 -0
  703. package/src/web/scrapers/rubygems.ts +117 -0
  704. package/src/web/scrapers/searchcode.ts +217 -0
  705. package/src/web/scrapers/sec-edgar.ts +274 -0
  706. package/src/web/scrapers/semantic-scholar.ts +190 -0
  707. package/src/web/scrapers/snapcraft.ts +200 -0
  708. package/src/web/scrapers/sourcegraph.ts +373 -0
  709. package/src/web/scrapers/spdx.ts +121 -0
  710. package/src/web/scrapers/spotify.ts +217 -0
  711. package/src/web/scrapers/stackoverflow.ts +124 -0
  712. package/src/web/scrapers/terraform.ts +304 -0
  713. package/src/web/scrapers/tldr.ts +51 -0
  714. package/src/web/scrapers/twitter.ts +97 -0
  715. package/src/web/scrapers/types.ts +200 -0
  716. package/src/web/scrapers/utils.ts +142 -0
  717. package/src/web/scrapers/vimeo.ts +152 -0
  718. package/src/web/scrapers/vscode-marketplace.ts +195 -0
  719. package/src/web/scrapers/w3c.ts +163 -0
  720. package/src/web/scrapers/wikidata.ts +357 -0
  721. package/src/web/scrapers/wikipedia.ts +95 -0
  722. package/src/web/scrapers/youtube.ts +312 -0
  723. package/src/web/search/auth.ts +178 -0
  724. package/src/web/search/index.ts +598 -0
  725. package/src/web/search/provider.ts +77 -0
  726. package/src/web/search/providers/anthropic.ts +284 -0
  727. package/src/web/search/providers/base.ts +22 -0
  728. package/src/web/search/providers/brave.ts +165 -0
  729. package/src/web/search/providers/codex.ts +377 -0
  730. package/src/web/search/providers/exa.ts +158 -0
  731. package/src/web/search/providers/gemini.ts +437 -0
  732. package/src/web/search/providers/jina.ts +99 -0
  733. package/src/web/search/providers/kimi.ts +196 -0
  734. package/src/web/search/providers/perplexity.ts +546 -0
  735. package/src/web/search/providers/synthetic.ts +136 -0
  736. package/src/web/search/providers/zai.ts +352 -0
  737. package/src/web/search/render.ts +299 -0
  738. package/src/web/search/types.ts +437 -0
@@ -0,0 +1,831 @@
1
+ # OMP IPython prelude helpers
2
+ if "__arc_prelude_loaded__" not in globals():
3
+ __arc_prelude_loaded__ = True
4
+ from pathlib import Path
5
+ import os, re, json, shutil, subprocess, inspect
6
+ from datetime import datetime
7
+ from IPython.display import display
8
+
9
+ def _emit_status(op: str, **data):
10
+ """Emit structured status event for TUI rendering."""
11
+ display({"application/x-arc-status": {"op": op, **data}}, raw=True)
12
+
13
+ def _category(cat: str):
14
+ """Decorator to tag a prelude function with its category."""
15
+ def decorator(fn):
16
+ fn._arc_category = cat
17
+ return fn
18
+ return decorator
19
+
20
+ @_category("Shell")
21
+ def env(key: str | None = None, value: str | None = None):
22
+ """Get/set environment variables."""
23
+ if key is None:
24
+ items = dict(sorted(os.environ.items()))
25
+ _emit_status("env", count=len(items), keys=list(items.keys())[:20])
26
+ return items
27
+ if value is not None:
28
+ os.environ[key] = value
29
+ _emit_status("env", key=key, value=value, action="set")
30
+ return value
31
+ val = os.environ.get(key)
32
+ _emit_status("env", key=key, value=val, action="get")
33
+ return val
34
+
35
+ @_category("File I/O")
36
+ def read(path: str | Path, *, offset: int = 1, limit: int | None = None) -> str:
37
+ """Read file contents. offset/limit are 1-indexed line numbers."""
38
+ p = Path(path)
39
+ data = p.read_text(encoding="utf-8")
40
+ lines = data.splitlines(keepends=True)
41
+ if offset > 1 or limit is not None:
42
+ start = max(0, offset - 1)
43
+ end = start + limit if limit else len(lines)
44
+ lines = lines[start:end]
45
+ data = "".join(lines)
46
+ preview = data[:500]
47
+ _emit_status("read", path=str(p), chars=len(data), preview=preview)
48
+ return data
49
+
50
+ @_category("File I/O")
51
+ def write(path: str | Path, content: str) -> Path:
52
+ """Write file contents (create parents)."""
53
+ p = Path(path)
54
+ p.parent.mkdir(parents=True, exist_ok=True)
55
+ p.write_text(content, encoding="utf-8")
56
+ _emit_status("write", path=str(p), chars=len(content))
57
+ return p
58
+
59
+ @_category("File I/O")
60
+ def append(path: str | Path, content: str) -> Path:
61
+ """Append to file."""
62
+ p = Path(path)
63
+ p.parent.mkdir(parents=True, exist_ok=True)
64
+ with p.open("a", encoding="utf-8") as f:
65
+ f.write(content)
66
+ _emit_status("append", path=str(p), chars=len(content))
67
+ return p
68
+
69
+ @_category("File ops")
70
+ def rm(path: str | Path, *, recursive: bool = False) -> None:
71
+ """Delete file or directory (recursive optional)."""
72
+ p = Path(path)
73
+ if p.is_dir():
74
+ if recursive:
75
+ shutil.rmtree(p)
76
+ _emit_status("rm", path=str(p), recursive=True)
77
+ return
78
+ _emit_status("rm", path=str(p), error="directory, use recursive=True")
79
+ return
80
+ if p.exists():
81
+ p.unlink()
82
+ _emit_status("rm", path=str(p))
83
+ else:
84
+ _emit_status("rm", path=str(p), error="missing")
85
+
86
+ @_category("File ops")
87
+ def mv(src: str | Path, dst: str | Path) -> Path:
88
+ """Move or rename a file/directory."""
89
+ src_p = Path(src)
90
+ dst_p = Path(dst)
91
+ dst_p.parent.mkdir(parents=True, exist_ok=True)
92
+ shutil.move(str(src_p), str(dst_p))
93
+ _emit_status("mv", src=str(src_p), dst=str(dst_p))
94
+ return dst_p
95
+
96
+ @_category("File ops")
97
+ def cp(src: str | Path, dst: str | Path) -> Path:
98
+ """Copy a file or directory."""
99
+ src_p = Path(src)
100
+ dst_p = Path(dst)
101
+ dst_p.parent.mkdir(parents=True, exist_ok=True)
102
+ if src_p.is_dir():
103
+ shutil.copytree(src_p, dst_p, dirs_exist_ok=True)
104
+ else:
105
+ shutil.copy2(src_p, dst_p)
106
+ _emit_status("cp", src=str(src_p), dst=str(dst_p))
107
+ return dst_p
108
+
109
+ def _load_gitignore_patterns(base: Path) -> list[str]:
110
+ """Load .gitignore patterns from base directory and parents."""
111
+ patterns: list[str] = []
112
+ # Always exclude these
113
+ patterns.extend(["**/.git", "**/.git/**", "**/node_modules", "**/node_modules/**"])
114
+ # Walk up to find .gitignore files
115
+ current = base.resolve()
116
+ for _ in range(20): # Limit depth
117
+ gitignore = current / ".gitignore"
118
+ if gitignore.exists():
119
+ try:
120
+ for line in gitignore.read_text().splitlines():
121
+ line = line.strip()
122
+ if line and not line.startswith("#"):
123
+ # Normalize pattern for fnmatch
124
+ if line.startswith("/"):
125
+ patterns.append(str(current / line[1:]))
126
+ else:
127
+ patterns.append(f"**/{line}")
128
+ except Exception:
129
+ pass
130
+ parent = current.parent
131
+ if parent == current:
132
+ break
133
+ current = parent
134
+ return patterns
135
+
136
+ def _match_gitignore(path: Path, patterns: list[str], base: Path) -> bool:
137
+ """Check if path matches any gitignore pattern."""
138
+ import fnmatch
139
+ rel = str(path.relative_to(base)) if path.is_relative_to(base) else str(path)
140
+ abs_path = str(path.resolve())
141
+ for pat in patterns:
142
+ if pat.startswith("**/"):
143
+ # Match against any part of the path
144
+ if fnmatch.fnmatch(rel, pat) or fnmatch.fnmatch(rel, pat[3:]):
145
+ return True
146
+ # Also check each path component
147
+ for part in path.parts:
148
+ if fnmatch.fnmatch(part, pat[3:]):
149
+ return True
150
+ elif fnmatch.fnmatch(abs_path, pat) or fnmatch.fnmatch(rel, pat):
151
+ return True
152
+ return False
153
+
154
+ @_category("Search")
155
+ def find(
156
+ pattern: str,
157
+ path: str | Path = ".",
158
+ *,
159
+ type: str = "file",
160
+ limit: int = 1000,
161
+ hidden: bool = False,
162
+ sort_by_mtime: bool = False,
163
+ maxdepth: int | None = None,
164
+ mindepth: int | None = None,
165
+ ) -> list[Path]:
166
+ """Recursive glob find. Respects .gitignore.
167
+
168
+ maxdepth/mindepth are relative to path (0 = path itself, 1 = direct children).
169
+ """
170
+ p = Path(path).resolve()
171
+ base_depth = len(p.parts)
172
+ ignore_patterns = _load_gitignore_patterns(p)
173
+ matches: list[Path] = []
174
+ for m in p.rglob(pattern):
175
+ if len(matches) >= limit:
176
+ break
177
+ # Check depth constraints
178
+ rel_depth = len(m.resolve().parts) - base_depth
179
+ if maxdepth is not None and rel_depth > maxdepth:
180
+ continue
181
+ if mindepth is not None and rel_depth < mindepth:
182
+ continue
183
+ # Skip hidden files unless requested
184
+ if not hidden and any(part.startswith(".") for part in m.parts):
185
+ continue
186
+ # Skip gitignored paths
187
+ if _match_gitignore(m, ignore_patterns, p):
188
+ continue
189
+ # Filter by type
190
+ if type == "file" and m.is_dir():
191
+ continue
192
+ if type == "dir" and not m.is_dir():
193
+ continue
194
+ matches.append(m)
195
+ if sort_by_mtime:
196
+ matches.sort(key=lambda x: x.stat().st_mtime, reverse=True)
197
+ else:
198
+ matches.sort()
199
+ _emit_status("find", pattern=pattern, path=str(p), count=len(matches), matches=[str(m) for m in matches[:20]])
200
+ return matches
201
+
202
+ @_category("Search")
203
+ def grep(
204
+ pattern: str,
205
+ path: str | Path,
206
+ *,
207
+ ignore_case: bool = False,
208
+ literal: bool = False,
209
+ context: int = 0,
210
+ ) -> list[tuple[int, str]]:
211
+ """Grep a single file. Returns (line_number, text) tuples."""
212
+ p = Path(path)
213
+ lines = p.read_text(encoding="utf-8").splitlines()
214
+ if literal:
215
+ if ignore_case:
216
+ match_fn = lambda line: pattern.lower() in line.lower()
217
+ else:
218
+ match_fn = lambda line: pattern in line
219
+ else:
220
+ flags = re.IGNORECASE if ignore_case else 0
221
+ rx = re.compile(pattern, flags)
222
+ match_fn = lambda line: rx.search(line) is not None
223
+
224
+ match_lines: set[int] = set()
225
+ for i, line in enumerate(lines, 1):
226
+ if match_fn(line):
227
+ match_lines.add(i)
228
+
229
+ # Expand with context
230
+ if context > 0:
231
+ expanded: set[int] = set()
232
+ for ln in match_lines:
233
+ for offset in range(-context, context + 1):
234
+ expanded.add(ln + offset)
235
+ output_lines = sorted(ln for ln in expanded if 1 <= ln <= len(lines))
236
+ else:
237
+ output_lines = sorted(match_lines)
238
+
239
+ hits = [(ln, lines[ln - 1]) for ln in output_lines]
240
+ _emit_status("grep", pattern=pattern, path=str(p), count=len(match_lines), hits=[{"line": h[0], "text": h[1][:100]} for h in hits[:10]])
241
+ return hits
242
+
243
+ @_category("Search")
244
+ def rgrep(
245
+ pattern: str,
246
+ path: str | Path = ".",
247
+ *,
248
+ glob_pattern: str = "*",
249
+ ignore_case: bool = False,
250
+ literal: bool = False,
251
+ limit: int = 100,
252
+ hidden: bool = False,
253
+ ) -> list[tuple[Path, int, str]]:
254
+ """Recursive grep across files matching glob_pattern. Respects .gitignore."""
255
+ if literal:
256
+ if ignore_case:
257
+ match_fn = lambda line: pattern.lower() in line.lower()
258
+ else:
259
+ match_fn = lambda line: pattern in line
260
+ else:
261
+ flags = re.IGNORECASE if ignore_case else 0
262
+ rx = re.compile(pattern, flags)
263
+ match_fn = lambda line: rx.search(line) is not None
264
+
265
+ base = Path(path)
266
+ ignore_patterns = _load_gitignore_patterns(base)
267
+ hits: list[tuple[Path, int, str]] = []
268
+ for file_path in base.rglob(glob_pattern):
269
+ if len(hits) >= limit:
270
+ break
271
+ if file_path.is_dir():
272
+ continue
273
+ # Skip hidden files unless requested
274
+ if not hidden and any(part.startswith(".") for part in file_path.parts):
275
+ continue
276
+ # Skip gitignored paths
277
+ if _match_gitignore(file_path, ignore_patterns, base):
278
+ continue
279
+ try:
280
+ lines = file_path.read_text(encoding="utf-8").splitlines()
281
+ except Exception:
282
+ continue
283
+ for i, line in enumerate(lines, 1):
284
+ if len(hits) >= limit:
285
+ break
286
+ if match_fn(line):
287
+ hits.append((file_path, i, line))
288
+ _emit_status("rgrep", pattern=pattern, path=str(base), count=len(hits), hits=[{"file": str(h[0]), "line": h[1], "text": h[2][:80]} for h in hits[:10]])
289
+ return hits
290
+
291
+ @_category("Find/Replace")
292
+ def replace(path: str | Path, pattern: str, repl: str, *, regex: bool = False) -> int:
293
+ """Replace text in a file (regex optional)."""
294
+ p = Path(path)
295
+ data = p.read_text(encoding="utf-8")
296
+ if regex:
297
+ new, count = re.subn(pattern, repl, data)
298
+ else:
299
+ new = data.replace(pattern, repl)
300
+ count = data.count(pattern)
301
+ p.write_text(new, encoding="utf-8")
302
+ _emit_status("replace", path=str(p), count=count)
303
+ return count
304
+
305
+ class ShellResult:
306
+ """Result from shell command execution."""
307
+ __slots__ = ("stdout", "stderr", "code")
308
+ def __init__(self, stdout: str, stderr: str, code: int):
309
+ self.stdout = stdout
310
+ self.stderr = stderr
311
+ self.code = code
312
+ def __repr__(self):
313
+ if self.code == 0:
314
+ return ""
315
+ return f"exit code {self.code}"
316
+ def __bool__(self):
317
+ return self.code == 0
318
+
319
+ def _make_shell_result(proc: subprocess.CompletedProcess[str], cmd: str) -> ShellResult:
320
+ """Create ShellResult and emit status."""
321
+ output = proc.stdout + proc.stderr if proc.stderr else proc.stdout
322
+ _emit_status("sh", cmd=cmd[:80], code=proc.returncode, output=output[:500])
323
+ return ShellResult(proc.stdout, proc.stderr, proc.returncode)
324
+
325
+ import signal as _signal
326
+
327
+ def _run_with_interrupt(args: list[str], cwd: str | None, timeout: int | None, cmd: str) -> ShellResult:
328
+ """Run subprocess with proper interrupt handling."""
329
+ proc = subprocess.Popen(
330
+ args,
331
+ cwd=cwd,
332
+ stdout=subprocess.PIPE,
333
+ stderr=subprocess.PIPE,
334
+ text=True,
335
+ start_new_session=True,
336
+ )
337
+ try:
338
+ stdout, stderr = proc.communicate(timeout=timeout)
339
+ except KeyboardInterrupt:
340
+ os.killpg(proc.pid, _signal.SIGINT)
341
+ try:
342
+ stdout, stderr = proc.communicate(timeout=2)
343
+ except subprocess.TimeoutExpired:
344
+ os.killpg(proc.pid, _signal.SIGKILL)
345
+ stdout, stderr = proc.communicate()
346
+ result = subprocess.CompletedProcess(args, -_signal.SIGINT, stdout, stderr)
347
+ return _make_shell_result(result, cmd)
348
+ except subprocess.TimeoutExpired:
349
+ os.killpg(proc.pid, _signal.SIGKILL)
350
+ stdout, stderr = proc.communicate()
351
+ result = subprocess.CompletedProcess(args, -_signal.SIGKILL, stdout, stderr)
352
+ return _make_shell_result(result, cmd)
353
+ result = subprocess.CompletedProcess(args, proc.returncode, stdout, stderr)
354
+ return _make_shell_result(result, cmd)
355
+
356
+ @_category("Shell")
357
+ def run(cmd: str, *, cwd: str | Path | None = None, timeout: int | None = None) -> ShellResult:
358
+ """Run a shell command."""
359
+ shell_path = shutil.which("bash") or shutil.which("sh") or "/bin/sh"
360
+ args = [shell_path, "-c", cmd]
361
+ return _run_with_interrupt(args, str(cwd) if cwd else None, timeout, cmd)
362
+
363
+ @_category("Text")
364
+ def sort_lines(text: str, *, reverse: bool = False, unique: bool = False) -> str:
365
+ """Sort lines of text."""
366
+ lines = text.splitlines()
367
+ if unique:
368
+ lines = list(dict.fromkeys(lines))
369
+ lines = sorted(lines, reverse=reverse)
370
+ out = "\n".join(lines)
371
+ _emit_status("sort_lines", lines=len(lines), unique=unique, reverse=reverse)
372
+ return out
373
+
374
+ @_category("Text")
375
+ def uniq(text: str, *, count: bool = False) -> str | list[tuple[int, str]]:
376
+ """Remove duplicate adjacent lines (like uniq)."""
377
+ lines = text.splitlines()
378
+ if not lines:
379
+ _emit_status("uniq", groups=0)
380
+ return [] if count else ""
381
+ groups: list[tuple[int, str]] = []
382
+ current = lines[0]
383
+ current_count = 1
384
+ for line in lines[1:]:
385
+ if line == current:
386
+ current_count += 1
387
+ continue
388
+ groups.append((current_count, current))
389
+ current = line
390
+ current_count = 1
391
+ groups.append((current_count, current))
392
+ _emit_status("uniq", groups=len(groups), count_mode=count)
393
+ if count:
394
+ return groups
395
+ return "\n".join(line for _, line in groups)
396
+
397
+ @_category("Text")
398
+ def counter(
399
+ items: str | list,
400
+ *,
401
+ limit: int | None = None,
402
+ reverse: bool = True,
403
+ ) -> list[tuple[int, str]]:
404
+ """Count occurrences and sort by frequency. Like sort | uniq -c | sort -rn.
405
+
406
+ items: text (splits into lines) or list of strings
407
+ reverse: True for descending (most common first), False for ascending
408
+ Returns: [(count, item), ...] sorted by count
409
+ """
410
+ from collections import Counter
411
+ if isinstance(items, str):
412
+ items = items.splitlines()
413
+ counts = Counter(items)
414
+ sorted_items = sorted(counts.items(), key=lambda x: (x[1], x[0]), reverse=reverse)
415
+ if limit is not None:
416
+ sorted_items = sorted_items[:limit]
417
+ result = [(count, item) for item, count in sorted_items]
418
+ _emit_status("counter", unique=len(counts), total=sum(counts.values()), top=result[:10])
419
+ return result
420
+
421
+ @_category("Text")
422
+ def cols(text: str, *indices: int, sep: str | None = None) -> str:
423
+ """Extract columns from text (0-indexed). Like cut."""
424
+ result_lines = []
425
+ for line in text.splitlines():
426
+ parts = line.split(sep) if sep else line.split()
427
+ selected = [parts[i] for i in indices if i < len(parts)]
428
+ result_lines.append(" ".join(selected))
429
+ out = "\n".join(result_lines)
430
+ _emit_status("cols", lines=len(result_lines), columns=list(indices))
431
+ return out
432
+
433
+ @_category("Navigation")
434
+ def tree(path: str | Path = ".", *, max_depth: int = 3, show_hidden: bool = False) -> str:
435
+ """Return directory tree."""
436
+ base = Path(path)
437
+ lines = []
438
+ def walk(p: Path, prefix: str, depth: int):
439
+ if depth > max_depth:
440
+ return
441
+ items = sorted(p.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
442
+ items = [i for i in items if show_hidden or not i.name.startswith(".")]
443
+ for i, item in enumerate(items):
444
+ is_last = i == len(items) - 1
445
+ connector = "└── " if is_last else "├── "
446
+ suffix = "/" if item.is_dir() else ""
447
+ lines.append(f"{prefix}{connector}{item.name}{suffix}")
448
+ if item.is_dir():
449
+ ext = " " if is_last else "│ "
450
+ walk(item, prefix + ext, depth + 1)
451
+ lines.append(str(base) + "/")
452
+ walk(base, "", 1)
453
+ out = "\n".join(lines)
454
+ _emit_status("tree", path=str(base), entries=len(lines) - 1, preview=out[:1000])
455
+ return out
456
+
457
+ @_category("Navigation")
458
+ def stat(path: str | Path) -> dict:
459
+ """Get file/directory info."""
460
+ p = Path(path)
461
+ s = p.stat()
462
+ info = {
463
+ "path": str(p),
464
+ "size": s.st_size,
465
+ "is_file": p.is_file(),
466
+ "is_dir": p.is_dir(),
467
+ "mtime": datetime.fromtimestamp(s.st_mtime).isoformat(),
468
+ "mode": oct(s.st_mode),
469
+ }
470
+ _emit_status("stat", path=str(p), size=s.st_size, is_dir=p.is_dir(), mtime=info["mtime"])
471
+ return info
472
+
473
+ @_category("Batch")
474
+ def diff(a: str | Path, b: str | Path) -> str:
475
+ """Compare two files, return unified diff."""
476
+ import difflib
477
+ path_a, path_b = Path(a), Path(b)
478
+ lines_a = path_a.read_text(encoding="utf-8").splitlines(keepends=True)
479
+ lines_b = path_b.read_text(encoding="utf-8").splitlines(keepends=True)
480
+ result = difflib.unified_diff(lines_a, lines_b, fromfile=str(path_a), tofile=str(path_b))
481
+ out = "".join(result)
482
+ _emit_status("diff", file_a=str(path_a), file_b=str(path_b), identical=not out, preview=out[:500])
483
+ return out
484
+
485
+ @_category("Search")
486
+ def glob_files(pattern: str, path: str | Path = ".", *, hidden: bool = False) -> list[Path]:
487
+ """Non-recursive glob (use find() for recursive). Respects .gitignore."""
488
+ p = Path(path)
489
+ ignore_patterns = _load_gitignore_patterns(p)
490
+ matches: list[Path] = []
491
+ for m in p.glob(pattern):
492
+ # Skip hidden files unless requested
493
+ if not hidden and m.name.startswith("."):
494
+ continue
495
+ # Skip gitignored paths
496
+ if _match_gitignore(m, ignore_patterns, p):
497
+ continue
498
+ matches.append(m)
499
+ matches = sorted(matches)
500
+ _emit_status("glob", pattern=pattern, path=str(p), count=len(matches), matches=[str(m) for m in matches[:20]])
501
+ return matches
502
+
503
+ @_category("Find/Replace")
504
+ def sed(path: str | Path, pattern: str, repl: str, *, flags: int = 0) -> int:
505
+ """Regex replace in file (like sed -i). Returns count."""
506
+ p = Path(path)
507
+ data = p.read_text(encoding="utf-8")
508
+ new, count = re.subn(pattern, repl, data, flags=flags)
509
+ p.write_text(new, encoding="utf-8")
510
+ _emit_status("sed", path=str(p), count=count)
511
+ return count
512
+
513
+ @_category("Find/Replace")
514
+ def rsed(
515
+ pattern: str,
516
+ repl: str,
517
+ path: str | Path = ".",
518
+ *,
519
+ glob_pattern: str = "*",
520
+ flags: int = 0,
521
+ hidden: bool = False,
522
+ ) -> int:
523
+ """Recursive sed across files matching glob_pattern. Respects .gitignore."""
524
+ base = Path(path)
525
+ ignore_patterns = _load_gitignore_patterns(base)
526
+ total = 0
527
+ files_changed = 0
528
+ changed_files = []
529
+ for file_path in base.rglob(glob_pattern):
530
+ if file_path.is_dir():
531
+ continue
532
+ # Skip hidden files unless requested
533
+ if not hidden and any(part.startswith(".") for part in file_path.parts):
534
+ continue
535
+ # Skip gitignored paths
536
+ if _match_gitignore(file_path, ignore_patterns, base):
537
+ continue
538
+ try:
539
+ data = file_path.read_text(encoding="utf-8")
540
+ new, count = re.subn(pattern, repl, data, flags=flags)
541
+ if count > 0:
542
+ file_path.write_text(new, encoding="utf-8")
543
+ total += count
544
+ files_changed += 1
545
+ if len(changed_files) < 10:
546
+ changed_files.append({"file": str(file_path), "count": count})
547
+ except Exception:
548
+ continue
549
+ _emit_status("rsed", path=str(base), count=total, files=files_changed, changed=changed_files)
550
+ return total
551
+
552
+ @_category("Line ops")
553
+ def lines(path: str | Path, start: int = 1, end: int | None = None) -> str:
554
+ """Extract line range from file (1-indexed, inclusive). Like sed -n 'N,Mp'."""
555
+ p = Path(path)
556
+ all_lines = p.read_text(encoding="utf-8").splitlines()
557
+ if end is None:
558
+ end = len(all_lines)
559
+ start = max(1, start)
560
+ end = min(len(all_lines), end)
561
+ selected = all_lines[start - 1 : end]
562
+ out = "\n".join(selected)
563
+ _emit_status("lines", path=str(p), start=start, end=end, count=len(selected), preview=out[:500])
564
+ return out
565
+
566
+ @_category("Line ops")
567
+ def delete_lines(path: str | Path, start: int, end: int | None = None) -> int:
568
+ """Delete line range from file (1-indexed, inclusive). Like sed -i 'N,Md'."""
569
+ p = Path(path)
570
+ all_lines = p.read_text(encoding="utf-8").splitlines()
571
+ if end is None:
572
+ end = start
573
+ start = max(1, start)
574
+ end = min(len(all_lines), end)
575
+ count = end - start + 1
576
+ new_lines = all_lines[: start - 1] + all_lines[end:]
577
+ p.write_text("\n".join(new_lines) + ("\n" if all_lines else ""), encoding="utf-8")
578
+ _emit_status("delete_lines", path=str(p), start=start, end=end, count=count)
579
+ return count
580
+
581
+ @_category("Line ops")
582
+ def delete_matching(path: str | Path, pattern: str, *, regex: bool = True) -> int:
583
+ """Delete lines matching pattern. Like sed -i '/pattern/d'."""
584
+ p = Path(path)
585
+ all_lines = p.read_text(encoding="utf-8").splitlines()
586
+ if regex:
587
+ rx = re.compile(pattern)
588
+ new_lines = [l for l in all_lines if not rx.search(l)]
589
+ else:
590
+ new_lines = [l for l in all_lines if pattern not in l]
591
+ count = len(all_lines) - len(new_lines)
592
+ p.write_text("\n".join(new_lines) + ("\n" if all_lines else ""), encoding="utf-8")
593
+ _emit_status("delete_matching", path=str(p), pattern=pattern, count=count)
594
+ return count
595
+
596
+ @_category("Line ops")
597
+ def insert_at(path: str | Path, line_num: int, text: str, *, after: bool = True) -> Path:
598
+ """Insert text at line. after=True (sed 'Na\\'), after=False (sed 'Ni\\')."""
599
+ p = Path(path)
600
+ all_lines = p.read_text(encoding="utf-8").splitlines()
601
+ new_lines = text.splitlines()
602
+ line_num = max(1, min(len(all_lines) + 1, line_num))
603
+ if after:
604
+ idx = min(line_num, len(all_lines))
605
+ all_lines = all_lines[:idx] + new_lines + all_lines[idx:]
606
+ pos = "after"
607
+ else:
608
+ idx = line_num - 1
609
+ all_lines = all_lines[:idx] + new_lines + all_lines[idx:]
610
+ pos = "before"
611
+ p.write_text("\n".join(all_lines) + "\n", encoding="utf-8")
612
+ _emit_status("insert_at", path=str(p), line=line_num, lines_inserted=len(new_lines), position=pos)
613
+ return p
614
+
615
+ @_category("Agent")
616
+ def output(
617
+ *ids: str,
618
+ format: str = "raw",
619
+ query: str | None = None,
620
+ offset: int | None = None,
621
+ limit: int | None = None,
622
+ ) -> str | dict | list[dict]:
623
+ """Read task/agent output by ID. Returns text or JSON depending on format.
624
+
625
+ Args:
626
+ *ids: Output IDs to read (e.g., 'explore_0', 'reviewer_1')
627
+ format: 'raw' (default), 'json' (dict with metadata), 'stripped' (no ANSI)
628
+ query: jq-like query for JSON outputs (e.g., '.endpoints[0].file')
629
+ offset: Line number to start reading from (1-indexed)
630
+ limit: Maximum number of lines to read
631
+
632
+ Returns:
633
+ Single ID: str (format='raw'/'stripped') or dict (format='json')
634
+ Multiple IDs: list of dict with 'id' and 'content'/'data' keys
635
+
636
+ Examples:
637
+ output('explore_0') # Read as raw text
638
+ output('reviewer_0', format='json') # Read with metadata
639
+ output('explore_0', query='.files[0]') # Extract JSON field
640
+ output('explore_0', offset=10, limit=20) # Lines 10-29
641
+ output('explore_0', 'reviewer_1') # Read multiple outputs
642
+ """
643
+ session_file = os.environ.get("ARCANE_SESSION_FILE")
644
+ if not session_file:
645
+ _emit_status("output", error="No session file available")
646
+ raise RuntimeError("No session - output artifacts unavailable")
647
+
648
+ artifacts_dir = session_file.rsplit(".", 1)[0] # Strip .jsonl extension
649
+ if not Path(artifacts_dir).exists():
650
+ _emit_status("output", error="Artifacts directory not found", path=artifacts_dir)
651
+ raise RuntimeError(f"No artifacts directory found: {artifacts_dir}")
652
+
653
+ if not ids:
654
+ _emit_status("output", error="No IDs provided")
655
+ raise ValueError("At least one output ID is required")
656
+
657
+ if query and (offset is not None or limit is not None):
658
+ _emit_status("output", error="query cannot be combined with offset/limit")
659
+ raise ValueError("query cannot be combined with offset/limit")
660
+
661
+ results: list[dict] = []
662
+ not_found: list[str] = []
663
+
664
+ for output_id in ids:
665
+ output_path = Path(artifacts_dir) / f"{output_id}.md"
666
+ if not output_path.exists():
667
+ not_found.append(output_id)
668
+ continue
669
+
670
+ raw_content = output_path.read_text(encoding="utf-8")
671
+ raw_lines = raw_content.splitlines()
672
+ total_lines = len(raw_lines)
673
+
674
+ selected_content = raw_content
675
+ range_info: dict | None = None
676
+
677
+ # Handle query
678
+ if query:
679
+ try:
680
+ json_value = json.loads(raw_content)
681
+ except json.JSONDecodeError as e:
682
+ _emit_status("output", id=output_id, error=f"Not valid JSON: {e}")
683
+ raise ValueError(f"Output {output_id} is not valid JSON: {e}")
684
+
685
+ # Apply jq-like query
686
+ result_value = _apply_query(json_value, query)
687
+ try:
688
+ selected_content = json.dumps(result_value, indent=2) if result_value is not None else "null"
689
+ except (TypeError, ValueError):
690
+ selected_content = str(result_value)
691
+
692
+ # Handle offset/limit
693
+ elif offset is not None or limit is not None:
694
+ start_line = max(1, offset or 1)
695
+ if start_line > total_lines:
696
+ _emit_status("output", id=output_id, error=f"Offset {start_line} beyond end ({total_lines} lines)")
697
+ raise ValueError(f"Offset {start_line} is beyond end of output ({total_lines} lines) for {output_id}")
698
+
699
+ effective_limit = limit if limit is not None else total_lines - start_line + 1
700
+ end_line = min(total_lines, start_line + effective_limit - 1)
701
+ selected_lines = raw_lines[start_line - 1 : end_line]
702
+ selected_content = "\n".join(selected_lines)
703
+ range_info = {"start_line": start_line, "end_line": end_line, "total_lines": total_lines}
704
+
705
+ # Strip ANSI codes if requested
706
+ if format == "stripped":
707
+ import re
708
+ selected_content = re.sub(r"\x1b\[[0-9;]*m", "", selected_content)
709
+
710
+ # Build result
711
+ if format == "json":
712
+ result_data = {
713
+ "id": output_id,
714
+ "path": str(output_path),
715
+ "line_count": total_lines if not query else len(selected_content.splitlines()),
716
+ "char_count": len(raw_content) if not query else len(selected_content),
717
+ "content": selected_content,
718
+ }
719
+ if range_info:
720
+ result_data["range"] = range_info
721
+ if query:
722
+ result_data["query"] = query
723
+ results.append(result_data)
724
+ else:
725
+ results.append({"id": output_id, "content": selected_content})
726
+
727
+ # Handle not found
728
+ if not_found:
729
+ available = sorted(
730
+ [f.stem for f in Path(artifacts_dir).glob("*.md")]
731
+ )
732
+ error_msg = f"Output not found: {', '.join(not_found)}"
733
+ if available:
734
+ error_msg += f"\n\nAvailable outputs: {', '.join(available[:20])}"
735
+ if len(available) > 20:
736
+ error_msg += f" (and {len(available) - 20} more)"
737
+ _emit_status("output", not_found=not_found, available_count=len(available))
738
+ raise FileNotFoundError(error_msg)
739
+
740
+ # Return format
741
+ if len(ids) == 1:
742
+ if format == "json":
743
+ _emit_status("output", id=ids[0], chars=results[0]["char_count"])
744
+ return results[0]
745
+ _emit_status("output", id=ids[0], chars=len(results[0]["content"]))
746
+ return results[0]["content"]
747
+
748
+ # Multiple IDs
749
+ if format == "json":
750
+ total_chars = sum(r["char_count"] for r in results)
751
+ _emit_status("output", count=len(results), total_chars=total_chars)
752
+ return results
753
+
754
+ combined_output: list[dict] = []
755
+ for r in results:
756
+ combined_output.append({"id": r["id"], "content": r["content"]})
757
+ total_chars = sum(len(r["content"]) for r in combined_output)
758
+ _emit_status("output", count=len(combined_output), total_chars=total_chars)
759
+ return combined_output
760
+
761
+ def _apply_query(data: any, query: str) -> any:
762
+ """Apply jq-like query to data. Supports .key, [index], and chaining."""
763
+ if not query:
764
+ return data
765
+
766
+ query = query.strip()
767
+ if query.startswith("."):
768
+ query = query[1:]
769
+ if not query:
770
+ return data
771
+
772
+ # Parse query into tokens
773
+ tokens = []
774
+ current_token = ""
775
+ i = 0
776
+ while i < len(query):
777
+ ch = query[i]
778
+ if ch == ".":
779
+ if current_token:
780
+ tokens.append(("key", current_token))
781
+ current_token = ""
782
+ elif ch == "[":
783
+ if current_token:
784
+ tokens.append(("key", current_token))
785
+ current_token = ""
786
+ # Find matching ]
787
+ j = i + 1
788
+ while j < len(query) and query[j] != "]":
789
+ j += 1
790
+ bracket_content = query[i+1:j]
791
+ if bracket_content.startswith('"') and bracket_content.endswith('"'):
792
+ tokens.append(("key", bracket_content[1:-1]))
793
+ else:
794
+ tokens.append(("index", int(bracket_content)))
795
+ i = j
796
+ else:
797
+ current_token += ch
798
+ i += 1
799
+ if current_token:
800
+ tokens.append(("key", current_token))
801
+
802
+ # Apply tokens
803
+ current = data
804
+ for token_type, value in tokens:
805
+ if token_type == "index":
806
+ if not isinstance(current, list) or value >= len(current):
807
+ return None
808
+ current = current[value]
809
+ elif token_type == "key":
810
+ if not isinstance(current, dict) or value not in current:
811
+ return None
812
+ current = current[value]
813
+
814
+ return current
815
+
816
+ def __arc_prelude_docs__() -> list[dict[str, str]]:
817
+ """Return prelude helper docs for templating. Discovers functions by _arc_category attribute."""
818
+ helpers: list[dict[str, str]] = []
819
+ for name, obj in globals().items():
820
+ if not callable(obj) or not hasattr(obj, "_arc_category"):
821
+ continue
822
+ signature = str(inspect.signature(obj))
823
+ doc = inspect.getdoc(obj) or ""
824
+ docline = doc.splitlines()[0] if doc else ""
825
+ helpers.append({
826
+ "name": name,
827
+ "signature": signature,
828
+ "docstring": docline,
829
+ "category": obj._arc_category,
830
+ })
831
+ return sorted(helpers, key=lambda h: (h["category"], h["name"]))