@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,448 @@
1
+ /**
2
+ * Headless Runner — non-interactive mode for benchmarks and automation.
3
+ *
4
+ * No REPL, no spinners, no approval prompts. Auto-approves all tools.
5
+ * Outputs structured JSONL to stdout for machine consumption.
6
+ * stderr gets minimal progress (optional with --verbose).
7
+ *
8
+ * Usage:
9
+ * bahulam-code --headless "Fix the bug in auth.py"
10
+ * bahulam-code --headless --timeout 300 --max-cost 2.00 "Refactor the login flow"
11
+ * bahulam-code --headless --model deepseek/deepseek-chat-v3-0324 "Add tests"
12
+ */
13
+
14
+ import { TarangStreamClient } from './stream-client.mjs';
15
+ import { createToolExecutor } from './tool-executor.mjs';
16
+ import { buildWorkScope, promptProjectRoots } from './work-scope.mjs';
17
+ import { persistProjectArtifacts } from './project-artifacts.mjs';
18
+ import { TarangAuth } from '../auth/tarang-auth.mjs';
19
+ import { ApprovalManager } from './approval.mjs';
20
+ import {
21
+ appendVisionAnalysisToInstruction,
22
+ prepareImageAttachments,
23
+ publicAttachmentMetadata,
24
+ } from './attachments.mjs';
25
+
26
+ /**
27
+ * Run a single instruction in headless mode.
28
+ * @param {object} opts
29
+ * @param {string} opts.instruction - the prompt to send
30
+ * @param {string} [opts.model] - model override
31
+ * @param {number} [opts.timeout] - max seconds (default: 300)
32
+ * @param {number} [opts.maxCost] - abort if cost exceeds this USD amount
33
+ * @param {boolean} [opts.verbose] - show progress on stderr
34
+ */
35
+ export async function runHeadless({ instruction, model, timeout = 300, maxCost, verbose = false, cacheReport = null, local = false, vision = [] }) {
36
+ const startTime = Date.now();
37
+
38
+ const log = (msg) => {
39
+ if (verbose) process.stderr.write(`[headless] ${msg}\n`);
40
+ };
41
+
42
+ const emit = (obj) => {
43
+ process.stdout.write(JSON.stringify(obj) + '\n');
44
+ };
45
+
46
+ // ── Auth ──
47
+ const auth = new TarangAuth();
48
+ const creds = auth.loadCredentials();
49
+ if (!creds.token) {
50
+ emit({ type: 'error', error: 'Not logged in. Run: bahulam-code login' });
51
+ process.exit(1);
52
+ }
53
+
54
+ // Projects are registered and indexed only when the agent requests an overview.
55
+ const toolExecutor = createToolExecutor();
56
+
57
+ // Auto-approve everything — no prompts
58
+ const approval = new ApprovalManager({ autoApprove: true });
59
+
60
+ // ── Client selection ──
61
+ // PRD-071 Phase 2 measurement — --local forces the CLI-side LocalAgent path,
62
+ // bypassing the backend so we can exercise the cache_control wiring we
63
+ // just added to _callClaude / _callOpenRouter. Model comes from the
64
+ // --model flag (which overrides settings dynamically for benchmarking).
65
+ let client;
66
+ if (local) {
67
+ const { LocalAgent } = await import('./local-agent.mjs');
68
+ const localModel = model || creds.models?.local || 'anthropic/claude-sonnet-4';
69
+ const orKey = process.env.OPENROUTER_API_KEY || creds.openRouterKey;
70
+ const anthKey = process.env.ANTHROPIC_API_KEY || creds.anthropicKey;
71
+ if (!orKey && !anthKey) {
72
+ emit({ type: 'error', error: '--local requires OPENROUTER_API_KEY or ANTHROPIC_API_KEY' });
73
+ process.exit(1);
74
+ }
75
+ client = {
76
+ execute: (instr, ctx) => new LocalAgent({
77
+ apiKey: anthKey,
78
+ openRouterKey: orKey,
79
+ model: localModel,
80
+ toolExecutor,
81
+ verbose,
82
+ cwd: process.cwd(),
83
+ maxTurns: 50,
84
+ }).execute(instr, ctx),
85
+ };
86
+ log(`Local mode: ${localModel}`);
87
+ } else {
88
+ client = new TarangStreamClient({
89
+ baseUrl: creds.backendUrl,
90
+ token: creds.token,
91
+ toolExecutor,
92
+ approvalManager: approval,
93
+ });
94
+ }
95
+
96
+ // ── Timeout ──
97
+ const timeoutMs = timeout * 1000;
98
+ const timeoutTimer = setTimeout(() => {
99
+ emit({ type: 'timeout', duration_s: timeout });
100
+ log(`Timeout after ${timeout}s`);
101
+ process.exit(2);
102
+ }, timeoutMs);
103
+
104
+ // ── Vision analysis preflight ──
105
+ if (!local) {
106
+ try {
107
+ const prepared = prepareImageAttachments(instruction, {
108
+ cwd: process.cwd(),
109
+ extraPaths: Array.isArray(vision) ? vision : [],
110
+ });
111
+ if (prepared.attachments.length) {
112
+ emit({
113
+ type: 'attachments',
114
+ attachments: prepared.attachments.map(publicAttachmentMetadata),
115
+ });
116
+ const analysis = await client.analyzeVision({
117
+ instruction: prepared.instruction,
118
+ attachments: prepared.attachments,
119
+ });
120
+ instruction = appendVisionAnalysisToInstruction(prepared.instruction, analysis);
121
+ emit({
122
+ type: 'vision_analysis',
123
+ model: analysis.model,
124
+ attachments: analysis.attachments || prepared.metadata,
125
+ summary_chars: String(analysis.summary || '').length,
126
+ });
127
+ } else {
128
+ instruction = prepared.instruction || instruction;
129
+ }
130
+ } catch (err) {
131
+ emit({ type: 'error', error: err.message || String(err), code: 'vision_analysis_failed' });
132
+ process.exit(1);
133
+ }
134
+ } else if (Array.isArray(vision) && vision.length) {
135
+ emit({ type: 'error', error: '--vision is not supported with --local yet', code: 'vision_local_unsupported' });
136
+ process.exit(1);
137
+ }
138
+
139
+ // ── Execute ──
140
+ emit({ type: 'start', timestamp: Date.now(), instruction, model: model || 'default', cwd: process.cwd() });
141
+
142
+ let projectResources = toolExecutor.getProjectResources();
143
+ const promptRoots = promptProjectRoots(instruction);
144
+ if (promptRoots.length > 0) {
145
+ await toolExecutor.registerProjectRoots(promptRoots);
146
+ projectResources = toolExecutor.getProjectResources();
147
+ }
148
+ const execContext = {
149
+ cwd: process.cwd(),
150
+ freeswim: true,
151
+ project_resources: projectResources,
152
+ work_scope: buildWorkScope({
153
+ instruction,
154
+ cwd: process.cwd(),
155
+ projectResources,
156
+ }),
157
+ agent_context: toolExecutor.getAgentContext(),
158
+ };
159
+ if (model) execContext.model_override = model;
160
+
161
+ let primaryToolCount = 0;
162
+ let subAgentForwardedToolCount = 0;
163
+ let backendToolCount = null;
164
+ let backendPrimaryToolCount = null;
165
+ let backendSubAgentToolCount = null;
166
+ let finalContent = '';
167
+ let totalCost = 0;
168
+ let rateLimit = null;
169
+
170
+ // ── Telemetry collectors ──
171
+ const toolCalls = []; // { tool, call_id, duration_ms, success, internal, sub_agent }
172
+ const emittedToolResults = new Set();
173
+ const subAgents = []; // { type, model, duration_s, tool_calls, success }
174
+ let stagnationCount = 0;
175
+ let usage = {}; // { input_tokens, output_tokens, cache_read, cache_write }
176
+
177
+ try {
178
+ for await (const event of client.execute(instruction, execContext)) {
179
+ const { type, data } = event;
180
+
181
+ if (type === 'tool_call' || type === 'tool_request') {
182
+ const isInternal = Boolean(data?.internal || data?.sub_agent);
183
+ const toolName = data?.tool || 'unknown';
184
+ const args = data?.args || {};
185
+ if (isInternal) subAgentForwardedToolCount++;
186
+ else primaryToolCount++;
187
+ toolCalls.push({
188
+ tool: toolName,
189
+ call_id: data?.call_id || data?.request_id || '',
190
+ success: null,
191
+ duration_ms: 0,
192
+ internal: isInternal,
193
+ sub_agent: data?.sub_agent || null,
194
+ });
195
+ emit({
196
+ type: 'tool_call',
197
+ tool: toolName,
198
+ args,
199
+ call_id: data?.call_id || data?.request_id || '',
200
+ approved: true,
201
+ internal: isInternal,
202
+ sub_agent: data?.sub_agent || null,
203
+ });
204
+ log(`Tool: ${toolName}`);
205
+ }
206
+
207
+ if (type === 'tool_result' || type === 'tool_done') {
208
+ const success = data?.success !== false;
209
+ const durationMs = data?.duration_ms ?? Math.round((data?.duration_s || 0) * 1000);
210
+ const callId = data?.call_id || data?._callId || data?.request_id || '';
211
+ const isInternal = Boolean(data?.internal || data?.sub_agent);
212
+ // Update last tool call with result
213
+ const last = toolCalls.findLast(t => (callId && t.call_id === callId) || t.tool === (data?.tool || ''));
214
+ if (last) {
215
+ last.success = success;
216
+ last.duration_ms = durationMs;
217
+ if (isInternal) {
218
+ last.internal = true;
219
+ last.sub_agent = data?.sub_agent || last.sub_agent || null;
220
+ }
221
+ } else if (data?.tool) {
222
+ // Some backend-owned meta-tools (for example explore) emit a
223
+ // result/complete event without a preceding client-visible
224
+ // tool_call. Keep the breakdown honest without relying on a
225
+ // duplicate rendered call line.
226
+ toolCalls.push({
227
+ tool: data.tool,
228
+ call_id: callId,
229
+ success,
230
+ duration_ms: durationMs,
231
+ internal: isInternal,
232
+ sub_agent: data?.sub_agent || null,
233
+ });
234
+ if (isInternal) subAgentForwardedToolCount++;
235
+ else primaryToolCount++;
236
+ }
237
+ if (callId && emittedToolResults.has(callId)) continue;
238
+ if (callId) emittedToolResults.add(callId);
239
+ emit({
240
+ type: 'tool_result',
241
+ tool: data?.tool || '',
242
+ call_id: callId,
243
+ success,
244
+ duration_ms: durationMs,
245
+ internal: isInternal,
246
+ sub_agent: data?.sub_agent || null,
247
+ });
248
+ }
249
+
250
+ if (type === 'file_diff') {
251
+ emit({
252
+ type: 'file_diff',
253
+ tool: data?.tool || '',
254
+ path: data?.path || '',
255
+ relative_path: data?.relative_path || '',
256
+ lines_added: data?.lines_added || 0,
257
+ lines_removed: data?.lines_removed || 0,
258
+ truncated: !!data?.truncated,
259
+ hunks: data?.hunks || [],
260
+ });
261
+ }
262
+
263
+ if (type === 'sub_agent_start') {
264
+ log(`SubAgent: ${data?.type} (${data?.model})`);
265
+ }
266
+
267
+ if (type === 'sub_agent_complete') {
268
+ subAgents.push({
269
+ type: data?.type || '',
270
+ model: data?.model || '',
271
+ duration_s: data?.duration_s || 0,
272
+ tool_calls: data?.tool_calls || 0,
273
+ success: data?.success !== false,
274
+ });
275
+ emit({ type: 'sub_agent', ...data });
276
+ log(`SubAgent done: ${data?.type} (${data?.tool_calls} tools, ${data?.duration_s}s)`);
277
+ }
278
+
279
+ if (type === 'plan_created' || type === 'goal_created') {
280
+ persistProjectArtifacts(
281
+ data,
282
+ toolExecutor.getProjectResources(),
283
+ log,
284
+ );
285
+ }
286
+
287
+ if (type === 'stagnation' || type === 'stagnation_detected') {
288
+ stagnationCount++;
289
+ emit({ type: 'stagnation', reason: data?.reason || '', strategy: data?.recovery_strategy || '' });
290
+ log(`Stagnation: ${data?.reason || ''}`);
291
+ }
292
+
293
+ if (type === 'content') {
294
+ finalContent = data?.text || '';
295
+ }
296
+
297
+ if (type === 'content_partial') {
298
+ const text = data?.text || '';
299
+ if (text) finalContent = text;
300
+ }
301
+
302
+ if (type === 'session_info') {
303
+ if (data?.rate_limit) rateLimit = data.rate_limit;
304
+ // Surface session_id in the JSONL so multi-turn harnesses can
305
+ // capture it from turn N and forward on turn N+1 (via TARANG_SESSION_ID).
306
+ if (data?.session_id) emit({ type: 'session_info', session_id: data.session_id });
307
+ }
308
+
309
+ if (type === 'complete') {
310
+ if (data?.rate_limit) rateLimit = data.rate_limit;
311
+ totalCost = data?.cost || data?.total_cost || 0;
312
+ if (data?.usage?.total_cost) totalCost = data.usage.total_cost;
313
+ if (Number.isFinite(data?.tool_calls)) backendToolCount = data.tool_calls;
314
+ if (Number.isFinite(data?.primary_tool_calls)) backendPrimaryToolCount = data.primary_tool_calls;
315
+ if (Number.isFinite(data?.sub_agent_tool_calls)) backendSubAgentToolCount = data.sub_agent_tool_calls;
316
+ // Capture token usage
317
+ if (data?.usage) {
318
+ usage = {
319
+ input_tokens: data.usage.total_input_tokens || data.usage.input_tokens || 0,
320
+ output_tokens: data.usage.total_output_tokens || data.usage.output_tokens || 0,
321
+ cache_read: data.usage.cache_read_input_tokens || data.usage.cache_read || 0,
322
+ cache_write: data.usage.cache_creation_input_tokens || data.usage.cache_write || 0,
323
+ };
324
+ }
325
+ }
326
+
327
+ if (type === 'error') {
328
+ emit({
329
+ type: 'error',
330
+ error: data?.message || 'Unknown error',
331
+ code: data?.code,
332
+ retry_after: data?.retry_after,
333
+ rate_limit: data?.rate_limit || null,
334
+ });
335
+ }
336
+
337
+ // ── Cost guard ──
338
+ if (maxCost && totalCost > maxCost) {
339
+ emit({ type: 'cost_exceeded', cost_usd: totalCost, max_cost: maxCost });
340
+ log(`Cost exceeded: $${totalCost.toFixed(3)} > $${maxCost}`);
341
+ break;
342
+ }
343
+ }
344
+ } catch (err) {
345
+ emit({ type: 'error', error: err.message });
346
+ }
347
+
348
+ clearTimeout(timeoutTimer);
349
+
350
+ const durationS = (Date.now() - startTime) / 1000;
351
+
352
+ // ── Tool breakdown ──
353
+ const countBreakdown = (items) => {
354
+ const out = {};
355
+ for (const t of items) out[t.tool] = (out[t.tool] || 0) + 1;
356
+ return out;
357
+ };
358
+ const toolBreakdown = countBreakdown(toolCalls);
359
+ const primaryToolBreakdown = countBreakdown(toolCalls.filter(t => !t.internal));
360
+ const subAgentToolBreakdown = countBreakdown(toolCalls.filter(t => t.internal));
361
+
362
+ const subAgentReportedToolCount = subAgents.reduce((sum, sa) => sum + (sa.tool_calls || 0), 0);
363
+ const subAgentToolCount = backendSubAgentToolCount ?? Math.max(subAgentReportedToolCount, subAgentForwardedToolCount);
364
+ const totalToolCount = backendToolCount ?? (primaryToolCount + subAgentToolCount);
365
+ const primaryToolTotal = backendPrimaryToolCount ?? (
366
+ backendToolCount != null
367
+ ? Math.max(0, totalToolCount - subAgentToolCount)
368
+ : primaryToolCount
369
+ );
370
+ if (subAgentToolCount > subAgentReportedToolCount && !backendSubAgentToolCount) {
371
+ subAgents.push({
372
+ type: 'forwarded',
373
+ model: '',
374
+ duration_s: 0,
375
+ tool_calls: subAgentToolCount - subAgentReportedToolCount,
376
+ success: true,
377
+ });
378
+ }
379
+
380
+ emit({
381
+ type: 'complete',
382
+ tools: totalToolCount,
383
+ tools_primary: primaryToolTotal,
384
+ tools_sub_agent: subAgentToolCount,
385
+ tools_forwarded_sub_agent_events: subAgentForwardedToolCount,
386
+ tool_breakdown: toolBreakdown,
387
+ tool_breakdown_primary: primaryToolBreakdown,
388
+ tool_breakdown_sub_agent: subAgentToolBreakdown,
389
+ sub_agents: subAgents,
390
+ stagnation_triggers: stagnationCount,
391
+ usage,
392
+ duration_s: Math.round(durationS * 10) / 10,
393
+ cost_usd: totalCost,
394
+ rate_limit: rateLimit,
395
+ model: model || 'default',
396
+ content_length: finalContent.length,
397
+ });
398
+
399
+ // PRD-071 §1.5 — cache summary for benchmark harness. Machine-readable,
400
+ // one file per run. Fields match what benchmark/cache-check.sh already
401
+ // computes (input, cache_read, cache_write, rate) so the shell script
402
+ // becomes a thin reader instead of re-doing the arithmetic.
403
+ if (cacheReport && usage) {
404
+ const cacheRead = usage.cache_read || 0;
405
+ const cacheWrite = usage.cache_write || 0;
406
+ const inputT = usage.input_tokens || 0;
407
+ // Two conventions in the wild:
408
+ // OpenAI/DeepSeek: input_tokens INCLUDES cached tokens
409
+ // → hit_rate = cache_read / input_tokens
410
+ // Anthropic: input_tokens EXCLUDES cache reads AND writes
411
+ // → hit_rate = cache_read / (input + cache_read + cache_write)
412
+ // Report both. Also expose `cache_hit_rate_pct` as the "sane" number
413
+ // — auto-detects convention by whether cache_read > input_tokens.
414
+ const rateOpenAI = inputT > 0 ? Math.round((cacheRead / inputT) * 100) : 0;
415
+ const anthropicDenom = inputT + cacheRead + cacheWrite;
416
+ const rateAnthropic = anthropicDenom > 0 ? Math.round((cacheRead / anthropicDenom) * 100) : 0;
417
+ const rateAuto = cacheRead > inputT ? rateAnthropic : rateOpenAI;
418
+ const report = {
419
+ schema: 'kepler.cache-report/1',
420
+ model: model || 'default',
421
+ input_tokens: inputT,
422
+ output_tokens: usage.output_tokens || 0,
423
+ cache_read_tokens: cacheRead,
424
+ cache_write_tokens: cacheWrite,
425
+ cache_hit_rate_pct: rateAuto,
426
+ cache_hit_rate_openai_pct: rateOpenAI,
427
+ cache_hit_rate_anthropic_pct: rateAnthropic,
428
+ duration_s: Math.round(durationS * 10) / 10,
429
+ cost_usd: totalCost,
430
+ };
431
+ try {
432
+ const fs = await import('node:fs');
433
+ fs.writeFileSync(cacheReport, JSON.stringify(report, null, 2) + '\n');
434
+ log(`Cache report written: ${cacheReport}`);
435
+ } catch (err) {
436
+ log(`Cache report write failed: ${err.message}`);
437
+ }
438
+ }
439
+
440
+ log(`Done: ${totalToolCount} tools (${primaryToolTotal} primary, ${subAgentToolCount} sub-agent), ${durationS.toFixed(1)}s, $${totalCost.toFixed(3)}`);
441
+
442
+ // Write final content to stderr so it's human-readable (stdout is JSONL)
443
+ if (verbose && finalContent) {
444
+ process.stderr.write(`\n--- Response ---\n${finalContent.slice(0, 2000)}\n`);
445
+ }
446
+
447
+ process.exit(0);
448
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Hooks Manager — T22: PreToolUse/PostToolUse hooks from config.
3
+ */
4
+
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import { execSync } from 'node:child_process';
8
+ import { projectHooksPath, globalHooksPath } from './paths.mjs';
9
+
10
+ export class HooksManager {
11
+ constructor(projectDir = process.cwd()) {
12
+ this.projectHooksPath = projectHooksPath(projectDir);
13
+ this.globalHooksPath = globalHooksPath();
14
+ this.hooks = this._loadHooks();
15
+ this.firedHooks = [];
16
+ }
17
+
18
+ _loadHooks() {
19
+ const hooks = { PreToolUse: [], PostToolUse: [] };
20
+
21
+ // Global hooks (lower priority)
22
+ try {
23
+ if (fs.existsSync(this.globalHooksPath)) {
24
+ const global = JSON.parse(fs.readFileSync(this.globalHooksPath, 'utf-8'));
25
+ if (global.PreToolUse) hooks.PreToolUse.push(...global.PreToolUse);
26
+ if (global.PostToolUse) hooks.PostToolUse.push(...global.PostToolUse);
27
+ }
28
+ } catch { /* skip corrupt */ }
29
+
30
+ // Project hooks (higher priority, appended after global)
31
+ try {
32
+ if (fs.existsSync(this.projectHooksPath)) {
33
+ const project = JSON.parse(fs.readFileSync(this.projectHooksPath, 'utf-8'));
34
+ if (project.PreToolUse) hooks.PreToolUse.push(...project.PreToolUse);
35
+ if (project.PostToolUse) hooks.PostToolUse.push(...project.PostToolUse);
36
+ }
37
+ } catch { /* skip corrupt */ }
38
+
39
+ return hooks;
40
+ }
41
+
42
+ /** Run PreToolUse hooks. Returns { allowed, message }. */
43
+ runPreToolUse(toolName, toolInput) {
44
+ return this._runHooks('PreToolUse', toolName, toolInput);
45
+ }
46
+
47
+ /** Run PostToolUse hooks. Returns { success }. */
48
+ runPostToolUse(toolName, toolInput, result) {
49
+ return this._runHooks('PostToolUse', toolName, toolInput, result);
50
+ }
51
+
52
+ _runHooks(event, toolName, toolInput, result = null) {
53
+ const hooks = (this.hooks[event] || []).filter(h => !h.tool || h.tool === toolName);
54
+ for (const hook of hooks) {
55
+ try {
56
+ const env = {
57
+ ...process.env,
58
+ HOOK_EVENT: event,
59
+ TOOL_NAME: toolName,
60
+ TOOL_INPUT: JSON.stringify(toolInput),
61
+ FILE_PATH: toolInput?.path || toolInput?.file_path || '',
62
+ };
63
+ if (result) env.TOOL_RESULT = JSON.stringify(result);
64
+
65
+ execSync(hook.command, { env, stdio: 'pipe', timeout: 10_000 });
66
+ this.firedHooks.push({ event, tool: toolName, command: hook.command, success: true });
67
+ } catch (err) {
68
+ this.firedHooks.push({ event, tool: toolName, command: hook.command, success: false });
69
+ if (event === 'PreToolUse') {
70
+ return { allowed: false, message: `Hook blocked ${toolName}: ${err.message}` };
71
+ }
72
+ }
73
+ }
74
+ return { allowed: true, success: true };
75
+ }
76
+
77
+ /** List configured hooks. */
78
+ listHooks() { return this.hooks; }
79
+
80
+ /** List hooks that fired this session. */
81
+ getFiredHooks() { return this.firedHooks; }
82
+
83
+ /** Check if any hooks are configured. */
84
+ hasHooks() {
85
+ return (this.hooks.PreToolUse.length + this.hooks.PostToolUse.length) > 0;
86
+ }
87
+ }