@bahulam/code 0.1.5 → 0.1.7
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 +10 -17
- package/package.json +9 -9
- package/src/agents/loader.mjs +1 -1
- package/src/agents/workflow_loader.mjs +2 -2
- package/src/auth/tarang-auth.mjs +3 -2
- package/src/config/model-catalog-default.json +4 -3
- package/src/config/model-catalog.mjs +2 -0
- package/src/context/prose-chunker.mjs +162 -5
- package/src/core/attachments.mjs +65 -3
- package/src/core/bundled-runtime.mjs +1 -1
- package/src/core/headless.mjs +14 -0
- package/src/core/jsonl-writer.mjs +3 -3
- package/src/core/lint-resolver.mjs +472 -0
- package/src/core/local-agent.mjs +15 -4
- package/src/core/local-store.mjs +4 -0
- package/src/core/output-filter.mjs +21 -25
- package/src/core/pricing.mjs +10 -3
- package/src/core/stream-client.mjs +112 -16
- package/src/core/tool-executor.mjs +123 -31
- 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/onboarding/preflight.mjs +1 -1
- package/src/terminal/analytics.mjs +2 -6
- package/src/terminal/ansi.mjs +11 -3
- package/src/terminal/main.mjs +37 -11
- 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 +35 -5
- package/src/terminal/repl-state.mjs +12 -0
- package/src/terminal/repl.mjs +255 -55
- package/src/terminal/tool-display.mjs +32 -0
- package/src/terminal/watch-state.mjs +118 -0
- 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 +5 -2
- package/src/tools/registry.mjs +4 -0
- package/src/ui/icons.mjs +2 -0
- package/src/ui/palette.mjs +1 -1
- package/src/ui/slash-commands.mjs +4 -6
- package/src/ui/tool-card.mjs +7 -3
- 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
|
+
}
|
|
@@ -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
|
|
@@ -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,
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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',
|
|
@@ -8,8 +8,15 @@
|
|
|
8
8
|
* mode on, redraw in place with cursor-up + erase-down, restore on exit.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { c } from './ansi.mjs';
|
|
11
|
+
import { c, stripAnsi } from './ansi.mjs';
|
|
12
12
|
import { fitAnsiLine, writeOverlayFrame, eraseOverlayFrame } from './repl-format.mjs';
|
|
13
|
+
import {
|
|
14
|
+
cacheProfileLabel,
|
|
15
|
+
formatCategoryBadge,
|
|
16
|
+
isImageGenerationModel,
|
|
17
|
+
modelCategory,
|
|
18
|
+
modelDisplayCategory,
|
|
19
|
+
} from './model-catalog-display.mjs';
|
|
13
20
|
|
|
14
21
|
const DEFAULT_SENTINEL = '__default__';
|
|
15
22
|
|
|
@@ -20,10 +27,33 @@ function creditBadge(row) {
|
|
|
20
27
|
return `~${credits < 10 ? credits.toFixed(1) : String(Math.round(credits))} cr/M`;
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
function formatTokenLimit(value, label) {
|
|
31
|
+
const n = Number(value);
|
|
32
|
+
if (!Number.isFinite(n) || n <= 0) return '';
|
|
33
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, '')}M ${label}`;
|
|
34
|
+
if (n >= 1_000) return `${Math.round(n / 1_000)}K ${label}`;
|
|
35
|
+
return `${Math.round(n)} ${label}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function optionRowsForRole(catalog, row) {
|
|
39
|
+
const curated = (catalog || []).filter(m => m?.harness_validated && m?.id);
|
|
40
|
+
const group = String(row?.optionGroup || 'text').toLowerCase();
|
|
41
|
+
if (group === 'image_analysis') {
|
|
42
|
+
return curated.filter(m => (
|
|
43
|
+
['image', 'multimodal'].includes(modelCategory(m))
|
|
44
|
+
&& !isImageGenerationModel(m)
|
|
45
|
+
));
|
|
46
|
+
}
|
|
47
|
+
if (group === 'image_generation') {
|
|
48
|
+
return curated.filter(m => modelCategory(m) === 'image' && isImageGenerationModel(m));
|
|
49
|
+
}
|
|
50
|
+
return curated.filter(m => ['text', 'chat'].includes(modelCategory(m)));
|
|
51
|
+
}
|
|
52
|
+
|
|
23
53
|
/**
|
|
24
54
|
* @param {object} opts
|
|
25
55
|
* @param {object|null} opts.rl readline instance to pause/resume
|
|
26
|
-
* @param {Array} opts.roles [{ role, label, current, defaultLabel }]
|
|
56
|
+
* @param {Array} opts.roles [{ role, label, description, current, defaultLabel, optionGroup }]
|
|
27
57
|
* @param {Array} opts.catalog raw /api/models rows (may be empty)
|
|
28
58
|
* @param {Array} [opts.fallbackIds] model ids to cycle when no curated catalog
|
|
29
59
|
* @param {string} [opts.unavailableNote] why the catalog is missing (shown in header)
|
|
@@ -33,25 +63,24 @@ export async function pickModelOverridesForm({ rl, roles, catalog, fallbackIds,
|
|
|
33
63
|
if (!process.stdin.isTTY) return null;
|
|
34
64
|
if (rl) rl.pause();
|
|
35
65
|
|
|
36
|
-
const
|
|
37
|
-
const usingFallback =
|
|
38
|
-
const
|
|
39
|
-
? [...new Set((fallbackIds || []).filter(Boolean))]
|
|
40
|
-
: curated.map(m => m.id);
|
|
41
|
-
const baseOptions = [DEFAULT_SENTINEL, ...optionIds];
|
|
42
|
-
const byId = new Map(curated.map(m => [m.id, m]));
|
|
66
|
+
const catalogRows = (catalog || []).filter(m => m?.harness_validated && m?.id);
|
|
67
|
+
const usingFallback = catalogRows.length === 0;
|
|
68
|
+
const byId = new Map(catalogRows.map(m => [m.id, m]));
|
|
43
69
|
|
|
44
70
|
// Per-row option list; a current override that isn't in the curated list
|
|
45
71
|
// is appended so it stays visible and selectable.
|
|
46
72
|
const rows = roles.map(r => {
|
|
47
|
-
|
|
73
|
+
const optionIds = usingFallback
|
|
74
|
+
? [...new Set((fallbackIds || []).filter(Boolean))]
|
|
75
|
+
: optionRowsForRole(catalogRows, r).map(m => m.id);
|
|
76
|
+
let opts = [DEFAULT_SENTINEL, ...optionIds];
|
|
48
77
|
let idx = 0;
|
|
49
78
|
if (r.current) {
|
|
50
|
-
const found =
|
|
79
|
+
const found = opts.indexOf(r.current);
|
|
51
80
|
if (found >= 0) {
|
|
52
81
|
idx = found;
|
|
53
82
|
} else {
|
|
54
|
-
opts = [...
|
|
83
|
+
opts = [...opts, r.current];
|
|
55
84
|
idx = opts.length - 1;
|
|
56
85
|
}
|
|
57
86
|
}
|
|
@@ -76,8 +105,38 @@ export async function pickModelOverridesForm({ rl, roles, catalog, fallbackIds,
|
|
|
76
105
|
return `${c.brand(value)}${badge ? ` ${c.dim(badge)}` : ''}${flag}`;
|
|
77
106
|
};
|
|
78
107
|
|
|
108
|
+
const valueDescription = (row) => {
|
|
109
|
+
const value = row.opts[row.idx];
|
|
110
|
+
if (value === DEFAULT_SENTINEL) {
|
|
111
|
+
return row.description || 'backend default for this role';
|
|
112
|
+
}
|
|
113
|
+
const meta = byId.get(value);
|
|
114
|
+
if (!meta) {
|
|
115
|
+
return usingFallback ? 'backend configured model' : 'custom override outside curated catalog';
|
|
116
|
+
}
|
|
117
|
+
const label = String(meta.label || '').trim();
|
|
118
|
+
const parts = [];
|
|
119
|
+
if (label && label !== value) parts.push(c.white(label));
|
|
120
|
+
if (meta.provider) parts.push(c.dim(String(meta.provider)));
|
|
121
|
+
const category = modelDisplayCategory(meta);
|
|
122
|
+
if (category) parts.push(formatCategoryBadge(category));
|
|
123
|
+
const context = formatTokenLimit(meta.context_length, 'ctx');
|
|
124
|
+
if (context) parts.push(c.dim(context));
|
|
125
|
+
const output = formatTokenLimit(meta.max_output, 'out');
|
|
126
|
+
if (output) parts.push(c.dim(output));
|
|
127
|
+
if (meta.supports_tools) parts.push(c.green('tools'));
|
|
128
|
+
if (meta.supports_reasoning) parts.push(c.yellow('reasoning'));
|
|
129
|
+
const cache = cacheProfileLabel(meta.cache_profile);
|
|
130
|
+
if (cache) parts.push(c.cyan(cache));
|
|
131
|
+
return parts.join(c.dim(' · '));
|
|
132
|
+
};
|
|
133
|
+
|
|
79
134
|
const render = () => {
|
|
80
135
|
const cols = Math.max(60, process.stderr.columns || 120);
|
|
136
|
+
const labelWidth = Math.min(
|
|
137
|
+
18,
|
|
138
|
+
Math.max(14, ...rows.map(row => stripAnsi(String(row.label || row.role)).length)),
|
|
139
|
+
);
|
|
81
140
|
const lines = [];
|
|
82
141
|
lines.push(` ${c.bold('Models')} ${c.dim('· session overrides · curated platform catalog')}`);
|
|
83
142
|
if (usingFallback) {
|
|
@@ -87,9 +146,17 @@ export async function pickModelOverridesForm({ rl, roles, catalog, fallbackIds,
|
|
|
87
146
|
lines.push('');
|
|
88
147
|
rows.forEach((row, i) => {
|
|
89
148
|
const marker = i === cursor ? c.brand('▸') : ' ';
|
|
90
|
-
const rawLabel = String(row.label || row.role).padEnd(
|
|
149
|
+
const rawLabel = String(row.label || row.role).padEnd(labelWidth, ' ');
|
|
91
150
|
const label = i === cursor ? c.brand(rawLabel) : rawLabel;
|
|
92
|
-
|
|
151
|
+
const prefix = ` ${marker} ${label} ${c.dim('‹')} `;
|
|
152
|
+
const suffix = ` ${c.dim('›')}`;
|
|
153
|
+
const descText = valueDescription(row);
|
|
154
|
+
const descMaxCols = Math.max(0, Math.min(72, cols - stripAnsi(prefix + suffix).length - 24));
|
|
155
|
+
const desc = descText && descMaxCols > 12
|
|
156
|
+
? ` ${fitAnsiLine(descText, descMaxCols)}`
|
|
157
|
+
: '';
|
|
158
|
+
const maxValueCols = Math.max(18, cols - stripAnsi(prefix + suffix + desc).length - 1);
|
|
159
|
+
lines.push(fitAnsiLine(`${prefix}${fitAnsiLine(valueLabel(row), maxValueCols)}${suffix}${desc}`, cols - 1));
|
|
93
160
|
});
|
|
94
161
|
lines.push('');
|
|
95
162
|
lines.push(fitAnsiLine(` ${c.dim('↑↓ role · ←→ model · Enter apply · c defaults · Esc cancel')}`, cols - 1));
|