@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,486 @@
1
+ /**
2
+ * Agent Loop — async generator yielding 13 event types.
3
+ * Handles streaming, tool calls, thinking, auto-compaction, hooks, multi-provider.
4
+ */
5
+ import { streamResponse, accumulateStream } from './streaming.mjs';
6
+ import { ContextManager } from './context-manager.mjs';
7
+ import { buildSystemPrompt } from './system-prompt.mjs';
8
+ import { createStagnationTracker, stagnationMessage } from './stagnation.mjs';
9
+ import { PromptCache } from './cache.mjs';
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ export function createAgentLoop({ model, tools, permissions, settings, hooks }) {
13
+ const contextManager = new ContextManager(settings.maxContextTokens || 180000);
14
+
15
+ // Build system prompt using the new builder
16
+ const promptResult = buildSystemPrompt({
17
+ cwd: process.cwd(),
18
+ tools: tools.list?.() || [],
19
+ override: settings.systemPromptOverride,
20
+ addDirs: settings.addDirs,
21
+ });
22
+
23
+ const state = {
24
+ messages: [],
25
+ systemPrompt: promptResult.full,
26
+ turnCount: 0,
27
+ tokenUsage: { input: 0, output: 0, cache_read: 0, cache_creation: 0 },
28
+ model,
29
+ tools,
30
+ _contextManager: contextManager,
31
+ _promptCache: new PromptCache(),
32
+ };
33
+ const stagnation = createStagnationTracker({
34
+ enabled: settings.stagnationDetection === true,
35
+ threshold: settings.stagnationThreshold,
36
+ });
37
+
38
+ async function* run(userMessage, options = {}) {
39
+ // Add user message (skip for continuation turns)
40
+ if (userMessage && !options.continuation) {
41
+ state.messages = contextManager.addMessage(state.messages, {
42
+ role: 'user',
43
+ content: userMessage,
44
+ });
45
+ state.turnCount++;
46
+ }
47
+
48
+ // Check max turns
49
+ if (settings.maxTurns && state.turnCount > settings.maxTurns) {
50
+ yield { type: 'error', message: `Max turns (${settings.maxTurns}) reached.` };
51
+ yield { type: 'stop', reason: 'max_turns' };
52
+ return;
53
+ }
54
+
55
+ // Auto-compact if needed
56
+ if (contextManager.shouldCompact(state.messages)) {
57
+ yield { type: 'compaction', count: contextManager.compactionCount + 1 };
58
+ state.messages = contextManager.compact(state.messages);
59
+ }
60
+
61
+ yield { type: 'stream_request_start', turn: state.turnCount };
62
+
63
+ // Detect provider and call API
64
+ const provider = detectProvider(model);
65
+ let response;
66
+
67
+ try {
68
+ if (settings.stream !== false) {
69
+ // Streaming mode
70
+ response = await callApiStreaming(provider, model, state, tools.list(), settings);
71
+ const collectedContent = [];
72
+ let currentText = '';
73
+ let currentThinking = '';
74
+
75
+ for await (const event of response.events) {
76
+ if (event.type === 'content_block_start') {
77
+ if (event.content_block?.type === 'thinking') {
78
+ currentThinking = '';
79
+ }
80
+ } else if (event.type === 'content_block_delta') {
81
+ if (event.delta?.type === 'text_delta') {
82
+ currentText += event.delta.text;
83
+ yield { type: 'stream_event', text: event.delta.text };
84
+ } else if (event.delta?.type === 'thinking_delta') {
85
+ currentThinking += event.delta.thinking;
86
+ yield { type: 'thinking', text: event.delta.thinking };
87
+ }
88
+ } else if (event.type === 'ping') {
89
+ // Keepalive, ignore
90
+ }
91
+ }
92
+
93
+ // Use the accumulated message
94
+ response = response.accumulated;
95
+ } else {
96
+ // Non-streaming mode
97
+ response = await callApi(provider, model, state, tools.list(), settings);
98
+ }
99
+ } catch (err) {
100
+ yield { type: 'error', message: err.message };
101
+ return;
102
+ }
103
+
104
+ // Track token usage (PRD-071 §1.1: also record cache hits/writes so
105
+ // /extra-usage and /status stop reporting zeros)
106
+ if (response.usage) {
107
+ state.tokenUsage.input += response.usage.input_tokens || 0;
108
+ state.tokenUsage.output += response.usage.output_tokens || 0;
109
+ state.tokenUsage.cache_read += response.usage.cache_read_input_tokens || 0;
110
+ state.tokenUsage.cache_creation += response.usage.cache_creation_input_tokens || 0;
111
+ state._promptCache.updateStats(response.usage);
112
+ }
113
+
114
+ // Build assistant message for history
115
+ const assistantMessage = { role: 'assistant', content: response.content };
116
+ state.messages.push(assistantMessage);
117
+
118
+ // Process content blocks
119
+ const toolUseBlocks = [];
120
+
121
+ for (const block of response.content || []) {
122
+ if (block.type === 'text') {
123
+ yield { type: 'assistant', content: block.text };
124
+ }
125
+
126
+ if (block.type === 'thinking') {
127
+ yield { type: 'thinking_complete', thinking: block.thinking };
128
+ }
129
+
130
+ if (block.type === 'tool_use') {
131
+ toolUseBlocks.push(block);
132
+ }
133
+ }
134
+
135
+ // Process tool calls
136
+ if (toolUseBlocks.length > 0) {
137
+ const toolResults = [];
138
+
139
+ for (const block of toolUseBlocks) {
140
+ // Only consecutive identical calls indicate a loop. The same read or
141
+ // validation later in a task can be legitimate progress verification.
142
+ const stagnationResult = stagnation.record(block.name, block.input);
143
+ if (stagnationResult.detected) {
144
+ yield { type: 'stagnation', tool: block.name, count: stagnationResult.count };
145
+ toolResults.push({
146
+ type: 'tool_result',
147
+ tool_use_id: block.id,
148
+ content: stagnationMessage(block.name, stagnationResult.count),
149
+ });
150
+ continue;
151
+ }
152
+
153
+ // Run pre-tool hooks
154
+ if (hooks) {
155
+ const hookResult = await hooks.runPreToolUse(block.name, block.input);
156
+ if (!hookResult.allow) {
157
+ yield { type: 'hookPermissionResult', tool: block.name, allowed: false, message: hookResult.message };
158
+ toolResults.push({
159
+ type: 'tool_result',
160
+ tool_use_id: block.id,
161
+ content: `Blocked by hook: ${hookResult.message}`,
162
+ });
163
+ continue;
164
+ }
165
+ }
166
+
167
+ // Check permission
168
+ const allowed = await permissions.check(block.name, block.input);
169
+ if (!allowed) {
170
+ yield { type: 'hookPermissionResult', tool: block.name, allowed: false };
171
+ toolResults.push({
172
+ type: 'tool_result',
173
+ tool_use_id: block.id,
174
+ content: 'Permission denied',
175
+ });
176
+ continue;
177
+ }
178
+
179
+ // Execute tool
180
+ yield { type: 'tool_progress', tool: block.name, status: 'running' };
181
+
182
+ let result;
183
+ try {
184
+ result = await tools.call(block.name, block.input);
185
+ } catch (err) {
186
+ result = `Tool error: ${err.message}`;
187
+ }
188
+
189
+ // Run post-tool hooks
190
+ if (hooks) {
191
+ result = await hooks.runPostToolUse(block.name, result);
192
+ }
193
+
194
+ yield { type: 'result', tool: block.name, result };
195
+
196
+ toolResults.push({
197
+ type: 'tool_result',
198
+ tool_use_id: block.id,
199
+ content: typeof result === 'string' ? result : JSON.stringify(result),
200
+ });
201
+ }
202
+
203
+ // Add tool results as a single user message
204
+ state.messages.push({ role: 'user', content: toolResults });
205
+
206
+ // Recursive: continue the loop after tool execution
207
+ yield* run(null, { continuation: true });
208
+ return;
209
+ }
210
+
211
+ // No tool calls — check stop hooks
212
+ if (hooks) {
213
+ const allowStop = await hooks.runStop();
214
+ if (!allowStop) {
215
+ // Hook prevented stopping — continue with a nudge
216
+ state.messages = contextManager.addMessage(state.messages, {
217
+ role: 'user',
218
+ content: '[System: A hook prevented stopping. Please continue with the task.]',
219
+ });
220
+ yield* run(null, { continuation: true });
221
+ return;
222
+ }
223
+ }
224
+
225
+ yield { type: 'stop', reason: response.stop_reason || 'end_turn' };
226
+ }
227
+
228
+ return { run, state };
229
+ }
230
+
231
+ function detectProvider(model) {
232
+ if (model.startsWith('gpt-') || model.startsWith('o1') || model.startsWith('o3')) return 'openai';
233
+ if (model.startsWith('gemini')) return 'google';
234
+ return 'anthropic';
235
+ }
236
+
237
+ async function callApi(provider, model, state, toolDefs, settings) {
238
+ const callers = { anthropic: callAnthropic, openai: callOpenAI, google: callGoogle };
239
+ const caller = callers[provider] || callers.anthropic;
240
+ return caller(model, state, toolDefs, settings, false);
241
+ }
242
+
243
+ async function callApiStreaming(provider, model, state, toolDefs, settings) {
244
+ const callers = { anthropic: callAnthropic, openai: callOpenAI, google: callGoogle };
245
+ const caller = callers[provider] || callers.anthropic;
246
+ return caller(model, state, toolDefs, settings, true);
247
+ }
248
+
249
+ async function callAnthropic(model, state, toolDefs, settings, stream) {
250
+ const apiKey = process.env.ANTHROPIC_API_KEY;
251
+ if (!apiKey) throw new Error('ANTHROPIC_API_KEY not set');
252
+
253
+ const body = {
254
+ model,
255
+ max_tokens: settings.maxTokens || 16384,
256
+ messages: state.messages,
257
+ ...(state.systemPrompt && { system: state.systemPrompt }),
258
+ ...(toolDefs.length > 0 && { tools: toolDefs }),
259
+ ...(stream && { stream: true }),
260
+ };
261
+
262
+ // Enable extended thinking if model supports it
263
+ if (model.includes('opus') || settings.thinking) {
264
+ body.thinking = { type: 'enabled', budget_tokens: settings.thinkingBudget || 10000 };
265
+ }
266
+
267
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
268
+ method: 'POST',
269
+ headers: {
270
+ 'Content-Type': 'application/json',
271
+ 'x-api-key': apiKey,
272
+ 'anthropic-version': '2023-06-01',
273
+ },
274
+ body: JSON.stringify(body),
275
+ });
276
+
277
+ if (!res.ok) {
278
+ const err = await res.text();
279
+ throw new Error(`Anthropic API error ${res.status}: ${err}`);
280
+ }
281
+
282
+ if (stream) {
283
+ const collected = [];
284
+ const eventGenerator = async function* () {
285
+ for await (const event of streamResponse(res)) {
286
+ collected.push(event);
287
+ yield event;
288
+ }
289
+ };
290
+ return {
291
+ events: eventGenerator(),
292
+ get accumulated() {
293
+ return accumulateFromCollected(collected);
294
+ },
295
+ };
296
+ }
297
+
298
+ return res.json();
299
+ }
300
+
301
+ async function callOpenAI(model, state, toolDefs, settings, stream) {
302
+ const apiKey = process.env.OPENAI_API_KEY;
303
+ if (!apiKey) throw new Error('OPENAI_API_KEY not set');
304
+
305
+ const messages = [];
306
+ if (state.systemPrompt) {
307
+ messages.push({ role: 'system', content: state.systemPrompt });
308
+ }
309
+ for (const msg of state.messages) {
310
+ if (typeof msg.content === 'string') {
311
+ messages.push({ role: msg.role, content: msg.content });
312
+ } else if (Array.isArray(msg.content)) {
313
+ for (const block of msg.content) {
314
+ if (block.type === 'tool_result') {
315
+ messages.push({
316
+ role: 'tool',
317
+ tool_call_id: block.tool_use_id,
318
+ content: block.content,
319
+ });
320
+ }
321
+ }
322
+ }
323
+ }
324
+
325
+ const tools = toolDefs.map(t => ({
326
+ type: 'function',
327
+ function: { name: t.name, description: t.description, parameters: t.input_schema },
328
+ }));
329
+
330
+ const body = {
331
+ model,
332
+ messages,
333
+ ...(tools.length > 0 && { tools }),
334
+ };
335
+
336
+ const baseUrl = process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1';
337
+ const res = await fetch(`${baseUrl}/chat/completions`, {
338
+ method: 'POST',
339
+ headers: {
340
+ 'Content-Type': 'application/json',
341
+ 'Authorization': `Bearer ${apiKey}`,
342
+ },
343
+ body: JSON.stringify(body),
344
+ });
345
+
346
+ if (!res.ok) {
347
+ const err = await res.text();
348
+ throw new Error(`OpenAI API error ${res.status}: ${err}`);
349
+ }
350
+
351
+ const data = await res.json();
352
+ return convertOpenAIResponse(data);
353
+ }
354
+
355
+ async function callGoogle(model, state, toolDefs, settings, stream) {
356
+ const apiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
357
+ if (!apiKey) throw new Error('GOOGLE_API_KEY or GEMINI_API_KEY not set');
358
+
359
+ const contents = [];
360
+ for (const msg of state.messages) {
361
+ const role = msg.role === 'assistant' ? 'model' : 'user';
362
+ if (typeof msg.content === 'string') {
363
+ contents.push({ role, parts: [{ text: msg.content }] });
364
+ }
365
+ }
366
+
367
+ const body = {
368
+ contents,
369
+ ...(state.systemPrompt && {
370
+ systemInstruction: { parts: [{ text: state.systemPrompt }] },
371
+ }),
372
+ };
373
+
374
+ const res = await fetch(
375
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`,
376
+ {
377
+ method: 'POST',
378
+ headers: { 'Content-Type': 'application/json' },
379
+ body: JSON.stringify(body),
380
+ }
381
+ );
382
+
383
+ if (!res.ok) {
384
+ const err = await res.text();
385
+ throw new Error(`Google API error ${res.status}: ${err}`);
386
+ }
387
+
388
+ const data = await res.json();
389
+ return convertGoogleResponse(data);
390
+ }
391
+
392
+ function convertOpenAIResponse(data) {
393
+ const choice = data.choices?.[0];
394
+ if (!choice) throw new Error('No choices in OpenAI response');
395
+
396
+ const content = [];
397
+ if (choice.message?.content) {
398
+ content.push({ type: 'text', text: choice.message.content });
399
+ }
400
+
401
+ if (choice.message?.tool_calls) {
402
+ for (const tc of choice.message.tool_calls) {
403
+ content.push({
404
+ type: 'tool_use',
405
+ id: tc.id,
406
+ name: tc.function.name,
407
+ input: JSON.parse(tc.function.arguments || '{}'),
408
+ });
409
+ }
410
+ }
411
+
412
+ return {
413
+ content,
414
+ stop_reason: choice.finish_reason === 'stop' ? 'end_turn' : choice.finish_reason,
415
+ usage: {
416
+ input_tokens: data.usage?.prompt_tokens || 0,
417
+ output_tokens: data.usage?.completion_tokens || 0,
418
+ },
419
+ };
420
+ }
421
+
422
+ function convertGoogleResponse(data) {
423
+ const candidate = data.candidates?.[0];
424
+ if (!candidate) throw new Error('No candidates in Google response');
425
+
426
+ const content = [];
427
+ for (const part of candidate.content?.parts || []) {
428
+ if (part.text) content.push({ type: 'text', text: part.text });
429
+ }
430
+
431
+ return {
432
+ content,
433
+ stop_reason: 'end_turn',
434
+ usage: {
435
+ input_tokens: data.usageMetadata?.promptTokenCount || 0,
436
+ output_tokens: data.usageMetadata?.candidatesTokenCount || 0,
437
+ },
438
+ };
439
+ }
440
+
441
+ function accumulateFromCollected(events) {
442
+ const message = {
443
+ content: [],
444
+ stop_reason: null,
445
+ usage: { input_tokens: 0, output_tokens: 0 },
446
+ };
447
+
448
+ let currentBlock = null;
449
+
450
+ for (const event of events) {
451
+ switch (event.type) {
452
+ case 'message_start':
453
+ if (event.message?.usage) {
454
+ message.usage.input_tokens = event.message.usage.input_tokens || 0;
455
+ }
456
+ break;
457
+ case 'content_block_start':
458
+ currentBlock = { ...event.content_block };
459
+ if (currentBlock.type === 'text') currentBlock.text = '';
460
+ if (currentBlock.type === 'thinking') currentBlock.thinking = '';
461
+ if (currentBlock.type === 'tool_use') currentBlock.input = '';
462
+ message.content.push(currentBlock);
463
+ break;
464
+ case 'content_block_delta':
465
+ if (!currentBlock) break;
466
+ if (event.delta?.type === 'text_delta') currentBlock.text += event.delta.text;
467
+ else if (event.delta?.type === 'thinking_delta') currentBlock.thinking += event.delta.thinking;
468
+ else if (event.delta?.type === 'input_json_delta') currentBlock.input += event.delta.partial_json;
469
+ break;
470
+ case 'content_block_stop':
471
+ if (currentBlock?.type === 'tool_use' && typeof currentBlock.input === 'string') {
472
+ try { currentBlock.input = JSON.parse(currentBlock.input || '{}'); } catch { currentBlock.input = {}; }
473
+ }
474
+ currentBlock = null;
475
+ break;
476
+ case 'message_delta':
477
+ if (event.delta?.stop_reason) message.stop_reason = event.delta.stop_reason;
478
+ if (event.usage) message.usage.output_tokens = event.usage.output_tokens || 0;
479
+ break;
480
+ case 'ping':
481
+ break;
482
+ }
483
+ }
484
+
485
+ return message;
486
+ }
@@ -0,0 +1,104 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+
4
+ const REDACTED = 'REDACTED';
5
+ const SENSITIVE_KEY_RE = /^(?:authorization|api[-_]?key|apikey|key|token|access[-_]?token|refresh[-_]?token|secret|password|passwd|pwd|access[-_]?key|secret[-_]?access[-_]?key)$|(?:^|[-_])(?:api[-_]?key|apikey|token|secret|password|passwd|pwd|access[-_]?key|secret[-_]?access[-_]?key)(?:$|[-_])/i;
6
+ const SENSITIVE_ASSIGNMENT_KEY = String.raw`[A-Z0-9_-]*(?:API[-_]?KEY|APIKEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|ACCESS[-_]?KEY|SECRET[-_]?ACCESS[-_]?KEY)[A-Z0-9_-]*`;
7
+ const SENSITIVE_JSON_KEY = String.raw`(?:authorization|api[-_]?key|apikey|key|token|access[-_]?token|refresh[-_]?token|secret|password|passwd|pwd|access[-_]?key|secret[-_]?access[-_]?key|[A-Z0-9_-]*(?:API[-_]?KEY|APIKEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|ACCESS[-_]?KEY|SECRET[-_]?ACCESS[-_]?KEY)[A-Z0-9_-]*)`;
8
+
9
+ export function redactSensitive(str) {
10
+ let s = String(str ?? '');
11
+
12
+ // Authorization headers in shell commands, curl args, and copied HTTP snippets.
13
+ s = s.replace(/(Authorization\s*:\s*(?:Bearer|Basic)\s+)(?:"[^"]*"|'[^']*'|[^\s"',;]+)/gi, `$1${REDACTED}`);
14
+
15
+ // Secret-bearing query/form parameters.
16
+ s = s.replace(/([?&](?:api_key|apikey|api-key|key|token|access_token|refresh_token|password|secret)=)([^&\s"']+)/gi, `$1${REDACTED}`);
17
+
18
+ // Env-style assignments, including quoted values.
19
+ s = s.replace(
20
+ new RegExp(`(^|[\\s;,])(${SENSITIVE_ASSIGNMENT_KEY}\\s*=\\s*)(?:"[^"]*"|'[^']*'|[^\\s"',;]+)`, 'gi'),
21
+ (_match, prefix, key) => `${prefix}${key}${REDACTED}`
22
+ );
23
+
24
+ // JSON fragments that arrive as strings rather than structured objects.
25
+ s = s.replace(
26
+ new RegExp(`("(?:${SENSITIVE_JSON_KEY})"\\s*:\\s*)(?:"(?:\\\\.|[^"\\\\])*"|[^,}\\s]+)`, 'gi'),
27
+ `$1"${REDACTED}"`
28
+ );
29
+
30
+ return s;
31
+ }
32
+
33
+ function isSensitiveKey(key) {
34
+ return SENSITIVE_KEY_RE.test(String(key || ''));
35
+ }
36
+
37
+ function sanitizeForLog(value, depth = 0) {
38
+ if (depth > 12) return '[MaxDepth]';
39
+ if (typeof value === 'string') return redactSensitive(value);
40
+ if (value == null || typeof value !== 'object') return value;
41
+ if (Array.isArray(value)) return value.slice(0, 100).map(item => sanitizeForLog(item, depth + 1));
42
+
43
+ const out = {};
44
+ for (const [key, item] of Object.entries(value)) {
45
+ out[key] = isSensitiveKey(key) ? REDACTED : sanitizeForLog(item, depth + 1);
46
+ }
47
+ return out;
48
+ }
49
+
50
+ function safeArgs(args) {
51
+ if (args?.command) return redactSensitive(String(args.command)).slice(0, 500);
52
+ if (args?.file_path || args?.path) return redactSensitive(String(args.file_path || args.path)).slice(0, 500);
53
+ try { return JSON.stringify(sanitizeForLog(args || {})).slice(0, 500); }
54
+ catch { return redactSensitive(String(args || '')).slice(0, 500); }
55
+ }
56
+
57
+ function safeText(value, max = 500) {
58
+ if (!value) return undefined;
59
+ return redactSensitive(String(value)).slice(0, max);
60
+ }
61
+
62
+ export class ApprovalLog {
63
+ constructor({ cwd = process.cwd() } = {}) {
64
+ this.cwd = cwd;
65
+ this.filePath = path.join(cwd, '.bahulam', 'approvals.log');
66
+ }
67
+
68
+ append(entry) {
69
+ try {
70
+ fs.mkdirSync(path.dirname(this.filePath), { recursive: true, mode: 0o700 });
71
+ const line = JSON.stringify({
72
+ ts: new Date().toISOString(),
73
+ tier: entry.tier,
74
+ tool: entry.tool,
75
+ args: safeArgs(entry.args),
76
+ decision: entry.decision,
77
+ scope: entry.scope || 'once',
78
+ rule_id: entry.rule_id || null,
79
+ reason: safeText(entry.reason),
80
+ });
81
+ fs.appendFileSync(this.filePath, line + '\n', { mode: 0o600 });
82
+ try { fs.chmodSync(this.filePath, 0o600); } catch {}
83
+ } catch { /* approval logging must not block execution */ }
84
+ }
85
+
86
+ readRecent(limit = 20) {
87
+ try {
88
+ if (!fs.existsSync(this.filePath)) return [];
89
+ return fs.readFileSync(this.filePath, 'utf-8')
90
+ .trim()
91
+ .split('\n')
92
+ .filter(Boolean)
93
+ .slice(-limit)
94
+ .map(line => {
95
+ try { return JSON.parse(line); }
96
+ catch { return null; }
97
+ })
98
+ .filter(Boolean)
99
+ .reverse();
100
+ } catch {
101
+ return [];
102
+ }
103
+ }
104
+ }