@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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local workspace session store.
|
|
3
|
+
*
|
|
4
|
+
* A local service session grants one local directory to the browser shell and
|
|
5
|
+
* to the remote-agent bridge. If the user opens a single file, the grant root
|
|
6
|
+
* is the file's parent and focus_path records the selected file.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as crypto from 'node:crypto';
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as os from 'node:os';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import {
|
|
14
|
+
localServiceSessionDir,
|
|
15
|
+
localServiceSessionPath,
|
|
16
|
+
localServiceSessionsRoot,
|
|
17
|
+
} from './paths.mjs';
|
|
18
|
+
import { getLocalMachineIdentity } from './machine.mjs';
|
|
19
|
+
|
|
20
|
+
const SESSION_PREFIX = 'lws';
|
|
21
|
+
|
|
22
|
+
export function createLocalAccessToken() {
|
|
23
|
+
return crypto.randomBytes(24).toString('base64url');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function hashLocalAccessToken(token) {
|
|
27
|
+
return crypto.createHash('sha256').update(String(token || '')).digest('hex');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function verifyLocalAccessToken(session, token) {
|
|
31
|
+
if (!session?.token_hash || !token) return false;
|
|
32
|
+
const expected = Buffer.from(session.token_hash, 'hex');
|
|
33
|
+
const actual = Buffer.from(hashLocalAccessToken(token), 'hex');
|
|
34
|
+
return expected.length === actual.length && crypto.timingSafeEqual(expected, actual);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createLocalWorkspaceSession({
|
|
38
|
+
targetPath = process.cwd(),
|
|
39
|
+
cwd = process.cwd(),
|
|
40
|
+
kind = null,
|
|
41
|
+
title = null,
|
|
42
|
+
token = createLocalAccessToken(),
|
|
43
|
+
} = {}) {
|
|
44
|
+
const grant = resolveGrant(targetPath, cwd);
|
|
45
|
+
const machine = getLocalMachineIdentity();
|
|
46
|
+
const now = new Date().toISOString();
|
|
47
|
+
const session = {
|
|
48
|
+
id: createSessionId(),
|
|
49
|
+
product: 'bahulam-local-service',
|
|
50
|
+
machine_id: machine.id,
|
|
51
|
+
machine_hostname: machine.hostname,
|
|
52
|
+
kind: kind || inferSessionKind(grant),
|
|
53
|
+
title: title || defaultSessionTitle(grant),
|
|
54
|
+
status: 'active',
|
|
55
|
+
root_path: grant.rootPath,
|
|
56
|
+
focus_path: grant.focusPath,
|
|
57
|
+
resource_type: grant.resourceType,
|
|
58
|
+
capabilities: inferCapabilities(grant),
|
|
59
|
+
created_at: now,
|
|
60
|
+
updated_at: now,
|
|
61
|
+
token_hash: hashLocalAccessToken(token),
|
|
62
|
+
remote_bridge: {
|
|
63
|
+
status: 'cli_relay',
|
|
64
|
+
contract: 'browser -> local-service -> cli-authenticated remote agent -> local tools',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
writeLocalWorkspaceSession(session);
|
|
69
|
+
return { session: publicSession(session), token };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function writeLocalWorkspaceSession(session) {
|
|
73
|
+
if (!session?.id) throw new Error('session.id is required');
|
|
74
|
+
const dir = localServiceSessionDir(session.id);
|
|
75
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
76
|
+
fs.writeFileSync(localServiceSessionPath(session.id), `${JSON.stringify(session, null, 2)}\n`, {
|
|
77
|
+
mode: 0o600,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function loadLocalWorkspaceSession(sessionId) {
|
|
82
|
+
if (!sessionId) return null;
|
|
83
|
+
const file = localServiceSessionPath(sessionId);
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function listLocalWorkspaceSessions({ limit = 20 } = {}) {
|
|
92
|
+
let names = [];
|
|
93
|
+
try {
|
|
94
|
+
names = fs.readdirSync(localServiceSessionsRoot());
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return names
|
|
100
|
+
.map((name) => loadLocalWorkspaceSession(name))
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.sort((a, b) => String(b.updated_at || '').localeCompare(String(a.updated_at || '')))
|
|
103
|
+
.slice(0, limit)
|
|
104
|
+
.map(publicSession);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function touchLocalWorkspaceSession(sessionId, patch = {}) {
|
|
108
|
+
const session = loadLocalWorkspaceSession(sessionId);
|
|
109
|
+
if (!session) return null;
|
|
110
|
+
const updated = {
|
|
111
|
+
...session,
|
|
112
|
+
...patch,
|
|
113
|
+
updated_at: new Date().toISOString(),
|
|
114
|
+
};
|
|
115
|
+
writeLocalWorkspaceSession(updated);
|
|
116
|
+
return publicSession(updated);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function publicSession(session) {
|
|
120
|
+
if (!session) return null;
|
|
121
|
+
const { token_hash, ...safe } = session;
|
|
122
|
+
return safe;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resolveGrant(inputPath, cwd = process.cwd()) {
|
|
126
|
+
const resolved = expandPath(inputPath || cwd, cwd);
|
|
127
|
+
let stat;
|
|
128
|
+
try {
|
|
129
|
+
stat = fs.statSync(resolved);
|
|
130
|
+
} catch {
|
|
131
|
+
throw new Error(`Path does not exist: ${resolved}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (stat.isDirectory()) {
|
|
135
|
+
return {
|
|
136
|
+
rootPath: fs.realpathSync(resolved),
|
|
137
|
+
focusPath: '',
|
|
138
|
+
resourceType: 'directory',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (stat.isFile()) {
|
|
143
|
+
const parent = fs.realpathSync(path.dirname(resolved));
|
|
144
|
+
return {
|
|
145
|
+
rootPath: parent,
|
|
146
|
+
focusPath: path.basename(resolved),
|
|
147
|
+
resourceType: 'file',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw new Error(`Path is not a file or directory: ${resolved}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function expandPath(inputPath, cwd = process.cwd()) {
|
|
155
|
+
let value = String(inputPath || '').trim();
|
|
156
|
+
if (!value) value = cwd;
|
|
157
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
158
|
+
value = value.slice(1, -1);
|
|
159
|
+
}
|
|
160
|
+
if (value === '~') value = os.homedir();
|
|
161
|
+
else if (value.startsWith('~/')) value = path.join(os.homedir(), value.slice(2));
|
|
162
|
+
if (!path.isAbsolute(value)) value = path.resolve(cwd, value);
|
|
163
|
+
return path.normalize(value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function createSessionId() {
|
|
167
|
+
const stamp = Date.now().toString(36);
|
|
168
|
+
const rand = crypto.randomBytes(5).toString('hex');
|
|
169
|
+
return `${SESSION_PREFIX}_${stamp}_${rand}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function defaultSessionTitle(grant) {
|
|
173
|
+
if (grant.focusPath) return grant.focusPath;
|
|
174
|
+
return path.basename(grant.rootPath) || grant.rootPath;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function inferSessionKind(grant) {
|
|
178
|
+
if (grant.resourceType === 'file' && isDocumentLike(grant.focusPath)) return 'documents';
|
|
179
|
+
if (looksLikeCodeWorkspace(grant.rootPath)) return 'coding';
|
|
180
|
+
return 'workspace';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function inferCapabilities(grant) {
|
|
184
|
+
const caps = new Set([
|
|
185
|
+
'local_files',
|
|
186
|
+
'local_shell',
|
|
187
|
+
'browser_workspace',
|
|
188
|
+
'remote_agent_bridge',
|
|
189
|
+
]);
|
|
190
|
+
if (grant.resourceType === 'file' && isDocumentLike(grant.focusPath)) caps.add('documents');
|
|
191
|
+
if (looksLikeCodeWorkspace(grant.rootPath)) caps.add('coding');
|
|
192
|
+
return [...caps];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isDocumentLike(fileName) {
|
|
196
|
+
return /\.(?:pdf|docx?|xlsx?|pptx?|csv|tsv|rtf|odt|ods)$/i.test(String(fileName || ''));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function looksLikeCodeWorkspace(rootPath) {
|
|
200
|
+
const markers = [
|
|
201
|
+
'.git',
|
|
202
|
+
'package.json',
|
|
203
|
+
'pyproject.toml',
|
|
204
|
+
'requirements.txt',
|
|
205
|
+
'Cargo.toml',
|
|
206
|
+
'go.mod',
|
|
207
|
+
'pom.xml',
|
|
208
|
+
'Makefile',
|
|
209
|
+
'Dockerfile',
|
|
210
|
+
'AGENTS.md',
|
|
211
|
+
'CLAUDE.md',
|
|
212
|
+
'BAHULAM.md',
|
|
213
|
+
];
|
|
214
|
+
return markers.some((marker) => {
|
|
215
|
+
try {
|
|
216
|
+
return fs.existsSync(path.join(rootPath, marker));
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
package/src/mcp/client.mjs
CHANGED
|
@@ -78,7 +78,7 @@ export class McpClient {
|
|
|
78
78
|
const initResult = await this._request('initialize', {
|
|
79
79
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
80
80
|
capabilities: {},
|
|
81
|
-
clientInfo: { name: '
|
|
81
|
+
clientInfo: { name: 'bahulam-code', version: '2.0.0' },
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
this.serverInfo = initResult;
|
|
@@ -115,7 +115,7 @@ export class McpClient {
|
|
|
115
115
|
this.serverInfo = await this.transport.request('initialize', {
|
|
116
116
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
117
117
|
capabilities: {},
|
|
118
|
-
clientInfo: { name: '
|
|
118
|
+
clientInfo: { name: 'bahulam-code', version: '2.0.0' },
|
|
119
119
|
});
|
|
120
120
|
return this.serverInfo;
|
|
121
121
|
}
|
|
@@ -130,7 +130,7 @@ export class McpClient {
|
|
|
130
130
|
this.serverInfo = await this.transport.request('initialize', {
|
|
131
131
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
132
132
|
capabilities: {},
|
|
133
|
-
clientInfo: { name: '
|
|
133
|
+
clientInfo: { name: 'bahulam-code', version: '2.0.0' },
|
|
134
134
|
});
|
|
135
135
|
return this.serverInfo;
|
|
136
136
|
}
|
|
@@ -139,7 +139,7 @@ export class McpClient {
|
|
|
139
139
|
const result = await this._transportRequest('initialize', {
|
|
140
140
|
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
141
141
|
capabilities: {},
|
|
142
|
-
clientInfo: { name: '
|
|
142
|
+
clientInfo: { name: 'bahulam-code', version: '2.0.0' },
|
|
143
143
|
});
|
|
144
144
|
this.serverInfo = result;
|
|
145
145
|
return result;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Prints a non-blocking summary of the runtime environment before the REPL
|
|
5
5
|
* starts so the user can see what is and is not aligned:
|
|
6
6
|
*
|
|
7
|
-
* 🔭 Bahulam Code
|
|
7
|
+
* 🔭 Bahulam Code v0.1.7 · initializing
|
|
8
8
|
*
|
|
9
9
|
* [✓] Auth token
|
|
10
10
|
* [✓] OpenRouter key
|
|
@@ -334,7 +334,7 @@ function formatRow(check) {
|
|
|
334
334
|
* collected check results.
|
|
335
335
|
*
|
|
336
336
|
* @param {object} opts
|
|
337
|
-
* @param {object} opts.auth —
|
|
337
|
+
* @param {object} opts.auth — BahulamAuth instance
|
|
338
338
|
* @param {string} opts.cwd — working directory
|
|
339
339
|
* @param {string} opts.version — package version string
|
|
340
340
|
* @param {boolean} [opts.silent] — if true, do not write (useful for tests)
|
package/src/terminal/agents.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { c } from './ansi.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import { BahulamStreamClient } from '../core/stream-client.mjs';
|
|
15
15
|
|
|
16
16
|
// ── Agent Definitions ────────────────────────────────────────
|
|
17
17
|
|
|
@@ -296,7 +296,7 @@ export async function runAgentDefinition(agentDefinition, instruction, ctx, sess
|
|
|
296
296
|
projectRoot: execContext.project_root || null,
|
|
297
297
|
});
|
|
298
298
|
|
|
299
|
-
const client = new
|
|
299
|
+
const client = new BahulamStreamClient({
|
|
300
300
|
baseUrl: creds.backendUrl,
|
|
301
301
|
token: creds.token,
|
|
302
302
|
toolExecutor,
|
|
@@ -202,11 +202,7 @@ export async function runHistoryCommand(args = []) {
|
|
|
202
202
|
process.stdout.write(formatHistoryReport(history, limit));
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
export async function _runDashboardCommand_REMOVED() { /* see pulse/cli.js */ }
|
|
208
|
-
|
|
209
|
-
export async function _OLD_runDashboardCommand(args = []) {
|
|
205
|
+
export async function runDashboardCommand(args = []) {
|
|
210
206
|
const requestedPort = parseNumber(readOption(args, '--port', '4318'), 4318);
|
|
211
207
|
const host = readOption(args, '--host', '127.0.0.1') || '127.0.0.1';
|
|
212
208
|
const shouldOpen = !hasFlag(args, '--no-open');
|
|
@@ -272,7 +268,7 @@ export async function _OLD_runDashboardCommand(args = []) {
|
|
|
272
268
|
const actualPort = typeof address === 'object' && address ? address.port : requestedPort;
|
|
273
269
|
const url = `http://${host}:${actualPort}`;
|
|
274
270
|
|
|
275
|
-
process.stderr.write(`\x1b[
|
|
271
|
+
process.stderr.write(`\x1b[36mBahulam dashboard\x1b[0m ${url}\n`);
|
|
276
272
|
process.stderr.write(`\x1b[2mReading local analytics from ${getStorePaths().bahulamDir}\x1b[0m\n`);
|
|
277
273
|
process.stderr.write(`\x1b[2mPress Ctrl+C to stop the dashboard server.\x1b[0m\n`);
|
|
278
274
|
|
package/src/terminal/ansi.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ANSI Terminal Renderer — cursor control, box drawing, status bars.
|
|
3
3
|
*
|
|
4
4
|
* Color helpers (the `c` object) now route through the semantic palette
|
|
5
|
-
* (`src/ui/palette.mjs`) so the entire CLI honors the
|
|
5
|
+
* (`src/ui/palette.mjs`) so the entire CLI honors the Bahulam brand and
|
|
6
6
|
* tier fallbacks (truecolor, ansi256, ansi16, none) without touching
|
|
7
7
|
* each call site. Hot-swap-friendly: external semantics like `c.red`,
|
|
8
8
|
* `c.bold`, `c.cyan` are preserved as the legacy contract; new code
|
|
@@ -35,7 +35,7 @@ export const cursor = {
|
|
|
35
35
|
// ── Colors ──
|
|
36
36
|
// Legacy color names re-mapped onto semantic palette tokens. The CLI's
|
|
37
37
|
// branding is centralized in palette.mjs; this object is preserved only
|
|
38
|
-
// so existing imports keep compiling. Internal
|
|
38
|
+
// so existing imports keep compiling. Internal Bahulam color choices are
|
|
39
39
|
// documented next to each mapping for the next code review.
|
|
40
40
|
|
|
41
41
|
const identity = (s) => String(s ?? '');
|
|
@@ -132,12 +132,20 @@ export function inPlace(text) {
|
|
|
132
132
|
}
|
|
133
133
|
if (text) {
|
|
134
134
|
write(text + '\n');
|
|
135
|
-
_lastLineCount = text
|
|
135
|
+
_lastLineCount = physicalLineCount(text);
|
|
136
136
|
} else {
|
|
137
137
|
_lastLineCount = 0;
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
function physicalLineCount(text) {
|
|
142
|
+
const width = Math.max(1, process.stderr.columns || process.stdout.columns || 80);
|
|
143
|
+
return String(text || '').split('\n').reduce((total, line) => {
|
|
144
|
+
const cells = visibleWidth(line);
|
|
145
|
+
return total + Math.max(1, Math.ceil(cells / width));
|
|
146
|
+
}, 0);
|
|
147
|
+
}
|
|
148
|
+
|
|
141
149
|
// ── Status Bar (persistent bottom) ──
|
|
142
150
|
|
|
143
151
|
export function statusBar(parts) {
|
package/src/terminal/main.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import * as fs from 'node:fs';
|
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import { startTerminalRepl } from './repl.mjs';
|
|
10
10
|
import {
|
|
11
|
+
runDashboardCommand,
|
|
11
12
|
runSessionsCommand,
|
|
12
13
|
runStatsCommand,
|
|
13
14
|
runHistoryCommand,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
import { parseArgs } from '../config/cli-args.mjs';
|
|
16
17
|
import * as telemetry from '../telemetry/index.mjs';
|
|
17
18
|
import { bahulamHome } from '../core/paths.mjs';
|
|
18
|
-
import {
|
|
19
|
+
import { BahulamAuth as Auth } from '../auth/bahulam-auth.mjs';
|
|
19
20
|
|
|
20
21
|
// ── Subcommands ──
|
|
21
22
|
|
|
@@ -123,16 +124,29 @@ async function main() {
|
|
|
123
124
|
} catch {}
|
|
124
125
|
|
|
125
126
|
if (subcommand === 'dashboard') {
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
127
|
+
// Try to open the live daemon's HTTP dashboard first
|
|
128
|
+
try {
|
|
129
|
+
const { findSessionForCwd } = await import('../daemon/daemonize.mjs');
|
|
130
|
+
const sid = await findSessionForCwd(process.cwd());
|
|
131
|
+
if (sid) {
|
|
132
|
+
const { daemonSessionDir } = await import('../core/paths.mjs');
|
|
133
|
+
const metaPath = path.join(daemonSessionDir(sid), 'meta.json');
|
|
134
|
+
if (fs.existsSync(metaPath)) {
|
|
135
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
136
|
+
if (meta.dashboard_url) {
|
|
137
|
+
process.stderr.write(`\x1b[2mOpening dashboard for session ${sid}…\x1b[0m\n`);
|
|
138
|
+
const { execSync } = await import('node:child_process');
|
|
139
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
140
|
+
: process.platform === 'win32' ? 'start'
|
|
141
|
+
: 'xdg-open';
|
|
142
|
+
execSync(`${cmd} "${meta.dashboard_url}"`, { stdio: 'ignore', timeout: 3000 });
|
|
143
|
+
process.exit(0);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch {}
|
|
149
|
+
await runDashboardCommand(subcommandArgs);
|
|
136
150
|
return;
|
|
137
151
|
}
|
|
138
152
|
|
|
@@ -169,8 +183,8 @@ async function main() {
|
|
|
169
183
|
}
|
|
170
184
|
|
|
171
185
|
if (subcommand === 'login') {
|
|
172
|
-
const {
|
|
173
|
-
const auth = new
|
|
186
|
+
const { BahulamAuth } = await import('../auth/bahulam-auth.mjs');
|
|
187
|
+
const auth = new BahulamAuth();
|
|
174
188
|
try {
|
|
175
189
|
telemetry.track('login_shown', { method: 'cli_subcommand' });
|
|
176
190
|
await auth.login();
|
|
@@ -184,8 +198,8 @@ async function main() {
|
|
|
184
198
|
}
|
|
185
199
|
|
|
186
200
|
if (subcommand === 'logout') {
|
|
187
|
-
const {
|
|
188
|
-
const auth = new
|
|
201
|
+
const { BahulamAuth } = await import('../auth/bahulam-auth.mjs');
|
|
202
|
+
const auth = new BahulamAuth();
|
|
189
203
|
const success = auth.logout();
|
|
190
204
|
if (success) {
|
|
191
205
|
process.stderr.write('\x1b[32m✓ Signed out. Credentials cleared.\x1b[0m\n');
|
|
@@ -227,6 +241,7 @@ async function main() {
|
|
|
227
241
|
Attach an image via the vision analysis pipeline
|
|
228
242
|
bahulam --resume Resume last conversation
|
|
229
243
|
bahulam dashboard Open analytics dashboard
|
|
244
|
+
bahulam workspace open [path] Open a local browser workspace
|
|
230
245
|
bahulam login Sign in via browser
|
|
231
246
|
bahulam logout Sign out and clear credentials
|
|
232
247
|
bahulam init Scaffold .bahulam config, memory, hooks, tasks
|
|
@@ -240,6 +255,11 @@ async function main() {
|
|
|
240
255
|
bahulam remote enable Enable relay connection
|
|
241
256
|
bahulam remote disable Disable relay connection (kill switch)
|
|
242
257
|
|
|
258
|
+
\x1b[1mLocal Workspaces:\x1b[0m
|
|
259
|
+
bahulam workspace open [path] Start localhost workspace service
|
|
260
|
+
bahulam workspace list List recent local workspace sessions
|
|
261
|
+
bahulam local open [path] Alias for workspace open
|
|
262
|
+
|
|
243
263
|
\x1b[1mAnalytics:\x1b[0m
|
|
244
264
|
bahulam sessions List recent local sessions
|
|
245
265
|
bahulam stats Show aggregate local session stats
|
|
@@ -261,7 +281,7 @@ async function main() {
|
|
|
261
281
|
/new Start a new session
|
|
262
282
|
/clear Clear conversation history
|
|
263
283
|
/safety Show safety guardrail status
|
|
264
|
-
/
|
|
284
|
+
/approvals clear Revoke all session auto-approvals
|
|
265
285
|
/explore <query> Spawn read-only codebase explorer
|
|
266
286
|
/review <query> Spawn code review agent
|
|
267
287
|
/architect <query> Spawn architecture planning agent
|
|
@@ -286,7 +306,7 @@ async function main() {
|
|
|
286
306
|
KEPLER_RECONNECT_MAX_ELAPSED_MS
|
|
287
307
|
Max reconnect window for dropped streams
|
|
288
308
|
BAHULAM_TTY_MODE=stable Scrollback-safe transcript if fixed dock redraws leak
|
|
289
|
-
|
|
309
|
+
BAHULAM_BLOCK_SEPARATOR Tool/content separator: space, dotted, or off
|
|
290
310
|
|
|
291
311
|
\x1b[2mDocs: https://bahulam.ai\x1b[0m
|
|
292
312
|
`);
|
|
@@ -331,6 +351,12 @@ async function main() {
|
|
|
331
351
|
return;
|
|
332
352
|
}
|
|
333
353
|
|
|
354
|
+
if (subcommand === 'workspace' || subcommand === 'workspaces' || subcommand === 'local') {
|
|
355
|
+
const { runLocalWorkspaceCommand } = await import('../local-service/command.mjs');
|
|
356
|
+
await runLocalWorkspaceCommand(subcommandArgs, { cwd: process.cwd() });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
334
360
|
// auto-daemon spawn.
|
|
335
361
|
// bahulam daemonize [prompt] → fork a detached bahulam child with
|
|
336
362
|
// the socket server up. Parent waits briefly for the child's session
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { c } from './ansi.mjs';
|
|
2
|
+
|
|
3
|
+
export const MODEL_CATEGORY_ORDER = [
|
|
4
|
+
'text',
|
|
5
|
+
'image_analysis',
|
|
6
|
+
'image_generation',
|
|
7
|
+
'image',
|
|
8
|
+
'multimodal',
|
|
9
|
+
'embedding',
|
|
10
|
+
'audio',
|
|
11
|
+
'video',
|
|
12
|
+
'other',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function modelCategory(model) {
|
|
16
|
+
const category = String(model?.category || 'text').trim().toLowerCase();
|
|
17
|
+
return category === 'chat' ? 'text' : (category || 'text');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isImageGenerationModel(model) {
|
|
21
|
+
const text = `${model?.id || ''} ${model?.label || ''}`.toLowerCase();
|
|
22
|
+
return (
|
|
23
|
+
text.includes('image generation') ||
|
|
24
|
+
text.includes('generate image') ||
|
|
25
|
+
text.includes('gemini-3-pro-image') ||
|
|
26
|
+
text.includes('nano banana')
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function modelDisplayCategory(model) {
|
|
31
|
+
const category = modelCategory(model);
|
|
32
|
+
if (category === 'image') {
|
|
33
|
+
return isImageGenerationModel(model) ? 'image_generation' : 'image_analysis';
|
|
34
|
+
}
|
|
35
|
+
return category;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function normalizeCatalogCategory(value) {
|
|
39
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
if (raw === 'chat') return 'text';
|
|
42
|
+
if (raw === 'image-analysis' || raw === 'vision') return 'image_analysis';
|
|
43
|
+
if (raw === 'image-generation' || raw === 'image-gen') return 'image_generation';
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function modelMatchesCatalogFilter(model, filter) {
|
|
48
|
+
if (!filter) return true;
|
|
49
|
+
if (filter === 'image') return modelCategory(model) === 'image';
|
|
50
|
+
return modelDisplayCategory(model) === filter || modelCategory(model) === filter;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function modelCategoryLabel(category, { plural = false } = {}) {
|
|
54
|
+
const labels = {
|
|
55
|
+
text: 'text',
|
|
56
|
+
image: 'image',
|
|
57
|
+
image_analysis: 'image analysis',
|
|
58
|
+
image_generation: 'image generation',
|
|
59
|
+
embedding: 'embedding',
|
|
60
|
+
audio: 'audio',
|
|
61
|
+
video: 'video',
|
|
62
|
+
other: 'other',
|
|
63
|
+
multimodal: 'multimodal',
|
|
64
|
+
};
|
|
65
|
+
const base = labels[String(category || '').trim().toLowerCase()] || 'other';
|
|
66
|
+
return plural ? `${base} models` : base;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function categoryColor(category) {
|
|
70
|
+
switch (String(category || '').trim().toLowerCase()) {
|
|
71
|
+
case 'text':
|
|
72
|
+
return c.brand;
|
|
73
|
+
case 'image':
|
|
74
|
+
case 'image_analysis':
|
|
75
|
+
return c.magenta;
|
|
76
|
+
case 'image_generation':
|
|
77
|
+
return c.yellow;
|
|
78
|
+
case 'multimodal':
|
|
79
|
+
return c.cyanBold;
|
|
80
|
+
case 'embedding':
|
|
81
|
+
return c.green;
|
|
82
|
+
case 'audio':
|
|
83
|
+
return c.cyanRegular;
|
|
84
|
+
case 'video':
|
|
85
|
+
return c.red;
|
|
86
|
+
default:
|
|
87
|
+
return c.gray;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function formatCategoryBadge(category, { plural = false } = {}) {
|
|
92
|
+
const normalized = String(category || 'text').trim().toLowerCase() || 'text';
|
|
93
|
+
return categoryColor(normalized)(`[${modelCategoryLabel(normalized, { plural })}]`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function formatCategoryHeading(category, count = null) {
|
|
97
|
+
const label = modelCategoryLabel(category, { plural: true });
|
|
98
|
+
const countText = Number.isFinite(Number(count)) ? ` ${c.dim(`(${count})`)}` : '';
|
|
99
|
+
return `${categoryColor(category)(c.bold(label))}${countText}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function cacheProfileLabel(value) {
|
|
103
|
+
if (!value) return '';
|
|
104
|
+
try {
|
|
105
|
+
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
|
106
|
+
if (parsed?.type === 'prefix_hash') return 'prefix cache';
|
|
107
|
+
} catch {
|
|
108
|
+
// Fall through to the generic cache hint.
|
|
109
|
+
}
|
|
110
|
+
return 'cache';
|
|
111
|
+
}
|
|
@@ -11,6 +11,8 @@ import * as path from 'node:path';
|
|
|
11
11
|
import { c, stripAnsi, formatElapsed, inPlace } from './ansi.mjs';
|
|
12
12
|
import * as rqueue from '../ui/render-queue.mjs';
|
|
13
13
|
|
|
14
|
+
const ANSI_RESET = '\x1b[0m';
|
|
15
|
+
|
|
14
16
|
// Transient one-line status writer that is safe under the render queue.
|
|
15
17
|
// Raw inPlace() writes get REDIRECTED into transcript content when the
|
|
16
18
|
// queue is active (cursor codes stripped) — that leaked one line per
|
|
@@ -102,7 +104,7 @@ export function fitAnsiLine(text, maxColumns) {
|
|
|
102
104
|
out += value[i];
|
|
103
105
|
visible++;
|
|
104
106
|
}
|
|
105
|
-
return `${out}${c.dim('…')}`;
|
|
107
|
+
return `${out}${ANSI_RESET}${c.dim('…')}`;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
// ── PRD-068 §5.14 helpers ────────────────────────────────────────────
|
|
@@ -284,6 +286,7 @@ export function normalizeResumableSession(s) {
|
|
|
284
286
|
resumeSummary: s.resumeSummary || null, // latest resume_summary checkpoint metadata
|
|
285
287
|
models: Array.isArray(s.models) ? s.models : [],
|
|
286
288
|
modelLimits: s.modelLimits && typeof s.modelLimits === 'object' ? s.modelLimits : {},
|
|
289
|
+
subAgentModels: s.subAgentModels && typeof s.subAgentModels === 'object' ? s.subAgentModels : {},
|
|
287
290
|
costUsd: typeof s.costUsd === 'number' ? s.costUsd : 0,
|
|
288
291
|
partial: !!s.partial, // true if the transcript file was partially malformed
|
|
289
292
|
source: 'transcript',
|