@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,65 @@
1
+ /**
2
+ * Web Fetch Tool โ€” fetch URL content using built-in Node.js fetch.
3
+ */
4
+
5
+ export const WebFetchTool = {
6
+ name: 'WebFetch',
7
+ description: 'Fetch content from a URL. Returns the response body as text.',
8
+ inputSchema: {
9
+ type: 'object',
10
+ properties: {
11
+ url: { type: 'string', description: 'The URL to fetch' },
12
+ headers: {
13
+ type: 'object',
14
+ description: 'Optional HTTP headers',
15
+ },
16
+ max_length: {
17
+ type: 'number',
18
+ description: 'Max response length in characters (default: 50000)',
19
+ },
20
+ },
21
+ required: ['url'],
22
+ },
23
+
24
+ validateInput(input) {
25
+ const errors = [];
26
+ if (!input.url) errors.push('url is required');
27
+ try {
28
+ new URL(input.url);
29
+ } catch {
30
+ errors.push('url must be a valid URL');
31
+ }
32
+ return errors;
33
+ },
34
+
35
+ async call(input) {
36
+ const maxLength = input.max_length || 50000;
37
+
38
+ try {
39
+ const controller = new AbortController();
40
+ const timeout = setTimeout(() => controller.abort(), 30000);
41
+
42
+ const res = await fetch(input.url, {
43
+ headers: input.headers || {},
44
+ signal: controller.signal,
45
+ redirect: 'follow',
46
+ });
47
+
48
+ clearTimeout(timeout);
49
+
50
+ if (!res.ok) {
51
+ return `HTTP ${res.status}: ${res.statusText}`;
52
+ }
53
+
54
+ const contentType = res.headers.get('content-type') || '';
55
+ const text = await res.text();
56
+ const truncated = text.length > maxLength
57
+ ? text.slice(0, maxLength) + `\n...[truncated at ${maxLength} chars]`
58
+ : text;
59
+
60
+ return `Content-Type: ${contentType}\nLength: ${text.length}\n\n${truncated}`;
61
+ } catch (err) {
62
+ return `Fetch error: ${err.message}`;
63
+ }
64
+ },
65
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Web Search Tool โ€” search the web via configurable API backends.
3
+ *
4
+ * Supports:
5
+ * - Brave Search API (BRAVE_API_KEY)
6
+ * - SearXNG (self-hosted, SEARXNG_URL)
7
+ * - Fallback: returns instructions to use web-fetch instead
8
+ */
9
+
10
+ export const WebSearchTool = {
11
+ name: 'WebSearch',
12
+ description: 'Search the web for information. Requires BRAVE_API_KEY or SEARXNG_URL.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ query: { type: 'string', description: 'Search query' },
17
+ limit: { type: 'number', description: 'Max results (default: 5)' },
18
+ },
19
+ required: ['query'],
20
+ },
21
+
22
+ validateInput(input) {
23
+ return input.query ? [] : ['query is required'];
24
+ },
25
+
26
+ async call(input) {
27
+ const limit = input.limit || 5;
28
+
29
+ // Try Brave Search API
30
+ const braveKey = process.env.BRAVE_API_KEY;
31
+ if (braveKey) {
32
+ return searchBrave(input.query, limit, braveKey);
33
+ }
34
+
35
+ // Try SearXNG
36
+ const searxUrl = process.env.SEARXNG_URL;
37
+ if (searxUrl) {
38
+ return searchSearxng(input.query, limit, searxUrl);
39
+ }
40
+
41
+ return 'No search API configured. Set BRAVE_API_KEY or SEARXNG_URL environment variable. ' +
42
+ 'Alternatively, use the WebFetch tool to fetch specific URLs directly.';
43
+ },
44
+ };
45
+
46
+ async function searchBrave(query, limit, apiKey) {
47
+ try {
48
+ const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${limit}`;
49
+ const res = await fetch(url, {
50
+ headers: {
51
+ 'Accept': 'application/json',
52
+ 'X-Subscription-Token': apiKey,
53
+ },
54
+ });
55
+
56
+ if (!res.ok) return `Brave API error: ${res.status}`;
57
+
58
+ const data = await res.json();
59
+ const results = (data.web?.results || []).slice(0, limit);
60
+
61
+ if (results.length === 0) return 'No results found.';
62
+
63
+ return results.map((r, i) =>
64
+ `${i + 1}. ${r.title}\n ${r.url}\n ${r.description || ''}`
65
+ ).join('\n\n');
66
+ } catch (err) {
67
+ return `Search error: ${err.message}`;
68
+ }
69
+ }
70
+
71
+ async function searchSearxng(query, limit, baseUrl) {
72
+ try {
73
+ const url = `${baseUrl}/search?q=${encodeURIComponent(query)}&format=json&categories=general`;
74
+ const res = await fetch(url);
75
+
76
+ if (!res.ok) return `SearXNG error: ${res.status}`;
77
+
78
+ const data = await res.json();
79
+ const results = (data.results || []).slice(0, limit);
80
+
81
+ if (results.length === 0) return 'No results found.';
82
+
83
+ return results.map((r, i) =>
84
+ `${i + 1}. ${r.title}\n ${r.url}\n ${r.content || ''}`
85
+ ).join('\n\n');
86
+ } catch (err) {
87
+ return `Search error: ${err.message}`;
88
+ }
89
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Write Tool โ€” matches Claude Code's exact behavior.
3
+ *
4
+ * Features:
5
+ * - Creates parent directories if needed
6
+ * - Requires Read first for existing file overwrites
7
+ * - No README creation unless explicitly asked
8
+ */
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { hasBeenRead, markRead } from './read.mjs';
12
+
13
+ export const WriteTool = {
14
+ name: 'Write',
15
+ description: 'Write content to a file. Creates parent dirs if needed.',
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ file_path: { type: 'string', description: 'Absolute path to the file' },
20
+ content: { type: 'string', description: 'The content to write' },
21
+ },
22
+ required: ['file_path', 'content'],
23
+ },
24
+ validateInput(input) {
25
+ const errors = [];
26
+ if (!input.file_path) errors.push('file_path required');
27
+ return errors;
28
+ },
29
+ async call(input) {
30
+ const filePath = path.resolve(input.file_path);
31
+
32
+ // Check if file already exists โ€” require Read first for overwrites
33
+ if (fs.existsSync(filePath)) {
34
+ if (!hasBeenRead(filePath)) {
35
+ return `Error: File ${filePath} already exists. You must Read it first before overwriting.`;
36
+ }
37
+ }
38
+
39
+ // Create parent directory if it doesn't exist
40
+ const dir = path.dirname(filePath);
41
+ try {
42
+ fs.mkdirSync(dir, { recursive: true });
43
+ } catch (e) {
44
+ return `Error creating directory ${dir}: ${e.message}`;
45
+ }
46
+
47
+ try {
48
+ fs.writeFileSync(filePath, input.content);
49
+ markRead(filePath); // Mark as read after writing
50
+ return `File written: ${filePath}`;
51
+ } catch (e) {
52
+ return `Error writing file: ${e.message}`;
53
+ }
54
+ },
55
+ };
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Approval prompt UI โ€” Mission Control (PRD-055 ยง8.2).
3
+ *
4
+ * โ”€โ”€ โš  APPROVAL ยท SHELL-MEDIUM ยท shell โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
5
+ * โš™๏ธ Running git add package.json && git status --short
6
+ * Decision
7
+ * โ–ธ [y] approve once run this call
8
+ * [t] always allow auto-approve future calls to this tool
9
+ * [n] cancel do not run
10
+ * โ†‘โ†“ move ยท Enter pick ยท letter shortcut ยท Esc cancel
11
+ *
12
+ * The rule colour is `brand.accent` (magenta) for explicit-approval
13
+ * tiers; safe-default prompts use `brand.data` so they read as advisory.
14
+ *
15
+ * Pure โ€” caller writes the returned string to stderr.
16
+ */
17
+
18
+ import { paint, width as visibleWidth } from './palette.mjs';
19
+ import { icon } from './icons.mjs';
20
+ import { shellCommandDisplay, toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
21
+ import { label as tierLabel, requiresExplicitApproval, TIERS } from '../core/risk-tier.mjs';
22
+
23
+ /**
24
+ * Default option set per tier. Caller can override via `opts.options`.
25
+ *
26
+ * Each option is `{ key, label, value, hint? }`:
27
+ * key โ€” single-letter shortcut (case-insensitive)
28
+ * label โ€” text shown in the menu
29
+ * value โ€” return value from the menu loop
30
+ * hint โ€” secondary description shown to the right of the label
31
+ */
32
+ export function defaultOptions(tier) {
33
+ const approve = { key: 'y', label: 'approve once', value: 'approve', hint: 'run this call' };
34
+ const cancel = { key: 'n', label: 'cancel', value: 'reject', hint: 'do not run' };
35
+ if (requiresExplicitApproval(tier)) {
36
+ return [approve, cancel];
37
+ }
38
+ return [
39
+ approve,
40
+ { key: 't', label: 'always allow', value: 'allow-type', hint: 'auto-approve future calls to this tool' },
41
+ cancel,
42
+ ];
43
+ }
44
+
45
+ /**
46
+ * Render the compact horizontal prompt with arrow-navigable options.
47
+ *
48
+ * โ”€โ”€ โš  APPROVAL ยท SHELL-MEDIUM ยท shell โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
49
+ * โš™๏ธ Running rm -rf node_modules && npm install
50
+ * Decision
51
+ * โ–ธ [y] approve once
52
+ * [t] always allow
53
+ * [n] cancel
54
+ * โ†‘โ†“ move ยท Enter pick ยท letter shortcut ยท Esc cancel
55
+ * โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
56
+ *
57
+ * @param {object} opts
58
+ * @param {string} opts.tool
59
+ * @param {object} opts.args
60
+ * @param {string} opts.tier
61
+ * @param {string} [opts.why]
62
+ * @param {number} [opts.width]
63
+ * @param {Array} [opts.options] โ€” override default option set
64
+ * @param {number} [opts.selected] โ€” index of the highlighted option
65
+ */
66
+ export function renderApprovalPrompt({
67
+ tool, args = {}, tier, why: _why = '', width,
68
+ options, selected = 0,
69
+ } = {}) {
70
+ const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
71
+ const explicit = requiresExplicitApproval(tier);
72
+ const accent = explicit ? paint.brand.accent : paint.brand.data;
73
+ const opts = options || defaultOptions(tier);
74
+ const title = `${riskIcon(tier)} ${approvalTitle(tier)} ยท ${tierLabel(tier)} ยท ${tool || 'tool'}`;
75
+
76
+ const lines = [
77
+ horizontalRule(title, cols, accent),
78
+ ...subjectRows(tool, args, cols, accent),
79
+ ...decisionRows(opts, selected, accent),
80
+ ` ${paint.text.dim('โ†‘โ†“ move ยท Enter pick ยท letter shortcut ยท Esc cancel')}`,
81
+ horizontalRule('', cols, accent),
82
+ ];
83
+
84
+ return '\n' + lines.join('\n');
85
+ }
86
+
87
+ export function renderTrustedApproval({ tool, args = {}, scope = 'session', ruleId = '', delaySeconds = 0 } = {}) {
88
+ const summary = approvalSubjectSummary(tool, args);
89
+ const subject = `${tool || 'tool'}${summary ? ` "${truncate(summary, 80)}"` : ''}`;
90
+ const rule = ruleId ? ` ยท rule ${ruleId}` : '';
91
+ const delay = delaySeconds > 0 ? ` ยท auto-approved after ${delaySeconds}s` : '';
92
+ return ` ${paint.state.success('โœ“')} ${paint.text.primary(subject)} ${paint.text.dim(`ยท pre-approved (${String(scope || 'session').toLowerCase()}${rule})${delay}`)}\n`;
93
+ }
94
+
95
+ function truncate(text, n) {
96
+ const s = String(text || '');
97
+ if (s.length <= n) return s;
98
+ return s.slice(0, Math.max(0, n - 1)) + 'โ€ฆ';
99
+ }
100
+
101
+ // โ”€โ”€ Compatibility wrapper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
102
+
103
+ /**
104
+ * Render the unified approval prompt. Kept as a named export for older call
105
+ * sites/tests that imported the previous inline renderer.
106
+ */
107
+ export function renderInlinePrompt({ tool, args = {}, tier, why = '' } = {}) {
108
+ return renderApprovalPrompt({ tool, args, tier, why });
109
+ }
110
+
111
+ export { TIERS };
112
+
113
+ function riskIcon(tier) {
114
+ switch (tier) {
115
+ case TIERS.SENSITIVE_READ:
116
+ return '๐Ÿ”';
117
+ case TIERS.SHELL_DANGEROUS:
118
+ case TIERS.DESTRUCTIVE:
119
+ return '๐Ÿ”ด';
120
+ case TIERS.SHELL_MEDIUM:
121
+ return 'โš ';
122
+ case TIERS.NETWORK:
123
+ return '๐ŸŒ';
124
+ case TIERS.LOCAL_EDIT:
125
+ return 'โœŽ';
126
+ default:
127
+ return 'โ—ˆ';
128
+ }
129
+ }
130
+
131
+ function tierTitle(tier) {
132
+ switch (tier) {
133
+ case TIERS.SENSITIVE_READ: return 'SENSITIVE';
134
+ case TIERS.SHELL_DANGEROUS: return 'DANGEROUS';
135
+ case TIERS.DESTRUCTIVE: return 'DESTRUCTIVE';
136
+ case TIERS.SHELL_MEDIUM: return 'MEDIUM';
137
+ case TIERS.NETWORK: return 'NETWORK';
138
+ case TIERS.LOCAL_EDIT: return 'EDIT';
139
+ case TIERS.SHELL_SAFE: return 'SAFE';
140
+ case TIERS.READ: return 'READ';
141
+ default: return tierLabel(tier);
142
+ }
143
+ }
144
+
145
+ function approvalTitle(tier) {
146
+ switch (tier) {
147
+ case TIERS.SENSITIVE_READ:
148
+ case TIERS.SHELL_DANGEROUS:
149
+ case TIERS.DESTRUCTIVE:
150
+ return tierTitle(tier);
151
+ default:
152
+ return 'APPROVAL';
153
+ }
154
+ }
155
+
156
+ function horizontalRule(title, cols, accent) {
157
+ if (!title) return ` ${accent('โ”€'.repeat(Math.max(24, cols - 2)))}`;
158
+ const label = ` ${paint.bold(accent(title))} `;
159
+ const remaining = Math.max(8, cols - 2 - visibleWidth(label) - 2);
160
+ return ` ${accent('โ”€')} ${label}${accent('โ”€'.repeat(remaining))}`;
161
+ }
162
+
163
+ function subjectRows(tool, args, cols, accent) {
164
+ const rows = [];
165
+ const available = Math.max(24, cols - 4);
166
+ const summary = toolDisplaySummary(tool, args, {});
167
+ const label = `${icon(tool)} ${paint.text.primary(toolDisplayLabel(tool))}`;
168
+ const details = subjectDetails(tool, args, summary, Math.max(24, available - visibleWidth(label) - 1));
169
+
170
+ if (details.length === 1 && visibleWidth(`${label} ${details[0]}`) <= available) {
171
+ rows.push(` ${label} ${paint.text.primary(details[0])}`);
172
+ return rows;
173
+ }
174
+
175
+ rows.push(` ${label}`);
176
+ for (const line of subjectDetails(tool, args, summary, Math.max(24, available - 2))) {
177
+ rows.push(` ${paint.text.primary(line)}`);
178
+ }
179
+ return rows;
180
+ }
181
+
182
+ function decisionRows(opts, selected, accent) {
183
+ const rows = [` ${paint.text.dim('Decision')}`];
184
+ for (let i = 0; i < opts.length; i++) {
185
+ rows.push(` ${optionToken(opts[i], i === selected, accent)}`);
186
+ }
187
+ return rows;
188
+ }
189
+
190
+ function optionToken(option, selected, accent) {
191
+ const cursor = selected ? accent('โ–ธ ') : paint.text.dim(' ');
192
+ const keyTag = paint.text.dim('[') + (selected ? accent(option.key) : paint.brand.data(option.key)) + paint.text.dim('] ');
193
+ const label = selected ? paint.bold(accent(option.label)) : paint.text.primary(option.label);
194
+ const hint = option.hint ? ` ${paint.text.muted(option.hint)}` : '';
195
+ return `${cursor}${keyTag}${label}${hint}`;
196
+ }
197
+
198
+ function subjectDetails(tool, args = {}, summary = '', available = 72) {
199
+ if (tool === 'shell') {
200
+ const display = shellCommandDisplay(args.command || args.cmd || summary || '');
201
+ const lines = wrapText(display.command, available);
202
+ if (display.cwdLabel) lines.push(`in ${display.cwdLabel}`);
203
+ return lines.length ? lines : ['(empty command)'];
204
+ }
205
+ if (tool === 'write_file') {
206
+ const file = args.file_path || args.path || summary || '';
207
+ const lineCount = typeof args.content === 'string' ? args.content.split('\n').length : null;
208
+ return [`${file}${lineCount ? ` ยท ${lineCount} lines` : ''}`];
209
+ }
210
+ if (tool === 'edit_file') {
211
+ const file = args.file_path || args.path || '';
212
+ const search = String(args.search || args.old_string || '').trim();
213
+ const replacement = String(args.replace || args.new_string || '').trim();
214
+ const details = [`${file || summary}`];
215
+ if (search) details.push(`match: ${truncate(search, available - 7)}`);
216
+ if (replacement) details.push(`replace: ${truncate(replacement, available - 9)}`);
217
+ return details.slice(0, 4);
218
+ }
219
+ if (tool === 'delete_file') {
220
+ return [args.file_path || args.path || summary || ''];
221
+ }
222
+ if (tool === 'WebFetch' || tool === 'fetch_url') {
223
+ const url = args.url || summary || '';
224
+ return [`Target ${hostFromUrl(url) || url}`];
225
+ }
226
+ return wrapText(summary || JSON.stringify(args || {}), available).slice(0, 4);
227
+ }
228
+
229
+ function approvalSubjectSummary(tool, args = {}) {
230
+ const summary = toolDisplaySummary(tool, args, {});
231
+ if (tool !== 'shell') return summary;
232
+ const display = shellCommandDisplay(args.command || args.cmd || summary || '');
233
+ return display.cwdLabel ? `${display.command} in ${display.cwdLabel}` : display.command;
234
+ }
235
+
236
+ function hostFromUrl(url) {
237
+ try {
238
+ return new URL(String(url)).host;
239
+ } catch {
240
+ return '';
241
+ }
242
+ }
243
+
244
+ function wrapText(text, width) {
245
+ const words = String(text || '').split(/\s+/).filter(Boolean);
246
+ if (!words.length) return [''];
247
+ const lines = [];
248
+ let line = '';
249
+ for (const word of words) {
250
+ if (!line) {
251
+ line = word;
252
+ continue;
253
+ }
254
+ if ((line + ' ' + word).length <= width) {
255
+ line += ' ' + word;
256
+ } else {
257
+ lines.push(line);
258
+ line = word;
259
+ }
260
+ }
261
+ if (line) lines.push(line);
262
+ return lines;
263
+ }