muonroi-cli 1.7.2 → 1.8.1

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 (326) hide show
  1. package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
  2. package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
  3. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
  4. package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
  5. package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
  6. package/dist/packages/agent-harness-core/src/selector.js +2 -2
  7. package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
  8. package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
  9. package/dist/packages/agent-harness-opentui/src/install.js +1 -1
  10. package/dist/src/__test-stubs__/vitest-setup.js +4 -0
  11. package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
  12. package/dist/src/agent-harness/mock-model.d.ts +1 -1
  13. package/dist/src/agent-harness/mock-model.js +143 -6
  14. package/dist/src/agent-harness/test-spawn.js +3 -1
  15. package/dist/src/chat/chat-keychain.js +9 -1
  16. package/dist/src/cli/cost-forensics.js +7 -4
  17. package/dist/src/cli/keys.d.ts +2 -2
  18. package/dist/src/cli/keys.js +107 -28
  19. package/dist/src/cli/usage-report.js +2 -2
  20. package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
  21. package/dist/src/council/__tests__/abort-threading.test.js +193 -0
  22. package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
  23. package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
  24. package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
  25. package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
  26. package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
  27. package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
  28. package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
  29. package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
  30. package/dist/src/council/__tests__/research-tools.test.js +4 -0
  31. package/dist/src/council/__tests__/round-tools.test.js +6 -0
  32. package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
  33. package/dist/src/council/clarifier-question-cap.test.js +73 -0
  34. package/dist/src/council/clarifier.d.ts +29 -1
  35. package/dist/src/council/clarifier.js +64 -1
  36. package/dist/src/council/debate-planner.d.ts +2 -1
  37. package/dist/src/council/debate-planner.js +5 -3
  38. package/dist/src/council/debate.d.ts +9 -0
  39. package/dist/src/council/debate.js +88 -8
  40. package/dist/src/council/index.d.ts +46 -1
  41. package/dist/src/council/index.js +125 -19
  42. package/dist/src/council/llm.js +41 -13
  43. package/dist/src/council/prompts.js +36 -1
  44. package/dist/src/council/types.d.ts +14 -1
  45. package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
  46. package/dist/src/ee/bridge.d.ts +3 -1
  47. package/dist/src/ee/bridge.js +9 -3
  48. package/dist/src/ee/bridge.test.js +1 -2
  49. package/dist/src/ee/client.js +15 -2
  50. package/dist/src/ee/recall-ledger.d.ts +20 -0
  51. package/dist/src/ee/recall-ledger.js +40 -1
  52. package/dist/src/ee/search.d.ts +25 -0
  53. package/dist/src/ee/search.js +73 -0
  54. package/dist/src/ee/types.d.ts +15 -1
  55. package/dist/src/ee/who-am-i.d.ts +41 -0
  56. package/dist/src/ee/who-am-i.js +143 -0
  57. package/dist/src/ee/who-am-i.test.d.ts +1 -0
  58. package/dist/src/ee/who-am-i.test.js +101 -0
  59. package/dist/src/flow/compaction/index.d.ts +1 -0
  60. package/dist/src/flow/compaction/index.js +4 -0
  61. package/dist/src/generated/version.d.ts +1 -1
  62. package/dist/src/generated/version.js +1 -1
  63. package/dist/src/headless/council-answers.js +1 -0
  64. package/dist/src/headless/output.d.ts +30 -1
  65. package/dist/src/headless/output.js +180 -0
  66. package/dist/src/headless/output.test.js +61 -1
  67. package/dist/src/hooks/index.js +48 -2
  68. package/dist/src/index.d.ts +2 -0
  69. package/dist/src/index.js +59 -62
  70. package/dist/src/lsp/client-normalize.test.d.ts +12 -0
  71. package/dist/src/lsp/client-normalize.test.js +34 -0
  72. package/dist/src/lsp/client.d.ts +1 -0
  73. package/dist/src/lsp/client.js +10 -2
  74. package/dist/src/lsp/manager.d.ts +1 -1
  75. package/dist/src/lsp/manager.js +13 -5
  76. package/dist/src/lsp/manager.test.js +41 -2
  77. package/dist/src/lsp/runtime.d.ts +1 -1
  78. package/dist/src/lsp/runtime.js +2 -2
  79. package/dist/src/maintain/__tests__/gh-create-pr.test.js +7 -3
  80. package/dist/src/maintain/__tests__/pr-builder.test.js +7 -3
  81. package/dist/src/mcp/__tests__/client-pool.spec.js +3 -3
  82. package/dist/src/mcp/__tests__/ee-tools.test.js +42 -14
  83. package/dist/src/mcp/__tests__/forensics-tools.test.js +3 -3
  84. package/dist/src/mcp/__tests__/lsp-tools.test.js +4 -4
  85. package/dist/src/mcp/auto-setup.js +0 -8
  86. package/dist/src/mcp/client-pool.d.ts +1 -1
  87. package/dist/src/mcp/client-pool.js +25 -9
  88. package/dist/src/mcp/ee-tools.d.ts +10 -0
  89. package/dist/src/mcp/ee-tools.js +34 -0
  90. package/dist/src/mcp/self-verify-jobs.d.ts +1 -0
  91. package/dist/src/mcp/self-verify-jobs.js +1 -3
  92. package/dist/src/mcp/self-verify-runner.js +1 -1
  93. package/dist/src/models/catalog-gemini.test.js +12 -10
  94. package/dist/src/models/catalog.json +106 -38
  95. package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
  96. package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
  97. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +3 -0
  98. package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
  99. package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
  100. package/dist/src/orchestrator/__tests__/message-processor.test.js +2 -0
  101. package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
  102. package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
  103. package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
  104. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
  105. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -0
  106. package/dist/src/orchestrator/agent.test.js +5 -2
  107. package/dist/src/orchestrator/auto-commit.d.ts +110 -0
  108. package/dist/src/orchestrator/auto-commit.js +366 -0
  109. package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
  110. package/dist/src/orchestrator/batch-turn-runner.js +2 -1
  111. package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
  112. package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
  113. package/dist/src/orchestrator/compaction.d.ts +22 -0
  114. package/dist/src/orchestrator/compaction.js +42 -0
  115. package/dist/src/orchestrator/delegations.d.ts +1 -0
  116. package/dist/src/orchestrator/delegations.js +41 -0
  117. package/dist/src/orchestrator/error-utils.js +27 -2
  118. package/dist/src/orchestrator/message-processor.d.ts +23 -5
  119. package/dist/src/orchestrator/message-processor.js +226 -2573
  120. package/dist/src/orchestrator/orchestrator.d.ts +11 -2
  121. package/dist/src/orchestrator/orchestrator.js +530 -43
  122. package/dist/src/orchestrator/pending-calls.js +2 -1
  123. package/dist/src/orchestrator/preprocessor.d.ts +13 -0
  124. package/dist/src/orchestrator/preprocessor.js +146 -0
  125. package/dist/src/orchestrator/prompts.d.ts +16 -0
  126. package/dist/src/orchestrator/prompts.js +130 -67
  127. package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
  128. package/dist/src/orchestrator/safety-askcard.js +74 -0
  129. package/dist/src/orchestrator/sandbox.test.js +8 -5
  130. package/dist/src/orchestrator/scope-ceiling.js +1 -1
  131. package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
  132. package/dist/src/orchestrator/stall-watchdog.js +23 -0
  133. package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
  134. package/dist/src/orchestrator/stream-runner.js +52 -30
  135. package/dist/src/orchestrator/sub-agent-cap.d.ts +2 -0
  136. package/dist/src/orchestrator/sub-agent-cap.js +12 -4
  137. package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
  138. package/dist/src/orchestrator/subagent-compactor.d.ts +21 -0
  139. package/dist/src/orchestrator/subagent-compactor.js +148 -24
  140. package/dist/src/orchestrator/subagent-compactor.spec.js +249 -1
  141. package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
  142. package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
  143. package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
  144. package/dist/src/orchestrator/tool-args-repair.js +1 -1
  145. package/dist/src/orchestrator/tool-engine.d.ts +153 -0
  146. package/dist/src/orchestrator/tool-engine.js +3138 -0
  147. package/dist/src/orchestrator/turn-runner-deps.d.ts +3 -0
  148. package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
  149. package/dist/src/pil/__tests__/discovery.test.js +89 -145
  150. package/dist/src/pil/__tests__/dual-run.test.js +2 -0
  151. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
  152. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  153. package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
  154. package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
  155. package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
  156. package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
  157. package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
  158. package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
  159. package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
  160. package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
  161. package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
  162. package/dist/src/pil/__tests__/llm-classify.test.js +79 -1
  163. package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
  164. package/dist/src/pil/__tests__/pipeline.test.js +11 -8
  165. package/dist/src/pil/__tests__/response-tools.test.js +52 -0
  166. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  167. package/dist/src/pil/agent-operating-contract.js +5 -3
  168. package/dist/src/pil/agent-operating-contract.test.js +2 -2
  169. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  170. package/dist/src/pil/cheap-model-playbook.js +1 -1
  171. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  172. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  173. package/dist/src/pil/config.d.ts +20 -0
  174. package/dist/src/pil/config.js +35 -0
  175. package/dist/src/pil/discovery-types.d.ts +32 -9
  176. package/dist/src/pil/discovery.d.ts +3 -3
  177. package/dist/src/pil/discovery.js +138 -244
  178. package/dist/src/pil/index.d.ts +1 -1
  179. package/dist/src/pil/index.js +1 -1
  180. package/dist/src/pil/layer1-intent.d.ts +9 -1
  181. package/dist/src/pil/layer1-intent.js +62 -7
  182. package/dist/src/pil/layer1-intent.test.js +112 -2
  183. package/dist/src/pil/layer15-context-scan.js +1 -1
  184. package/dist/src/pil/layer16-clarity.d.ts +21 -25
  185. package/dist/src/pil/layer16-clarity.js +52 -88
  186. package/dist/src/pil/layer18-acceptance.d.ts +13 -3
  187. package/dist/src/pil/layer18-acceptance.js +1 -75
  188. package/dist/src/pil/layer2-personality.js +7 -8
  189. package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
  190. package/dist/src/pil/layer2_5-ponytail.js +41 -0
  191. package/dist/src/pil/layer3-ee-injection.js +180 -14
  192. package/dist/src/pil/layer4-gsd.js +22 -8
  193. package/dist/src/pil/layer5-context.d.ts +1 -0
  194. package/dist/src/pil/layer5-context.js +26 -11
  195. package/dist/src/pil/layer6-output.js +23 -5
  196. package/dist/src/pil/llm-classify.d.ts +11 -0
  197. package/dist/src/pil/llm-classify.js +196 -3
  198. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  199. package/dist/src/pil/native-capabilities-workbook.js +4 -3
  200. package/dist/src/pil/pipeline.js +15 -1
  201. package/dist/src/pil/response-tools.d.ts +16 -0
  202. package/dist/src/pil/response-tools.js +41 -0
  203. package/dist/src/pil/schema.d.ts +8 -0
  204. package/dist/src/pil/schema.js +24 -3
  205. package/dist/src/pil/task-tier-map.js +7 -7
  206. package/dist/src/pil/types.d.ts +9 -3
  207. package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
  208. package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
  209. package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
  210. package/dist/src/product-loop/discovery-interview.d.ts +20 -0
  211. package/dist/src/product-loop/discovery-interview.js +121 -7
  212. package/dist/src/product-loop/gather.js +19 -5
  213. package/dist/src/product-loop/loop-driver.js +2 -2
  214. package/dist/src/product-loop/sprint-self-verify.js +2 -2
  215. package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
  216. package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
  217. package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
  218. package/dist/src/providers/auth/gcloud.d.ts +28 -0
  219. package/dist/src/providers/auth/gcloud.js +102 -0
  220. package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
  221. package/dist/src/providers/auth/gemini-oauth.js +292 -105
  222. package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
  223. package/dist/src/providers/auth/grok-oauth.js +1 -1
  224. package/dist/src/providers/auth/registry.js +28 -6
  225. package/dist/src/providers/gemini.d.ts +1 -1
  226. package/dist/src/providers/gemini.js +10 -3
  227. package/dist/src/providers/keychain.d.ts +2 -2
  228. package/dist/src/providers/keychain.js +20 -2
  229. package/dist/src/providers/openai.js +5 -1
  230. package/dist/src/providers/runtime.d.ts +1 -1
  231. package/dist/src/providers/runtime.js +11 -2
  232. package/dist/src/providers/siliconflow-sse-repair.js +1 -1
  233. package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
  234. package/dist/src/providers/strategies/google.strategy.js +159 -9
  235. package/dist/src/providers/strategies/thinking-mode.js +19 -6
  236. package/dist/src/providers/types.d.ts +1 -1
  237. package/dist/src/router/decide.test.js +40 -8
  238. package/dist/src/router/step-router.d.ts +20 -0
  239. package/dist/src/router/step-router.js +73 -0
  240. package/dist/src/self-qa/agentic-loop.js +30 -21
  241. package/dist/src/self-qa/orchestrator.js +5 -5
  242. package/dist/src/self-qa/scenario-planner.js +1 -1
  243. package/dist/src/self-qa/spec-emitter.js +4 -4
  244. package/dist/src/storage/__tests__/hierarchical-session.test.d.ts +1 -0
  245. package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
  246. package/dist/src/storage/__tests__/sweep-stale-pending.test.js +2 -2
  247. package/dist/src/storage/index.d.ts +1 -1
  248. package/dist/src/storage/index.js +1 -1
  249. package/dist/src/storage/migrations.js +13 -1
  250. package/dist/src/storage/transcript-response-entry.test.js +18 -5
  251. package/dist/src/storage/transcript-view.js +7 -1
  252. package/dist/src/storage/transcript.d.ts +3 -0
  253. package/dist/src/storage/transcript.js +254 -73
  254. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  255. package/dist/src/storage/usage.js +22 -5
  256. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  257. package/dist/src/tools/bash.d.ts +36 -23
  258. package/dist/src/tools/bash.js +59 -301
  259. package/dist/src/tools/bash.test.js +2 -257
  260. package/dist/src/tools/file-tracker.d.ts +5 -0
  261. package/dist/src/tools/file-tracker.js +14 -0
  262. package/dist/src/tools/file.js +2 -0
  263. package/dist/src/tools/git-safety.d.ts +38 -1
  264. package/dist/src/tools/git-safety.js +94 -4
  265. package/dist/src/tools/git-safety.test.js +45 -1
  266. package/dist/src/tools/native-tools.d.ts +1 -1
  267. package/dist/src/tools/native-tools.js +66 -1
  268. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  269. package/dist/src/tools/registry-git-safety.test.js +17 -6
  270. package/dist/src/tools/registry.d.ts +6 -0
  271. package/dist/src/tools/registry.js +215 -13
  272. package/dist/src/types/index.d.ts +8 -2
  273. package/dist/src/ui/app.d.ts +1 -0
  274. package/dist/src/ui/app.js +0 -0
  275. package/dist/src/ui/components/council-question-card.js +1 -0
  276. package/dist/src/ui/components/message-view.js +37 -3
  277. package/dist/src/ui/components/structured-response-view.js +7 -0
  278. package/dist/src/ui/components/task-list-panel.js +3 -12
  279. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  280. package/dist/src/ui/containers/chat-feed.js +66 -0
  281. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  282. package/dist/src/ui/containers/modals-layer.js +18 -0
  283. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  284. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  285. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  286. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  287. package/dist/src/ui/slash/expand.js +14 -1
  288. package/dist/src/ui/slash/export.js +16 -1
  289. package/dist/src/ui/slash/menu-items.js +1 -0
  290. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  291. package/dist/src/ui/slash/ponytail.js +19 -0
  292. package/dist/src/ui/status-bar/store.js +0 -1
  293. package/dist/src/ui/status-bar/store.test.js +2 -2
  294. package/dist/src/ui/use-app-logic.d.ts +26 -0
  295. package/dist/src/ui/use-app-logic.js +0 -0
  296. package/dist/src/ui/utils/relaunch.js +1 -1
  297. package/dist/src/ui/utils/text.d.ts +11 -0
  298. package/dist/src/ui/utils/text.js +21 -1
  299. package/dist/src/ui/utils/text.test.js +25 -1
  300. package/dist/src/ui/utils/tools.js +3 -1
  301. package/dist/src/usage/cost-log.js +2 -2
  302. package/dist/src/usage/estimator.d.ts +14 -0
  303. package/dist/src/usage/estimator.js +22 -0
  304. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  305. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  306. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  307. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  308. package/dist/src/utils/__tests__/llm-deadline.test.js +16 -4
  309. package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
  310. package/dist/src/utils/__tests__/logger.test.js +115 -0
  311. package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
  312. package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
  313. package/dist/src/utils/install-manager.d.ts +1 -0
  314. package/dist/src/utils/install-manager.js +76 -7
  315. package/dist/src/utils/install-manager.test.js +43 -1
  316. package/dist/src/utils/llm-deadline.d.ts +9 -1
  317. package/dist/src/utils/llm-deadline.js +31 -2
  318. package/dist/src/utils/logger.d.ts +28 -0
  319. package/dist/src/utils/logger.js +171 -0
  320. package/dist/src/utils/permission-mode.d.ts +33 -0
  321. package/dist/src/utils/permission-mode.js +164 -3
  322. package/dist/src/utils/permission-mode.test.js +92 -1
  323. package/dist/src/utils/settings.d.ts +36 -32
  324. package/dist/src/utils/settings.js +151 -123
  325. package/dist/src/utils/update-checker.test.js +5 -1
  326. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { spawn } from "child_process";
1
+ import { exec, spawn } from "child_process";
2
2
  import { createHash } from "crypto";
3
3
  import fs from "fs";
4
4
  import os from "os";
@@ -83,6 +83,34 @@ export function getScriptInstallContext(homeDir = os.homedir()) {
83
83
  }
84
84
  return null;
85
85
  }
86
+ export function fetchLatestGitTag(gitDir) {
87
+ return new Promise((resolve) => {
88
+ exec(`git -C "${gitDir}" ls-remote --tags origin`, (error, stdout) => {
89
+ if (error) {
90
+ resolve(null);
91
+ return;
92
+ }
93
+ const lines = stdout.split(/\r?\n/);
94
+ let maxVersion = null;
95
+ for (const line of lines) {
96
+ const match = line.match(/refs\/tags\/(v?[0-9]+\.[0-9]+\.[0-9]+[^\s]*)$/);
97
+ if (!match)
98
+ continue;
99
+ let tag = match[1];
100
+ if (tag.endsWith("^{}")) {
101
+ tag = tag.slice(0, -3);
102
+ }
103
+ const version = normalizeReleaseVersion(tag);
104
+ if (!version)
105
+ continue;
106
+ if (!maxVersion || semverGt(version, maxVersion)) {
107
+ maxVersion = version;
108
+ }
109
+ }
110
+ resolve(maxVersion);
111
+ });
112
+ });
113
+ }
86
114
  export async function fetchLatestReleaseVersion() {
87
115
  const release = await fetchReleaseJson(`${RELEASES_API}/latest`);
88
116
  return release ? normalizeReleaseVersion(release.tag_name) : null;
@@ -179,31 +207,66 @@ export async function runManagedUpdate(currentVersion) {
179
207
  const method = detectInstallMethod();
180
208
  if (method === "script")
181
209
  return runScriptManagedUpdate(currentVersion);
210
+ const root = findGitRoot(path.dirname(runningModulePath()));
211
+ let latestVersion = null;
212
+ if (method === "dev-link" && root) {
213
+ latestVersion = await fetchLatestGitTag(root);
214
+ }
215
+ else {
216
+ latestVersion = await fetchLatestReleaseVersion();
217
+ }
218
+ const normalizedCurrent = semverValid(currentVersion);
219
+ let statusHeader = "";
220
+ let hasUpdate = false;
221
+ if (latestVersion && normalizedCurrent) {
222
+ hasUpdate = semverGt(latestVersion, normalizedCurrent);
223
+ if (hasUpdate) {
224
+ statusHeader = `A new version of muonroi-cli is available!\n Current version: v${normalizedCurrent}\n Latest version: v${latestVersion}\n\n`;
225
+ }
226
+ else {
227
+ statusHeader = `You are already up to date!\n Current version: v${normalizedCurrent}\n Latest version: v${latestVersion}\n\n`;
228
+ }
229
+ }
230
+ else if (normalizedCurrent) {
231
+ statusHeader = `Current version: v${normalizedCurrent}\nUnable to check the latest version from GitHub.\n\n`;
232
+ }
182
233
  const cmd = getUpdateCommandForMethod(method);
183
234
  if (cmd) {
184
235
  const pm = method === "bun-global" ? "bun" : "npm";
236
+ const instruction = hasUpdate
237
+ ? `To update, run this in a fresh terminal:\n\n ${cmd}\n\nThen restart muonroi-cli.`
238
+ : `If you want to reinstall, run this in a fresh terminal:\n\n ${cmd}`;
185
239
  return {
186
240
  success: true,
187
- output: `Installed via ${pm} (global package). To update, run this in a fresh terminal:\n\n ${cmd}\n\nThen restart muonroi-cli.`,
241
+ output: `${statusHeader}${instruction}`,
188
242
  };
189
243
  }
190
244
  if (method === "compiled") {
191
245
  const target = getReleaseTargetForPlatform();
192
246
  const asset = target?.assetName ?? "the release asset for your platform";
247
+ const instruction = hasUpdate
248
+ ? `Download the latest ${asset} from https://github.com/${GITHUB_REPO}/releases/latest and replace the current binary, or rebuild from source.`
249
+ : `If you want to reinstall, download the latest ${asset} from https://github.com/${GITHUB_REPO}/releases/latest and replace the current binary.`;
193
250
  return {
194
251
  success: true,
195
- output: `Standalone binary install. Download the latest ${asset} from https://github.com/${GITHUB_REPO}/releases/latest and replace the current binary, or rebuild from source.`,
252
+ output: `${statusHeader}${instruction}`,
196
253
  };
197
254
  }
198
255
  if (method === "dev-link") {
199
- const root = findGitRoot(path.dirname(runningModulePath()));
200
256
  const target = root ?? "the muonroi-cli checkout";
257
+ const instruction = hasUpdate
258
+ ? `To update, pull the latest changes and rebuild:\n\n git -C "${target}" pull && bun install && bun run build\n\nThen restart muonroi-cli. (If you also use the compiled muonroi-cli-dev binary, rebuild that separately.)`
259
+ : `To rebuild your local installation:\n\n git -C "${target}" pull && bun install && bun run build\n\nThen restart muonroi-cli.`;
201
260
  return {
202
261
  success: true,
203
- output: `Running a linked/source build from ${target}. To update, rebuild it:\n\n git -C "${target}" pull && bun install && bun run build\n\nThen restart muonroi-cli. (If you also use the compiled muonroi-cli-dev binary, rebuild that separately.)`,
262
+ output: `${statusHeader}${instruction}`,
204
263
  };
205
264
  }
206
- return notScriptManaged("update");
265
+ const fallback = notScriptManaged("update");
266
+ return {
267
+ success: fallback.success,
268
+ output: `${statusHeader}${fallback.output}`,
269
+ };
207
270
  }
208
271
  export async function runScriptManagedUpdate(currentVersion) {
209
272
  const context = getScriptInstallContext();
@@ -348,7 +411,13 @@ async function fetchReleaseJson(url) {
348
411
  try {
349
412
  const controller = new AbortController();
350
413
  const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
351
- const res = await fetch(url, { signal: controller.signal, headers: { Accept: "application/vnd.github+json" } });
414
+ const res = await fetch(url, {
415
+ signal: controller.signal,
416
+ headers: {
417
+ Accept: "application/vnd.github+json",
418
+ "User-Agent": "muonroi-cli",
419
+ },
420
+ });
352
421
  clearTimeout(timer);
353
422
  return res.ok ? (await res.json()) : null;
354
423
  }
@@ -1,7 +1,15 @@
1
+ import { exec } from "child_process";
1
2
  import fs from "fs";
2
3
  import os from "os";
3
4
  import path from "path";
4
- import { afterEach, describe, expect, it } from "vitest";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ vi.mock("child_process", async () => {
7
+ const actual = await vi.importActual("child_process");
8
+ return {
9
+ ...actual,
10
+ exec: vi.fn(),
11
+ };
12
+ });
5
13
  import { buildScriptUninstallPlan, getInstallMetadataPath, getReleaseTargetForPlatform, getScriptInstallContext, getScriptInstallDir, loadScriptInstallMetadata, parseChecksumsFile, saveScriptInstallMetadata, } from "./install-manager.js";
6
14
  let tempDirs = [];
7
15
  afterEach(() => {
@@ -124,4 +132,38 @@ describe("buildScriptUninstallPlan", () => {
124
132
  expect(plan?.removePaths).toContain(path.join(installDir, currentTarget.binaryName));
125
133
  });
126
134
  });
135
+ describe("runManagedUpdate", () => {
136
+ beforeEach(() => {
137
+ vi.stubGlobal("fetch", vi.fn());
138
+ });
139
+ afterEach(() => {
140
+ vi.restoreAllMocks();
141
+ });
142
+ it("handles dev-link update when newer version is available", async () => {
143
+ vi.mocked(exec).mockImplementation(((cmd, callback) => {
144
+ callback(null, "hash\trefs/tags/v2.0.0\n", "");
145
+ return {};
146
+ }));
147
+ const { runManagedUpdate } = await import("./install-manager.js");
148
+ const result = await runManagedUpdate("1.0.0");
149
+ expect(result.success).toBe(true);
150
+ expect(result.output).toContain("A new version of muonroi-cli is available!");
151
+ expect(result.output).toContain("Current version: v1.0.0");
152
+ expect(result.output).toContain("Latest version: v2.0.0");
153
+ expect(result.output).toContain("git -C");
154
+ expect(result.output).toContain("pull && bun install && bun run build");
155
+ });
156
+ it("handles dev-link when already up to date", async () => {
157
+ vi.mocked(exec).mockImplementation(((cmd, callback) => {
158
+ callback(null, "hash\trefs/tags/v1.0.0\n", "");
159
+ return {};
160
+ }));
161
+ const { runManagedUpdate } = await import("./install-manager.js");
162
+ const result = await runManagedUpdate("1.0.0");
163
+ expect(result.success).toBe(true);
164
+ expect(result.output).toContain("You are already up to date!");
165
+ expect(result.output).toContain("Current version: v1.0.0");
166
+ expect(result.output).toContain("Latest version: v1.0.0");
167
+ });
168
+ });
127
169
  //# sourceMappingURL=install-manager.test.js.map
@@ -41,4 +41,12 @@ export declare function withTimeoutSignal(parent: AbortSignal | undefined, timeo
41
41
  * still be aborted via `withTimeoutSignal` for cleanup; this layer just ensures
42
42
  * the caller is never blocked past the deadline.
43
43
  */
44
- export declare function withDeadlineRace<T>(fn: () => Promise<T>, deadlineMs: number, label: string): Promise<T>;
44
+ export declare function withDeadlineRace<T>(fn: () => Promise<T>, deadlineMs: number, label: string,
45
+ /**
46
+ * User-abort signal. When it fires, the race rejects within `abortGraceMs`
47
+ * even if `fn()` hasn't settled — so a provider that ignores its abortSignal
48
+ * mid-call (observed with DeepSeek/grok socket stalls) can't keep the caller
49
+ * (and, for council, the locked composer) blocked until the full `deadlineMs`.
50
+ * The short grace lets the normal fetch-level abort win first when it's quick.
51
+ */
52
+ abortSignal?: AbortSignal, abortGraceMs?: number): Promise<T>;
@@ -62,19 +62,48 @@ export function withTimeoutSignal(parent, timeoutMs) {
62
62
  * still be aborted via `withTimeoutSignal` for cleanup; this layer just ensures
63
63
  * the caller is never blocked past the deadline.
64
64
  */
65
- export async function withDeadlineRace(fn, deadlineMs, label) {
65
+ export async function withDeadlineRace(fn, deadlineMs, label,
66
+ /**
67
+ * User-abort signal. When it fires, the race rejects within `abortGraceMs`
68
+ * even if `fn()` hasn't settled — so a provider that ignores its abortSignal
69
+ * mid-call (observed with DeepSeek/grok socket stalls) can't keep the caller
70
+ * (and, for council, the locked composer) blocked until the full `deadlineMs`.
71
+ * The short grace lets the normal fetch-level abort win first when it's quick.
72
+ */
73
+ abortSignal, abortGraceMs = 1500) {
66
74
  let timer = null;
75
+ let abortTimer = null;
76
+ let abortListener = null;
67
77
  const deadline = new Promise((_, reject) => {
68
78
  timer = setTimeout(() => {
69
79
  reject(new Error(`${label} exceeded ${deadlineMs}ms deadline (timeout)`));
70
80
  }, deadlineMs);
71
81
  });
82
+ const racers = [fn(), deadline];
83
+ if (abortSignal) {
84
+ const abortRace = new Promise((_, reject) => {
85
+ const arm = () => {
86
+ abortTimer = setTimeout(() => reject(new Error(`${label} aborted by user`)), abortGraceMs);
87
+ };
88
+ if (abortSignal.aborted)
89
+ arm();
90
+ else {
91
+ abortListener = arm;
92
+ abortSignal.addEventListener("abort", arm, { once: true });
93
+ }
94
+ });
95
+ racers.push(abortRace);
96
+ }
72
97
  try {
73
- return await Promise.race([fn(), deadline]);
98
+ return await Promise.race(racers);
74
99
  }
75
100
  finally {
76
101
  if (timer)
77
102
  clearTimeout(timer);
103
+ if (abortTimer)
104
+ clearTimeout(abortTimer);
105
+ if (abortListener && abortSignal)
106
+ abortSignal.removeEventListener("abort", abortListener);
78
107
  }
79
108
  }
80
109
  //# sourceMappingURL=llm-deadline.js.map
@@ -0,0 +1,28 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error";
2
+ export type LogNamespace = "cli" | "ui" | "orchestrator" | "storage" | "ee" | "mcp" | "pil" | "router";
3
+ export interface LogContext {
4
+ elapsedMs?: number;
5
+ error?: Error | unknown;
6
+ [key: string]: unknown;
7
+ }
8
+ /**
9
+ * Checks if the given log level is enabled based on the current process level weight.
10
+ */
11
+ export declare function isLogLevelEnabled(level: LogLevel): boolean;
12
+ /**
13
+ * Redacts common patterns of API keys and credential strings from log messages.
14
+ */
15
+ export declare function redactSecrets(str: string): string;
16
+ /**
17
+ * Recursively redacts sensitive fields from context objects.
18
+ */
19
+ export declare function redactObject(obj: unknown): unknown;
20
+ /**
21
+ * Structured unified logging system.
22
+ */
23
+ export declare const logger: {
24
+ debug(ns: LogNamespace, msg: string, ctx?: LogContext): void;
25
+ info(ns: LogNamespace, msg: string, ctx?: LogContext): void;
26
+ warn(ns: LogNamespace, msg: string, ctx?: LogContext): void;
27
+ error(ns: LogNamespace, msg: string, ctx?: LogContext): void;
28
+ };
@@ -0,0 +1,171 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ const LEVEL_WEIGHTS = {
5
+ debug: 0,
6
+ info: 1,
7
+ warn: 2,
8
+ error: 3,
9
+ };
10
+ // Default log level is 'info' unless explicitly configured.
11
+ const CURRENT_WEIGHT = (() => {
12
+ const envLevel = process.env.MUONROI_LOG_LEVEL?.toLowerCase();
13
+ if (envLevel === "debug")
14
+ return LEVEL_WEIGHTS.debug;
15
+ if (envLevel === "info")
16
+ return LEVEL_WEIGHTS.info;
17
+ if (envLevel === "warn")
18
+ return LEVEL_WEIGHTS.warn;
19
+ if (envLevel === "error")
20
+ return LEVEL_WEIGHTS.error;
21
+ return LEVEL_WEIGHTS.info;
22
+ })();
23
+ /**
24
+ * Checks if the given log level is enabled based on the current process level weight.
25
+ */
26
+ export function isLogLevelEnabled(level) {
27
+ return LEVEL_WEIGHTS[level] >= CURRENT_WEIGHT;
28
+ }
29
+ /**
30
+ * Redacts common patterns of API keys and credential strings from log messages.
31
+ */
32
+ export function redactSecrets(str) {
33
+ return str
34
+ .replace(/\bsk-[A-Za-z0-9-_]{20,}\b/g, "[REDACTED_API_KEY]")
35
+ .replace(/\bxai-[A-Za-z0-9-_]{20,}\b/g, "[REDACTED_API_KEY]")
36
+ .replace(/\bAIzaSy[A-Za-z0-9-_]{30,}\b/g, "[REDACTED_API_KEY]");
37
+ }
38
+ /**
39
+ * Recursively redacts sensitive fields from context objects.
40
+ */
41
+ export function redactObject(obj) {
42
+ if (obj === null || obj === undefined)
43
+ return obj;
44
+ if (typeof obj === "string") {
45
+ return redactSecrets(obj);
46
+ }
47
+ if (Array.isArray(obj)) {
48
+ return obj.map(redactObject);
49
+ }
50
+ if (typeof obj === "object") {
51
+ const res = {};
52
+ for (const [k, v] of Object.entries(obj)) {
53
+ const lowerK = k.toLowerCase();
54
+ if (lowerK.includes("key") ||
55
+ lowerK.includes("secret") ||
56
+ lowerK.includes("token") ||
57
+ lowerK.includes("password") ||
58
+ lowerK.includes("auth")) {
59
+ res[k] = "[REDACTED]";
60
+ }
61
+ else {
62
+ res[k] = redactObject(v);
63
+ }
64
+ }
65
+ return res;
66
+ }
67
+ return obj;
68
+ }
69
+ /**
70
+ * Returns true if the interactive TUI is active, preventing console writes.
71
+ */
72
+ function isTuiActive() {
73
+ try {
74
+ return globalThis.__muonroiTuiActive === true;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ /**
81
+ * Writes logs safely to ~/.muonroi-cli/debug.log.
82
+ */
83
+ function appendToFile(level, ns, msg, ctx) {
84
+ try {
85
+ const dir = path.join(os.homedir(), ".muonroi-cli");
86
+ if (!fs.existsSync(dir)) {
87
+ fs.mkdirSync(dir, { recursive: true });
88
+ }
89
+ const logPath = path.join(dir, "debug.log");
90
+ const ts = new Date().toISOString();
91
+ const redactedCtx = ctx ? redactObject(ctx) : null;
92
+ const ctxStr = redactedCtx ? ` ${JSON.stringify(redactedCtx)}` : "";
93
+ const logLine = `[${ts}] [${level.toUpperCase()}] [${ns.toUpperCase()}] ${redactSecrets(msg)}${ctxStr}\n`;
94
+ fs.appendFileSync(logPath, logLine, "utf8");
95
+ }
96
+ catch {
97
+ /* fail-open */
98
+ }
99
+ }
100
+ /**
101
+ * Formats console log lines with colors.
102
+ */
103
+ function formatConsole(level, ns, msg, ctx) {
104
+ const ts = new Date().toISOString().split("T")[1].slice(0, -1); // HH:MM:SS.mmm
105
+ const levelStr = level.toUpperCase();
106
+ const nsStr = ns.toUpperCase();
107
+ const cleanMsg = redactSecrets(msg);
108
+ const redactedCtx = ctx ? redactObject(ctx) : null;
109
+ const ctxStr = redactedCtx ? ` ${JSON.stringify(redactedCtx)}` : "";
110
+ // Apply colors for developer convenience in terminal logs (non-TUI)
111
+ let color = "\x1b[0m"; // Reset
112
+ if (level === "debug")
113
+ color = "\x1b[90m"; // Gray
114
+ if (level === "info")
115
+ color = "\x1b[32m"; // Green
116
+ if (level === "warn")
117
+ color = "\x1b[33m"; // Yellow
118
+ if (level === "error")
119
+ color = "\x1b[31m"; // Red
120
+ return `${color}[${ts}] [${levelStr}] [${nsStr}] ${cleanMsg}${ctxStr}\x1b[0m`;
121
+ }
122
+ /**
123
+ * Structured unified logging system.
124
+ */
125
+ export const logger = {
126
+ debug(ns, msg, ctx) {
127
+ if (!isLogLevelEnabled("debug"))
128
+ return;
129
+ if (isTuiActive()) {
130
+ appendToFile("debug", ns, msg, ctx);
131
+ }
132
+ else {
133
+ // eslint-disable-next-line no-console
134
+ console.log(formatConsole("debug", ns, msg, ctx));
135
+ }
136
+ },
137
+ info(ns, msg, ctx) {
138
+ if (!isLogLevelEnabled("info"))
139
+ return;
140
+ if (isTuiActive()) {
141
+ appendToFile("info", ns, msg, ctx);
142
+ }
143
+ else {
144
+ // eslint-disable-next-line no-console
145
+ console.log(formatConsole("info", ns, msg, ctx));
146
+ }
147
+ },
148
+ warn(ns, msg, ctx) {
149
+ if (!isLogLevelEnabled("warn"))
150
+ return;
151
+ if (isTuiActive()) {
152
+ appendToFile("warn", ns, msg, ctx);
153
+ }
154
+ else {
155
+ // eslint-disable-next-line no-console
156
+ console.warn(formatConsole("warn", ns, msg, ctx));
157
+ }
158
+ },
159
+ error(ns, msg, ctx) {
160
+ if (!isLogLevelEnabled("error"))
161
+ return;
162
+ if (isTuiActive()) {
163
+ appendToFile("error", ns, msg, ctx);
164
+ }
165
+ else {
166
+ // eslint-disable-next-line no-console
167
+ console.error(formatConsole("error", ns, msg, ctx));
168
+ }
169
+ },
170
+ };
171
+ //# sourceMappingURL=logger.js.map
@@ -5,6 +5,39 @@ export type PermissionMode = "safe" | "auto-edit" | "yolo";
5
5
  * without manual confirmation when the user has opted into auto-edit mode.
6
6
  */
7
7
  export declare const AUTO_EDIT_ALLOWED: ReadonlySet<string>;
8
+ /**
9
+ * Structured block result returned by safety checks.
10
+ * `null` means the command is safe to proceed.
11
+ */
12
+ export interface SafetyBlockResult {
13
+ /** Machine-readable kind for routing to the right askcard layout. */
14
+ kind: "catastrophic" | "dangerous" | "git-safety" | "empty-bash";
15
+ /** Human-readable reason explaining why it was blocked. */
16
+ reason: string;
17
+ /** The original command text that was blocked. */
18
+ command: string;
19
+ }
20
+ /**
21
+ * Returns a SafetyBlockResult if `command` matches a catastrophic
22
+ * pattern, or `null` if the command is safe to proceed.
23
+ *
24
+ * This is checked at the tool layer (BashTool.prepareCommand) and is
25
+ * independent of the UI-layer permission approval system — it applies even
26
+ * in yolo mode.
27
+ */
28
+ export declare function checkCatastrophicCommand(command: string): SafetyBlockResult | null;
29
+ /**
30
+ * Check a command against the "dangerous" patterns used by `toolNeedsApproval`.
31
+ * These are less severe than catastrophic but should still prompt for approval
32
+ * in safe / auto-edit modes.
33
+ *
34
+ * Returns a SafetyBlockResult or null if safe.
35
+ */
36
+ export declare function checkDangerousCommand(command: string): SafetyBlockResult | null;
37
+ /**
38
+ * Check if a command is a safety-blocked git operation (push-on-red, broad staging).
39
+ */
40
+ export declare function checkGitSafetyCommand(command: string, gitSafetyKey: string): SafetyBlockResult | null;
8
41
  /**
9
42
  * Returns true if the given tool call requires manual user approval
10
43
  * under the specified permission mode.
@@ -19,6 +19,159 @@ export const AUTO_EDIT_ALLOWED = new Set([
19
19
  "grep",
20
20
  "list_directory",
21
21
  ]);
22
+ /**
23
+ * Catastrophic commands: unrecoverable operations that are ALWAYS hard-blocked
24
+ * at the tool layer regardless of permission mode (safe/auto-edit/yolo).
25
+ *
26
+ * These are patterns where the potential for irreversible damage to the host
27
+ * system is high enough that no user approval dialog is sufficient — they must
28
+ * never execute. This list intentionally errs on the side of caution.
29
+ *
30
+ * Override: set MUONROI_ALLOW_CATASTROPHIC=1 to bypass (not recommended;
31
+ * intended only for explicit test harness scenarios).
32
+ */
33
+ const CATASTROPHIC_PATTERNS = [
34
+ // Disk / filesystem destruction
35
+ {
36
+ pattern: /\bdd\b[^|&;\n]*\bof=\/dev\//i,
37
+ reason: "dd writing to a raw device can destroy disk contents irreversibly.",
38
+ },
39
+ {
40
+ pattern: /\bmkfs\b/i,
41
+ reason: "mkfs formats a filesystem, destroying all data on the target device.",
42
+ },
43
+ // Fork bomb — classic bash pattern: :(){ :|:& };:
44
+ // Match the defining signature: a function named `:` that calls itself recursively.
45
+ // This is tight enough to avoid false positives on URLs containing `:/`.
46
+ {
47
+ pattern: /:\s*\(\s*\)\s*\{[^}]*:\s*\|\s*:/,
48
+ reason: "Fork bomb pattern detected — would exhaust host process table.",
49
+ },
50
+ // Remote code execution via piped shell
51
+ {
52
+ pattern: /\bcurl\b[^|&;\n]*\|\s*(?:ba)?sh\b/i,
53
+ reason: "curl piped to a shell executes arbitrary remote code without inspection.",
54
+ },
55
+ {
56
+ pattern: /\bwget\b[^|&;\n]*\|\s*(?:ba)?sh\b/i,
57
+ reason: "wget piped to a shell executes arbitrary remote code without inspection.",
58
+ },
59
+ // Privilege escalation
60
+ {
61
+ pattern: /\bsudo\s+/i,
62
+ reason: "sudo privilege escalation is not permitted from the agent shell.",
63
+ },
64
+ {
65
+ pattern: /\bsu\s+-\s*/i,
66
+ reason: "su to root is not permitted from the agent shell.",
67
+ },
68
+ // Persistence mechanisms — block crontab edits/writes; allow only -l (list, read-only)
69
+ // crontab <file>, crontab -, crontab -e all create/modify the crontab.
70
+ {
71
+ pattern: /\bcrontab\b(?!\s+-l\b)/i,
72
+ reason: "crontab write/edit commands create persistent execution outside the session.",
73
+ },
74
+ {
75
+ pattern: /\/etc\/(?:rc\.d|init\.d|systemd|cron)/i,
76
+ reason: "Writing to system init or cron directories would create persistence.",
77
+ },
78
+ // Reverse shells
79
+ {
80
+ pattern: /\bnc\b[^|&;\n]*(?:-e|-c)\s+/i,
81
+ reason: "nc with -e/-c flag can open a reverse shell.",
82
+ },
83
+ {
84
+ pattern: /\bsocat\b[^|&;\n]*(?:EXEC|SYSTEM):/i,
85
+ reason: "socat with EXEC/SYSTEM can open a reverse shell.",
86
+ },
87
+ {
88
+ pattern: /\/dev\/tcp\//i,
89
+ reason: "Bash /dev/tcp redirect can open a covert reverse shell connection.",
90
+ },
91
+ // Exfiltration of credentials / secrets directories
92
+ {
93
+ pattern: /(?:tar|zip|gzip|rsync|scp|sftp|ftp)\b[^|&;\n]*\.(?:muonroi-cli|ssh|gnupg|aws|kube)/i,
94
+ reason: "Archiving or uploading credential directories is not permitted.",
95
+ },
96
+ ];
97
+ /**
98
+ * Returns a SafetyBlockResult if `command` matches a catastrophic
99
+ * pattern, or `null` if the command is safe to proceed.
100
+ *
101
+ * This is checked at the tool layer (BashTool.prepareCommand) and is
102
+ * independent of the UI-layer permission approval system — it applies even
103
+ * in yolo mode.
104
+ */
105
+ export function checkCatastrophicCommand(command) {
106
+ if (process.env.MUONROI_ALLOW_CATASTROPHIC === "1")
107
+ return null;
108
+ for (const { pattern, reason } of CATASTROPHIC_PATTERNS) {
109
+ if (pattern.test(command)) {
110
+ return { kind: "catastrophic", reason, command };
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+ /**
116
+ * Check a command against the "dangerous" patterns used by `toolNeedsApproval`.
117
+ * These are less severe than catastrophic but should still prompt for approval
118
+ * in safe / auto-edit modes.
119
+ *
120
+ * Returns a SafetyBlockResult or null if safe.
121
+ */
122
+ export function checkDangerousCommand(command) {
123
+ const dangerousPatterns = [
124
+ // Filesystem destruction
125
+ /rm\s+-rf?\s+\/(?!tmp|var\/tmp)/i,
126
+ // Unrestricted chmod
127
+ /chmod\s+(?:777|a\+[rwx])/i,
128
+ // Code injection
129
+ /eval\s*\(/i,
130
+ /exec\s*\(/i,
131
+ // External network fetches (non-local)
132
+ /curl\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
133
+ /wget\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
134
+ // Process substitution with external commands
135
+ /\$\([^)]*(?:curl|wget|nc|socat)[^)]*\)/i,
136
+ // Writing to /etc (besides allowed subdirs)
137
+ /\bchown\s+root/i,
138
+ ];
139
+ for (const pattern of dangerousPatterns) {
140
+ if (pattern.test(command)) {
141
+ return {
142
+ kind: "dangerous",
143
+ reason: `Command matched dangerous pattern: ${pattern.source}`,
144
+ command,
145
+ };
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ /**
151
+ * Check if a command is a safety-blocked git operation (push-on-red, broad staging).
152
+ */
153
+ export function checkGitSafetyCommand(command, gitSafetyKey) {
154
+ // Lazy import to avoid circular deps at module level
155
+ const { analyzeGitCommand, checkPushGate, checkSensitiveStaging } = require_inline_safety_deps();
156
+ const gt = analyzeGitCommand(command);
157
+ if (gt.isPush) {
158
+ const gate = checkPushGate(gitSafetyKey);
159
+ if (gate.blocked) {
160
+ return {
161
+ kind: "git-safety",
162
+ reason: `git push blocked: ${gate.failed.join(", ")}`,
163
+ command,
164
+ };
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+ // Inline require to avoid circular dependency at module load.
170
+ function require_inline_safety_deps() {
171
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
172
+ const mod = require("../tools/git-safety.js");
173
+ return mod;
174
+ }
22
175
  /**
23
176
  * Returns true if the given tool call requires manual user approval
24
177
  * under the specified permission mode.
@@ -34,12 +187,20 @@ export function toolNeedsApproval(toolName, mode, context) {
34
187
  if (!cmd)
35
188
  return false;
36
189
  const patterns = [
190
+ // Filesystem destruction
37
191
  /rm\s+-rf?\s+\/(?!tmp|var\/tmp)/i,
38
- /curl\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
39
- /wget\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
40
- /chmod\s+777/i,
192
+ // Unrestricted chmod
193
+ /chmod\s+(?:777|a\+[rwx])/i,
194
+ // Code injection
41
195
  /eval\s*\(/i,
42
196
  /exec\s*\(/i,
197
+ // External network fetches (non-local)
198
+ /curl\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
199
+ /wget\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
200
+ // Process substitution with external commands
201
+ /\$\([^)]*(?:curl|wget|nc|socat)[^)]*\)/i,
202
+ // Writing to /etc (besides allowed subdirs)
203
+ /\bchown\s+root/i,
43
204
  ];
44
205
  return patterns.some((p) => p.test(cmd));
45
206
  };