@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 +8 -1
- package/package.json +8 -3
- package/pulse/app/activity/page.tsx +1 -1
- package/pulse/app/globals.css +3 -3
- package/pulse/app/help/page.tsx +10 -10
- package/pulse/app/layout.tsx +2 -2
- package/pulse/app/memory/page.tsx +1 -1
- package/pulse/app/overview-client.tsx +1 -1
- package/pulse/app/page.tsx +2 -2
- package/pulse/app/plans/page.tsx +1 -1
- package/pulse/app/sessions/page.tsx +1 -1
- package/pulse/app/settings/page.tsx +1 -1
- package/pulse/app/tools/page.tsx +1 -1
- package/pulse/cli.js +8 -8
- package/pulse/components/layout/sidebar.tsx +2 -2
- package/pulse/package.json +2 -2
- package/src/agents/workflow_scaffold.mjs +1 -1
- package/src/core/attachments.mjs +287 -1
- package/src/core/backend-url.mjs +18 -5
- package/src/core/bundled-runtime.mjs +379 -0
- package/src/core/headless.mjs +1 -1
- package/src/core/settings-sync.mjs +1 -1
- package/src/core/stream-client.mjs +122 -3
- package/src/core/tool-executor.mjs +10 -3
- package/src/index.mjs +16 -16
- package/src/onboarding/preflight.mjs +14 -0
- package/src/terminal/main.mjs +19 -19
- package/src/terminal/repl-explore.mjs +9 -0
- package/src/terminal/repl.mjs +122 -11
- package/src/terminal/skills.mjs +1 -1
- package/src/tools/project-overview.mjs +66 -4
- package/src/ui/banner.mjs +5 -5
- package/src/ui/input-dock.mjs +76 -18
- package/src/ui/palette.mjs +2 -2
- package/src/ui/tool-card.mjs +23 -3
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
|
|
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.
|
|
4
|
-
"description": "Bahulam Code
|
|
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
|
|
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} />
|
package/pulse/app/globals.css
CHANGED
|
@@ -86,9 +86,9 @@
|
|
|
86
86
|
--radius-lg: var(--radius);
|
|
87
87
|
--radius-xl: calc(var(--radius) + 4px);
|
|
88
88
|
|
|
89
|
-
/*
|
|
90
|
-
--color-
|
|
91
|
-
--color-
|
|
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;
|
package/pulse/app/help/page.tsx
CHANGED
|
@@ -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: '
|
|
6
|
-
{ cmd: '
|
|
7
|
-
{ cmd: '
|
|
8
|
-
{ cmd: '
|
|
9
|
-
{ cmd: '
|
|
10
|
-
{ cmd: '
|
|
11
|
-
{ cmd: '
|
|
12
|
-
{ cmd: '
|
|
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
|
|
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="
|
|
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>
|
package/pulse/app/layout.tsx
CHANGED
|
@@ -20,8 +20,8 @@ const pressStart2P = Press_Start_2P({
|
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
export const metadata: Metadata = {
|
|
23
|
-
title: '
|
|
24
|
-
description: 'Real-time analytics for your
|
|
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="
|
|
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
|
|
384
|
+
<CardDescription>Your latest Bahulam Code sessions</CardDescription>
|
|
385
385
|
</CardHeader>
|
|
386
386
|
<CardContent>
|
|
387
387
|
<OverviewConversationTable sessions={sessions} />
|
package/pulse/app/page.tsx
CHANGED
|
@@ -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="
|
|
9
|
-
subtitle="Real-time analytics for your
|
|
8
|
+
title="Bahulam Pulse"
|
|
9
|
+
subtitle="Real-time analytics for your Bahulam Code agent sessions"
|
|
10
10
|
/>
|
|
11
11
|
<OverviewClient />
|
|
12
12
|
</div>
|
package/pulse/app/plans/page.tsx
CHANGED
|
@@ -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="
|
|
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="
|
|
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="
|
|
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 && (
|
package/pulse/app/tools/page.tsx
CHANGED
|
@@ -201,7 +201,7 @@ export default function ToolsPage() {
|
|
|
201
201
|
{data.versions.length > 0 && (
|
|
202
202
|
<Card>
|
|
203
203
|
<CardHeader>
|
|
204
|
-
<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
|
-
*
|
|
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 —
|
|
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}
|
|
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: '
|
|
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
|
|
128
|
+
const configDir = resolveConfigDir()
|
|
129
129
|
const env = {
|
|
130
130
|
...process.env,
|
|
131
131
|
PORT: String(port),
|
|
132
|
-
BAHULAM_CONFIG_DIR:
|
|
133
|
-
KEPLER_CONFIG_DIR:
|
|
134
|
-
CLAUDE_CONFIG_DIR:
|
|
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
|
-
|
|
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
|
-
|
|
144
|
+
Bahulam Pulse
|
|
145
145
|
</span>
|
|
146
146
|
)}
|
|
147
147
|
<button
|
package/pulse/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "bahulam-pulse",
|
|
3
3
|
"version": "1.0.1",
|
|
4
4
|
"private": true,
|
|
5
|
-
"description": "
|
|
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:
|
|
144
|
+
'apiVersion: bahulam.workflow/v1',
|
|
145
145
|
'kind: MultiWorkflow',
|
|
146
146
|
'metadata:',
|
|
147
147
|
` name: ${yamlQuote(name)}`,
|
package/src/core/attachments.mjs
CHANGED
|
@@ -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 || '');
|
package/src/core/backend-url.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|