@bahulam/code 2.6.1 → 2.6.3

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/README.md CHANGED
@@ -9,6 +9,13 @@ prior sessions, and keeps local project context in `~/.bahulam/`.
9
9
  - Sub-agents, skills, workflows, and MCP support
10
10
  - Bring your own model (40+ supported)
11
11
 
12
+ ## About the name
13
+
14
+ **Bahulam** (बहुलम्) is Sanskrit for *abundance*. The name reflects a
15
+ philosophy: tokens are not rationed, model choice is not gated, and
16
+ intelligence is not metered per feature. The restrictions most agents
17
+ ship with are design decisions — ones we chose not to make.
18
+
12
19
  ## Install
13
20
 
14
21
  ```bash
@@ -56,7 +63,7 @@ skills, workflows, session tools).
56
63
 
57
64
  - Published under both `@bahulam/code` and `@bahulamai/code` scopes.
58
65
  - Full removal of legacy `b0` references from CLI help, error messages,
59
- analytics headers, and internal comments.
66
+ analytics headers, internal comments, and Pulse dashboard.
60
67
 
61
68
  ## 2.6.0 Highlights
62
69
 
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@bahulam/code",
3
- "version": "2.6.1",
4
- "description": "Bahulam Code Bahulam's coding agent. CLI-first, reliability-first, sub-agents, 65.6% SWE-bench Verified.",
3
+ "version": "2.6.3",
4
+ "description": "Bahulam Code \u2014 abundance, in your terminal. CLI-first, reliability-first, sub-agents, 65.6% SWE-bench Verified.",
5
5
  "type": "module",
6
6
  "bin": {
7
+ "bahulam": "src/terminal/main.mjs",
7
8
  "bahulam-code": "src/terminal/main.mjs"
8
9
  },
9
10
  "files": [
@@ -13,7 +14,7 @@
13
14
  ],
14
15
  "scripts": {
15
16
  "start": "node src/terminal/main.mjs",
16
- "test": "BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-backend-url.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-agent-history.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-sse-client.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-tool-executor.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-project-artifacts.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-work-scope.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-skills.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-callback.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-rate-limit-display.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-preflight.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-formatter.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-terminal-rendering.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-input-dock.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-slash-commands.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-approval.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-approval-log.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-kepler-contract.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-session-manager.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-safety.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-jsonl-writer.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-analytics.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-stagnation.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-attachments.mjs"
17
+ "test": "BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-backend-url.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-agent-history.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-sse-client.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-tool-executor.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-project-artifacts.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-work-scope.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-skills.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-callback.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-rate-limit-display.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-preflight.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-formatter.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-terminal-rendering.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-input-dock.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-live-steering-client.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-slash-commands.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-approval.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-approval-log.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-kepler-contract.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-session-manager.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-safety.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-jsonl-writer.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-analytics.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-stagnation.mjs && BAHULAM_HOME=/tmp/bahulam-code-test-home node test/test-attachments.mjs"
17
18
  },
18
19
  "engines": {
19
20
  "node": ">=18.0.0"
@@ -43,7 +44,11 @@
43
44
  "url": "git+https://github.com/raviakasapu/codekepler-npm.git"
44
45
  },
45
46
  "dependencies": {
47
+ "pdf-parse": "^1.1.1",
46
48
  "tree-sitter-wasms": "^0.1.13",
47
49
  "web-tree-sitter": "^0.26.9"
50
+ },
51
+ "optionalDependencies": {
52
+ "@bahulam/runtime-darwin-arm64": "2.6.3"
48
53
  }
49
54
  }
@@ -175,7 +175,7 @@ export default function ActivityPage() {
175
175
  <CalendarDays className="h-4 w-4 shrink-0 text-muted-foreground" />
176
176
  Day of Week
177
177
  </CardTitle>
178
- <CardDescription>Which days you use b0 most</CardDescription>
178
+ <CardDescription>Which days you use Bahulam Code most</CardDescription>
179
179
  </CardHeader>
180
180
  <CardContent className="flex flex-1 flex-col justify-between pt-3 pb-0">
181
181
  <DayOfWeekChart data={data.dow_counts} />
@@ -86,9 +86,9 @@
86
86
  --radius-lg: var(--radius);
87
87
  --radius-xl: calc(var(--radius) + 4px);
88
88
 
89
- /* b0 accent colors */
90
- --color-kepler-cyan: #06b6d4;
91
- --color-kepler-cyan-light: #22d3ee;
89
+ /* Bahulam accent colors */
90
+ --color-bahulam-cyan: #06b6d4;
91
+ --color-bahulam-cyan-light: #22d3ee;
92
92
  --color-terminal-cyan: #06b6d4;
93
93
  --color-terminal-green: #34d399;
94
94
  --color-terminal-amber: #f59e0b;
@@ -2,14 +2,14 @@ import { TopBar } from '@/components/layout/top-bar'
2
2
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
3
3
 
4
4
  const CLI_COMMANDS = [
5
- { cmd: 'b0', desc: 'Start interactive REPL' },
6
- { cmd: 'b0 "instruction"', desc: 'Run a single instruction and exit' },
7
- { cmd: 'b0 dashboard', desc: 'Open b0 Pulse analytics dashboard' },
8
- { cmd: 'b0 sessions', desc: 'List recent local sessions' },
9
- { cmd: 'b0 stats', desc: 'Show aggregate local session stats' },
10
- { cmd: 'b0 history', desc: 'Show recent prompt history' },
11
- { cmd: 'b0 login', desc: 'Sign in via browser' },
12
- { cmd: 'b0 version', desc: 'Show version' },
5
+ { cmd: 'bahulam-code', desc: 'Start interactive REPL' },
6
+ { cmd: 'bahulam-code "instruction"', desc: 'Run a single instruction and exit' },
7
+ { cmd: 'bahulam-code dashboard', desc: 'Open Bahulam Pulse analytics dashboard' },
8
+ { cmd: 'bahulam-code sessions', desc: 'List recent local sessions' },
9
+ { cmd: 'bahulam-code stats', desc: 'Show aggregate local session stats' },
10
+ { cmd: 'bahulam-code history', desc: 'Show recent prompt history' },
11
+ { cmd: 'bahulam-code login', desc: 'Sign in via browser' },
12
+ { cmd: 'bahulam-code version', desc: 'Show version' },
13
13
  ]
14
14
 
15
15
  const REPL_COMMANDS = [
@@ -47,7 +47,7 @@ const FEATURES = [
47
47
  { title: 'Multi-Agent Orchestration', desc: 'Primary agent spawns explore, plan, and review sub-agents for complex tasks.' },
48
48
  { title: 'Smart Tool Routing', desc: 'Write operations routed to CLI for local execution. Read tools run server-side.' },
49
49
  { title: 'Safety Guardrails', desc: 'Destructive commands (rm, delete) always require approval. Path validation on all file operations.' },
50
- { title: 'Local JSONL Analytics', desc: 'Every session is recorded to ~/.bahulam/ for offline analytics via b0 Pulse.' },
50
+ { title: 'Local JSONL Analytics', desc: 'Every session is recorded to ~/.bahulam/ for offline analytics via Bahulam Pulse.' },
51
51
  { title: 'Conversation History', desc: 'Multi-turn conversations within a REPL session. Context preserved across turns.' },
52
52
  { title: 'BM25 Code Index', desc: 'Project files indexed on startup for fast semantic code search.' },
53
53
  ]
@@ -68,7 +68,7 @@ function CommandTable({ commands }: { commands: { cmd: string; desc: string }[]
68
68
  export default function HelpPage() {
69
69
  return (
70
70
  <div className="flex flex-col min-h-screen">
71
- <TopBar title="Help" subtitle="b0 CLI commands, shortcuts, and configuration" />
71
+ <TopBar title="Help" subtitle="Bahulam Code CLI commands, shortcuts, and configuration" />
72
72
  <div className="px-6 py-6 space-y-6">
73
73
 
74
74
  <Card>
@@ -20,8 +20,8 @@ const pressStart2P = Press_Start_2P({
20
20
  })
21
21
 
22
22
  export const metadata: Metadata = {
23
- title: 'b0 Pulse',
24
- description: 'Real-time analytics for your b0 agent sessions',
23
+ title: 'Bahulam Pulse',
24
+ description: 'Real-time analytics for your Bahulam Code agent sessions',
25
25
  }
26
26
 
27
27
  export default function RootLayout({
@@ -260,7 +260,7 @@ export default function MemoryPage() {
260
260
 
261
261
  return (
262
262
  <div className="flex flex-col min-h-screen">
263
- <TopBar title="b0-pulse · memory" subtitle="~/.bahulam/projects/*/memory/" />
263
+ <TopBar title="Bahulam Pulse · memory" subtitle="~/.bahulam/projects/*/memory/" />
264
264
  <div className="p-4 md:p-6 space-y-5">
265
265
 
266
266
  {error && <p className="text-[#f87171] text-sm font-mono">Error loading memories.</p>}
@@ -381,7 +381,7 @@ export function OverviewClient() {
381
381
  <Card>
382
382
  <CardHeader>
383
383
  <CardTitle>Recent Sessions</CardTitle>
384
- <CardDescription>Your latest b0 sessions</CardDescription>
384
+ <CardDescription>Your latest Bahulam Code sessions</CardDescription>
385
385
  </CardHeader>
386
386
  <CardContent>
387
387
  <OverviewConversationTable sessions={sessions} />
@@ -5,8 +5,8 @@ export default function OverviewPage() {
5
5
  return (
6
6
  <div className="flex flex-col min-h-screen">
7
7
  <TopBar
8
- title="b0 Pulse"
9
- subtitle="Real-time analytics for your b0 agent sessions"
8
+ title="Bahulam Pulse"
9
+ subtitle="Real-time analytics for your Bahulam Code agent sessions"
10
10
  />
11
11
  <OverviewClient />
12
12
  </div>
@@ -251,7 +251,7 @@ export default function PlansPage() {
251
251
 
252
252
  return (
253
253
  <div className="flex flex-col min-h-screen">
254
- <TopBar title="b0-pulse · plans" subtitle="~/.bahulam/plans/" />
254
+ <TopBar title="Bahulam Pulse · plans" subtitle="~/.bahulam/plans/" />
255
255
  <div className="p-4 md:p-6 space-y-5">
256
256
 
257
257
  {error && <p className="text-[#f87171] text-sm font-mono">Error: {String(error)}</p>}
@@ -18,7 +18,7 @@ export default function SessionsPage() {
18
18
  return (
19
19
  <div className="flex flex-col min-h-screen">
20
20
  <TopBar
21
- title="b0 Pulse · Sessions"
21
+ title="Bahulam Pulse · Sessions"
22
22
  subtitle={data ? `${data.total} total sessions` : 'loading...'}
23
23
  />
24
24
  <div className="p-6">
@@ -83,7 +83,7 @@ export default function SettingsPage() {
83
83
 
84
84
  return (
85
85
  <div className="flex flex-col min-h-screen">
86
- <TopBar title="b0-pulse · settings" subtitle="~/.bahulam/settings.json" />
86
+ <TopBar title="Bahulam Pulse · settings" subtitle="~/.bahulam/settings.json" />
87
87
  <div className="p-4 md:p-6 space-y-6">
88
88
  {error && <p className="text-[#f87171] text-sm font-mono">Error: {String(error)}</p>}
89
89
  {isLoading && (
@@ -201,7 +201,7 @@ export default function ToolsPage() {
201
201
  {data.versions.length > 0 && (
202
202
  <Card>
203
203
  <CardHeader>
204
- <CardTitle>b0 Version History</CardTitle>
204
+ <CardTitle>Bahulam Version History</CardTitle>
205
205
  <CardDescription>Versions seen across your sessions</CardDescription>
206
206
  </CardHeader>
207
207
  <CardContent>
package/pulse/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * b0 Pulse — local analytics dashboard launcher.
3
+ * Bahulam Pulse — local analytics dashboard launcher.
4
4
  * Launches a Next.js dev server from ~/.bahulam-pulse/ cache directory.
5
5
  */
6
6
 
@@ -13,7 +13,7 @@ const fs = require('fs')
13
13
  const PKG_DIR = __dirname
14
14
  const CACHE_DIR = path.join(os.homedir(), '.bahulam-pulse')
15
15
 
16
- // ANSI helpers — b0 cyan palette
16
+ // ANSI helpers — Bahulam cyan palette
17
17
  const C = '\x1b[36m' // cyan
18
18
  const C2 = '\x1b[96m' // bright cyan
19
19
  const DIM = '\x1b[2m'
@@ -25,7 +25,7 @@ function printBanner() {
25
25
  console.log()
26
26
  const configDir = resolveConfigDir()
27
27
  console.log(` ${B}${C}B · 0${R}`)
28
- console.log(` ${B}${C2}b0 Pulse${R} ${DIM}real-time agent analytics${R}`)
28
+ console.log(` ${B}${C2}Bahulam Pulse${R} ${DIM}real-time agent analytics${R}`)
29
29
  console.log()
30
30
  console.log(` ${DIM}Data dir:${R} ${C2}${configDir}${R}`)
31
31
  console.log()
@@ -82,7 +82,7 @@ function syncSource(pkg) {
82
82
  }
83
83
  // Write a minimal package.json with only runtime dependencies
84
84
  fs.writeFileSync(path.join(CACHE_DIR, 'package.json'), JSON.stringify({
85
- name: 'b0-pulse-runtime',
85
+ name: 'bahulam-pulse-runtime',
86
86
  version: pkg.version,
87
87
  dependencies: pkg.dependencies,
88
88
  }, null, 2))
@@ -125,13 +125,13 @@ async function main() {
125
125
  const url = `http://localhost:${port}`
126
126
 
127
127
  // Pass config vars to the Next.js process so it reads from ~/.bahulam.
128
- const keplerDir = resolveConfigDir()
128
+ const configDir = resolveConfigDir()
129
129
  const env = {
130
130
  ...process.env,
131
131
  PORT: String(port),
132
- BAHULAM_CONFIG_DIR: keplerDir,
133
- KEPLER_CONFIG_DIR: keplerDir,
134
- CLAUDE_CONFIG_DIR: keplerDir,
132
+ BAHULAM_CONFIG_DIR: configDir,
133
+ KEPLER_CONFIG_DIR: configDir,
134
+ CLAUDE_CONFIG_DIR: configDir,
135
135
  }
136
136
 
137
137
  console.log(` ${DIM}Starting server on${R} ${C2}${B}${url}${R}\n`)
@@ -102,7 +102,7 @@ function SidebarContents({
102
102
  <span
103
103
  className="dark:[text-shadow:0_1px_0_#0e4f5c,0_2px_0_#083344,0_3px_6px_rgba(0,0,0,0.35)] [text-shadow:0_1px_0_rgba(255,255,255,0.4)]"
104
104
  >
105
- b0 Pulse
105
+ Bahulam Pulse
106
106
  </span>
107
107
  </span>
108
108
  )}
@@ -141,7 +141,7 @@ function SidebarContents({
141
141
  )}>
142
142
  {!collapsed && (
143
143
  <span className="text-xs text-sidebar-foreground/50">
144
- b0 Pulse
144
+ Bahulam Pulse
145
145
  </span>
146
146
  )}
147
147
  <button
@@ -1,8 +1,8 @@
1
1
  {
2
- "name": "b0-pulse",
2
+ "name": "bahulam-pulse",
3
3
  "version": "1.0.1",
4
4
  "private": true,
5
- "description": "b0 Pulse — real-time analytics dashboard for b0 agent sessions",
5
+ "description": "Bahulam Pulse — real-time analytics dashboard for Bahulam Code agent sessions",
6
6
  "scripts": {
7
7
  "dev": "next dev",
8
8
  "build": "next build",
@@ -141,7 +141,7 @@ export function createWorkflowFile({
141
141
  const normalizedEdges = normalizeWorkflowEdges(edges, agentSlugs);
142
142
 
143
143
  const lines = [
144
- 'apiVersion: kepler.workflow/v1',
144
+ 'apiVersion: bahulam.workflow/v1',
145
145
  'kind: MultiWorkflow',
146
146
  'metadata:',
147
147
  ` name: ${yamlQuote(name)}`,
@@ -5,8 +5,23 @@ import { createHash, randomUUID } from 'node:crypto';
5
5
  import { execFileSync } from 'node:child_process';
6
6
 
7
7
  const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif']);
8
+ // Text-first document formats we can extract client-side and inline into the
9
+ // user turn as plain text — zero backend upload, zero tool call. PDFs go
10
+ // through pdf-parse; the rest are read as UTF-8.
11
+ const DOCUMENT_EXTENSIONS = new Set([
12
+ '.pdf',
13
+ '.txt', '.md', '.mdx', '.rst',
14
+ '.csv', '.tsv', '.log',
15
+ '.json', '.yaml', '.yml', '.toml', '.ini', '.env',
16
+ '.html', '.htm', '.xml',
17
+ ]);
8
18
  const DEFAULT_MAX_IMAGE_BYTES = 8 * 1024 * 1024;
9
19
  const DEFAULT_MAX_TURN_BYTES = 20 * 1024 * 1024;
20
+ // Per-document and per-turn caps for inlined document text. Keep well under
21
+ // context so multiple attachments still leave room for the actual work.
22
+ const DEFAULT_MAX_DOC_CHARS = 80_000;
23
+ const DEFAULT_MAX_TURN_DOC_CHARS = 200_000;
24
+ const DEFAULT_MAX_DOC_BYTES = 5 * 1024 * 1024;
10
25
 
11
26
  function envInt(name, fallback) {
12
27
  const raw = process.env[name];
@@ -78,11 +93,27 @@ export function parseImageReferences(input, { cwd = process.cwd() } = {}) {
78
93
  parsed = readBare(text, i + 1);
79
94
  }
80
95
 
81
- if (!parsed || !looksLikeImagePath(parsed.value)) {
96
+ if (!parsed || (!looksLikeImagePath(parsed.value) && !isClipboardAlias(parsed.value))) {
82
97
  cleaned += text[i++];
83
98
  continue;
84
99
  }
85
100
 
101
+ // @clipboard / @paste — resolve via OS clipboard, attach the resulting
102
+ // temp file path. Skipping (with a stderr note) on unsupported platforms
103
+ // or empty clipboards so the rest of the parse continues cleanly.
104
+ if (isClipboardAlias(parsed.value)) {
105
+ try {
106
+ const filePath = writeClipboardImageToTemp();
107
+ attachments.push({ raw: parsed.value, path: filePath, source: 'clipboard' });
108
+ } catch (err) {
109
+ try {
110
+ process.stderr.write(` ! @${parsed.value} skipped — ${err.message || String(err)}\n`);
111
+ } catch {}
112
+ }
113
+ i = parsed.end;
114
+ continue;
115
+ }
116
+
86
117
  attachments.push({
87
118
  raw: parsed.value,
88
119
  path: resolveAttachmentPath(parsed.value, cwd),
@@ -361,6 +392,261 @@ export function prepareImageAttachments(input, {
361
392
  };
362
393
  }
363
394
 
395
+ // ── Document attachments (PRD-091 shared tools mirror) ──────────────────
396
+ //
397
+ // Same @path parser shape as images, but for text-first formats we can
398
+ // extract client-side and inline as text into the user turn. Zero upload,
399
+ // zero tool round-trip. PDFs go through pdf-parse; text formats are read
400
+ // as UTF-8 with a byte + char cap. Kept in this module so callers get one
401
+ // unified attachment pipeline.
402
+
403
+ function looksLikeDocumentPath(value) {
404
+ const ext = path.extname(String(value || '').toLowerCase());
405
+ return DOCUMENT_EXTENSIONS.has(ext);
406
+ }
407
+
408
+ // Special reference tokens (no path extension) — parser recognises them
409
+ // and resolves at parse time.
410
+ const CLIPBOARD_ALIASES = new Set(['clipboard', 'paste']);
411
+
412
+ function isClipboardAlias(value) {
413
+ return CLIPBOARD_ALIASES.has(String(value || '').trim().toLowerCase());
414
+ }
415
+
416
+ function looksLikeAttachment(value) {
417
+ return looksLikeImagePath(value) || looksLikeDocumentPath(value) || isClipboardAlias(value);
418
+ }
419
+
420
+ /**
421
+ * Parse @path references from `input` for BOTH images and documents.
422
+ * Returns a cleaned instruction (with @refs removed) plus separate
423
+ * image + document reference lists. Mirrors parseImageReferences but
424
+ * dispatches on extension.
425
+ */
426
+ export function parseAttachmentReferences(input, { cwd = process.cwd() } = {}) {
427
+ const text = String(input || '');
428
+ const images = [];
429
+ const documents = [];
430
+ let cleaned = '';
431
+ let i = 0;
432
+
433
+ while (i < text.length) {
434
+ if (text[i] !== '@') {
435
+ cleaned += text[i++];
436
+ continue;
437
+ }
438
+ const next = text[i + 1];
439
+ let parsed = null;
440
+ if (next === '"' || next === "'") {
441
+ parsed = readQuoted(text, i + 1);
442
+ } else if (next && !/\s/.test(next)) {
443
+ parsed = readBare(text, i + 1);
444
+ }
445
+ if (!parsed || !looksLikeAttachment(parsed.value)) {
446
+ cleaned += text[i++];
447
+ continue;
448
+ }
449
+
450
+ // @clipboard / @paste — resolve now via the OS clipboard helper. Side
451
+ // effect (writes a temp PNG) is fine at parse time because the parser
452
+ // is called once per turn, matching when the user typed @clipboard.
453
+ if (isClipboardAlias(parsed.value)) {
454
+ try {
455
+ const filePath = writeClipboardImageToTemp();
456
+ images.push({ raw: parsed.value, path: filePath, source: 'clipboard' });
457
+ } catch (err) {
458
+ // Emit a stderr note so the user knows why their @clipboard
459
+ // reference didn't attach anything, then drop it from the parse.
460
+ try {
461
+ process.stderr.write(` ! @${parsed.value} skipped — ${err.message || String(err)}\n`);
462
+ } catch {}
463
+ }
464
+ i = parsed.end;
465
+ continue;
466
+ }
467
+
468
+ const attachment = {
469
+ raw: parsed.value,
470
+ path: resolveAttachmentPath(parsed.value, cwd),
471
+ };
472
+ if (looksLikeImagePath(parsed.value)) images.push(attachment);
473
+ else documents.push(attachment);
474
+ i = parsed.end;
475
+ }
476
+
477
+ return {
478
+ instruction: cleaned.replace(/\s+/g, ' ').trim(),
479
+ images,
480
+ documents,
481
+ };
482
+ }
483
+
484
+ async function extractPdfText(filePath, { maxChars }) {
485
+ // pdf-parse is a CJS module. Dynamic import so ESM callers stay clean and
486
+ // we don't pay the parse cost on startup for CLIs that never touch PDFs.
487
+ const { default: pdfParse } = await import('pdf-parse');
488
+ const buffer = fs.readFileSync(filePath);
489
+ const parsed = await pdfParse(buffer);
490
+ const raw = String(parsed?.text || '').trim();
491
+ const truncated = raw.length > maxChars;
492
+ return {
493
+ text: truncated ? raw.slice(0, maxChars) : raw,
494
+ pages: parsed?.numpages || 0,
495
+ truncated,
496
+ };
497
+ }
498
+
499
+ function extractPlainText(filePath, { maxChars }) {
500
+ const buffer = fs.readFileSync(filePath);
501
+ // Best-effort UTF-8; binary files that sneak through the ext list will
502
+ // still decode but likely with replacement chars. That's a signal to the
503
+ // model that this attachment is not meaningful text.
504
+ const raw = buffer.toString('utf-8');
505
+ const truncated = raw.length > maxChars;
506
+ return {
507
+ text: truncated ? raw.slice(0, maxChars) : raw,
508
+ pages: 0,
509
+ truncated,
510
+ };
511
+ }
512
+
513
+ /**
514
+ * Load a single document attachment. Reads locally, extracts text, returns
515
+ * a public metadata block plus the extracted text. Errors bubble as
516
+ * throws — callers should catch per-document and continue.
517
+ */
518
+ export async function loadDocumentAttachment(filePath, {
519
+ cwd = process.cwd(),
520
+ maxBytes = DEFAULT_MAX_DOC_BYTES,
521
+ maxChars = DEFAULT_MAX_DOC_CHARS,
522
+ } = {}) {
523
+ const resolved = path.isAbsolute(filePath) ? filePath : resolveAttachmentPath(filePath, cwd);
524
+ if (!fs.existsSync(resolved)) {
525
+ throw new Error(`Document not found: ${filePath}`);
526
+ }
527
+ const stat = fs.statSync(resolved);
528
+ if (!stat.isFile()) {
529
+ throw new Error(`Not a file: ${filePath}`);
530
+ }
531
+ if (stat.size > maxBytes) {
532
+ throw new Error(
533
+ `Document ${path.basename(resolved)} is ${Math.round(stat.size / 1024)} KB, ` +
534
+ `exceeds ${Math.round(maxBytes / 1024)} KB cap`,
535
+ );
536
+ }
537
+
538
+ const ext = path.extname(resolved).toLowerCase();
539
+ const isPdf = ext === '.pdf';
540
+ const { text, pages, truncated } = isPdf
541
+ ? await extractPdfText(resolved, { maxChars })
542
+ : extractPlainText(resolved, { maxChars });
543
+
544
+ const buffer = fs.readFileSync(resolved);
545
+ const sha256 = createHash('sha256').update(buffer).digest('hex');
546
+
547
+ return {
548
+ id: randomUUID(),
549
+ path: resolved,
550
+ name: path.basename(resolved),
551
+ ext,
552
+ kind: isPdf ? 'pdf' : 'text',
553
+ bytes: stat.size,
554
+ chars: text.length,
555
+ pages,
556
+ truncated,
557
+ sha256,
558
+ text,
559
+ };
560
+ }
561
+
562
+ /**
563
+ * Public metadata for a document (drop the text body — that's for the
564
+ * inline block, not the transcript log).
565
+ */
566
+ export function publicDocumentMetadata(doc) {
567
+ if (!doc) return null;
568
+ return {
569
+ id: doc.id,
570
+ name: doc.name,
571
+ path: doc.path,
572
+ ext: doc.ext,
573
+ kind: doc.kind,
574
+ bytes: doc.bytes,
575
+ chars: doc.chars,
576
+ pages: doc.pages,
577
+ truncated: doc.truncated,
578
+ sha256: doc.sha256,
579
+ };
580
+ }
581
+
582
+ export function documentSummaryLine(doc) {
583
+ if (!doc) return '';
584
+ const size = doc.bytes > 1024
585
+ ? `${Math.round(doc.bytes / 1024)} KB`
586
+ : `${doc.bytes} B`;
587
+ const extra = doc.pages ? `, ${doc.pages} page${doc.pages === 1 ? '' : 's'}` : '';
588
+ const trunc = doc.truncated ? ' · truncated' : '';
589
+ return `${doc.name} (${doc.kind}${extra}, ${size}${trunc})`;
590
+ }
591
+
592
+ /**
593
+ * Batch-prepare document attachments (parse + load) with a per-turn
594
+ * character budget. Returns { instruction (@refs stripped), documents,
595
+ * metadata }. Throws if the combined extracted text exceeds
596
+ * DEFAULT_MAX_TURN_DOC_CHARS.
597
+ */
598
+ export async function prepareDocumentAttachments(input, {
599
+ cwd = process.cwd(),
600
+ extraPaths = [],
601
+ maxDocBytes = envInt('BAHULAM_DOC_MAX_BYTES', DEFAULT_MAX_DOC_BYTES),
602
+ maxDocChars = envInt('BAHULAM_DOC_MAX_CHARS', DEFAULT_MAX_DOC_CHARS),
603
+ maxTurnDocChars = envInt('BAHULAM_DOC_MAX_TURN_CHARS', DEFAULT_MAX_TURN_DOC_CHARS),
604
+ } = {}) {
605
+ const parsed = parseAttachmentReferences(input, { cwd });
606
+ const paths = [
607
+ ...parsed.documents.map(ref => ref.path),
608
+ ...extraPaths.map(p => resolveAttachmentPath(p, cwd)),
609
+ ];
610
+ const uniquePaths = [...new Set(paths)];
611
+ const documents = [];
612
+ for (const p of uniquePaths) {
613
+ documents.push(await loadDocumentAttachment(p, { cwd, maxBytes: maxDocBytes, maxChars: maxDocChars }));
614
+ }
615
+ const totalChars = documents.reduce((sum, d) => sum + (d.chars || 0), 0);
616
+ if (totalChars > maxTurnDocChars) {
617
+ throw new Error(
618
+ `Attached documents total ${totalChars.toLocaleString()} chars, ` +
619
+ `exceeds per-turn cap of ${maxTurnDocChars.toLocaleString()}`,
620
+ );
621
+ }
622
+ return {
623
+ instruction: parsed.instruction || String(input || '').trim(),
624
+ documents,
625
+ metadata: documents.map(publicDocumentMetadata),
626
+ };
627
+ }
628
+
629
+ /**
630
+ * Fold extracted document text into the user turn as a bounded block. The
631
+ * agent sees the content directly — no tool call needed. Format is stable
632
+ * so read_attachment output and inlined output look the same shape to the
633
+ * model.
634
+ */
635
+ export function appendDocumentsToInstruction(instruction, documents) {
636
+ const docs = (documents || []).filter(d => d && d.text);
637
+ if (!docs.length) return String(instruction || '');
638
+ const parts = [String(instruction || '').trim()];
639
+ for (const doc of docs) {
640
+ const meta = `${doc.name}` +
641
+ (doc.pages ? ` · ${doc.pages} page${doc.pages === 1 ? '' : 's'}` : '') +
642
+ (doc.truncated ? ` · truncated to ${doc.chars.toLocaleString()} chars` : '');
643
+ parts.push('');
644
+ parts.push(`[Attached document: ${meta}]`);
645
+ parts.push(doc.text);
646
+ }
647
+ return parts.join('\n');
648
+ }
649
+
364
650
  export function appendVisionAnalysisToInstruction(instruction, analysis) {
365
651
  const summary = String(analysis?.summary || '').trim();
366
652
  if (!summary) return String(instruction || '');
@@ -8,20 +8,33 @@
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
18
  local: 'http://127.0.0.1:8150',
12
- treetop: 'https://codekepler-backend-dev.kindisland-9034322d.eastus.azurecontainerapps.io',
19
+ dev: 'https://codekepler-backend-dev.kindisland-9034322d.eastus.azurecontainerapps.io',
13
20
  production: 'https://codekepler-backend-prod.gentlerock-9816c6b8.centralus.azurecontainerapps.io',
21
+ bundled: 'http://127.0.0.1:0', // sentinel — real URL comes from bundled-runtime.mjs
14
22
  };
15
23
 
16
- // Aliases
17
- BACKEND_URLS.prod = BACKEND_URLS.production;
24
+ // Aliases (backwards compat + convenience)
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
18
28
 
19
29
  const WEB_URLS = {
20
30
  local: 'http://localhost:3100',
21
- treetop: 'https://treetop.codekepler.ai',
31
+ dev: 'https://treetop.codekepler.ai',
22
32
  production: 'https://codekepler.ai',
33
+ bundled: 'http://localhost:3100', // bundled mode reuses local web if user runs it
23
34
  };
24
- WEB_URLS.prod = WEB_URLS.production;
35
+ WEB_URLS.prod = WEB_URLS.production;
36
+ WEB_URLS.treetop = WEB_URLS.dev; // legacy alias
37
+ WEB_URLS.docker = WEB_URLS.local;
25
38
 
26
39
  /**
27
40
  * Resolve the web dashboard URL from environment.