@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.
Files changed (204) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +11 -18
  4. package/package.json +11 -11
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +4 -3
  8. package/src/commands/agent.mjs +3 -3
  9. package/src/commands/device.mjs +2 -2
  10. package/src/commands/pair.mjs +2 -2
  11. package/src/commands/remote.mjs +3 -3
  12. package/src/commands/workflow.mjs +5 -5
  13. package/src/config/env.mjs +9 -2
  14. package/src/config/memory-loader.mjs +1 -1
  15. package/src/config/model-catalog-default.json +4 -3
  16. package/src/config/model-catalog.mjs +2 -0
  17. package/src/config/settings.mjs +2 -2
  18. package/src/context/prose-chunker.mjs +162 -5
  19. package/src/core/agent-loop.mjs +17 -0
  20. package/src/core/attachments.mjs +66 -4
  21. package/src/core/backend-url.mjs +11 -12
  22. package/src/core/bundled-runtime.mjs +1 -1
  23. package/src/core/callback-client.mjs +1 -1
  24. package/src/core/headless.mjs +18 -4
  25. package/src/core/jsonl-writer.mjs +18 -18
  26. package/src/core/lint-resolver.mjs +472 -0
  27. package/src/core/local-agent.mjs +40 -8
  28. package/src/core/local-store.mjs +5 -1
  29. package/src/core/mode-selector.mjs +1 -1
  30. package/src/core/output-filter.mjs +22 -26
  31. package/src/core/pricing.mjs +10 -3
  32. package/src/core/project-artifacts.mjs +3 -3
  33. package/src/core/project-context-loader.mjs +9 -9
  34. package/src/core/settings-sync.mjs +1 -1
  35. package/src/core/stagnation.mjs +61 -1
  36. package/src/core/stream-client.mjs +13 -8
  37. package/src/core/system-prompt.mjs +2 -2
  38. package/src/core/tool-executor.mjs +178 -84
  39. package/src/daemon/http-dashboard.mjs +400 -0
  40. package/src/daemon/session-core.mjs +1 -0
  41. package/src/local-service/agent-relay.mjs +868 -0
  42. package/src/local-service/approval-bridge.mjs +222 -0
  43. package/src/local-service/browser.mjs +24 -0
  44. package/src/local-service/command.mjs +155 -0
  45. package/src/local-service/file-access.mjs +393 -0
  46. package/src/local-service/machine.mjs +86 -0
  47. package/src/local-service/paths.mjs +29 -0
  48. package/src/local-service/preview-converters.mjs +260 -0
  49. package/src/local-service/server.mjs +2644 -0
  50. package/src/local-service/session-store.mjs +221 -0
  51. package/src/mcp/client.mjs +4 -4
  52. package/src/onboarding/preflight.mjs +2 -2
  53. package/src/terminal/agents.mjs +2 -2
  54. package/src/terminal/analytics.mjs +2 -6
  55. package/src/terminal/ansi.mjs +11 -3
  56. package/src/terminal/main.mjs +43 -17
  57. package/src/terminal/model-catalog-display.mjs +111 -0
  58. package/src/terminal/repl-format.mjs +4 -1
  59. package/src/terminal/repl-model-form.mjs +81 -14
  60. package/src/terminal/repl-render.mjs +37 -7
  61. package/src/terminal/repl-resume.mjs +2 -2
  62. package/src/terminal/repl-state.mjs +12 -0
  63. package/src/terminal/repl.mjs +248 -70
  64. package/src/terminal/tool-display.mjs +32 -0
  65. package/src/terminal/watch-state.mjs +118 -0
  66. package/src/tools/agent.mjs +2 -2
  67. package/src/tools/analyze-image.mjs +158 -0
  68. package/src/tools/bash.mjs +11 -9
  69. package/src/tools/generate-image.mjs +411 -0
  70. package/src/tools/lint.mjs +24 -43
  71. package/src/tools/project-overview.mjs +12 -12
  72. package/src/tools/registry.mjs +4 -0
  73. package/src/ui/formatter.mjs +2 -2
  74. package/src/ui/icons.mjs +2 -0
  75. package/src/ui/input-dock.mjs +7 -7
  76. package/src/ui/palette.mjs +1 -1
  77. package/src/ui/slash-commands.mjs +4 -6
  78. package/src/ui/spinner.mjs +1 -1
  79. package/src/ui/term.mjs +2 -6
  80. package/src/ui/tool-card.mjs +81 -6
  81. package/pulse/app/activity/page.tsx +0 -190
  82. package/pulse/app/api/activity/route.ts +0 -138
  83. package/pulse/app/api/benchmark/route.ts +0 -113
  84. package/pulse/app/api/benchmarks/route.ts +0 -195
  85. package/pulse/app/api/costs/route.ts +0 -88
  86. package/pulse/app/api/export/route.ts +0 -77
  87. package/pulse/app/api/history/route.ts +0 -11
  88. package/pulse/app/api/import/route.ts +0 -31
  89. package/pulse/app/api/memory/route.ts +0 -50
  90. package/pulse/app/api/plans/route.ts +0 -9
  91. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  92. package/pulse/app/api/projects/route.ts +0 -121
  93. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  94. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  95. package/pulse/app/api/sessions/route.ts +0 -112
  96. package/pulse/app/api/settings/route.ts +0 -14
  97. package/pulse/app/api/stats/route.ts +0 -143
  98. package/pulse/app/api/todos/route.ts +0 -9
  99. package/pulse/app/api/tools/route.ts +0 -160
  100. package/pulse/app/benchmarks/page.tsx +0 -224
  101. package/pulse/app/costs/page.tsx +0 -179
  102. package/pulse/app/export/page.tsx +0 -465
  103. package/pulse/app/favicon.ico +0 -0
  104. package/pulse/app/globals.css +0 -263
  105. package/pulse/app/help/page.tsx +0 -143
  106. package/pulse/app/history/page.tsx +0 -157
  107. package/pulse/app/layout.tsx +0 -46
  108. package/pulse/app/memory/page.tsx +0 -365
  109. package/pulse/app/overview-client.tsx +0 -393
  110. package/pulse/app/page.tsx +0 -14
  111. package/pulse/app/plans/page.tsx +0 -308
  112. package/pulse/app/projects/[slug]/page.tsx +0 -390
  113. package/pulse/app/projects/page.tsx +0 -110
  114. package/pulse/app/sessions/[id]/page.tsx +0 -243
  115. package/pulse/app/sessions/page.tsx +0 -39
  116. package/pulse/app/settings/page.tsx +0 -188
  117. package/pulse/app/todos/page.tsx +0 -211
  118. package/pulse/app/tools/page.tsx +0 -249
  119. package/pulse/cli.js +0 -164
  120. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  121. package/pulse/components/activity/streak-card.tsx +0 -36
  122. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  123. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  124. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  125. package/pulse/components/costs/model-token-table.tsx +0 -60
  126. package/pulse/components/global-search.tsx +0 -193
  127. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  128. package/pulse/components/layout/bottom-nav.tsx +0 -53
  129. package/pulse/components/layout/client-layout.tsx +0 -31
  130. package/pulse/components/layout/sidebar-context.tsx +0 -50
  131. package/pulse/components/layout/sidebar.tsx +0 -183
  132. package/pulse/components/layout/top-bar.tsx +0 -121
  133. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  134. package/pulse/components/overview/conversation-table.tsx +0 -148
  135. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  136. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  137. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  138. package/pulse/components/overview/stat-card.tsx +0 -102
  139. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  140. package/pulse/components/projects/project-card.tsx +0 -175
  141. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  142. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  143. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  144. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  145. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  146. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  147. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  148. package/pulse/components/sessions/session-badges.tsx +0 -49
  149. package/pulse/components/sessions/session-table.tsx +0 -299
  150. package/pulse/components/theme-provider.tsx +0 -44
  151. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  152. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  153. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  154. package/pulse/components/tools/version-history-table.tsx +0 -32
  155. package/pulse/components/ui/alert.tsx +0 -66
  156. package/pulse/components/ui/badge.tsx +0 -48
  157. package/pulse/components/ui/breadcrumb.tsx +0 -109
  158. package/pulse/components/ui/button.tsx +0 -64
  159. package/pulse/components/ui/calendar.tsx +0 -220
  160. package/pulse/components/ui/card.tsx +0 -92
  161. package/pulse/components/ui/command.tsx +0 -158
  162. package/pulse/components/ui/dialog.tsx +0 -158
  163. package/pulse/components/ui/input.tsx +0 -21
  164. package/pulse/components/ui/popover.tsx +0 -89
  165. package/pulse/components/ui/progress.tsx +0 -31
  166. package/pulse/components/ui/select.tsx +0 -190
  167. package/pulse/components/ui/separator.tsx +0 -28
  168. package/pulse/components/ui/sheet.tsx +0 -143
  169. package/pulse/components/ui/skeleton.tsx +0 -13
  170. package/pulse/components/ui/table.tsx +0 -116
  171. package/pulse/components/ui/tabs.tsx +0 -91
  172. package/pulse/components/ui/tooltip.tsx +0 -57
  173. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  174. package/pulse/components.json +0 -23
  175. package/pulse/eslint.config.mjs +0 -18
  176. package/pulse/lib/bahulam-paths.ts +0 -23
  177. package/pulse/lib/claude-reader.ts +0 -592
  178. package/pulse/lib/decode.ts +0 -129
  179. package/pulse/lib/pricing.ts +0 -102
  180. package/pulse/lib/replay-parser.ts +0 -165
  181. package/pulse/lib/tool-categories.ts +0 -140
  182. package/pulse/lib/utils.ts +0 -6
  183. package/pulse/next-env.d.ts +0 -6
  184. package/pulse/next.config.ts +0 -16
  185. package/pulse/package.json +0 -45
  186. package/pulse/postcss.config.mjs +0 -7
  187. package/pulse/public/activity.png +0 -0
  188. package/pulse/public/cc-lens.png +0 -0
  189. package/pulse/public/command-k.png +0 -0
  190. package/pulse/public/costs.png +0 -0
  191. package/pulse/public/dashboard-dark.png +0 -0
  192. package/pulse/public/dashboard-white.png +0 -0
  193. package/pulse/public/export.png +0 -0
  194. package/pulse/public/file.svg +0 -1
  195. package/pulse/public/globe.svg +0 -1
  196. package/pulse/public/next.svg +0 -1
  197. package/pulse/public/projects.png +0 -0
  198. package/pulse/public/session-chat.png +0 -0
  199. package/pulse/public/todos.png +0 -0
  200. package/pulse/public/tools.png +0 -0
  201. package/pulse/public/vercel.svg +0 -1
  202. package/pulse/public/window.svg +0 -1
  203. package/pulse/tsconfig.json +0 -34
  204. 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
+ }
@@ -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: 'open-claude-code', version: '2.0.0' },
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: 'open-claude-code', version: '2.0.0' },
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: 'open-claude-code', version: '2.0.0' },
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: 'open-claude-code', version: '2.0.0' },
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 v2.6.12 · initializing orbit
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 — TarangAuth instance
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)
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { c } from './ansi.mjs';
14
- import { TarangStreamClient } from '../core/stream-client.mjs';
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 TarangStreamClient({
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
- // Dashboard removed now using Bahulam Pulse (pulse/cli.js)
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[36mb0 dashboard\x1b[0m ${url}\n`);
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
 
@@ -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 Kepler brand and
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 Kepler color choices are
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.split('\n').length;
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) {
@@ -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 { TarangAuth as Auth } from '../auth/tarang-auth.mjs';
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
- // Launch Bahulam Pulse Next.js dashboard
127
- const { spawn } = await import('node:child_process');
128
- const { fileURLToPath } = await import('node:url');
129
- const path = await import('node:path');
130
- const cliPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'pulse', 'cli.js');
131
- const child = spawn(process.execPath, [cliPath, ...subcommandArgs], {
132
- stdio: 'inherit',
133
- env: process.env,
134
- });
135
- child.on('exit', (code) => process.exit(code ?? 0));
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 { TarangAuth } = await import('../auth/tarang-auth.mjs');
173
- const auth = new TarangAuth();
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 { TarangAuth } = await import('../auth/tarang-auth.mjs');
188
- const auth = new TarangAuth();
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
- /revoke Revoke auto-approvals
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
- KEPLER_BLOCK_SEPARATOR Tool/content separator: space, dotted, or off
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',