@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.
- package/LICENSE +1 -1
- package/NOTICE +6 -6
- package/README.md +11 -18
- package/package.json +11 -11
- package/src/agents/loader.mjs +1 -1
- package/src/agents/workflow_loader.mjs +2 -2
- package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +4 -3
- package/src/commands/agent.mjs +3 -3
- package/src/commands/device.mjs +2 -2
- package/src/commands/pair.mjs +2 -2
- package/src/commands/remote.mjs +3 -3
- package/src/commands/workflow.mjs +5 -5
- package/src/config/env.mjs +9 -2
- package/src/config/memory-loader.mjs +1 -1
- package/src/config/model-catalog-default.json +4 -3
- package/src/config/model-catalog.mjs +2 -0
- package/src/config/settings.mjs +2 -2
- package/src/context/prose-chunker.mjs +162 -5
- package/src/core/agent-loop.mjs +17 -0
- package/src/core/attachments.mjs +66 -4
- package/src/core/backend-url.mjs +11 -12
- package/src/core/bundled-runtime.mjs +1 -1
- package/src/core/callback-client.mjs +1 -1
- package/src/core/headless.mjs +18 -4
- package/src/core/jsonl-writer.mjs +18 -18
- package/src/core/lint-resolver.mjs +472 -0
- package/src/core/local-agent.mjs +40 -8
- package/src/core/local-store.mjs +5 -1
- package/src/core/mode-selector.mjs +1 -1
- package/src/core/output-filter.mjs +22 -26
- package/src/core/pricing.mjs +10 -3
- package/src/core/project-artifacts.mjs +3 -3
- package/src/core/project-context-loader.mjs +9 -9
- package/src/core/settings-sync.mjs +1 -1
- package/src/core/stagnation.mjs +61 -1
- package/src/core/stream-client.mjs +13 -8
- package/src/core/system-prompt.mjs +2 -2
- package/src/core/tool-executor.mjs +178 -84
- package/src/daemon/http-dashboard.mjs +400 -0
- package/src/daemon/session-core.mjs +1 -0
- package/src/local-service/agent-relay.mjs +868 -0
- package/src/local-service/approval-bridge.mjs +222 -0
- package/src/local-service/browser.mjs +24 -0
- package/src/local-service/command.mjs +155 -0
- package/src/local-service/file-access.mjs +393 -0
- package/src/local-service/machine.mjs +86 -0
- package/src/local-service/paths.mjs +29 -0
- package/src/local-service/preview-converters.mjs +260 -0
- package/src/local-service/server.mjs +2644 -0
- package/src/local-service/session-store.mjs +221 -0
- package/src/mcp/client.mjs +4 -4
- package/src/onboarding/preflight.mjs +2 -2
- package/src/terminal/agents.mjs +2 -2
- package/src/terminal/analytics.mjs +2 -6
- package/src/terminal/ansi.mjs +11 -3
- package/src/terminal/main.mjs +43 -17
- package/src/terminal/model-catalog-display.mjs +111 -0
- package/src/terminal/repl-format.mjs +4 -1
- package/src/terminal/repl-model-form.mjs +81 -14
- package/src/terminal/repl-render.mjs +37 -7
- package/src/terminal/repl-resume.mjs +2 -2
- package/src/terminal/repl-state.mjs +12 -0
- package/src/terminal/repl.mjs +248 -70
- package/src/terminal/tool-display.mjs +32 -0
- package/src/terminal/watch-state.mjs +118 -0
- package/src/tools/agent.mjs +2 -2
- package/src/tools/analyze-image.mjs +158 -0
- package/src/tools/bash.mjs +11 -9
- package/src/tools/generate-image.mjs +411 -0
- package/src/tools/lint.mjs +24 -43
- package/src/tools/project-overview.mjs +12 -12
- package/src/tools/registry.mjs +4 -0
- package/src/ui/formatter.mjs +2 -2
- package/src/ui/icons.mjs +2 -0
- package/src/ui/input-dock.mjs +7 -7
- package/src/ui/palette.mjs +1 -1
- package/src/ui/slash-commands.mjs +4 -6
- package/src/ui/spinner.mjs +1 -1
- package/src/ui/term.mjs +2 -6
- package/src/ui/tool-card.mjs +81 -6
- package/pulse/app/activity/page.tsx +0 -190
- package/pulse/app/api/activity/route.ts +0 -138
- package/pulse/app/api/benchmark/route.ts +0 -113
- package/pulse/app/api/benchmarks/route.ts +0 -195
- package/pulse/app/api/costs/route.ts +0 -88
- package/pulse/app/api/export/route.ts +0 -77
- package/pulse/app/api/history/route.ts +0 -11
- package/pulse/app/api/import/route.ts +0 -31
- package/pulse/app/api/memory/route.ts +0 -50
- package/pulse/app/api/plans/route.ts +0 -9
- package/pulse/app/api/projects/[slug]/route.ts +0 -96
- package/pulse/app/api/projects/route.ts +0 -121
- package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
- package/pulse/app/api/sessions/[id]/route.ts +0 -31
- package/pulse/app/api/sessions/route.ts +0 -112
- package/pulse/app/api/settings/route.ts +0 -14
- package/pulse/app/api/stats/route.ts +0 -143
- package/pulse/app/api/todos/route.ts +0 -9
- package/pulse/app/api/tools/route.ts +0 -160
- package/pulse/app/benchmarks/page.tsx +0 -224
- package/pulse/app/costs/page.tsx +0 -179
- package/pulse/app/export/page.tsx +0 -465
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +0 -263
- package/pulse/app/help/page.tsx +0 -143
- package/pulse/app/history/page.tsx +0 -157
- package/pulse/app/layout.tsx +0 -46
- package/pulse/app/memory/page.tsx +0 -365
- package/pulse/app/overview-client.tsx +0 -393
- package/pulse/app/page.tsx +0 -14
- package/pulse/app/plans/page.tsx +0 -308
- package/pulse/app/projects/[slug]/page.tsx +0 -390
- package/pulse/app/projects/page.tsx +0 -110
- package/pulse/app/sessions/[id]/page.tsx +0 -243
- package/pulse/app/sessions/page.tsx +0 -39
- package/pulse/app/settings/page.tsx +0 -188
- package/pulse/app/todos/page.tsx +0 -211
- package/pulse/app/tools/page.tsx +0 -249
- package/pulse/cli.js +0 -164
- package/pulse/components/activity/day-of-week-chart.tsx +0 -35
- package/pulse/components/activity/streak-card.tsx +0 -36
- package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
- package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
- package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
- package/pulse/components/costs/model-token-table.tsx +0 -60
- package/pulse/components/global-search.tsx +0 -193
- package/pulse/components/keyboard-nav-provider.tsx +0 -23
- package/pulse/components/layout/bottom-nav.tsx +0 -53
- package/pulse/components/layout/client-layout.tsx +0 -31
- package/pulse/components/layout/sidebar-context.tsx +0 -50
- package/pulse/components/layout/sidebar.tsx +0 -183
- package/pulse/components/layout/top-bar.tsx +0 -121
- package/pulse/components/overview/activity-heatmap.tsx +0 -107
- package/pulse/components/overview/conversation-table.tsx +0 -148
- package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
- package/pulse/components/overview/peak-hours-chart.tsx +0 -87
- package/pulse/components/overview/project-activity-donut.tsx +0 -96
- package/pulse/components/overview/stat-card.tsx +0 -102
- package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
- package/pulse/components/projects/project-card.tsx +0 -175
- package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
- package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
- package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
- package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
- package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
- package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
- package/pulse/components/sessions/session-badges.tsx +0 -49
- package/pulse/components/sessions/session-table.tsx +0 -299
- package/pulse/components/theme-provider.tsx +0 -44
- package/pulse/components/tools/feature-adoption-table.tsx +0 -58
- package/pulse/components/tools/mcp-server-panel.tsx +0 -45
- package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
- package/pulse/components/tools/version-history-table.tsx +0 -32
- package/pulse/components/ui/alert.tsx +0 -66
- package/pulse/components/ui/badge.tsx +0 -48
- package/pulse/components/ui/breadcrumb.tsx +0 -109
- package/pulse/components/ui/button.tsx +0 -64
- package/pulse/components/ui/calendar.tsx +0 -220
- package/pulse/components/ui/card.tsx +0 -92
- package/pulse/components/ui/command.tsx +0 -158
- package/pulse/components/ui/dialog.tsx +0 -158
- package/pulse/components/ui/input.tsx +0 -21
- package/pulse/components/ui/popover.tsx +0 -89
- package/pulse/components/ui/progress.tsx +0 -31
- package/pulse/components/ui/select.tsx +0 -190
- package/pulse/components/ui/separator.tsx +0 -28
- package/pulse/components/ui/sheet.tsx +0 -143
- package/pulse/components/ui/skeleton.tsx +0 -13
- package/pulse/components/ui/table.tsx +0 -116
- package/pulse/components/ui/tabs.tsx +0 -91
- package/pulse/components/ui/tooltip.tsx +0 -57
- package/pulse/components/use-global-keyboard-nav.ts +0 -79
- package/pulse/components.json +0 -23
- package/pulse/eslint.config.mjs +0 -18
- package/pulse/lib/bahulam-paths.ts +0 -23
- package/pulse/lib/claude-reader.ts +0 -592
- package/pulse/lib/decode.ts +0 -129
- package/pulse/lib/pricing.ts +0 -102
- package/pulse/lib/replay-parser.ts +0 -165
- package/pulse/lib/tool-categories.ts +0 -140
- package/pulse/lib/utils.ts +0 -6
- package/pulse/next-env.d.ts +0 -6
- package/pulse/next.config.ts +0 -16
- package/pulse/package.json +0 -45
- package/pulse/postcss.config.mjs +0 -7
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +0 -1
- package/pulse/public/globe.svg +0 -1
- package/pulse/public/next.svg +0 -1
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +0 -1
- package/pulse/public/window.svg +0 -1
- package/pulse/tsconfig.json +0 -34
- 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
|
|
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
|
-
|
|
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
|
}
|
package/src/core/agent-loop.mjs
CHANGED
|
@@ -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({
|
package/src/core/attachments.mjs
CHANGED
|
@@ -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, '
|
|
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
|
-
:
|
|
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,
|
package/src/core/backend-url.mjs
CHANGED
|
@@ -8,32 +8,29 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const BACKEND_URLS = {
|
|
11
|
-
//
|
|
12
|
-
// local
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
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.
|
|
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', '
|
|
168
|
+
['delete-generic-password', '-s', 'ai.bahulam.agent.runtime', '-a', 'af_state'],
|
|
169
169
|
{ stdio: 'ignore' });
|
|
170
170
|
} catch { /* best-effort */ }
|
|
171
171
|
}
|
package/src/core/headless.mjs
CHANGED
|
@@ -11,17 +11,18 @@
|
|
|
11
11
|
* bahulam-code --headless --model deepseek/deepseek-chat-v3-0324 "Add tests"
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 /
|
|
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(
|
|
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.
|
|
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.
|
|
105
|
-
const pending = this.
|
|
106
|
-
this.
|
|
107
|
-
for (const event of pending) this.
|
|
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
|
|
147
|
-
* This is a
|
|
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
|
-
|
|
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.
|
|
157
|
+
this._pendingBahulamEvents.push(sanitized);
|
|
158
158
|
return;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
const entry = {
|
|
162
|
-
type: '
|
|
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
|
|
208
|
-
if (
|
|
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(
|
|
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(
|
|
396
|
+
fs.mkdirSync(path.join(BAHULAM_DIR, 'projects'), { recursive: true, mode: 0o700 });
|
|
397
397
|
} catch { /* ignore */ }
|
|
398
398
|
}
|
|
399
399
|
|