@bahulam/code 0.1.6 → 0.1.8

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 (204) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +11 -18
  4. package/package.json +11 -11
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +4 -3
  8. package/src/commands/agent.mjs +3 -3
  9. package/src/commands/device.mjs +2 -2
  10. package/src/commands/pair.mjs +2 -2
  11. package/src/commands/remote.mjs +3 -3
  12. package/src/commands/workflow.mjs +5 -5
  13. package/src/config/env.mjs +9 -2
  14. package/src/config/memory-loader.mjs +1 -1
  15. package/src/config/model-catalog-default.json +4 -3
  16. package/src/config/model-catalog.mjs +2 -0
  17. package/src/config/settings.mjs +2 -2
  18. package/src/context/prose-chunker.mjs +162 -5
  19. package/src/core/agent-loop.mjs +17 -0
  20. package/src/core/attachments.mjs +66 -4
  21. package/src/core/backend-url.mjs +11 -12
  22. package/src/core/bundled-runtime.mjs +1 -1
  23. package/src/core/callback-client.mjs +1 -1
  24. package/src/core/headless.mjs +18 -4
  25. package/src/core/jsonl-writer.mjs +18 -18
  26. package/src/core/lint-resolver.mjs +472 -0
  27. package/src/core/local-agent.mjs +40 -8
  28. package/src/core/local-store.mjs +5 -1
  29. package/src/core/mode-selector.mjs +1 -1
  30. package/src/core/output-filter.mjs +22 -26
  31. package/src/core/pricing.mjs +10 -3
  32. package/src/core/project-artifacts.mjs +3 -3
  33. package/src/core/project-context-loader.mjs +9 -9
  34. package/src/core/settings-sync.mjs +1 -1
  35. package/src/core/stagnation.mjs +61 -1
  36. package/src/core/stream-client.mjs +13 -8
  37. package/src/core/system-prompt.mjs +2 -2
  38. package/src/core/tool-executor.mjs +178 -84
  39. package/src/daemon/http-dashboard.mjs +400 -0
  40. package/src/daemon/session-core.mjs +1 -0
  41. package/src/local-service/agent-relay.mjs +868 -0
  42. package/src/local-service/approval-bridge.mjs +222 -0
  43. package/src/local-service/browser.mjs +24 -0
  44. package/src/local-service/command.mjs +155 -0
  45. package/src/local-service/file-access.mjs +393 -0
  46. package/src/local-service/machine.mjs +86 -0
  47. package/src/local-service/paths.mjs +29 -0
  48. package/src/local-service/preview-converters.mjs +260 -0
  49. package/src/local-service/server.mjs +2644 -0
  50. package/src/local-service/session-store.mjs +221 -0
  51. package/src/mcp/client.mjs +4 -4
  52. package/src/onboarding/preflight.mjs +2 -2
  53. package/src/terminal/agents.mjs +2 -2
  54. package/src/terminal/analytics.mjs +2 -6
  55. package/src/terminal/ansi.mjs +11 -3
  56. package/src/terminal/main.mjs +43 -17
  57. package/src/terminal/model-catalog-display.mjs +111 -0
  58. package/src/terminal/repl-format.mjs +4 -1
  59. package/src/terminal/repl-model-form.mjs +81 -14
  60. package/src/terminal/repl-render.mjs +37 -7
  61. package/src/terminal/repl-resume.mjs +2 -2
  62. package/src/terminal/repl-state.mjs +12 -0
  63. package/src/terminal/repl.mjs +248 -70
  64. package/src/terminal/tool-display.mjs +32 -0
  65. package/src/terminal/watch-state.mjs +118 -0
  66. package/src/tools/agent.mjs +2 -2
  67. package/src/tools/analyze-image.mjs +158 -0
  68. package/src/tools/bash.mjs +11 -9
  69. package/src/tools/generate-image.mjs +411 -0
  70. package/src/tools/lint.mjs +24 -43
  71. package/src/tools/project-overview.mjs +12 -12
  72. package/src/tools/registry.mjs +4 -0
  73. package/src/ui/formatter.mjs +2 -2
  74. package/src/ui/icons.mjs +2 -0
  75. package/src/ui/input-dock.mjs +7 -7
  76. package/src/ui/palette.mjs +1 -1
  77. package/src/ui/slash-commands.mjs +4 -6
  78. package/src/ui/spinner.mjs +1 -1
  79. package/src/ui/term.mjs +2 -6
  80. package/src/ui/tool-card.mjs +81 -6
  81. package/pulse/app/activity/page.tsx +0 -190
  82. package/pulse/app/api/activity/route.ts +0 -138
  83. package/pulse/app/api/benchmark/route.ts +0 -113
  84. package/pulse/app/api/benchmarks/route.ts +0 -195
  85. package/pulse/app/api/costs/route.ts +0 -88
  86. package/pulse/app/api/export/route.ts +0 -77
  87. package/pulse/app/api/history/route.ts +0 -11
  88. package/pulse/app/api/import/route.ts +0 -31
  89. package/pulse/app/api/memory/route.ts +0 -50
  90. package/pulse/app/api/plans/route.ts +0 -9
  91. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  92. package/pulse/app/api/projects/route.ts +0 -121
  93. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  94. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  95. package/pulse/app/api/sessions/route.ts +0 -112
  96. package/pulse/app/api/settings/route.ts +0 -14
  97. package/pulse/app/api/stats/route.ts +0 -143
  98. package/pulse/app/api/todos/route.ts +0 -9
  99. package/pulse/app/api/tools/route.ts +0 -160
  100. package/pulse/app/benchmarks/page.tsx +0 -224
  101. package/pulse/app/costs/page.tsx +0 -179
  102. package/pulse/app/export/page.tsx +0 -465
  103. package/pulse/app/favicon.ico +0 -0
  104. package/pulse/app/globals.css +0 -263
  105. package/pulse/app/help/page.tsx +0 -143
  106. package/pulse/app/history/page.tsx +0 -157
  107. package/pulse/app/layout.tsx +0 -46
  108. package/pulse/app/memory/page.tsx +0 -365
  109. package/pulse/app/overview-client.tsx +0 -393
  110. package/pulse/app/page.tsx +0 -14
  111. package/pulse/app/plans/page.tsx +0 -308
  112. package/pulse/app/projects/[slug]/page.tsx +0 -390
  113. package/pulse/app/projects/page.tsx +0 -110
  114. package/pulse/app/sessions/[id]/page.tsx +0 -243
  115. package/pulse/app/sessions/page.tsx +0 -39
  116. package/pulse/app/settings/page.tsx +0 -188
  117. package/pulse/app/todos/page.tsx +0 -211
  118. package/pulse/app/tools/page.tsx +0 -249
  119. package/pulse/cli.js +0 -164
  120. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  121. package/pulse/components/activity/streak-card.tsx +0 -36
  122. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  123. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  124. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  125. package/pulse/components/costs/model-token-table.tsx +0 -60
  126. package/pulse/components/global-search.tsx +0 -193
  127. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  128. package/pulse/components/layout/bottom-nav.tsx +0 -53
  129. package/pulse/components/layout/client-layout.tsx +0 -31
  130. package/pulse/components/layout/sidebar-context.tsx +0 -50
  131. package/pulse/components/layout/sidebar.tsx +0 -183
  132. package/pulse/components/layout/top-bar.tsx +0 -121
  133. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  134. package/pulse/components/overview/conversation-table.tsx +0 -148
  135. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  136. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  137. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  138. package/pulse/components/overview/stat-card.tsx +0 -102
  139. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  140. package/pulse/components/projects/project-card.tsx +0 -175
  141. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  142. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  143. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  144. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  145. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  146. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  147. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  148. package/pulse/components/sessions/session-badges.tsx +0 -49
  149. package/pulse/components/sessions/session-table.tsx +0 -299
  150. package/pulse/components/theme-provider.tsx +0 -44
  151. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  152. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  153. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  154. package/pulse/components/tools/version-history-table.tsx +0 -32
  155. package/pulse/components/ui/alert.tsx +0 -66
  156. package/pulse/components/ui/badge.tsx +0 -48
  157. package/pulse/components/ui/breadcrumb.tsx +0 -109
  158. package/pulse/components/ui/button.tsx +0 -64
  159. package/pulse/components/ui/calendar.tsx +0 -220
  160. package/pulse/components/ui/card.tsx +0 -92
  161. package/pulse/components/ui/command.tsx +0 -158
  162. package/pulse/components/ui/dialog.tsx +0 -158
  163. package/pulse/components/ui/input.tsx +0 -21
  164. package/pulse/components/ui/popover.tsx +0 -89
  165. package/pulse/components/ui/progress.tsx +0 -31
  166. package/pulse/components/ui/select.tsx +0 -190
  167. package/pulse/components/ui/separator.tsx +0 -28
  168. package/pulse/components/ui/sheet.tsx +0 -143
  169. package/pulse/components/ui/skeleton.tsx +0 -13
  170. package/pulse/components/ui/table.tsx +0 -116
  171. package/pulse/components/ui/tabs.tsx +0 -91
  172. package/pulse/components/ui/tooltip.tsx +0 -57
  173. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  174. package/pulse/components.json +0 -23
  175. package/pulse/eslint.config.mjs +0 -18
  176. package/pulse/lib/bahulam-paths.ts +0 -23
  177. package/pulse/lib/claude-reader.ts +0 -592
  178. package/pulse/lib/decode.ts +0 -129
  179. package/pulse/lib/pricing.ts +0 -102
  180. package/pulse/lib/replay-parser.ts +0 -165
  181. package/pulse/lib/tool-categories.ts +0 -140
  182. package/pulse/lib/utils.ts +0 -6
  183. package/pulse/next-env.d.ts +0 -6
  184. package/pulse/next.config.ts +0 -16
  185. package/pulse/package.json +0 -45
  186. package/pulse/postcss.config.mjs +0 -7
  187. package/pulse/public/activity.png +0 -0
  188. package/pulse/public/cc-lens.png +0 -0
  189. package/pulse/public/command-k.png +0 -0
  190. package/pulse/public/costs.png +0 -0
  191. package/pulse/public/dashboard-dark.png +0 -0
  192. package/pulse/public/dashboard-white.png +0 -0
  193. package/pulse/public/export.png +0 -0
  194. package/pulse/public/file.svg +0 -1
  195. package/pulse/public/globe.svg +0 -1
  196. package/pulse/public/next.svg +0 -1
  197. package/pulse/public/projects.png +0 -0
  198. package/pulse/public/session-chat.png +0 -0
  199. package/pulse/public/todos.png +0 -0
  200. package/pulse/public/tools.png +0 -0
  201. package/pulse/public/vercel.svg +0 -1
  202. package/pulse/public/window.svg +0 -1
  203. package/pulse/tsconfig.json +0 -34
  204. package/pulse/types/claude.ts +0 -294
@@ -62,7 +62,8 @@ export const TEXT_MIMES = new Set([
62
62
  'text/tab-separated-values',
63
63
  ]);
64
64
  export const PDF_MIMES = new Set(['application/pdf']);
65
- export const DOCUMENT_MIMES = new Set([...TEXT_MIMES, ...PDF_MIMES]);
65
+ export const NOTEBOOK_MIMES = new Set(['application/x-ipynb+json']);
66
+ export const DOCUMENT_MIMES = new Set([...TEXT_MIMES, ...PDF_MIMES, ...NOTEBOOK_MIMES]);
66
67
 
67
68
  // Wider set the CLI already supports at the `read_attachment` tool layer.
68
69
  // Chunking works fine on any UTF-8 text — retrieval quality on structured
@@ -72,17 +73,26 @@ export const TEXT_LIKE_MIMES = new Set([
72
73
  'application/json',
73
74
  'application/x-yaml',
74
75
  'application/toml',
76
+ 'application/xml',
77
+ 'application/sql',
75
78
  'text/yaml',
76
79
  'text/html',
80
+ 'text/xml',
77
81
  'text/x-log',
78
82
  'text/x-rst',
79
83
  'text/x-restructuredtext',
84
+ 'text/x-sql',
85
+ 'text/x-sh',
86
+ 'text/x-ini',
87
+ 'text/x-env',
88
+ 'text/x-dockerfile',
80
89
  ]);
81
90
 
82
91
  // Extension → mime map. Kept small and explicit — Python's
83
92
  // mimetypes.guess_type varies by OS registry; this table is stable.
84
93
  const EXT_TO_MIME = new Map([
85
94
  ['.txt', 'text/plain'],
95
+ ['.text', 'text/plain'],
86
96
  ['.log', 'text/x-log'],
87
97
  ['.md', 'text/markdown'],
88
98
  ['.markdown', 'text/markdown'],
@@ -90,15 +100,114 @@ const EXT_TO_MIME = new Map([
90
100
  ['.csv', 'text/csv'],
91
101
  ['.tsv', 'text/tab-separated-values'],
92
102
  ['.json', 'application/json'],
103
+ ['.ipynb', 'application/x-ipynb+json'],
93
104
  ['.yaml', 'application/x-yaml'],
94
105
  ['.yml', 'application/x-yaml'],
95
106
  ['.toml', 'application/toml'],
96
107
  ['.html', 'text/html'],
97
108
  ['.htm', 'text/html'],
109
+ ['.xml', 'application/xml'],
98
110
  ['.rst', 'text/x-rst'],
99
111
  ['.pdf', 'application/pdf'],
112
+ // Config + shell + database extensions users actually paste in as
113
+ // attachments (Supabase seeds, .env for triage, Dockerfile review, …).
114
+ ['.sql', 'text/x-sql'],
115
+ ['.env', 'text/x-env'],
116
+ ['.ini', 'text/x-ini'],
117
+ ['.conf', 'text/plain'],
118
+ ['.cfg', 'text/plain'],
119
+ ['.properties', 'text/plain'],
120
+ ['.editorconfig', 'text/plain'],
121
+ ['.gitignore', 'text/plain'],
122
+ ['.gitattributes', 'text/plain'],
123
+ ['.dockerignore', 'text/plain'],
124
+ ['.dockerfile', 'text/x-dockerfile'],
125
+ ['.sh', 'text/x-sh'],
126
+ ['.bash', 'text/x-sh'],
127
+ ['.zsh', 'text/x-sh'],
100
128
  ]);
101
129
 
130
+ function sourceToText(value) {
131
+ if (Array.isArray(value)) return value.map(part => String(part ?? '')).join('');
132
+ if (typeof value === 'string') return value;
133
+ return '';
134
+ }
135
+
136
+ function outputText(output) {
137
+ if (!output || typeof output !== 'object') return '';
138
+ if (output.output_type === 'error') {
139
+ const traceback = Array.isArray(output.traceback)
140
+ ? output.traceback.map(line => String(line ?? '')).join('\n')
141
+ : '';
142
+ const header = [output.ename, output.evalue].filter(Boolean).join(': ');
143
+ return [header, traceback].filter(Boolean).join('\n');
144
+ }
145
+ if (output.text) return sourceToText(output.text);
146
+ const data = output.data && typeof output.data === 'object' ? output.data : null;
147
+ if (data?.['text/plain']) return sourceToText(data['text/plain']);
148
+ if (data?.['text/markdown']) return sourceToText(data['text/markdown']);
149
+ if (data?.['text/html']) return sourceToText(data['text/html']).replace(/<[^>]*>/g, ' ');
150
+ return '';
151
+ }
152
+
153
+ function notebookLanguage(notebook) {
154
+ const languageInfo = notebook?.metadata?.language_info;
155
+ const kernelspec = notebook?.metadata?.kernelspec;
156
+ return String(languageInfo?.name || kernelspec?.language || '').trim() || 'python';
157
+ }
158
+
159
+ export function formatNotebookText(rawNotebook, { maxOutputChars = 12_000 } = {}) {
160
+ let notebook;
161
+ try {
162
+ notebook = typeof rawNotebook === 'string'
163
+ ? JSON.parse(rawNotebook)
164
+ : JSON.parse(Buffer.from(rawNotebook || '').toString('utf8'));
165
+ } catch {
166
+ return '';
167
+ }
168
+
169
+ const cells = Array.isArray(notebook?.cells) ? notebook.cells : [];
170
+ if (!cells.length) return '';
171
+
172
+ const language = notebookLanguage(notebook);
173
+ const parts = [
174
+ `Jupyter notebook (${cells.length} cell${cells.length === 1 ? '' : 's'}, language=${language})`,
175
+ ];
176
+
177
+ cells.forEach((cell, index) => {
178
+ const type = String(cell?.cell_type || 'raw').toLowerCase();
179
+ const execution = cell?.execution_count != null ? ` execution_count=${cell.execution_count}` : '';
180
+ const source = sourceToText(cell?.source).trimEnd();
181
+ parts.push('');
182
+ parts.push(`Cell ${index + 1} [${type}${execution}]`);
183
+ if (type === 'code') {
184
+ parts.push(`\`\`\`${language}`);
185
+ parts.push(source);
186
+ parts.push('```');
187
+ } else {
188
+ parts.push(source || '(empty)');
189
+ }
190
+
191
+ const outputs = Array.isArray(cell?.outputs) ? cell.outputs : [];
192
+ const renderedOutputs = outputs
193
+ .map(outputText)
194
+ .map(text => text.trimEnd())
195
+ .filter(Boolean);
196
+ if (renderedOutputs.length) {
197
+ let output = renderedOutputs.join('\n\n');
198
+ if (output.length > maxOutputChars) {
199
+ output = `${output.slice(0, maxOutputChars)}\n... [output truncated]`;
200
+ }
201
+ parts.push('Output:');
202
+ parts.push('```text');
203
+ parts.push(output);
204
+ parts.push('```');
205
+ }
206
+ });
207
+
208
+ return parts.join('\n').trim();
209
+ }
210
+
102
211
  /**
103
212
  * Best-effort mime for a local file (extension-only, no magic-byte sniff).
104
213
  * Files with no matching extension return 'application/octet-stream',
@@ -200,6 +309,16 @@ export async function extractFromBytes(buffer, mime, opts = {}) {
200
309
  }));
201
310
  }
202
311
 
312
+ if (NOTEBOOK_MIMES.has(normalizedMime)) {
313
+ const text = formatNotebookText(buffer);
314
+ return chunkText(text).map(c => ({
315
+ page: null,
316
+ chunk_no: c.chunk_no,
317
+ text: c.text,
318
+ tokens: c.tokens,
319
+ }));
320
+ }
321
+
203
322
  if (PDF_MIMES.has(normalizedMime)) {
204
323
  const pages = await extractPdfPages(buffer);
205
324
  const out = [];
@@ -217,6 +336,32 @@ export async function extractFromBytes(buffer, mime, opts = {}) {
217
336
  return [];
218
337
  }
219
338
 
339
+ // Sniff whether a buffer is UTF-8 text vs binary. Cheap heuristic used
340
+ // only as a fallback when the file extension didn't map to a known text
341
+ // mime — the goal is to let obscure but genuinely-textual files (.sql,
342
+ // .env, Dockerfile, .terraformrc, whatever) through instead of dropping
343
+ // them at the mime gate.
344
+ //
345
+ // Rules:
346
+ // * NUL byte in the first probe window → binary
347
+ // * Not decodable as strict UTF-8 → binary
348
+ // * Otherwise → text
349
+ //
350
+ // Probe window bounded to 8KB — enough to catch a binary header on
351
+ // large files, small enough to be a no-op on the hot path.
352
+ const _TEXT_PROBE_BYTES = 8192;
353
+ function looksLikeText(buffer) {
354
+ if (!buffer || buffer.length === 0) return false;
355
+ const probe = buffer.length > _TEXT_PROBE_BYTES ? buffer.subarray(0, _TEXT_PROBE_BYTES) : buffer;
356
+ if (probe.includes(0x00)) return false;
357
+ try {
358
+ new TextDecoder('utf-8', { fatal: true }).decode(probe);
359
+ return true;
360
+ } catch {
361
+ return false;
362
+ }
363
+ }
364
+
220
365
  /**
221
366
  * Read a local file and chunk it. Returns { mime, chunks }; both empty
222
367
  * on unresolvable path or unsupported mime — same contract as Python's
@@ -237,12 +382,11 @@ export async function extractFromPath(absPath, opts = {}) {
237
382
  }
238
383
  if (!stat.isFile()) return { mime: '', chunks: [] };
239
384
 
240
- const mime = guessMime(absPath);
385
+ let mime = guessMime(absPath);
241
386
  const textMimes = opts.textMimes || TEXT_LIKE_MIMES;
242
- if (!textMimes.has(mime) && !PDF_MIMES.has(mime)) {
243
- return { mime, chunks: [] };
244
- }
245
387
 
388
+ // Read the buffer up front so the text-sniff fallback can see the
389
+ // actual bytes when the extension didn't map to a known mime.
246
390
  let buffer;
247
391
  try {
248
392
  buffer = fs.readFileSync(absPath);
@@ -250,6 +394,19 @@ export async function extractFromPath(absPath, opts = {}) {
250
394
  return { mime, chunks: [] };
251
395
  }
252
396
 
397
+ // Fallback for unknown extensions: if guessMime returned octet-stream
398
+ // (or any mime not in our allow-list), sniff the buffer. If the bytes
399
+ // look like valid UTF-8 text with no NULs in the first probe window,
400
+ // treat as text/plain. This unblocks .sql, .env, Dockerfile, and any
401
+ // other text file we haven't explicitly listed.
402
+ if (!textMimes.has(mime) && !PDF_MIMES.has(mime) && !NOTEBOOK_MIMES.has(mime)) {
403
+ if (looksLikeText(buffer)) {
404
+ mime = 'text/plain';
405
+ } else {
406
+ return { mime, chunks: [] };
407
+ }
408
+ }
409
+
253
410
  const chunks = await extractFromBytes(buffer, mime, opts);
254
411
  return { mime, chunks };
255
412
  }
@@ -191,6 +191,23 @@ export function createAgentLoop({ model, tools, permissions, settings, hooks })
191
191
  result = await hooks.runPostToolUse(block.name, result);
192
192
  }
193
193
 
194
+ const resultStagnation = stagnation.recordResult(block.name, block.input || {}, result);
195
+ if (resultStagnation.detected) {
196
+ yield {
197
+ type: 'stagnation',
198
+ tool: block.name,
199
+ count: resultStagnation.count,
200
+ kind: resultStagnation.kind,
201
+ target: resultStagnation.target,
202
+ };
203
+ result = {
204
+ ...(typeof result === 'object' && result !== null ? result : {}),
205
+ success: false,
206
+ output: stagnationMessage(block.name, resultStagnation.count, resultStagnation),
207
+ _stagnation: true,
208
+ };
209
+ }
210
+
194
211
  yield { type: 'result', tool: block.name, result };
195
212
 
196
213
  toolResults.push({
@@ -3,6 +3,7 @@ import * as path from 'node:path';
3
3
  import * as os from 'node:os';
4
4
  import { createHash, randomUUID } from 'node:crypto';
5
5
  import { execFileSync } from 'node:child_process';
6
+ import { formatNotebookText } from '../context/prose-chunker.mjs';
6
7
 
7
8
  const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
8
9
  // Text-first document formats we can extract client-side and inline into the
@@ -12,7 +13,7 @@ const DOCUMENT_EXTENSIONS = new Set([
12
13
  '.pdf',
13
14
  '.txt', '.md', '.mdx', '.rst',
14
15
  '.csv', '.tsv', '.log',
15
- '.json', '.yaml', '.yml', '.toml', '.ini', '.env',
16
+ '.json', '.ipynb', '.yaml', '.yml', '.toml', '.ini', '.env',
16
17
  '.html', '.htm', '.xml',
17
18
  ]);
18
19
  const DEFAULT_MAX_IMAGE_BYTES = 8 * 1024 * 1024;
@@ -143,6 +144,53 @@ function sniffImage(buffer) {
143
144
  return null;
144
145
  }
145
146
 
147
+ export function detectImageFile(filePath, { cwd = process.cwd() } = {}) {
148
+ const resolved = resolveAttachmentPath(filePath, cwd);
149
+ let stat;
150
+ try {
151
+ stat = fs.statSync(resolved);
152
+ } catch {
153
+ return null;
154
+ }
155
+ if (!stat.isFile() || stat.size <= 0) return null;
156
+
157
+ const ext = path.extname(resolved).toLowerCase();
158
+ const probe = Buffer.alloc(Math.min(64, stat.size));
159
+ let fd = null;
160
+ try {
161
+ fd = fs.openSync(resolved, 'r');
162
+ fs.readSync(fd, probe, 0, probe.length, 0);
163
+ } catch {
164
+ return null;
165
+ } finally {
166
+ if (fd !== null) {
167
+ try { fs.closeSync(fd); } catch {}
168
+ }
169
+ }
170
+
171
+ const sniffed = sniffImage(probe);
172
+ if (sniffed) {
173
+ return {
174
+ path: resolved,
175
+ mime_type: sniffed.mime_type,
176
+ bytes: stat.size,
177
+ ext: sniffed.ext,
178
+ verified: true,
179
+ };
180
+ }
181
+ if (!IMAGE_EXTENSIONS.has(ext)) return null;
182
+ const mimeType = ext === '.jpg' || ext === '.jpeg'
183
+ ? 'image/jpeg'
184
+ : `image/${ext.slice(1)}`;
185
+ return {
186
+ path: resolved,
187
+ mime_type: mimeType,
188
+ bytes: stat.size,
189
+ ext,
190
+ verified: false,
191
+ };
192
+ }
193
+
146
194
  function pngDimensions(buffer) {
147
195
  if (buffer.length < 24) return {};
148
196
  return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
@@ -217,7 +265,7 @@ function powershellString(value) {
217
265
  }
218
266
 
219
267
  function ensureClipboardDir(baseDir = os.tmpdir()) {
220
- const dir = path.join(baseDir, 'kepler-clipboard-images');
268
+ const dir = path.join(baseDir, 'bahulam-clipboard-images');
221
269
  fs.mkdirSync(dir, { recursive: true });
222
270
  return dir;
223
271
  }
@@ -510,6 +558,17 @@ function extractPlainText(filePath, { maxChars }) {
510
558
  };
511
559
  }
512
560
 
561
+ function extractNotebookText(filePath, { maxChars }) {
562
+ const buffer = fs.readFileSync(filePath);
563
+ const raw = formatNotebookText(buffer);
564
+ const truncated = raw.length > maxChars;
565
+ return {
566
+ text: truncated ? raw.slice(0, maxChars) : raw,
567
+ pages: 0,
568
+ truncated,
569
+ };
570
+ }
571
+
513
572
  /**
514
573
  * Load a single document attachment. Reads locally, extracts text, returns
515
574
  * a public metadata block plus the extracted text. Errors bubble as
@@ -537,9 +596,12 @@ export async function loadDocumentAttachment(filePath, {
537
596
 
538
597
  const ext = path.extname(resolved).toLowerCase();
539
598
  const isPdf = ext === '.pdf';
599
+ const isNotebook = ext === '.ipynb';
540
600
  const { text, pages, truncated } = isPdf
541
601
  ? await extractPdfText(resolved, { maxChars })
542
- : extractPlainText(resolved, { maxChars });
602
+ : isNotebook
603
+ ? extractNotebookText(resolved, { maxChars })
604
+ : extractPlainText(resolved, { maxChars });
543
605
 
544
606
  const buffer = fs.readFileSync(resolved);
545
607
  const sha256 = createHash('sha256').update(buffer).digest('hex');
@@ -549,7 +611,7 @@ export async function loadDocumentAttachment(filePath, {
549
611
  path: resolved,
550
612
  name: path.basename(resolved),
551
613
  ext,
552
- kind: isPdf ? 'pdf' : 'text',
614
+ kind: isPdf ? 'pdf' : isNotebook ? 'notebook' : 'text',
553
615
  bytes: stat.size,
554
616
  chars: text.length,
555
617
  pages,
@@ -8,32 +8,29 @@
8
8
  */
9
9
 
10
10
  const BACKEND_URLS = {
11
- // Four supported environments:
12
- // local — Docker Compose backend on the developer's own machine.
13
- // dev — Bahulam Cloud development (Azure Container Apps, eastus).
14
- // production Bahulam Cloud live (Azure Container Apps, centralus).
15
- // bundled — CLI-local Python runtime spawned as a subprocess. URL is
16
- // overridden by bundled-runtime.mjs at spawn time; sentinel
17
- // value below is only used if the runtime isn't up yet.
11
+ // Supported environments:
12
+ // local — Docker Compose backend on the developer's own machine.
13
+ // production — Bahulam Cloud live (api.bahulam.ai).
14
+ // bundled CLI-local Python runtime spawned as a subprocess. URL is
15
+ // overridden by bundled-runtime.mjs at spawn time; sentinel
16
+ // value below is only used if the runtime isn't up yet.
17
+ //
18
+ // For custom dev/staging URLs, set TARANG_BACKEND_URL directly.
18
19
  local: 'http://127.0.0.1:8150',
19
- dev: 'https://codekepler-backend-dev.kindisland-9034322d.eastus.azurecontainerapps.io',
20
20
  production: 'https://api.bahulam.ai',
21
21
  bundled: 'http://127.0.0.1:0', // sentinel — real URL comes from bundled-runtime.mjs
22
22
  };
23
23
 
24
24
  // Aliases (backwards compat + convenience)
25
25
  BACKEND_URLS.prod = BACKEND_URLS.production;
26
- BACKEND_URLS.treetop = BACKEND_URLS.dev; // legacy alias
27
- BACKEND_URLS.docker = BACKEND_URLS.local; // convenience alias
26
+ BACKEND_URLS.docker = BACKEND_URLS.local;
28
27
 
29
28
  const WEB_URLS = {
30
29
  local: 'http://localhost:3100',
31
- dev: 'https://treetop.bahulam.ai',
32
30
  production: 'https://bahulam.ai',
33
31
  bundled: 'http://localhost:3100', // bundled mode reuses local web if user runs it
34
32
  };
35
33
  WEB_URLS.prod = WEB_URLS.production;
36
- WEB_URLS.treetop = WEB_URLS.dev; // legacy alias
37
34
  WEB_URLS.docker = WEB_URLS.local;
38
35
 
39
36
  /**
@@ -50,6 +47,8 @@ export function resolveWebUrl() {
50
47
 
51
48
  /**
52
49
  * Resolve the backend URL from environment.
50
+ *
51
+ * For dev/staging, set TARANG_BACKEND_URL=<your-url> explicitly.
53
52
  * @returns {string}
54
53
  */
55
54
  export function resolveBackendUrl() {
@@ -165,7 +165,7 @@ function _clearLicenseActivationState() {
165
165
  if (process.platform === 'darwin') {
166
166
  try {
167
167
  spawnSync('security',
168
- ['delete-generic-password', '-s', 'com.axplusb.agent.runtime', '-a', 'af_state'],
168
+ ['delete-generic-password', '-s', 'ai.bahulam.agent.runtime', '-a', 'af_state'],
169
169
  { stdio: 'ignore' });
170
170
  } catch { /* best-effort */ }
171
171
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Callback Client — POST tool results back to Tarang backend.
2
+ * Callback Client — POST tool results back to Bahulam backend.
3
3
  * Implements retry with backoff: 2 retries, 500ms delay.
4
4
  * 4xx: no retry. 5xx/network: retry.
5
5
  */
@@ -11,17 +11,18 @@
11
11
  * bahulam-code --headless --model deepseek/deepseek-chat-v3-0324 "Add tests"
12
12
  */
13
13
 
14
- import { TarangStreamClient } from './stream-client.mjs';
14
+ import { BahulamStreamClient } from './stream-client.mjs';
15
15
  import { createToolExecutor } from './tool-executor.mjs';
16
16
  import { buildWorkScope, promptProjectRoots } from './work-scope.mjs';
17
17
  import { persistProjectArtifacts } from './project-artifacts.mjs';
18
- import { TarangAuth } from '../auth/tarang-auth.mjs';
18
+ import { BahulamAuth } from '../auth/bahulam-auth.mjs';
19
19
  import { ApprovalManager } from './approval.mjs';
20
20
  // daemon wiring — headless (and `bahulam daemonize`) also starts the socket
21
21
  // server + relay bridge when eventlog is enabled. Without this the daemon
22
22
  // is invisible to attach clients and to paired mobile devices.
23
23
  import { tapSseEvent, registerBroadcaster } from '../daemon/event-tap.mjs';
24
24
  import { startSocketServer } from '../daemon/socket-server.mjs';
25
+ import { startHttpDashboard } from '../daemon/http-dashboard.mjs';
25
26
  import { resolvePending } from '../daemon/approval-store.mjs';
26
27
  import { startRelayBridge } from '../daemon/relay-client.mjs';
27
28
  import { loadRemoteConfig } from '../commands/remote.mjs';
@@ -57,7 +58,7 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
57
58
  };
58
59
 
59
60
  // ── Auth ──
60
- const auth = new TarangAuth();
61
+ const auth = new BahulamAuth();
61
62
  const creds = auth.loadCredentials();
62
63
  if (!creds.token) {
63
64
  emit({ type: 'error', error: 'Not logged in. Run: bahulam login' });
@@ -98,7 +99,7 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
98
99
  };
99
100
  log(`Local mode: ${localModel}`);
100
101
  } else {
101
- client = new TarangStreamClient({
102
+ client = new BahulamStreamClient({
102
103
  baseUrl: creds.backendUrl,
103
104
  token: creds.token,
104
105
  toolExecutor,
@@ -348,6 +349,18 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
348
349
  });
349
350
  registerBroadcaster(evt => server.broadcastEvent(evt));
350
351
 
352
+ // Optional HTTP browser dashboard (opt-in with BAHULAM_DASHBOARD=1)
353
+ let dashboardUrl = null;
354
+ if (process.env.BAHULAM_DASHBOARD === '1') {
355
+ try {
356
+ const dashboard = await startHttpDashboard({ sessionId: _sid });
357
+ dashboardUrl = dashboard.url;
358
+ process.stderr.write(`[prd-092] dashboard: ${dashboard.url}\n`);
359
+ } catch (err) {
360
+ try { process.stderr.write(`[prd-092] dashboard start: ${err.message}\n`); } catch {}
361
+ }
362
+ }
363
+
351
364
  // Write meta.json + daemon.pid so `bahulam list` /
352
365
  // `bahulam stop` and the mobile session directory
353
366
  // can find this session.
@@ -361,6 +374,7 @@ export async function runHeadless({ instruction, model, timeout = 300, maxCost,
361
374
  sock_path: server.sockPath,
362
375
  opened_at: new Date().toISOString(),
363
376
  headless: true,
377
+ dashboard_url: dashboardUrl,
364
378
  },
365
379
  });
366
380
  fsSync.writeFileSync(
@@ -2,7 +2,7 @@
2
2
  * JSONL Writer — writes cc-lens compatible session transcripts to ~/.bahulam/.
3
3
  *
4
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.
5
+ * cc-lens (CLAUDE_CONFIG_DIR=~/.bahulam npx cc-lens) can read Bahulam sessions.
6
6
  *
7
7
  * Design:
8
8
  * - Non-blocking: buffered writes, flushed every 500ms or on turn end
@@ -17,13 +17,13 @@ import { randomUUID } from 'node:crypto';
17
17
  import * as childProcessModule from 'node:child_process';
18
18
  import { bahulamHome } from './paths.mjs';
19
19
 
20
- const KEPLER_DIR = bahulamHome();
20
+ const BAHULAM_DIR = bahulamHome();
21
21
  const FLUSH_INTERVAL_MS = 500;
22
22
 
23
23
  /**
24
24
  * Sanitize a cwd path into a project slug for the directory name.
25
25
  * /Users/sree/Sites/myproject → -Users-sree-Sites-myproject
26
- * Mirrors the Claude Code / kepler-cli convention.
26
+ * Mirrors the Claude Code / Bahulam CLI convention.
27
27
  */
28
28
  function sanitizePath(p) {
29
29
  return p.replace(/\//g, '-').replace(/^-/, '-');
@@ -67,7 +67,7 @@ export class JsonlWriter {
67
67
  this.version = version;
68
68
  this.sessionId = null; // set by setSessionId() when backend assigns it
69
69
  this.slug = sanitizePath(cwd);
70
- this.projectDir = path.join(KEPLER_DIR, 'projects', this.slug);
70
+ this.projectDir = path.join(BAHULAM_DIR, 'projects', this.slug);
71
71
 
72
72
  // UUID chain for parent linking (cc-lens replay)
73
73
  this.lastUuid = null;
@@ -85,7 +85,7 @@ export class JsonlWriter {
85
85
  this._turnToolResults = []; // [{tool_use_id, content, is_error}, ...]
86
86
  this._turnUsage = null;
87
87
  this._turnModel = null;
88
- this._pendingKeplerEvents = [];
88
+ this._pendingBahulamEvents = [];
89
89
 
90
90
  // Git branch (captured once at construction)
91
91
  this._gitBranch = this._detectGitBranch();
@@ -101,10 +101,10 @@ export class JsonlWriter {
101
101
  this.sessionId = id;
102
102
  this._transcriptPath = path.join(this.projectDir, `${id}.jsonl`);
103
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);
104
+ if (this._pendingBahulamEvents.length > 0) {
105
+ const pending = this._pendingBahulamEvents;
106
+ this._pendingBahulamEvents = [];
107
+ for (const event of pending) this.writeBahulamEvent(event);
108
108
  }
109
109
  // Flush any buffered entries now that we have a path
110
110
  if (this._buffer.length > 0) this._flush();
@@ -143,10 +143,10 @@ export class JsonlWriter {
143
143
  }
144
144
 
145
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.
146
+ * Write a Bahulam UI/SSE event for exact-ish terminal replay on /resume.
147
+ * This is a Bahulam extension; cc-lens readers should ignore unknown types.
148
148
  */
149
- writeKeplerEvent(event) {
149
+ writeBahulamEvent(event) {
150
150
  if (!event || !event.type) return;
151
151
  const sanitized = sanitizeEventValue({
152
152
  type: event.type,
@@ -154,12 +154,12 @@ export class JsonlWriter {
154
154
  });
155
155
 
156
156
  if (!this.sessionId) {
157
- this._pendingKeplerEvents.push(sanitized);
157
+ this._pendingBahulamEvents.push(sanitized);
158
158
  return;
159
159
  }
160
160
 
161
161
  const entry = {
162
- type: 'kepler_event',
162
+ type: 'bahulam_event',
163
163
  timestamp: new Date().toISOString(),
164
164
  cwd: this.cwd,
165
165
  sessionId: this.sessionId,
@@ -204,8 +204,8 @@ export class JsonlWriter {
204
204
  content: normalizeToolResultContent(output),
205
205
  is_error: !!isError,
206
206
  };
207
- const kepler = compactToolResultMetadata(metadata);
208
- if (kepler) entry.bahulam = kepler;
207
+ const bahulamMeta = compactToolResultMetadata(metadata);
208
+ if (bahulamMeta) entry.bahulam = bahulamMeta;
209
209
  this._turnToolResults.push(entry);
210
210
  }
211
211
 
@@ -305,7 +305,7 @@ export class JsonlWriter {
305
305
  project: this.cwd,
306
306
  sessionId: this.sessionId,
307
307
  };
308
- const historyPath = path.join(KEPLER_DIR, 'history.jsonl');
308
+ const historyPath = path.join(BAHULAM_DIR, 'history.jsonl');
309
309
  fs.promises.appendFile(historyPath, JSON.stringify(entry) + '\n', { mode: 0o600 })
310
310
  .catch(() => {}); // best effort
311
311
  }
@@ -393,7 +393,7 @@ export class JsonlWriter {
393
393
  fs.mkdirSync(this.projectDir, { recursive: true, mode: 0o700 });
394
394
  } catch { /* ignore */ }
395
395
  try {
396
- fs.mkdirSync(path.join(KEPLER_DIR, 'projects'), { recursive: true, mode: 0o700 });
396
+ fs.mkdirSync(path.join(BAHULAM_DIR, 'projects'), { recursive: true, mode: 0o700 });
397
397
  } catch { /* ignore */ }
398
398
  }
399
399