@bahulam/code 0.1.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 (278) hide show
  1. package/README.md +93 -0
  2. package/package.json +56 -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 +223 -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 +312 -0
  136. package/src/commands/agent.mjs +221 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +202 -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/model-catalog.mjs +57 -0
  143. package/src/config/settings-loader.mjs +45 -0
  144. package/src/config/settings.mjs +132 -0
  145. package/src/context/ast-parser.mjs +298 -0
  146. package/src/context/bm25.mjs +85 -0
  147. package/src/context/prose-chunker.mjs +255 -0
  148. package/src/context/retriever.mjs +425 -0
  149. package/src/context/skeleton.mjs +134 -0
  150. package/src/context/symbol-indexer.mjs +375 -0
  151. package/src/core/agent-history.mjs +111 -0
  152. package/src/core/agent-loop.mjs +486 -0
  153. package/src/core/approval-log.mjs +145 -0
  154. package/src/core/approval.mjs +700 -0
  155. package/src/core/attachments.mjs +666 -0
  156. package/src/core/backend-url.mjs +68 -0
  157. package/src/core/bundled-runtime.mjs +418 -0
  158. package/src/core/cache-control.mjs +92 -0
  159. package/src/core/cache.mjs +105 -0
  160. package/src/core/callback-client.mjs +180 -0
  161. package/src/core/checkpoints.mjs +142 -0
  162. package/src/core/compact-history.mjs +127 -0
  163. package/src/core/context-envelope.mjs +54 -0
  164. package/src/core/context-manager.mjs +198 -0
  165. package/src/core/error-guidance.mjs +331 -0
  166. package/src/core/file-diff.mjs +217 -0
  167. package/src/core/headless.mjs +460 -0
  168. package/src/core/hooks-manager.mjs +87 -0
  169. package/src/core/jsonl-writer.mjs +449 -0
  170. package/src/core/local-agent.mjs +538 -0
  171. package/src/core/local-store.mjs +836 -0
  172. package/src/core/mode-selector.mjs +51 -0
  173. package/src/core/output-filter.mjs +177 -0
  174. package/src/core/paths.mjs +190 -0
  175. package/src/core/policy-resolver.mjs +156 -0
  176. package/src/core/pricing.mjs +336 -0
  177. package/src/core/project-artifacts.mjs +39 -0
  178. package/src/core/project-context-loader.mjs +139 -0
  179. package/src/core/providers.mjs +219 -0
  180. package/src/core/rate-limit-display.mjs +121 -0
  181. package/src/core/rate-limiter.mjs +119 -0
  182. package/src/core/resume-mode.mjs +192 -0
  183. package/src/core/risk-tier.mjs +388 -0
  184. package/src/core/safety.mjs +260 -0
  185. package/src/core/scheduler.mjs +173 -0
  186. package/src/core/session-manager.mjs +360 -0
  187. package/src/core/session.mjs +143 -0
  188. package/src/core/settings-sync.mjs +85 -0
  189. package/src/core/stagnation.mjs +57 -0
  190. package/src/core/stream-client.mjs +957 -0
  191. package/src/core/streaming.mjs +182 -0
  192. package/src/core/system-prompt.mjs +140 -0
  193. package/src/core/tasks.mjs +196 -0
  194. package/src/core/tool-executor.mjs +2231 -0
  195. package/src/core/trust.mjs +160 -0
  196. package/src/core/work-scope.mjs +248 -0
  197. package/src/hooks/engine.mjs +162 -0
  198. package/src/mcp/client.mjs +253 -0
  199. package/src/mcp/transport-shttp.mjs +130 -0
  200. package/src/mcp/transport-sse.mjs +131 -0
  201. package/src/mcp/transport-ws.mjs +134 -0
  202. package/src/onboarding/preflight.mjs +374 -0
  203. package/src/permissions/checker.mjs +57 -0
  204. package/src/permissions/command-classifier.mjs +700 -0
  205. package/src/permissions/injection-check.mjs +60 -0
  206. package/src/permissions/path-check.mjs +102 -0
  207. package/src/permissions/prompt.mjs +73 -0
  208. package/src/permissions/sandbox.mjs +112 -0
  209. package/src/plugins/loader.mjs +138 -0
  210. package/src/skills/installer.mjs +188 -0
  211. package/src/skills/loader.mjs +252 -0
  212. package/src/skills/runner.mjs +55 -0
  213. package/src/state/orbit.mjs +263 -0
  214. package/src/state/verbosity.mjs +99 -0
  215. package/src/telemetry/index.mjs +122 -0
  216. package/src/terminal/agents.mjs +353 -0
  217. package/src/terminal/analytics.mjs +292 -0
  218. package/src/terminal/ansi.mjs +695 -0
  219. package/src/terminal/init.mjs +145 -0
  220. package/src/terminal/main.mjs +310 -0
  221. package/src/terminal/repl-ask-form.mjs +120 -0
  222. package/src/terminal/repl-explore.mjs +44 -0
  223. package/src/terminal/repl-format.mjs +317 -0
  224. package/src/terminal/repl-model-form.mjs +132 -0
  225. package/src/terminal/repl-render.mjs +833 -0
  226. package/src/terminal/repl-resume.mjs +640 -0
  227. package/src/terminal/repl-state.mjs +120 -0
  228. package/src/terminal/repl-utils.mjs +34 -0
  229. package/src/terminal/repl.mjs +5032 -0
  230. package/src/terminal/skills.mjs +54 -0
  231. package/src/terminal/tool-display.mjs +392 -0
  232. package/src/tools/agent.mjs +137 -0
  233. package/src/tools/ask-user.mjs +61 -0
  234. package/src/tools/bash.mjs +231 -0
  235. package/src/tools/cron-create.mjs +120 -0
  236. package/src/tools/cron-delete.mjs +49 -0
  237. package/src/tools/cron-list.mjs +37 -0
  238. package/src/tools/edit.mjs +82 -0
  239. package/src/tools/enter-worktree.mjs +69 -0
  240. package/src/tools/exit-worktree.mjs +57 -0
  241. package/src/tools/glob.mjs +117 -0
  242. package/src/tools/grep.mjs +129 -0
  243. package/src/tools/lint.mjs +71 -0
  244. package/src/tools/ls.mjs +58 -0
  245. package/src/tools/lsp.mjs +115 -0
  246. package/src/tools/multi-edit.mjs +94 -0
  247. package/src/tools/notebook-edit.mjs +96 -0
  248. package/src/tools/project-overview.mjs +703 -0
  249. package/src/tools/read-mcp-resource.mjs +57 -0
  250. package/src/tools/read.mjs +138 -0
  251. package/src/tools/registry.mjs +116 -0
  252. package/src/tools/remote-trigger.mjs +84 -0
  253. package/src/tools/send-message.mjs +64 -0
  254. package/src/tools/skill.mjs +52 -0
  255. package/src/tools/test-runner.mjs +49 -0
  256. package/src/tools/todo-write.mjs +68 -0
  257. package/src/tools/tool-search.mjs +77 -0
  258. package/src/tools/web-fetch.mjs +65 -0
  259. package/src/tools/web-search.mjs +89 -0
  260. package/src/tools/write.mjs +55 -0
  261. package/src/ui/approval.mjs +510 -0
  262. package/src/ui/banner.mjs +232 -0
  263. package/src/ui/commands.mjs +537 -0
  264. package/src/ui/formatter.mjs +409 -0
  265. package/src/ui/icons.mjs +170 -0
  266. package/src/ui/input-dock.mjs +772 -0
  267. package/src/ui/markdown.mjs +278 -0
  268. package/src/ui/mission-report.mjs +296 -0
  269. package/src/ui/palette.mjs +189 -0
  270. package/src/ui/render-queue.mjs +500 -0
  271. package/src/ui/slash-commands.mjs +257 -0
  272. package/src/ui/spinner.mjs +116 -0
  273. package/src/ui/sub-agent.mjs +167 -0
  274. package/src/ui/term.mjs +174 -0
  275. package/src/ui/text-layout.mjs +127 -0
  276. package/src/ui/tool-card.mjs +740 -0
  277. package/src/ui/tool-details.mjs +504 -0
  278. package/src/ui/transcript-block.mjs +20 -0
@@ -0,0 +1,388 @@
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
+ import { isApprovalRequiredWritePath, isSensitiveConfigPath } from './safety.mjs';
18
+
19
+ // ── Tier enum ────────────────────────────────────────────────────────────
20
+
21
+ export const TIERS = Object.freeze({
22
+ READ: 'read',
23
+ SENSITIVE_READ: 'sensitive-read',
24
+ LOCAL_EDIT: 'local-edit',
25
+ PROTECTED_EDIT: 'protected-edit',
26
+ SHELL_SAFE: 'shell-safe',
27
+ SHELL_MEDIUM: 'shell-medium',
28
+ SHELL_DANGEROUS: 'shell-dangerous',
29
+ DESTRUCTIVE: 'destructive',
30
+ NETWORK: 'network',
31
+ });
32
+
33
+ /**
34
+ * Default behavior by tier:
35
+ * auto — proceed silently
36
+ * auto-with-undo — proceed but record a checkpoint first
37
+ * prompt-safe — prompt with Enter=approve default
38
+ * prompt-explicit — magenta-bordered prompt, no default
39
+ */
40
+ export const BEHAVIOR = Object.freeze({
41
+ [TIERS.READ]: 'auto',
42
+ [TIERS.SENSITIVE_READ]: 'prompt-explicit',
43
+ [TIERS.LOCAL_EDIT]: 'auto-with-undo',
44
+ [TIERS.PROTECTED_EDIT]: 'prompt-explicit',
45
+ [TIERS.SHELL_SAFE]: 'auto',
46
+ [TIERS.SHELL_MEDIUM]: 'prompt-safe',
47
+ [TIERS.SHELL_DANGEROUS]: 'prompt-explicit',
48
+ [TIERS.DESTRUCTIVE]: 'prompt-explicit',
49
+ [TIERS.NETWORK]: 'prompt-safe',
50
+ });
51
+
52
+ export function behavior(tier) {
53
+ return BEHAVIOR[tier] || 'prompt-safe';
54
+ }
55
+
56
+ // ── Tool → tier (non-shell) ─────────────────────────────────────────────
57
+
58
+ const READ_TOOLS = new Set([
59
+ 'read_file', 'read_files',
60
+ 'search_code', 'search_files', 'grep',
61
+ 'list_files', 'get_file_info', 'get_project_overview',
62
+ 'git_status', 'git_diff',
63
+ 'analyze_code',
64
+ 'validate_file', 'validate_structure',
65
+ 'agents_list', 'workflow_list',
66
+ ]);
67
+
68
+ const READ_PATH_KEYS = [
69
+ 'path', 'file_path', 'file', 'target',
70
+ 'pattern', 'glob',
71
+ ];
72
+
73
+ const READ_PATH_ARRAY_KEYS = [
74
+ 'paths', 'file_paths', 'files', 'targets', 'patterns', 'globs',
75
+ ];
76
+
77
+ const LOCAL_EDIT_TOOLS = new Set([
78
+ 'edit_file', 'write_file', 'write_project',
79
+ 'skill_install', 'skill_update',
80
+ 'agent_create', 'agent_sync',
81
+ 'workflow_create_multi', 'workflow_sync_multi',
82
+ ]);
83
+
84
+ const WRITE_PATH_KEYS = [
85
+ 'path', 'file_path', 'file', 'target',
86
+ ];
87
+
88
+ const WRITE_PATH_ARRAY_KEYS = [
89
+ 'paths', 'file_paths', 'targets',
90
+ ];
91
+
92
+ const DESTRUCTIVE_TOOLS = new Set([
93
+ 'delete_file', 'skill_remove',
94
+ ]);
95
+
96
+ const NETWORK_TOOLS = new Set([
97
+ 'WebFetch', 'fetch_url',
98
+ 'workflow_run_multi',
99
+ ]);
100
+
101
+ // ── Shell sub-classifier ────────────────────────────────────────────────
102
+
103
+ const SHELL_SAFE_RE = [
104
+ // Inspection / read-only + harmless shell navigation built-ins.
105
+ // `cd` / `pushd` / `popd` only change the process working directory; if
106
+ // chained with something dangerous, the multi-segment classifier still
107
+ // catches the danger (`cd /x && rm -rf .` → SHELL_DANGEROUS).
108
+ /^\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,
109
+ // mkdir -p / touch are creation primitives but harmless in scope.
110
+ /^\s*mkdir\s+-p\b/i,
111
+ /^\s*touch\s/i,
112
+ /^\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,
113
+ // Test-only invocations
114
+ /^\s*(npm|pnpm|yarn)\s+(test|run\s+test|run\s+lint|list|ls|view|info|outdated)\b/i,
115
+ /^\s*node\s+--check\b/i,
116
+ /^\s*python3?\s+-m\s+py_compile\b/i,
117
+ /^\s*pytest\b(?!.*--?(delete|rm|destructive))/i,
118
+ /^\s*cargo\s+(check|test|clippy|build)\b/i,
119
+ /^\s*go\s+(test|vet|build)\b/i,
120
+ /^\s*make\s+(test|check|lint|build)\b/i,
121
+ ];
122
+
123
+ const SHELL_DANGEROUS_RE = [
124
+ /\brm\s/i,
125
+ /\brm\s+-r/i,
126
+ /\brm\s+--recursive/i,
127
+ /\brm\s+-rf?\b/i,
128
+ /\bunlink\s/i,
129
+ /\brmdir\s+-/i,
130
+ /\bgit\s+push.*--force/i,
131
+ /\bgit\s+push.*-f\b/i,
132
+ /\bgit\s+reset\s+--hard/i,
133
+ /\bgit\s+clean\s+-f/i,
134
+ /\bgit\s+checkout\s+\./i,
135
+ /\bgit\s+stash\s+drop/i,
136
+ /\bgit\s+branch\s+-D/i,
137
+ /\bgit\s+filter-branch/i,
138
+ /\bsudo\b/i,
139
+ /\bsu\s+-/i,
140
+ /\bcurl\b.*\|\s*(sh|bash|zsh)/i,
141
+ /\bwget\b.*\|\s*(sh|bash|zsh)/i,
142
+ /\beval\s+["'$(]/i,
143
+ /\bfind\s+\/(?:\s|$)/i,
144
+ /\bfind\b.*\s-(?:exec|execdir|ok|okdir|delete|fprint|fprint0|fls|fprintf)\b/i,
145
+ /\bkubectl\s+delete/i,
146
+ /\bdocker\s+(rm|rmi|system\s+prune|volume\s+rm|network\s+rm)/i,
147
+ /\bdrop\s+(table|database|schema)/i,
148
+ /\btruncate\s+table/i,
149
+ /\bmkfs\b/i,
150
+ /\bdd\s+if=/i,
151
+ /:\s*\(\s*\)\s*\{.*:\|/i, // fork bomb
152
+ />\s*\/dev\/sda/i,
153
+ ];
154
+
155
+ const SHELL_MEDIUM_RE = [
156
+ /^\s*(npm|pnpm|yarn)\s+(install|i|add|remove|uninstall|update|upgrade|publish|deploy)\b/i,
157
+ /^\s*pip\s+(install|uninstall|--upgrade)\b/i,
158
+ /^\s*pipx\s+(install|uninstall)\b/i,
159
+ /^\s*brew\s+(install|uninstall|upgrade|update)\b/i,
160
+ /^\s*apt(-get)?\s+(install|remove|upgrade|update)\b/i,
161
+ /^\s*cargo\s+(install|uninstall|publish|run)\b/i,
162
+ /^\s*go\s+(install|get|mod\s+tidy|mod\s+download)\b/i,
163
+ /^\s*make(\s|$)/i,
164
+ /^\s*git\s+(commit|push|pull|merge|rebase|fetch|checkout(?!\s+\.)|cherry-pick|revert|tag|stash(?!\s+drop))/i,
165
+ /^\s*docker\s+(build|run|exec|compose|pull|push|tag)/i,
166
+ /^\s*sed\b.*\s-i\S*(?:\s|$)/i,
167
+ /^\s*sed\b.*\s--in-place(?:=|\s|$)/i,
168
+ ];
169
+
170
+ export function classifyShell(command) {
171
+ const cmd = String(command || '').trim();
172
+ if (!cmd) return TIERS.SHELL_MEDIUM;
173
+
174
+ // Dangerous wins over safe — never let a safe-looking prefix mask `&& rm -rf`.
175
+ if (SHELL_DANGEROUS_RE.some(re => re.test(cmd))) return TIERS.SHELL_DANGEROUS;
176
+
177
+ if (hasUnsafeOutputRedirection(cmd)) return TIERS.SHELL_MEDIUM;
178
+
179
+ // For a chained command, classify each segment and take the riskiest —
180
+ // never let a safe-looking prefix mask `&& npm install` or worse.
181
+ if (/&&|\|\||;|\|(?!\|)/.test(cmd)) {
182
+ const segments = splitShellSegments(cmd);
183
+ if (segments.length > 1) {
184
+ let worst = TIERS.SHELL_SAFE;
185
+ for (const seg of segments) {
186
+ const t = classifyShell(seg);
187
+ worst = riskier(worst, t);
188
+ if (worst === TIERS.SHELL_DANGEROUS) return worst;
189
+ }
190
+ return worst;
191
+ }
192
+ }
193
+
194
+ if (SHELL_MEDIUM_RE.some(re => re.test(cmd))) return TIERS.SHELL_MEDIUM;
195
+ if (SHELL_SAFE_RE.some(re => re.test(cmd))) return TIERS.SHELL_SAFE;
196
+ return TIERS.SHELL_MEDIUM;
197
+ }
198
+
199
+ function hasUnsafeOutputRedirection(cmd) {
200
+ let inSingle = false;
201
+ let inDouble = false;
202
+ let escaped = false;
203
+ for (let i = 0; i < cmd.length; i++) {
204
+ const ch = cmd[i];
205
+ if (escaped) { escaped = false; continue; }
206
+ if (ch === '\\' && !inSingle) { escaped = true; continue; }
207
+ if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; }
208
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; }
209
+ if (inSingle || inDouble || ch !== '>') continue;
210
+
211
+ const prev = cmd[i - 1] || '';
212
+ const next = cmd[i + 1] || '';
213
+ if (next === '&') continue;
214
+ let cursor = next === '>' ? i + 2 : i + 1;
215
+ while (/\s/.test(cmd[cursor] || '')) cursor++;
216
+ const target = cmd.slice(cursor).match(/^[^\s;&|]+/)?.[0] || '';
217
+ if (target === '/dev/null') continue;
218
+ if (prev === '2' && target === '/dev/null') continue;
219
+ return true;
220
+ }
221
+ return false;
222
+ }
223
+
224
+ function splitShellSegments(cmd) {
225
+ // Split on top-level &&, ||, ;, | — naive but enough for the classifier.
226
+ return cmd.split(/&&|\|\||;|\|/).map(s => s.trim()).filter(Boolean);
227
+ }
228
+
229
+ const TIER_ORDER = [
230
+ TIERS.READ,
231
+ TIERS.SENSITIVE_READ,
232
+ TIERS.SHELL_SAFE,
233
+ TIERS.LOCAL_EDIT,
234
+ TIERS.PROTECTED_EDIT,
235
+ TIERS.NETWORK,
236
+ TIERS.SHELL_MEDIUM,
237
+ TIERS.DESTRUCTIVE,
238
+ TIERS.SHELL_DANGEROUS,
239
+ ];
240
+
241
+ function riskier(a, b) {
242
+ return TIER_ORDER.indexOf(b) > TIER_ORDER.indexOf(a) ? b : a;
243
+ }
244
+
245
+ // ── Top-level classify ──────────────────────────────────────────────────
246
+
247
+ /**
248
+ * Classify a tool call into a risk tier. Always returns one of TIERS.
249
+ *
250
+ * @param {string} tool Tool name (e.g. 'shell', 'edit_file')
251
+ * @param {object} args Tool arguments (e.g. { command: 'rm -rf x' })
252
+ */
253
+ export function classify(tool, args = {}) {
254
+ if (!tool) return TIERS.SHELL_MEDIUM;
255
+
256
+ if (READ_TOOLS.has(tool)) {
257
+ return isSensitiveRead(args) ? TIERS.SENSITIVE_READ : TIERS.READ;
258
+ }
259
+ if (LOCAL_EDIT_TOOLS.has(tool)) {
260
+ return isApprovalRequiredWrite(args) ? TIERS.PROTECTED_EDIT : TIERS.LOCAL_EDIT;
261
+ }
262
+ if (DESTRUCTIVE_TOOLS.has(tool)) return TIERS.DESTRUCTIVE;
263
+ if (NETWORK_TOOLS.has(tool)) return TIERS.NETWORK;
264
+
265
+ if (tool === 'shell' || tool === 'run_tests' || tool === 'validate_build' || tool === 'lint_check') {
266
+ return classifyShell(args.command || args.cmd || '');
267
+ }
268
+
269
+ // Sub-agents inherit their parent's risk (read-ish by default).
270
+ if (['explore', 'plan', 'verify', 'debug', 'refactor', 'analyze_code'].includes(tool)) {
271
+ return TIERS.READ;
272
+ }
273
+
274
+ // MCP tools: assume network unless the name implies read.
275
+ if (tool.startsWith('mcp')) {
276
+ return /(?:read|get|list|search|describe|info|status)/i.test(tool) ? TIERS.READ : TIERS.NETWORK;
277
+ }
278
+
279
+ return TIERS.SHELL_MEDIUM;
280
+ }
281
+
282
+ /**
283
+ * Convenience: human label for a tier (used in approval prompts and the
284
+ * status bar). Returned strings already uppercased / hyphenated.
285
+ */
286
+ export function label(tier) {
287
+ switch (tier) {
288
+ case TIERS.READ: return 'READ';
289
+ case TIERS.SENSITIVE_READ: return 'SENSITIVE-READ';
290
+ case TIERS.LOCAL_EDIT: return 'LOCAL-EDIT';
291
+ case TIERS.PROTECTED_EDIT: return 'PROTECTED-EDIT';
292
+ case TIERS.SHELL_SAFE: return 'SHELL-SAFE';
293
+ case TIERS.SHELL_MEDIUM: return 'SHELL-MEDIUM';
294
+ case TIERS.SHELL_DANGEROUS: return 'SHELL-DANGEROUS';
295
+ case TIERS.DESTRUCTIVE: return 'DESTRUCTIVE';
296
+ case TIERS.NETWORK: return 'NETWORK';
297
+ default: return String(tier || '').toUpperCase();
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Whether the tool needs an explicit human keystroke before running.
303
+ */
304
+ export function requiresExplicitApproval(tier) {
305
+ return behavior(tier) === 'prompt-explicit';
306
+ }
307
+
308
+ /**
309
+ * Whether the tier should auto-create an undo checkpoint before running.
310
+ */
311
+ export function requiresCheckpoint(tier) {
312
+ return behavior(tier) === 'auto-with-undo';
313
+ }
314
+
315
+ export function isSensitiveRead(args = {}) {
316
+ return extractReadPaths(args).some(isSensitiveReadPath);
317
+ }
318
+
319
+ export function isSensitiveReadPath(filePath = '') {
320
+ const normalized = String(filePath || '')
321
+ .replace(/\\/g, '/')
322
+ .replace(/\/+/g, '/')
323
+ .replace(/^\.\//, '')
324
+ .trim();
325
+ if (!normalized) return false;
326
+
327
+ const parts = normalized.split('/').filter(Boolean);
328
+ const base = parts[parts.length - 1] || normalized;
329
+ if (isSensitiveConfigPath(base)) return true;
330
+ if (/\.pem$/i.test(base)) return true;
331
+ return parts.includes('secrets');
332
+ }
333
+
334
+ export function isApprovalRequiredWrite(args = {}) {
335
+ return extractWritePaths(args).some(isApprovalRequiredWritePath);
336
+ }
337
+
338
+ function extractReadPaths(args = {}) {
339
+ const paths = [];
340
+ for (const key of READ_PATH_KEYS) {
341
+ if (typeof args[key] === 'string') paths.push(args[key]);
342
+ }
343
+ for (const key of READ_PATH_ARRAY_KEYS) {
344
+ const value = args[key];
345
+ if (!Array.isArray(value)) continue;
346
+ for (const item of value) {
347
+ if (typeof item === 'string') {
348
+ paths.push(item);
349
+ } else if (item && typeof item === 'object') {
350
+ for (const nestedKey of READ_PATH_KEYS) {
351
+ if (typeof item[nestedKey] === 'string') paths.push(item[nestedKey]);
352
+ }
353
+ }
354
+ }
355
+ }
356
+ return paths;
357
+ }
358
+
359
+ function extractWritePaths(args = {}) {
360
+ const paths = [];
361
+ for (const key of WRITE_PATH_KEYS) {
362
+ if (typeof args[key] === 'string') paths.push(args[key]);
363
+ }
364
+ for (const key of WRITE_PATH_ARRAY_KEYS) {
365
+ const value = args[key];
366
+ if (!Array.isArray(value)) continue;
367
+ for (const item of value) {
368
+ if (typeof item === 'string') {
369
+ paths.push(item);
370
+ } else if (item && typeof item === 'object') {
371
+ for (const nestedKey of WRITE_PATH_KEYS) {
372
+ if (typeof item[nestedKey] === 'string') paths.push(item[nestedKey]);
373
+ }
374
+ }
375
+ }
376
+ }
377
+ if (Array.isArray(args.files)) {
378
+ for (const item of args.files) {
379
+ if (typeof item === 'string') paths.push(item);
380
+ else if (item && typeof item === 'object') {
381
+ for (const nestedKey of WRITE_PATH_KEYS) {
382
+ if (typeof item[nestedKey] === 'string') paths.push(item[nestedKey]);
383
+ }
384
+ }
385
+ }
386
+ }
387
+ return paths;
388
+ }
@@ -0,0 +1,260 @@
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
+ const APPROVAL_REQUIRED_WRITE_NAMES = new Set([
32
+ '.env',
33
+ '.env.local',
34
+ '.env.production',
35
+ 'package.json',
36
+ 'package-lock.json',
37
+ 'yarn.lock',
38
+ 'pnpm-lock.yaml',
39
+ 'Cargo.lock',
40
+ 'go.sum',
41
+ ]);
42
+
43
+ /** Directory names that indicate a source root — never delete the dir itself. */
44
+ const SOURCE_DIRS = new Set([
45
+ 'src',
46
+ 'lib',
47
+ 'app',
48
+ 'pages',
49
+ 'components',
50
+ 'packages',
51
+ 'dist',
52
+ 'build',
53
+ 'test',
54
+ 'tests',
55
+ '__tests__',
56
+ 'spec',
57
+ ]);
58
+
59
+ /** Shell patterns that are always blocked. */
60
+ const DANGEROUS_SHELL_PATTERNS = [
61
+ /rm\s+(-\w*[rR]\w*\s+)*(?:\/(?:\s|$)|~(?:\s|\/?\s*$)|\$HOME(?:\s|\/?\s*$)|\.\.(?:\s|\/|$)|\.\/\.\.)/, // rm -rf /, ~, $HOME, or ..
62
+ /rm\s+(-\w*[rR]\w*\s+)*\.\s*$/, // rm -rf .
63
+ /rm\s+(-\w*[rR]\w*\s+)*\*\s*$/, // rm -rf *
64
+ /:\(\)\s*\{\s*:\|\s*:\s*&\s*\}\s*;/, // fork bomb
65
+ /mkfs\./, // format filesystem
66
+ /dd\s+.*of=\/dev\//, // disk wipe
67
+ />\s*\/dev\/sd/, // overwrite disk
68
+ /chmod\s+(-\w+\s+)*777\s+\//, // chmod 777 /
69
+ /curl.*\|\s*(ba)?sh/, // pipe curl to shell
70
+ /wget.*\|\s*(ba)?sh/, // pipe wget to shell
71
+ /eval\s*\$\(/, // eval command substitution
72
+ /\bfind\s+\/\s/, // find / (scans entire filesystem)
73
+ /\bfind\s+\/\s*$/, // find / at end
74
+ /\bls\s+(-\w+\s+)*\/\s*$/, // ls / (root listing)
75
+ ];
76
+
77
+ /** Commands that need extra scrutiny — require approval even if auto-approved. */
78
+ const HIGH_RISK_COMMANDS = [
79
+ /\brm\s/, // ALL rm commands require explicit approval
80
+ /\bunlink\s/, // unlink
81
+ /\brmdir\s/, // rmdir
82
+ /\bkill\s/, // process termination requires explicit approval
83
+ /\bpkill\s/, // process termination by pattern requires explicit approval
84
+ /\bxargs\s+kill\b/, // process termination via piped pid list
85
+ /git\s+push\s+--force/,
86
+ /git\s+reset\s+--hard/,
87
+ /git\s+clean\s+-[fd]/,
88
+ /drop\s+table/i,
89
+ /drop\s+database/i,
90
+ /truncate\s+table/i,
91
+ ];
92
+
93
+ // ── Validation Functions ─────────────────────────────────────
94
+
95
+ function basenameFor(filePath = '') {
96
+ return path.basename(String(filePath || '').replace(/\\/g, '/'));
97
+ }
98
+
99
+ function isEnvLikeName(name = '') {
100
+ return /^\.env(?:\.|$)/.test(String(name || ''));
101
+ }
102
+
103
+ function isProtectedName(name = '') {
104
+ return PROTECTED_NAMES.has(name) || isEnvLikeName(name);
105
+ }
106
+
107
+ export function isSensitiveConfigPath(filePath = '') {
108
+ return isEnvLikeName(basenameFor(filePath));
109
+ }
110
+
111
+ export function isApprovalRequiredWritePath(filePath = '') {
112
+ const basename = basenameFor(filePath);
113
+ return isSensitiveConfigPath(filePath) || APPROVAL_REQUIRED_WRITE_NAMES.has(basename);
114
+ }
115
+
116
+ /**
117
+ * Check if a path is protected (should not be deleted/overwritten entirely).
118
+ * @param {string} filePath - Absolute path
119
+ * @param {string} cwd - Current working directory
120
+ * @param {{ allowApprovalRequiredWrite?: boolean }} options
121
+ * @returns {{ safe: boolean, reason?: string, requiresApproval?: boolean, sensitive?: boolean, protected?: boolean }}
122
+ */
123
+ export function validatePath(filePath, cwd = process.cwd(), options = {}) {
124
+ if (!filePath) return { safe: false, reason: 'Empty path' };
125
+
126
+ const resolved = path.resolve(cwd, filePath);
127
+ const basename = path.basename(resolved);
128
+ const relative = path.relative(cwd, resolved);
129
+
130
+ // Block anything outside CWD parent
131
+ const cwdParent = path.dirname(cwd);
132
+ if (!resolved.startsWith(cwdParent)) {
133
+ return { safe: false, reason: `Path outside workspace: ${filePath}` };
134
+ }
135
+
136
+ // Block protected file names at any level
137
+ if (isProtectedName(basename) && !resolved.includes('node_modules/')) {
138
+ if (options.allowApprovalRequiredWrite && isApprovalRequiredWritePath(resolved)) {
139
+ return {
140
+ safe: true,
141
+ reason: `Approval required for protected path: ${basename}`,
142
+ requiresApproval: true,
143
+ protected: true,
144
+ sensitive: isSensitiveConfigPath(resolved),
145
+ };
146
+ }
147
+ return { safe: false, reason: `Protected path: ${basename}` };
148
+ }
149
+
150
+ return { safe: true };
151
+ }
152
+
153
+ /**
154
+ * Check if a delete operation is safe.
155
+ * Stricter than validatePath — also blocks source directories.
156
+ * @param {string} filePath - Path to delete
157
+ * @param {string} cwd - Current working directory
158
+ * @returns {{ safe: boolean, reason?: string }}
159
+ */
160
+ export function validateDelete(filePath, cwd = process.cwd()) {
161
+ const pathCheck = validatePath(filePath, cwd);
162
+ if (!pathCheck.safe) return pathCheck;
163
+
164
+ const resolved = path.resolve(cwd, filePath);
165
+ const basename = path.basename(resolved);
166
+
167
+ // Never delete source root directories
168
+ if (SOURCE_DIRS.has(basename)) {
169
+ try {
170
+ const stat = fs.statSync(resolved);
171
+ if (stat.isDirectory()) {
172
+ return { safe: false, reason: `Cannot delete source directory: ${basename}/` };
173
+ }
174
+ } catch { /* file doesn't exist, safe to proceed */ }
175
+ }
176
+
177
+ // Never delete CWD itself
178
+ if (resolved === cwd || resolved === path.dirname(cwd)) {
179
+ return { safe: false, reason: 'Cannot delete working directory' };
180
+ }
181
+
182
+ return { safe: true };
183
+ }
184
+
185
+ /**
186
+ * Check if a shell command is safe to execute.
187
+ * @param {string} command
188
+ * @returns {{ safe: boolean, reason?: string, highRisk?: boolean }}
189
+ */
190
+ export function validateShellCommand(command) {
191
+ if (!command) return { safe: false, reason: 'Empty command' };
192
+
193
+ // Always block dangerous patterns
194
+ for (const pattern of DANGEROUS_SHELL_PATTERNS) {
195
+ if (pattern.test(command)) {
196
+ return { safe: false, reason: `Blocked dangerous command: ${command.slice(0, 60)}` };
197
+ }
198
+ }
199
+
200
+ // Flag high-risk commands (still allowed, but should force approval)
201
+ for (const pattern of HIGH_RISK_COMMANDS) {
202
+ if (pattern.test(command)) {
203
+ return { safe: true, highRisk: true, reason: `High-risk command: ${command.slice(0, 60)}` };
204
+ }
205
+ }
206
+
207
+ return { safe: true };
208
+ }
209
+
210
+ /**
211
+ * Check if a write operation targets a sensible path.
212
+ * @param {string} filePath
213
+ * @param {string} content
214
+ * @param {string} cwd
215
+ * @returns {{ safe: boolean, reason?: string }}
216
+ */
217
+ export function validateWrite(filePath, content, cwd = process.cwd()) {
218
+ const pathCheck = validatePath(filePath, cwd, { allowApprovalRequiredWrite: true });
219
+ if (!pathCheck.safe) return pathCheck;
220
+
221
+ const resolved = path.resolve(cwd, filePath);
222
+ const basename = path.basename(resolved);
223
+
224
+ // Don't allow overwriting .git internals
225
+ if (resolved.includes('/.git/')) {
226
+ return { safe: false, reason: 'Cannot write to .git directory' };
227
+ }
228
+
229
+ if (pathCheck.requiresApproval) {
230
+ return {
231
+ safe: true,
232
+ reason: pathCheck.sensitive
233
+ ? `Sensitive config write requires approval: ${basename}`
234
+ : `Protected file write requires approval: ${basename}`,
235
+ requiresApproval: true,
236
+ protected: true,
237
+ sensitive: pathCheck.sensitive,
238
+ };
239
+ }
240
+
241
+ // Warn if writing a very large file (> 1MB)
242
+ if (content && content.length > 1_000_000) {
243
+ return { safe: true, reason: `Large file write: ${(content.length / 1024).toFixed(0)}KB` };
244
+ }
245
+
246
+ return { safe: true };
247
+ }
248
+
249
+ /**
250
+ * Get a human-readable summary of safety rules.
251
+ */
252
+ export function getSafetyRules() {
253
+ return {
254
+ protectedNames: [...PROTECTED_NAMES],
255
+ approvalRequiredWriteNames: [...APPROVAL_REQUIRED_WRITE_NAMES],
256
+ sourceDirs: [...SOURCE_DIRS],
257
+ blockedPatterns: DANGEROUS_SHELL_PATTERNS.length,
258
+ highRiskPatterns: HIGH_RISK_COMMANDS.length,
259
+ };
260
+ }