@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,449 @@
1
+ /**
2
+ * JSONL Writer — writes cc-lens compatible session transcripts to ~/.bahulam/.
3
+ *
4
+ * Format mirrors Claude Code's ~/.claude/ JSONL structure so that
5
+ * cc-lens (CLAUDE_CONFIG_DIR=~/.bahulam npx cc-lens) can read Kepler sessions.
6
+ *
7
+ * Design:
8
+ * - Non-blocking: buffered writes, flushed every 500ms or on turn end
9
+ * - Accumulates content + tool_use blocks during a turn
10
+ * - Writes single assistant entry on complete event
11
+ * - Tracks UUID chain for cc-lens replay
12
+ */
13
+
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import { randomUUID } from 'node:crypto';
17
+ import * as childProcessModule from 'node:child_process';
18
+ import { bahulamHome } from './paths.mjs';
19
+
20
+ const KEPLER_DIR = bahulamHome();
21
+ const FLUSH_INTERVAL_MS = 500;
22
+
23
+ /**
24
+ * Sanitize a cwd path into a project slug for the directory name.
25
+ * /Users/sree/Sites/myproject → -Users-sree-Sites-myproject
26
+ * Mirrors the Claude Code / kepler-cli convention.
27
+ */
28
+ function sanitizePath(p) {
29
+ return p.replace(/\//g, '-').replace(/^-/, '-');
30
+ }
31
+
32
+ function normalizeToolResultContent(output) {
33
+ if (typeof output === 'string') return output;
34
+ if (output == null) return '';
35
+ try {
36
+ const serialized = JSON.stringify(output);
37
+ return typeof serialized === 'string' ? serialized : String(output);
38
+ } catch {
39
+ return String(output);
40
+ }
41
+ }
42
+
43
+ function sanitizeEventValue(value, depth = 0) {
44
+ if (depth > 8) return '[truncated-depth]';
45
+ if (typeof value === 'string') {
46
+ return value.length > 10000 ? value.slice(0, 10000) + '\n[...truncated...]' : value;
47
+ }
48
+ if (value == null || typeof value === 'number' || typeof value === 'boolean') return value;
49
+ if (Array.isArray(value)) return value.slice(0, 100).map(item => sanitizeEventValue(item, depth + 1));
50
+ if (typeof value === 'object') {
51
+ const out = {};
52
+ for (const [key, item] of Object.entries(value).slice(0, 100)) {
53
+ out[key] = sanitizeEventValue(item, depth + 1);
54
+ }
55
+ return out;
56
+ }
57
+ return String(value);
58
+ }
59
+
60
+ export class JsonlWriter {
61
+ /**
62
+ * @param {string} cwd — project working directory
63
+ * @param {string} version — CLI version string
64
+ */
65
+ constructor(cwd, version) {
66
+ this.cwd = cwd;
67
+ this.version = version;
68
+ this.sessionId = null; // set by setSessionId() when backend assigns it
69
+ this.slug = sanitizePath(cwd);
70
+ this.projectDir = path.join(KEPLER_DIR, 'projects', this.slug);
71
+
72
+ // UUID chain for parent linking (cc-lens replay)
73
+ this.lastUuid = null;
74
+
75
+ // Write buffer
76
+ this._buffer = [];
77
+ this._flushTimer = null;
78
+ this._flushPromise = null;
79
+ this._transcriptPath = null; // set when sessionId is known
80
+ this._ready = false;
81
+
82
+ // Turn accumulator (reset per assistant turn)
83
+ this._turnContent = []; // [{type: 'text', text: '...'}, ...]
84
+ this._turnToolCalls = []; // [{id, name, input}, ...]
85
+ this._turnToolResults = []; // [{tool_use_id, content, is_error}, ...]
86
+ this._turnUsage = null;
87
+ this._turnModel = null;
88
+ this._pendingKeplerEvents = [];
89
+
90
+ // Git branch (captured once at construction)
91
+ this._gitBranch = this._detectGitBranch();
92
+
93
+ this._ensureDir();
94
+ }
95
+
96
+ /**
97
+ * Set the session ID (called when backend returns session_info).
98
+ * Until this is called, entries are buffered with a local fallback ID.
99
+ */
100
+ setSessionId(id) {
101
+ this.sessionId = id;
102
+ this._transcriptPath = path.join(this.projectDir, `${id}.jsonl`);
103
+ this._ready = true;
104
+ if (this._pendingKeplerEvents.length > 0) {
105
+ const pending = this._pendingKeplerEvents;
106
+ this._pendingKeplerEvents = [];
107
+ for (const event of pending) this.writeKeplerEvent(event);
108
+ }
109
+ // Flush any buffered entries now that we have a path
110
+ if (this._buffer.length > 0) this._flush();
111
+ }
112
+
113
+ /**
114
+ * Generate a local session ID (for --local mode or before backend assigns one).
115
+ */
116
+ ensureSessionId() {
117
+ if (!this.sessionId) {
118
+ this.setSessionId(randomUUID());
119
+ }
120
+ }
121
+
122
+ // ── Write Methods (called from REPL) ──
123
+
124
+ /**
125
+ * Write a user turn entry.
126
+ */
127
+ writeUserTurn(content) {
128
+ this.ensureSessionId();
129
+ const uuid = randomUUID();
130
+ const entry = {
131
+ type: 'user',
132
+ uuid,
133
+ parentUuid: this.lastUuid,
134
+ timestamp: new Date().toISOString(),
135
+ cwd: this.cwd,
136
+ sessionId: this.sessionId,
137
+ version: this.version,
138
+ gitBranch: this._gitBranch,
139
+ message: { role: 'user', content },
140
+ };
141
+ this._appendEntry(entry);
142
+ this.lastUuid = uuid;
143
+ }
144
+
145
+ /**
146
+ * Write a Kepler UI/SSE event for exact-ish terminal replay on /resume.
147
+ * This is a Kepler extension; cc-lens readers should ignore unknown types.
148
+ */
149
+ writeKeplerEvent(event) {
150
+ if (!event || !event.type) return;
151
+ const sanitized = sanitizeEventValue({
152
+ type: event.type,
153
+ data: event.data || {},
154
+ });
155
+
156
+ if (!this.sessionId) {
157
+ this._pendingKeplerEvents.push(sanitized);
158
+ return;
159
+ }
160
+
161
+ const entry = {
162
+ type: 'kepler_event',
163
+ timestamp: new Date().toISOString(),
164
+ cwd: this.cwd,
165
+ sessionId: this.sessionId,
166
+ version: this.version,
167
+ event: sanitized,
168
+ };
169
+ this._appendEntry(entry);
170
+ }
171
+
172
+ /**
173
+ * Accumulate content during streaming (call on content/content_partial events).
174
+ */
175
+ accumulateContent(text) {
176
+ if (!text) return;
177
+ // Merge into last text block or create new one
178
+ const last = this._turnContent[this._turnContent.length - 1];
179
+ if (last && last.type === 'text') {
180
+ last.text += text;
181
+ } else {
182
+ this._turnContent.push({ type: 'text', text });
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Accumulate a tool call (call on tool_call/tool_request events).
188
+ */
189
+ accumulateToolCall(callId, toolName, args) {
190
+ this._turnToolCalls.push({
191
+ type: 'tool_use',
192
+ id: callId,
193
+ name: toolName,
194
+ input: args || {},
195
+ });
196
+ }
197
+
198
+ /**
199
+ * Record a tool result (call on tool_done/tool_result events).
200
+ */
201
+ recordToolResult(callId, output, isError, metadata = null) {
202
+ const entry = {
203
+ tool_use_id: callId,
204
+ content: normalizeToolResultContent(output),
205
+ is_error: !!isError,
206
+ };
207
+ const kepler = compactToolResultMetadata(metadata);
208
+ if (kepler) entry.bahulam = kepler;
209
+ this._turnToolResults.push(entry);
210
+ }
211
+
212
+ /**
213
+ * Set usage and model for the current turn (call on complete event).
214
+ */
215
+ setTurnUsage(usage, model) {
216
+ this._turnUsage = usage || null;
217
+ this._turnModel = model || null;
218
+ }
219
+
220
+ /**
221
+ * Finalize and write the assistant turn entry + tool result entries.
222
+ * Call this on the 'complete' event.
223
+ */
224
+ flushAssistantTurn() {
225
+ this.ensureSessionId();
226
+
227
+ // Build content array: text blocks + tool_use blocks
228
+ const contentBlocks = [...this._turnContent, ...this._turnToolCalls];
229
+ if (contentBlocks.length === 0) {
230
+ this._resetTurn();
231
+ return;
232
+ }
233
+
234
+ // Simplify: if only one text block and no tools, use string content
235
+ const content = (contentBlocks.length === 1 && contentBlocks[0].type === 'text')
236
+ ? contentBlocks[0].text
237
+ : contentBlocks;
238
+
239
+ const uuid = randomUUID();
240
+ const usage = this._turnUsage || {};
241
+ const entry = {
242
+ type: 'assistant',
243
+ uuid,
244
+ parentUuid: this.lastUuid,
245
+ timestamp: new Date().toISOString(),
246
+ cwd: this.cwd,
247
+ sessionId: this.sessionId,
248
+ version: this.version,
249
+ message: {
250
+ role: 'assistant',
251
+ model: this._turnModel || undefined,
252
+ usage: {
253
+ input_tokens: usage.total_input_tokens || usage.input_tokens || 0,
254
+ output_tokens: usage.total_output_tokens || usage.output_tokens || 0,
255
+ cache_read_input_tokens: usage.cache_read_input_tokens || 0,
256
+ cache_creation_input_tokens: usage.cache_creation_input_tokens || 0,
257
+ },
258
+ content,
259
+ },
260
+ };
261
+ this._appendEntry(entry);
262
+ this.lastUuid = uuid;
263
+
264
+ // Write tool result entries (as user messages with tool_result content)
265
+ if (this._turnToolResults.length > 0) {
266
+ const toolResultUuid = randomUUID();
267
+ const toolResultEntry = {
268
+ type: 'user',
269
+ uuid: toolResultUuid,
270
+ parentUuid: uuid,
271
+ timestamp: new Date().toISOString(),
272
+ cwd: this.cwd,
273
+ sessionId: this.sessionId,
274
+ version: this.version,
275
+ message: {
276
+ role: 'user',
277
+ content: this._turnToolResults.map(r => {
278
+ const block = {
279
+ type: 'tool_result',
280
+ tool_use_id: r.tool_use_id,
281
+ content: r.content.slice(0, 5000), // truncate large outputs
282
+ is_error: r.is_error,
283
+ };
284
+ if (r.bahulam) block.bahulam = r.bahulam;
285
+ return block;
286
+ }),
287
+ },
288
+ };
289
+ this._appendEntry(toolResultEntry);
290
+ this.lastUuid = toolResultUuid;
291
+ }
292
+
293
+ this._resetTurn();
294
+ this._flush(); // force flush at end of turn
295
+ }
296
+
297
+ /**
298
+ * Write a prompt entry to ~/.bahulam/history.jsonl.
299
+ */
300
+ writeHistory(prompt) {
301
+ const entry = {
302
+ display: prompt,
303
+ pastedContents: {},
304
+ timestamp: Date.now(),
305
+ project: this.cwd,
306
+ sessionId: this.sessionId,
307
+ };
308
+ const historyPath = path.join(KEPLER_DIR, 'history.jsonl');
309
+ fs.promises.appendFile(historyPath, JSON.stringify(entry) + '\n', { mode: 0o600 })
310
+ .catch(() => {}); // best effort
311
+ }
312
+
313
+ /**
314
+ * Final flush on session end.
315
+ */
316
+ async close() {
317
+ if (this._flushTimer) {
318
+ clearTimeout(this._flushTimer);
319
+ this._flushTimer = null;
320
+ }
321
+ await this._flush();
322
+ if (this._flushPromise) {
323
+ await this._flushPromise;
324
+ }
325
+ }
326
+
327
+ async flush() {
328
+ await this._flush();
329
+ if (this._flushPromise) await this._flushPromise;
330
+ }
331
+
332
+ get transcriptPath() {
333
+ return this._transcriptPath;
334
+ }
335
+
336
+ // ── Internal ──
337
+
338
+ _resetTurn() {
339
+ this._turnContent = [];
340
+ this._turnToolCalls = [];
341
+ this._turnToolResults = [];
342
+ this._turnUsage = null;
343
+ this._turnModel = null;
344
+ }
345
+
346
+ _appendEntry(entry) {
347
+ this._buffer.push(JSON.stringify(entry));
348
+ if (!this._flushTimer && this._ready) {
349
+ this._flushTimer = setTimeout(() => this._flush(), FLUSH_INTERVAL_MS);
350
+ }
351
+ }
352
+
353
+ async _flush() {
354
+ if (this._flushTimer) {
355
+ clearTimeout(this._flushTimer);
356
+ this._flushTimer = null;
357
+ }
358
+ if (this._buffer.length === 0) {
359
+ return this._flushPromise;
360
+ }
361
+ if (!this._transcriptPath) return; // no session ID yet, keep buffered
362
+
363
+ const lines = this._buffer.join('\n') + '\n';
364
+ this._buffer = [];
365
+
366
+ const pending = this._flushPromise || Promise.resolve();
367
+ const writePromise = pending.then(async () => {
368
+ try {
369
+ await fs.promises.appendFile(this._transcriptPath, lines, { mode: 0o600 });
370
+ } catch {
371
+ // If directory was deleted, try re-creating
372
+ try {
373
+ await this._ensureDirAsync();
374
+ await fs.promises.appendFile(this._transcriptPath, lines, { mode: 0o600 });
375
+ } catch {
376
+ // silent — local logging is best-effort
377
+ }
378
+ }
379
+ });
380
+
381
+ this._flushPromise = writePromise;
382
+ try {
383
+ await writePromise;
384
+ } finally {
385
+ if (this._flushPromise === writePromise) {
386
+ this._flushPromise = null;
387
+ }
388
+ }
389
+ }
390
+
391
+ _ensureDir() {
392
+ try {
393
+ fs.mkdirSync(this.projectDir, { recursive: true, mode: 0o700 });
394
+ } catch { /* ignore */ }
395
+ try {
396
+ fs.mkdirSync(path.join(KEPLER_DIR, 'projects'), { recursive: true, mode: 0o700 });
397
+ } catch { /* ignore */ }
398
+ }
399
+
400
+ async _ensureDirAsync() {
401
+ await fs.promises.mkdir(this.projectDir, { recursive: true, mode: 0o700 });
402
+ }
403
+
404
+ _detectGitBranch() {
405
+ try {
406
+ const { execSync } = childProcessModule;
407
+ return execSync('git rev-parse --abbrev-ref HEAD', {
408
+ cwd: this.cwd,
409
+ encoding: 'utf-8',
410
+ timeout: 2000,
411
+ stdio: ['ignore', 'pipe', 'ignore'],
412
+ }).trim();
413
+ } catch {
414
+ return undefined;
415
+ }
416
+ }
417
+ }
418
+
419
+ function compactToolResultMetadata(metadata) {
420
+ if (!metadata || typeof metadata !== 'object') return null;
421
+ const diffs = Array.isArray(metadata.file_diffs)
422
+ ? metadata.file_diffs
423
+ : metadata.file_diff ? [metadata.file_diff] : [];
424
+ const out = {
425
+ tool: metadata.tool || metadata._tool || '',
426
+ lines_added: metadata.lines_added,
427
+ lines_removed: metadata.lines_removed,
428
+ };
429
+ if (diffs.length) {
430
+ out.file_diffs = diffs.map(diff => ({
431
+ type: 'file_diff',
432
+ path: diff.path || '',
433
+ relative_path: diff.relative_path || '',
434
+ lines_added: diff.lines_added || 0,
435
+ lines_removed: diff.lines_removed || 0,
436
+ truncated: !!diff.truncated,
437
+ truncated_line_count: diff.truncated_line_count || 0,
438
+ hunks: diff.hunks || [],
439
+ unified: diff.unified || '',
440
+ }));
441
+ }
442
+
443
+ for (const key of Object.keys(out)) {
444
+ if (out[key] == null || out[key] === '' || (Array.isArray(out[key]) && out[key].length === 0)) {
445
+ delete out[key];
446
+ }
447
+ }
448
+ return Object.keys(out).length ? sanitizeEventValue(out) : null;
449
+ }