@bahulam/code 2.6.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 (273) hide show
  1. package/README.md +80 -0
  2. package/package.json +49 -0
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/benchmark/route.ts +113 -0
  6. package/pulse/app/api/benchmarks/route.ts +195 -0
  7. package/pulse/app/api/costs/route.ts +88 -0
  8. package/pulse/app/api/export/route.ts +77 -0
  9. package/pulse/app/api/history/route.ts +11 -0
  10. package/pulse/app/api/import/route.ts +31 -0
  11. package/pulse/app/api/memory/route.ts +50 -0
  12. package/pulse/app/api/plans/route.ts +9 -0
  13. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  14. package/pulse/app/api/projects/route.ts +121 -0
  15. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  16. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  17. package/pulse/app/api/sessions/route.ts +112 -0
  18. package/pulse/app/api/settings/route.ts +14 -0
  19. package/pulse/app/api/stats/route.ts +143 -0
  20. package/pulse/app/api/todos/route.ts +9 -0
  21. package/pulse/app/api/tools/route.ts +160 -0
  22. package/pulse/app/benchmarks/page.tsx +224 -0
  23. package/pulse/app/costs/page.tsx +179 -0
  24. package/pulse/app/export/page.tsx +465 -0
  25. package/pulse/app/favicon.ico +0 -0
  26. package/pulse/app/globals.css +263 -0
  27. package/pulse/app/help/page.tsx +143 -0
  28. package/pulse/app/history/page.tsx +157 -0
  29. package/pulse/app/layout.tsx +46 -0
  30. package/pulse/app/memory/page.tsx +365 -0
  31. package/pulse/app/overview-client.tsx +393 -0
  32. package/pulse/app/page.tsx +14 -0
  33. package/pulse/app/plans/page.tsx +308 -0
  34. package/pulse/app/projects/[slug]/page.tsx +390 -0
  35. package/pulse/app/projects/page.tsx +110 -0
  36. package/pulse/app/sessions/[id]/page.tsx +243 -0
  37. package/pulse/app/sessions/page.tsx +39 -0
  38. package/pulse/app/settings/page.tsx +188 -0
  39. package/pulse/app/todos/page.tsx +211 -0
  40. package/pulse/app/tools/page.tsx +249 -0
  41. package/pulse/cli.js +164 -0
  42. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  43. package/pulse/components/activity/streak-card.tsx +36 -0
  44. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  45. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  46. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  47. package/pulse/components/costs/model-token-table.tsx +60 -0
  48. package/pulse/components/global-search.tsx +193 -0
  49. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  50. package/pulse/components/layout/bottom-nav.tsx +53 -0
  51. package/pulse/components/layout/client-layout.tsx +31 -0
  52. package/pulse/components/layout/sidebar-context.tsx +50 -0
  53. package/pulse/components/layout/sidebar.tsx +183 -0
  54. package/pulse/components/layout/top-bar.tsx +121 -0
  55. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  56. package/pulse/components/overview/conversation-table.tsx +148 -0
  57. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  58. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  59. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  60. package/pulse/components/overview/stat-card.tsx +102 -0
  61. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  62. package/pulse/components/projects/project-card.tsx +175 -0
  63. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  64. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  65. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  66. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  67. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  68. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  69. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  70. package/pulse/components/sessions/session-badges.tsx +49 -0
  71. package/pulse/components/sessions/session-table.tsx +299 -0
  72. package/pulse/components/theme-provider.tsx +44 -0
  73. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  74. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  75. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  76. package/pulse/components/tools/version-history-table.tsx +32 -0
  77. package/pulse/components/ui/alert.tsx +66 -0
  78. package/pulse/components/ui/badge.tsx +48 -0
  79. package/pulse/components/ui/breadcrumb.tsx +109 -0
  80. package/pulse/components/ui/button.tsx +64 -0
  81. package/pulse/components/ui/calendar.tsx +220 -0
  82. package/pulse/components/ui/card.tsx +92 -0
  83. package/pulse/components/ui/command.tsx +158 -0
  84. package/pulse/components/ui/dialog.tsx +158 -0
  85. package/pulse/components/ui/input.tsx +21 -0
  86. package/pulse/components/ui/popover.tsx +89 -0
  87. package/pulse/components/ui/progress.tsx +31 -0
  88. package/pulse/components/ui/select.tsx +190 -0
  89. package/pulse/components/ui/separator.tsx +28 -0
  90. package/pulse/components/ui/sheet.tsx +143 -0
  91. package/pulse/components/ui/skeleton.tsx +13 -0
  92. package/pulse/components/ui/table.tsx +116 -0
  93. package/pulse/components/ui/tabs.tsx +91 -0
  94. package/pulse/components/ui/tooltip.tsx +57 -0
  95. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  96. package/pulse/components.json +23 -0
  97. package/pulse/eslint.config.mjs +18 -0
  98. package/pulse/lib/bahulam-paths.ts +23 -0
  99. package/pulse/lib/claude-reader.ts +592 -0
  100. package/pulse/lib/decode.ts +129 -0
  101. package/pulse/lib/pricing.ts +102 -0
  102. package/pulse/lib/replay-parser.ts +165 -0
  103. package/pulse/lib/tool-categories.ts +127 -0
  104. package/pulse/lib/utils.ts +6 -0
  105. package/pulse/next-env.d.ts +6 -0
  106. package/pulse/next.config.ts +16 -0
  107. package/pulse/package.json +45 -0
  108. package/pulse/postcss.config.mjs +7 -0
  109. package/pulse/public/activity.png +0 -0
  110. package/pulse/public/cc-lens.png +0 -0
  111. package/pulse/public/command-k.png +0 -0
  112. package/pulse/public/costs.png +0 -0
  113. package/pulse/public/dashboard-dark.png +0 -0
  114. package/pulse/public/dashboard-white.png +0 -0
  115. package/pulse/public/export.png +0 -0
  116. package/pulse/public/file.svg +1 -0
  117. package/pulse/public/globe.svg +1 -0
  118. package/pulse/public/next.svg +1 -0
  119. package/pulse/public/projects.png +0 -0
  120. package/pulse/public/session-chat.png +0 -0
  121. package/pulse/public/todos.png +0 -0
  122. package/pulse/public/tools.png +0 -0
  123. package/pulse/public/vercel.svg +1 -0
  124. package/pulse/public/window.svg +1 -0
  125. package/pulse/tsconfig.json +34 -0
  126. package/pulse/types/claude.ts +294 -0
  127. package/src/agents/loader.mjs +94 -0
  128. package/src/agents/multi_workflow_loader.mjs +330 -0
  129. package/src/agents/parser.mjs +205 -0
  130. package/src/agents/scaffold.mjs +222 -0
  131. package/src/agents/teams.mjs +123 -0
  132. package/src/agents/workflow_loader.mjs +122 -0
  133. package/src/agents/workflow_scaffold.mjs +249 -0
  134. package/src/auth/oauth.mjs +220 -0
  135. package/src/auth/tarang-auth.mjs +306 -0
  136. package/src/commands/agent.mjs +220 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +200 -0
  139. package/src/config/env.mjs +263 -0
  140. package/src/config/hook-runner.mjs +100 -0
  141. package/src/config/memory-loader.mjs +32 -0
  142. package/src/config/settings-loader.mjs +45 -0
  143. package/src/config/settings.mjs +132 -0
  144. package/src/context/ast-parser.mjs +298 -0
  145. package/src/context/bm25.mjs +85 -0
  146. package/src/context/retriever.mjs +308 -0
  147. package/src/context/skeleton.mjs +134 -0
  148. package/src/context/symbol-indexer.mjs +375 -0
  149. package/src/core/agent-history.mjs +111 -0
  150. package/src/core/agent-loop.mjs +486 -0
  151. package/src/core/approval-log.mjs +104 -0
  152. package/src/core/approval.mjs +476 -0
  153. package/src/core/attachments.mjs +380 -0
  154. package/src/core/backend-url.mjs +55 -0
  155. package/src/core/cache-control.mjs +92 -0
  156. package/src/core/cache.mjs +105 -0
  157. package/src/core/callback-client.mjs +180 -0
  158. package/src/core/checkpoints.mjs +142 -0
  159. package/src/core/compact-history.mjs +127 -0
  160. package/src/core/context-envelope.mjs +54 -0
  161. package/src/core/context-manager.mjs +198 -0
  162. package/src/core/error-guidance.mjs +311 -0
  163. package/src/core/file-diff.mjs +217 -0
  164. package/src/core/headless.mjs +448 -0
  165. package/src/core/hooks-manager.mjs +87 -0
  166. package/src/core/jsonl-writer.mjs +449 -0
  167. package/src/core/local-agent.mjs +537 -0
  168. package/src/core/local-store.mjs +836 -0
  169. package/src/core/mode-selector.mjs +51 -0
  170. package/src/core/output-filter.mjs +177 -0
  171. package/src/core/paths.mjs +190 -0
  172. package/src/core/policy-resolver.mjs +156 -0
  173. package/src/core/pricing.mjs +336 -0
  174. package/src/core/project-artifacts.mjs +39 -0
  175. package/src/core/project-context-loader.mjs +139 -0
  176. package/src/core/providers.mjs +219 -0
  177. package/src/core/rate-limit-display.mjs +121 -0
  178. package/src/core/rate-limiter.mjs +119 -0
  179. package/src/core/resume-mode.mjs +192 -0
  180. package/src/core/risk-tier.mjs +337 -0
  181. package/src/core/safety.mjs +203 -0
  182. package/src/core/scheduler.mjs +173 -0
  183. package/src/core/session-manager.mjs +360 -0
  184. package/src/core/session.mjs +143 -0
  185. package/src/core/settings-sync.mjs +85 -0
  186. package/src/core/stagnation.mjs +57 -0
  187. package/src/core/stream-client.mjs +829 -0
  188. package/src/core/streaming.mjs +182 -0
  189. package/src/core/system-prompt.mjs +140 -0
  190. package/src/core/tasks.mjs +196 -0
  191. package/src/core/tool-executor.mjs +1950 -0
  192. package/src/core/trust.mjs +158 -0
  193. package/src/core/work-scope.mjs +248 -0
  194. package/src/hooks/engine.mjs +162 -0
  195. package/src/index.mjs +426 -0
  196. package/src/mcp/client.mjs +253 -0
  197. package/src/mcp/transport-shttp.mjs +130 -0
  198. package/src/mcp/transport-sse.mjs +131 -0
  199. package/src/mcp/transport-ws.mjs +134 -0
  200. package/src/onboarding/preflight.mjs +360 -0
  201. package/src/permissions/checker.mjs +57 -0
  202. package/src/permissions/command-classifier.mjs +652 -0
  203. package/src/permissions/injection-check.mjs +60 -0
  204. package/src/permissions/path-check.mjs +102 -0
  205. package/src/permissions/prompt.mjs +73 -0
  206. package/src/permissions/sandbox.mjs +112 -0
  207. package/src/plugins/loader.mjs +138 -0
  208. package/src/skills/installer.mjs +188 -0
  209. package/src/skills/loader.mjs +252 -0
  210. package/src/skills/runner.mjs +55 -0
  211. package/src/state/orbit.mjs +263 -0
  212. package/src/state/verbosity.mjs +99 -0
  213. package/src/telemetry/index.mjs +96 -0
  214. package/src/terminal/agents.mjs +177 -0
  215. package/src/terminal/analytics.mjs +292 -0
  216. package/src/terminal/ansi.mjs +695 -0
  217. package/src/terminal/init.mjs +145 -0
  218. package/src/terminal/main.mjs +269 -0
  219. package/src/terminal/repl-explore.mjs +35 -0
  220. package/src/terminal/repl-format.mjs +257 -0
  221. package/src/terminal/repl-render.mjs +561 -0
  222. package/src/terminal/repl-resume.mjs +625 -0
  223. package/src/terminal/repl-state.mjs +103 -0
  224. package/src/terminal/repl-utils.mjs +34 -0
  225. package/src/terminal/repl.mjs +3832 -0
  226. package/src/terminal/skills.mjs +54 -0
  227. package/src/terminal/tool-display.mjs +240 -0
  228. package/src/tools/agent.mjs +137 -0
  229. package/src/tools/ask-user.mjs +61 -0
  230. package/src/tools/bash.mjs +231 -0
  231. package/src/tools/cron-create.mjs +120 -0
  232. package/src/tools/cron-delete.mjs +49 -0
  233. package/src/tools/cron-list.mjs +37 -0
  234. package/src/tools/edit.mjs +82 -0
  235. package/src/tools/enter-worktree.mjs +69 -0
  236. package/src/tools/exit-worktree.mjs +57 -0
  237. package/src/tools/glob.mjs +117 -0
  238. package/src/tools/grep.mjs +129 -0
  239. package/src/tools/lint.mjs +71 -0
  240. package/src/tools/ls.mjs +58 -0
  241. package/src/tools/lsp.mjs +115 -0
  242. package/src/tools/multi-edit.mjs +94 -0
  243. package/src/tools/notebook-edit.mjs +96 -0
  244. package/src/tools/project-overview.mjs +641 -0
  245. package/src/tools/read-mcp-resource.mjs +57 -0
  246. package/src/tools/read.mjs +138 -0
  247. package/src/tools/registry.mjs +116 -0
  248. package/src/tools/remote-trigger.mjs +84 -0
  249. package/src/tools/send-message.mjs +64 -0
  250. package/src/tools/skill.mjs +52 -0
  251. package/src/tools/test-runner.mjs +49 -0
  252. package/src/tools/todo-write.mjs +68 -0
  253. package/src/tools/tool-search.mjs +77 -0
  254. package/src/tools/web-fetch.mjs +65 -0
  255. package/src/tools/web-search.mjs +89 -0
  256. package/src/tools/write.mjs +55 -0
  257. package/src/ui/approval.mjs +263 -0
  258. package/src/ui/banner.mjs +235 -0
  259. package/src/ui/commands.mjs +537 -0
  260. package/src/ui/formatter.mjs +409 -0
  261. package/src/ui/icons.mjs +164 -0
  262. package/src/ui/input-dock.mjs +444 -0
  263. package/src/ui/markdown.mjs +278 -0
  264. package/src/ui/mission-report.mjs +296 -0
  265. package/src/ui/palette.mjs +189 -0
  266. package/src/ui/slash-commands.mjs +245 -0
  267. package/src/ui/spinner.mjs +116 -0
  268. package/src/ui/sub-agent.mjs +152 -0
  269. package/src/ui/term.mjs +159 -0
  270. package/src/ui/text-layout.mjs +127 -0
  271. package/src/ui/tool-card.mjs +463 -0
  272. package/src/ui/tool-details.mjs +312 -0
  273. package/src/ui/transcript-block.mjs +21 -0
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Risk tier classifier — Mission Control (PRD-055 §8.1).
3
+ *
4
+ * import { classify, TIERS, behavior } from './risk-tier.mjs';
5
+ * const tier = classify('shell', { command: 'rm -rf node_modules' });
6
+ * // → 'shell-dangerous'
7
+ *
8
+ * Pure: no I/O, no async. Tested in isolation; the rest of the CLI relies on
9
+ * the return value to decide whether to auto-approve, auto-approve with
10
+ * checkpoint, or hold for explicit approval.
11
+ *
12
+ * The CLI never asks the backend for the tier. The backend's job is to say
13
+ * "this is what I want to run"; we map that to a tier locally so dangerous
14
+ * intent can't be hidden behind a friendly description.
15
+ */
16
+
17
+ // ── Tier enum ────────────────────────────────────────────────────────────
18
+
19
+ export const TIERS = Object.freeze({
20
+ READ: 'read',
21
+ SENSITIVE_READ: 'sensitive-read',
22
+ LOCAL_EDIT: 'local-edit',
23
+ SHELL_SAFE: 'shell-safe',
24
+ SHELL_MEDIUM: 'shell-medium',
25
+ SHELL_DANGEROUS: 'shell-dangerous',
26
+ DESTRUCTIVE: 'destructive',
27
+ NETWORK: 'network',
28
+ });
29
+
30
+ /**
31
+ * Default behavior by tier:
32
+ * auto — proceed silently
33
+ * auto-with-undo — proceed but record a checkpoint first
34
+ * prompt-safe — prompt with Enter=approve default
35
+ * prompt-explicit — magenta-bordered prompt, no default
36
+ */
37
+ export const BEHAVIOR = Object.freeze({
38
+ [TIERS.READ]: 'auto',
39
+ [TIERS.SENSITIVE_READ]: 'prompt-explicit',
40
+ [TIERS.LOCAL_EDIT]: 'auto-with-undo',
41
+ [TIERS.SHELL_SAFE]: 'auto',
42
+ [TIERS.SHELL_MEDIUM]: 'prompt-safe',
43
+ [TIERS.SHELL_DANGEROUS]: 'prompt-explicit',
44
+ [TIERS.DESTRUCTIVE]: 'prompt-explicit',
45
+ [TIERS.NETWORK]: 'prompt-safe',
46
+ });
47
+
48
+ export function behavior(tier) {
49
+ return BEHAVIOR[tier] || 'prompt-safe';
50
+ }
51
+
52
+ // ── Tool → tier (non-shell) ─────────────────────────────────────────────
53
+
54
+ const READ_TOOLS = new Set([
55
+ 'read_file', 'read_files',
56
+ 'search_code', 'search_files', 'grep',
57
+ 'list_files', 'get_file_info', 'get_project_overview',
58
+ 'git_status', 'git_diff',
59
+ 'analyze_code',
60
+ 'validate_file', 'validate_structure',
61
+ 'agents_list', 'workflow_list',
62
+ ]);
63
+
64
+ const READ_PATH_KEYS = [
65
+ 'path', 'file_path', 'file', 'target',
66
+ 'pattern', 'glob',
67
+ ];
68
+
69
+ const READ_PATH_ARRAY_KEYS = [
70
+ 'paths', 'file_paths', 'files', 'targets', 'patterns', 'globs',
71
+ ];
72
+
73
+ const LOCAL_EDIT_TOOLS = new Set([
74
+ 'edit_file', 'write_file', 'write_project',
75
+ 'skill_install', 'skill_update',
76
+ 'agent_create', 'agent_sync',
77
+ 'workflow_create_multi', 'workflow_sync_multi',
78
+ ]);
79
+
80
+ const DESTRUCTIVE_TOOLS = new Set([
81
+ 'delete_file', 'skill_remove',
82
+ ]);
83
+
84
+ const NETWORK_TOOLS = new Set([
85
+ 'WebFetch', 'fetch_url',
86
+ 'workflow_run_multi',
87
+ ]);
88
+
89
+ // ── Shell sub-classifier ────────────────────────────────────────────────
90
+
91
+ const SHELL_SAFE_RE = [
92
+ // Inspection / read-only + harmless shell navigation built-ins.
93
+ // `cd` / `pushd` / `popd` only change the process working directory; if
94
+ // chained with something dangerous, the multi-segment classifier still
95
+ // catches the danger (`cd /x && rm -rf .` → SHELL_DANGEROUS).
96
+ /^\s*(cd|pushd|popd|ls|cat|head|tail|less|more|wc|file|stat|tree|find|grep|rg|ag|fd|sed|echo|printf|pwd|whoami|date|which|type|env|printenv|uname|hostname|id|df|du|uptime|free|top|ps|lsof)\b/i,
97
+ // mkdir -p / touch are creation primitives but harmless in scope.
98
+ /^\s*mkdir\s+-p\b/i,
99
+ /^\s*touch\s/i,
100
+ /^\s*git\s+(status|log|diff|show|branch|tag|remote|stash\s+list|blame|shortlog|describe|rev-parse|ls-files|ls-tree|config\s+--get)\b/i,
101
+ // Test-only invocations
102
+ /^\s*(npm|pnpm|yarn)\s+(test|run\s+test|run\s+lint|list|ls|view|info|outdated)\b/i,
103
+ /^\s*node\s+--check\b/i,
104
+ /^\s*python3?\s+-m\s+py_compile\b/i,
105
+ /^\s*pytest\b(?!.*--?(delete|rm|destructive))/i,
106
+ /^\s*cargo\s+(check|test|clippy|build)\b/i,
107
+ /^\s*go\s+(test|vet|build)\b/i,
108
+ /^\s*make\s+(test|check|lint|build)\b/i,
109
+ ];
110
+
111
+ const SHELL_DANGEROUS_RE = [
112
+ /\brm\s/i,
113
+ /\brm\s+-r/i,
114
+ /\brm\s+--recursive/i,
115
+ /\brm\s+-rf?\b/i,
116
+ /\bunlink\s/i,
117
+ /\brmdir\s+-/i,
118
+ /\bgit\s+push.*--force/i,
119
+ /\bgit\s+push.*-f\b/i,
120
+ /\bgit\s+reset\s+--hard/i,
121
+ /\bgit\s+clean\s+-f/i,
122
+ /\bgit\s+checkout\s+\./i,
123
+ /\bgit\s+stash\s+drop/i,
124
+ /\bgit\s+branch\s+-D/i,
125
+ /\bgit\s+filter-branch/i,
126
+ /\bsudo\b/i,
127
+ /\bsu\s+-/i,
128
+ /\bcurl\b.*\|\s*(sh|bash|zsh)/i,
129
+ /\bwget\b.*\|\s*(sh|bash|zsh)/i,
130
+ /\beval\s+["'$(]/i,
131
+ /\bfind\s+\/(?:\s|$)/i,
132
+ /\bfind\b.*\s-(?:exec|execdir|ok|okdir|delete|fprint|fprint0|fls|fprintf)\b/i,
133
+ /\bkubectl\s+delete/i,
134
+ /\bdocker\s+(rm|rmi|system\s+prune|volume\s+rm|network\s+rm)/i,
135
+ /\bdrop\s+(table|database|schema)/i,
136
+ /\btruncate\s+table/i,
137
+ /\bmkfs\b/i,
138
+ /\bdd\s+if=/i,
139
+ /:\s*\(\s*\)\s*\{.*:\|/i, // fork bomb
140
+ />\s*\/dev\/sda/i,
141
+ ];
142
+
143
+ const SHELL_MEDIUM_RE = [
144
+ /^\s*(npm|pnpm|yarn)\s+(install|i|add|remove|uninstall|update|upgrade|publish|deploy)\b/i,
145
+ /^\s*pip\s+(install|uninstall|--upgrade)\b/i,
146
+ /^\s*pipx\s+(install|uninstall)\b/i,
147
+ /^\s*brew\s+(install|uninstall|upgrade|update)\b/i,
148
+ /^\s*apt(-get)?\s+(install|remove|upgrade|update)\b/i,
149
+ /^\s*cargo\s+(install|uninstall|publish|run)\b/i,
150
+ /^\s*go\s+(install|get|mod\s+tidy|mod\s+download)\b/i,
151
+ /^\s*make(\s|$)/i,
152
+ /^\s*git\s+(commit|push|pull|merge|rebase|fetch|checkout(?!\s+\.)|cherry-pick|revert|tag|stash(?!\s+drop))/i,
153
+ /^\s*docker\s+(build|run|exec|compose|pull|push|tag)/i,
154
+ /^\s*sed\b.*\s-i\S*(?:\s|$)/i,
155
+ /^\s*sed\b.*\s--in-place(?:=|\s|$)/i,
156
+ ];
157
+
158
+ export function classifyShell(command) {
159
+ const cmd = String(command || '').trim();
160
+ if (!cmd) return TIERS.SHELL_MEDIUM;
161
+
162
+ // Dangerous wins over safe — never let a safe-looking prefix mask `&& rm -rf`.
163
+ if (SHELL_DANGEROUS_RE.some(re => re.test(cmd))) return TIERS.SHELL_DANGEROUS;
164
+
165
+ if (hasUnsafeOutputRedirection(cmd)) return TIERS.SHELL_MEDIUM;
166
+
167
+ // For a chained command, classify each segment and take the riskiest —
168
+ // never let a safe-looking prefix mask `&& npm install` or worse.
169
+ if (/&&|\|\||;|\|(?!\|)/.test(cmd)) {
170
+ const segments = splitShellSegments(cmd);
171
+ if (segments.length > 1) {
172
+ let worst = TIERS.SHELL_SAFE;
173
+ for (const seg of segments) {
174
+ const t = classifyShell(seg);
175
+ worst = riskier(worst, t);
176
+ if (worst === TIERS.SHELL_DANGEROUS) return worst;
177
+ }
178
+ return worst;
179
+ }
180
+ }
181
+
182
+ if (SHELL_MEDIUM_RE.some(re => re.test(cmd))) return TIERS.SHELL_MEDIUM;
183
+ if (SHELL_SAFE_RE.some(re => re.test(cmd))) return TIERS.SHELL_SAFE;
184
+ return TIERS.SHELL_MEDIUM;
185
+ }
186
+
187
+ function hasUnsafeOutputRedirection(cmd) {
188
+ let inSingle = false;
189
+ let inDouble = false;
190
+ let escaped = false;
191
+ for (let i = 0; i < cmd.length; i++) {
192
+ const ch = cmd[i];
193
+ if (escaped) { escaped = false; continue; }
194
+ if (ch === '\\' && !inSingle) { escaped = true; continue; }
195
+ if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
196
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
197
+ if (inSingle || inDouble || ch !== '>') continue;
198
+
199
+ const prev = cmd[i - 1] || '';
200
+ const next = cmd[i + 1] || '';
201
+ if (next === '&') continue;
202
+ let cursor = next === '>' ? i + 2 : i + 1;
203
+ while (/\s/.test(cmd[cursor] || '')) cursor++;
204
+ const target = cmd.slice(cursor).match(/^[^\s;&|]+/)?.[0] || '';
205
+ if (target === '/dev/null') continue;
206
+ if (prev === '2' && target === '/dev/null') continue;
207
+ return true;
208
+ }
209
+ return false;
210
+ }
211
+
212
+ function splitShellSegments(cmd) {
213
+ // Split on top-level &&, ||, ;, | — naive but enough for the classifier.
214
+ return cmd.split(/&&|\|\||;|\|/).map(s => s.trim()).filter(Boolean);
215
+ }
216
+
217
+ const TIER_ORDER = [
218
+ TIERS.READ,
219
+ TIERS.SENSITIVE_READ,
220
+ TIERS.SHELL_SAFE,
221
+ TIERS.LOCAL_EDIT,
222
+ TIERS.NETWORK,
223
+ TIERS.SHELL_MEDIUM,
224
+ TIERS.DESTRUCTIVE,
225
+ TIERS.SHELL_DANGEROUS,
226
+ ];
227
+
228
+ function riskier(a, b) {
229
+ return TIER_ORDER.indexOf(b) > TIER_ORDER.indexOf(a) ? b : a;
230
+ }
231
+
232
+ // ── Top-level classify ──────────────────────────────────────────────────
233
+
234
+ /**
235
+ * Classify a tool call into a risk tier. Always returns one of TIERS.
236
+ *
237
+ * @param {string} tool Tool name (e.g. 'shell', 'edit_file')
238
+ * @param {object} args Tool arguments (e.g. { command: 'rm -rf x' })
239
+ */
240
+ export function classify(tool, args = {}) {
241
+ if (!tool) return TIERS.SHELL_MEDIUM;
242
+
243
+ if (READ_TOOLS.has(tool)) {
244
+ return isSensitiveRead(args) ? TIERS.SENSITIVE_READ : TIERS.READ;
245
+ }
246
+ if (LOCAL_EDIT_TOOLS.has(tool)) return TIERS.LOCAL_EDIT;
247
+ if (DESTRUCTIVE_TOOLS.has(tool)) return TIERS.DESTRUCTIVE;
248
+ if (NETWORK_TOOLS.has(tool)) return TIERS.NETWORK;
249
+
250
+ if (tool === 'shell' || tool === 'run_tests' || tool === 'validate_build' || tool === 'lint_check') {
251
+ return classifyShell(args.command || args.cmd || '');
252
+ }
253
+
254
+ // Sub-agents inherit their parent's risk (read-ish by default).
255
+ if (['explore', 'plan', 'verify', 'debug', 'refactor', 'analyze_code'].includes(tool)) {
256
+ return TIERS.READ;
257
+ }
258
+
259
+ // MCP tools: assume network unless the name implies read.
260
+ if (tool.startsWith('mcp')) {
261
+ return /(?:read|get|list|search|describe|info|status)/i.test(tool) ? TIERS.READ : TIERS.NETWORK;
262
+ }
263
+
264
+ return TIERS.SHELL_MEDIUM;
265
+ }
266
+
267
+ /**
268
+ * Convenience: human label for a tier (used in approval prompts and the
269
+ * status bar). Returned strings already uppercased / hyphenated.
270
+ */
271
+ export function label(tier) {
272
+ switch (tier) {
273
+ case TIERS.READ: return 'READ';
274
+ case TIERS.SENSITIVE_READ: return 'SENSITIVE-READ';
275
+ case TIERS.LOCAL_EDIT: return 'LOCAL-EDIT';
276
+ case TIERS.SHELL_SAFE: return 'SHELL-SAFE';
277
+ case TIERS.SHELL_MEDIUM: return 'SHELL-MEDIUM';
278
+ case TIERS.SHELL_DANGEROUS: return 'SHELL-DANGEROUS';
279
+ case TIERS.DESTRUCTIVE: return 'DESTRUCTIVE';
280
+ case TIERS.NETWORK: return 'NETWORK';
281
+ default: return String(tier || '').toUpperCase();
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Whether the tool needs an explicit human keystroke before running.
287
+ */
288
+ export function requiresExplicitApproval(tier) {
289
+ return behavior(tier) === 'prompt-explicit';
290
+ }
291
+
292
+ /**
293
+ * Whether the tier should auto-create an undo checkpoint before running.
294
+ */
295
+ export function requiresCheckpoint(tier) {
296
+ return behavior(tier) === 'auto-with-undo';
297
+ }
298
+
299
+ export function isSensitiveRead(args = {}) {
300
+ return extractReadPaths(args).some(isSensitiveReadPath);
301
+ }
302
+
303
+ export function isSensitiveReadPath(filePath = '') {
304
+ const normalized = String(filePath || '')
305
+ .replace(/\\/g, '/')
306
+ .replace(/\/+/g, '/')
307
+ .replace(/^\.\//, '')
308
+ .trim();
309
+ if (!normalized) return false;
310
+
311
+ const parts = normalized.split('/').filter(Boolean);
312
+ const base = parts[parts.length - 1] || normalized;
313
+ if (base === '.env') return true;
314
+ if (/\.pem$/i.test(base)) return true;
315
+ return parts.includes('secrets');
316
+ }
317
+
318
+ function extractReadPaths(args = {}) {
319
+ const paths = [];
320
+ for (const key of READ_PATH_KEYS) {
321
+ if (typeof args[key] === 'string') paths.push(args[key]);
322
+ }
323
+ for (const key of READ_PATH_ARRAY_KEYS) {
324
+ const value = args[key];
325
+ if (!Array.isArray(value)) continue;
326
+ for (const item of value) {
327
+ if (typeof item === 'string') {
328
+ paths.push(item);
329
+ } else if (item && typeof item === 'object') {
330
+ for (const nestedKey of READ_PATH_KEYS) {
331
+ if (typeof item[nestedKey] === 'string') paths.push(item[nestedKey]);
332
+ }
333
+ }
334
+ }
335
+ }
336
+ return paths;
337
+ }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Safety Guardrails — prevent destructive tool execution.
3
+ *
4
+ * Protects against:
5
+ * - Deleting/overwriting source code directories
6
+ * - rm -rf on critical paths
7
+ * - Writing outside allowed boundaries
8
+ * - Dangerous shell commands (fork bombs, disk wipes, etc.)
9
+ */
10
+
11
+ import * as path from 'node:path';
12
+ import * as fs from 'node:fs';
13
+
14
+ // ── Protected Paths ──────────────────────────────────────────
15
+
16
+ /** Files/dirs that should NEVER be deleted or overwritten wholesale. */
17
+ const PROTECTED_NAMES = new Set([
18
+ '.git',
19
+ '.env',
20
+ '.env.local',
21
+ '.env.production',
22
+ 'node_modules',
23
+ 'package.json',
24
+ 'package-lock.json',
25
+ 'yarn.lock',
26
+ 'pnpm-lock.yaml',
27
+ 'Cargo.lock',
28
+ 'go.sum',
29
+ ]);
30
+
31
+ /** Directory names that indicate a source root — never delete the dir itself. */
32
+ const SOURCE_DIRS = new Set([
33
+ 'src',
34
+ 'lib',
35
+ 'app',
36
+ 'pages',
37
+ 'components',
38
+ 'packages',
39
+ 'dist',
40
+ 'build',
41
+ 'test',
42
+ 'tests',
43
+ '__tests__',
44
+ 'spec',
45
+ ]);
46
+
47
+ /** Shell patterns that are always blocked. */
48
+ const DANGEROUS_SHELL_PATTERNS = [
49
+ /rm\s+(-\w*[rR]\w*\s+)*(?:\/(?:\s|$)|~(?:\s|\/?\s*$)|\$HOME(?:\s|\/?\s*$)|\.\.(?:\s|\/|$)|\.\/\.\.)/, // rm -rf /, ~, $HOME, or ..
50
+ /rm\s+(-\w*[rR]\w*\s+)*\.\s*$/, // rm -rf .
51
+ /rm\s+(-\w*[rR]\w*\s+)*\*\s*$/, // rm -rf *
52
+ /:\(\)\s*\{\s*:\|\s*:\s*&\s*\}\s*;/, // fork bomb
53
+ /mkfs\./, // format filesystem
54
+ /dd\s+.*of=\/dev\//, // disk wipe
55
+ />\s*\/dev\/sd/, // overwrite disk
56
+ /chmod\s+(-\w+\s+)*777\s+\//, // chmod 777 /
57
+ /curl.*\|\s*(ba)?sh/, // pipe curl to shell
58
+ /wget.*\|\s*(ba)?sh/, // pipe wget to shell
59
+ /eval\s*\$\(/, // eval command substitution
60
+ /\bfind\s+\/\s/, // find / (scans entire filesystem)
61
+ /\bfind\s+\/\s*$/, // find / at end
62
+ /\bls\s+(-\w+\s+)*\/\s*$/, // ls / (root listing)
63
+ ];
64
+
65
+ /** Commands that need extra scrutiny — require approval even if auto-approved. */
66
+ const HIGH_RISK_COMMANDS = [
67
+ /\brm\s/, // ALL rm commands require explicit approval
68
+ /\bunlink\s/, // unlink
69
+ /\brmdir\s/, // rmdir
70
+ /\bkill\s/, // process termination requires explicit approval
71
+ /\bpkill\s/, // process termination by pattern requires explicit approval
72
+ /\bxargs\s+kill\b/, // process termination via piped pid list
73
+ /git\s+push\s+--force/,
74
+ /git\s+reset\s+--hard/,
75
+ /git\s+clean\s+-[fd]/,
76
+ /drop\s+table/i,
77
+ /drop\s+database/i,
78
+ /truncate\s+table/i,
79
+ ];
80
+
81
+ // ── Validation Functions ─────────────────────────────────────
82
+
83
+ /**
84
+ * Check if a path is protected (should not be deleted/overwritten entirely).
85
+ * @param {string} filePath - Absolute path
86
+ * @param {string} cwd - Current working directory
87
+ * @returns {{ safe: boolean, reason?: string }}
88
+ */
89
+ export function validatePath(filePath, cwd = process.cwd()) {
90
+ if (!filePath) return { safe: false, reason: 'Empty path' };
91
+
92
+ const resolved = path.resolve(cwd, filePath);
93
+ const basename = path.basename(resolved);
94
+ const relative = path.relative(cwd, resolved);
95
+
96
+ // Block anything outside CWD parent
97
+ const cwdParent = path.dirname(cwd);
98
+ if (!resolved.startsWith(cwdParent)) {
99
+ return { safe: false, reason: `Path outside workspace: ${filePath}` };
100
+ }
101
+
102
+ // Block protected file names at any level
103
+ if (PROTECTED_NAMES.has(basename) && !resolved.includes('node_modules/')) {
104
+ return { safe: false, reason: `Protected path: ${basename}` };
105
+ }
106
+
107
+ return { safe: true };
108
+ }
109
+
110
+ /**
111
+ * Check if a delete operation is safe.
112
+ * Stricter than validatePath — also blocks source directories.
113
+ * @param {string} filePath - Path to delete
114
+ * @param {string} cwd - Current working directory
115
+ * @returns {{ safe: boolean, reason?: string }}
116
+ */
117
+ export function validateDelete(filePath, cwd = process.cwd()) {
118
+ const pathCheck = validatePath(filePath, cwd);
119
+ if (!pathCheck.safe) return pathCheck;
120
+
121
+ const resolved = path.resolve(cwd, filePath);
122
+ const basename = path.basename(resolved);
123
+
124
+ // Never delete source root directories
125
+ if (SOURCE_DIRS.has(basename)) {
126
+ try {
127
+ const stat = fs.statSync(resolved);
128
+ if (stat.isDirectory()) {
129
+ return { safe: false, reason: `Cannot delete source directory: ${basename}/` };
130
+ }
131
+ } catch { /* file doesn't exist, safe to proceed */ }
132
+ }
133
+
134
+ // Never delete CWD itself
135
+ if (resolved === cwd || resolved === path.dirname(cwd)) {
136
+ return { safe: false, reason: 'Cannot delete working directory' };
137
+ }
138
+
139
+ return { safe: true };
140
+ }
141
+
142
+ /**
143
+ * Check if a shell command is safe to execute.
144
+ * @param {string} command
145
+ * @returns {{ safe: boolean, reason?: string, highRisk?: boolean }}
146
+ */
147
+ export function validateShellCommand(command) {
148
+ if (!command) return { safe: false, reason: 'Empty command' };
149
+
150
+ // Always block dangerous patterns
151
+ for (const pattern of DANGEROUS_SHELL_PATTERNS) {
152
+ if (pattern.test(command)) {
153
+ return { safe: false, reason: `Blocked dangerous command: ${command.slice(0, 60)}` };
154
+ }
155
+ }
156
+
157
+ // Flag high-risk commands (still allowed, but should force approval)
158
+ for (const pattern of HIGH_RISK_COMMANDS) {
159
+ if (pattern.test(command)) {
160
+ return { safe: true, highRisk: true, reason: `High-risk command: ${command.slice(0, 60)}` };
161
+ }
162
+ }
163
+
164
+ return { safe: true };
165
+ }
166
+
167
+ /**
168
+ * Check if a write operation targets a sensible path.
169
+ * @param {string} filePath
170
+ * @param {string} content
171
+ * @param {string} cwd
172
+ * @returns {{ safe: boolean, reason?: string }}
173
+ */
174
+ export function validateWrite(filePath, content, cwd = process.cwd()) {
175
+ const pathCheck = validatePath(filePath, cwd);
176
+ if (!pathCheck.safe) return pathCheck;
177
+
178
+ const resolved = path.resolve(cwd, filePath);
179
+
180
+ // Don't allow overwriting .git internals
181
+ if (resolved.includes('/.git/')) {
182
+ return { safe: false, reason: 'Cannot write to .git directory' };
183
+ }
184
+
185
+ // Warn if writing a very large file (> 1MB)
186
+ if (content && content.length > 1_000_000) {
187
+ return { safe: true, reason: `Large file write: ${(content.length / 1024).toFixed(0)}KB` };
188
+ }
189
+
190
+ return { safe: true };
191
+ }
192
+
193
+ /**
194
+ * Get a human-readable summary of safety rules.
195
+ */
196
+ export function getSafetyRules() {
197
+ return {
198
+ protectedNames: [...PROTECTED_NAMES],
199
+ sourceDirs: [...SOURCE_DIRS],
200
+ blockedPatterns: DANGEROUS_SHELL_PATTERNS.length,
201
+ highRiskPatterns: HIGH_RISK_COMMANDS.length,
202
+ };
203
+ }