@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.
Files changed (179) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +10 -17
  4. package/package.json +9 -9
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/tarang-auth.mjs +3 -2
  8. package/src/config/model-catalog-default.json +4 -3
  9. package/src/config/model-catalog.mjs +2 -0
  10. package/src/context/prose-chunker.mjs +162 -5
  11. package/src/core/attachments.mjs +65 -3
  12. package/src/core/bundled-runtime.mjs +1 -1
  13. package/src/core/headless.mjs +14 -0
  14. package/src/core/jsonl-writer.mjs +3 -3
  15. package/src/core/lint-resolver.mjs +472 -0
  16. package/src/core/local-agent.mjs +15 -4
  17. package/src/core/local-store.mjs +4 -0
  18. package/src/core/output-filter.mjs +21 -25
  19. package/src/core/pricing.mjs +10 -3
  20. package/src/core/stream-client.mjs +112 -16
  21. package/src/core/tool-executor.mjs +123 -31
  22. package/src/daemon/http-dashboard.mjs +400 -0
  23. package/src/daemon/session-core.mjs +1 -0
  24. package/src/local-service/agent-relay.mjs +868 -0
  25. package/src/local-service/approval-bridge.mjs +222 -0
  26. package/src/local-service/browser.mjs +24 -0
  27. package/src/local-service/command.mjs +155 -0
  28. package/src/local-service/file-access.mjs +393 -0
  29. package/src/local-service/machine.mjs +86 -0
  30. package/src/local-service/paths.mjs +29 -0
  31. package/src/local-service/preview-converters.mjs +260 -0
  32. package/src/local-service/server.mjs +2644 -0
  33. package/src/local-service/session-store.mjs +221 -0
  34. package/src/onboarding/preflight.mjs +1 -1
  35. package/src/terminal/analytics.mjs +2 -6
  36. package/src/terminal/ansi.mjs +11 -3
  37. package/src/terminal/main.mjs +37 -11
  38. package/src/terminal/model-catalog-display.mjs +111 -0
  39. package/src/terminal/repl-format.mjs +4 -1
  40. package/src/terminal/repl-model-form.mjs +81 -14
  41. package/src/terminal/repl-render.mjs +35 -5
  42. package/src/terminal/repl-state.mjs +12 -0
  43. package/src/terminal/repl.mjs +255 -55
  44. package/src/terminal/tool-display.mjs +32 -0
  45. package/src/terminal/watch-state.mjs +118 -0
  46. package/src/tools/analyze-image.mjs +158 -0
  47. package/src/tools/bash.mjs +11 -9
  48. package/src/tools/generate-image.mjs +411 -0
  49. package/src/tools/lint.mjs +24 -43
  50. package/src/tools/project-overview.mjs +5 -2
  51. package/src/tools/registry.mjs +4 -0
  52. package/src/ui/icons.mjs +2 -0
  53. package/src/ui/palette.mjs +1 -1
  54. package/src/ui/slash-commands.mjs +4 -6
  55. package/src/ui/tool-card.mjs +7 -3
  56. package/pulse/app/activity/page.tsx +0 -190
  57. package/pulse/app/api/activity/route.ts +0 -138
  58. package/pulse/app/api/benchmark/route.ts +0 -113
  59. package/pulse/app/api/benchmarks/route.ts +0 -195
  60. package/pulse/app/api/costs/route.ts +0 -88
  61. package/pulse/app/api/export/route.ts +0 -77
  62. package/pulse/app/api/history/route.ts +0 -11
  63. package/pulse/app/api/import/route.ts +0 -31
  64. package/pulse/app/api/memory/route.ts +0 -50
  65. package/pulse/app/api/plans/route.ts +0 -9
  66. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  67. package/pulse/app/api/projects/route.ts +0 -121
  68. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  69. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  70. package/pulse/app/api/sessions/route.ts +0 -112
  71. package/pulse/app/api/settings/route.ts +0 -14
  72. package/pulse/app/api/stats/route.ts +0 -143
  73. package/pulse/app/api/todos/route.ts +0 -9
  74. package/pulse/app/api/tools/route.ts +0 -160
  75. package/pulse/app/benchmarks/page.tsx +0 -224
  76. package/pulse/app/costs/page.tsx +0 -179
  77. package/pulse/app/export/page.tsx +0 -465
  78. package/pulse/app/favicon.ico +0 -0
  79. package/pulse/app/globals.css +0 -263
  80. package/pulse/app/help/page.tsx +0 -143
  81. package/pulse/app/history/page.tsx +0 -157
  82. package/pulse/app/layout.tsx +0 -46
  83. package/pulse/app/memory/page.tsx +0 -365
  84. package/pulse/app/overview-client.tsx +0 -393
  85. package/pulse/app/page.tsx +0 -14
  86. package/pulse/app/plans/page.tsx +0 -308
  87. package/pulse/app/projects/[slug]/page.tsx +0 -390
  88. package/pulse/app/projects/page.tsx +0 -110
  89. package/pulse/app/sessions/[id]/page.tsx +0 -243
  90. package/pulse/app/sessions/page.tsx +0 -39
  91. package/pulse/app/settings/page.tsx +0 -188
  92. package/pulse/app/todos/page.tsx +0 -211
  93. package/pulse/app/tools/page.tsx +0 -249
  94. package/pulse/cli.js +0 -164
  95. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  96. package/pulse/components/activity/streak-card.tsx +0 -36
  97. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  98. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  99. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  100. package/pulse/components/costs/model-token-table.tsx +0 -60
  101. package/pulse/components/global-search.tsx +0 -193
  102. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  103. package/pulse/components/layout/bottom-nav.tsx +0 -53
  104. package/pulse/components/layout/client-layout.tsx +0 -31
  105. package/pulse/components/layout/sidebar-context.tsx +0 -50
  106. package/pulse/components/layout/sidebar.tsx +0 -183
  107. package/pulse/components/layout/top-bar.tsx +0 -121
  108. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  109. package/pulse/components/overview/conversation-table.tsx +0 -148
  110. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  111. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  112. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  113. package/pulse/components/overview/stat-card.tsx +0 -102
  114. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  115. package/pulse/components/projects/project-card.tsx +0 -175
  116. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  117. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  118. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  119. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  120. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  121. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  122. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  123. package/pulse/components/sessions/session-badges.tsx +0 -49
  124. package/pulse/components/sessions/session-table.tsx +0 -299
  125. package/pulse/components/theme-provider.tsx +0 -44
  126. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  127. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  128. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  129. package/pulse/components/tools/version-history-table.tsx +0 -32
  130. package/pulse/components/ui/alert.tsx +0 -66
  131. package/pulse/components/ui/badge.tsx +0 -48
  132. package/pulse/components/ui/breadcrumb.tsx +0 -109
  133. package/pulse/components/ui/button.tsx +0 -64
  134. package/pulse/components/ui/calendar.tsx +0 -220
  135. package/pulse/components/ui/card.tsx +0 -92
  136. package/pulse/components/ui/command.tsx +0 -158
  137. package/pulse/components/ui/dialog.tsx +0 -158
  138. package/pulse/components/ui/input.tsx +0 -21
  139. package/pulse/components/ui/popover.tsx +0 -89
  140. package/pulse/components/ui/progress.tsx +0 -31
  141. package/pulse/components/ui/select.tsx +0 -190
  142. package/pulse/components/ui/separator.tsx +0 -28
  143. package/pulse/components/ui/sheet.tsx +0 -143
  144. package/pulse/components/ui/skeleton.tsx +0 -13
  145. package/pulse/components/ui/table.tsx +0 -116
  146. package/pulse/components/ui/tabs.tsx +0 -91
  147. package/pulse/components/ui/tooltip.tsx +0 -57
  148. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  149. package/pulse/components.json +0 -23
  150. package/pulse/eslint.config.mjs +0 -18
  151. package/pulse/lib/bahulam-paths.ts +0 -23
  152. package/pulse/lib/claude-reader.ts +0 -592
  153. package/pulse/lib/decode.ts +0 -129
  154. package/pulse/lib/pricing.ts +0 -102
  155. package/pulse/lib/replay-parser.ts +0 -165
  156. package/pulse/lib/tool-categories.ts +0 -140
  157. package/pulse/lib/utils.ts +0 -6
  158. package/pulse/next-env.d.ts +0 -6
  159. package/pulse/next.config.ts +0 -16
  160. package/pulse/package.json +0 -45
  161. package/pulse/postcss.config.mjs +0 -7
  162. package/pulse/public/activity.png +0 -0
  163. package/pulse/public/cc-lens.png +0 -0
  164. package/pulse/public/command-k.png +0 -0
  165. package/pulse/public/costs.png +0 -0
  166. package/pulse/public/dashboard-dark.png +0 -0
  167. package/pulse/public/dashboard-white.png +0 -0
  168. package/pulse/public/export.png +0 -0
  169. package/pulse/public/file.svg +0 -1
  170. package/pulse/public/globe.svg +0 -1
  171. package/pulse/public/next.svg +0 -1
  172. package/pulse/public/projects.png +0 -0
  173. package/pulse/public/session-chat.png +0 -0
  174. package/pulse/public/todos.png +0 -0
  175. package/pulse/public/tools.png +0 -0
  176. package/pulse/public/vercel.svg +0 -1
  177. package/pulse/public/window.svg +0 -1
  178. package/pulse/tsconfig.json +0 -34
  179. package/pulse/types/claude.ts +0 -294
@@ -0,0 +1,222 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { ApprovalManager } from '../core/approval.mjs';
3
+
4
+ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
5
+
6
+ export class BrowserApprovalManager extends ApprovalManager {
7
+ constructor({ emit, timeoutMs = DEFAULT_TIMEOUT_MS, ...options } = {}) {
8
+ super({ ...options, autoApprove: false });
9
+ if (typeof emit !== 'function') throw new Error('BrowserApprovalManager requires emit');
10
+ this.emit = emit;
11
+ this.timeoutMs = timeoutMs;
12
+ this.pending = new Map();
13
+ }
14
+
15
+ setAutoMode(enabled) {
16
+ this.autoApprove = false;
17
+ this.approveAll = Boolean(enabled);
18
+ if (!enabled) this.approvedToolTypes.clear();
19
+ return this.browserSummary();
20
+ }
21
+
22
+ browserSummary() {
23
+ const summary = this.getSummary();
24
+ return {
25
+ mode: this.approveAll ? 'auto' : 'ask',
26
+ auto: Boolean(this.approveAll),
27
+ total: summary.total,
28
+ approved: summary.approved,
29
+ denied: summary.denied,
30
+ trust: summary.trust,
31
+ };
32
+ }
33
+
34
+ _optionsFor(tier, toolName, args) {
35
+ return super._optionsFor(tier, toolName, args)
36
+ .filter((option) => option.value === 'approve' || option.value === 'reject');
37
+ }
38
+
39
+ async _prompt(toolName, args, context = {}) {
40
+ const tier = context.tier;
41
+ const why = context.reason || context.why || '';
42
+ const options = this._optionsFor(tier, toolName, args);
43
+ const approvalId = `approval_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
44
+ const prompt = promptForBrowser(toolName, args, tier, why);
45
+
46
+ this.emit('agent_approval_required', {
47
+ approval_id: approvalId,
48
+ tool_id: context.tool_id || context.call_id || context.request_id || '',
49
+ call_id: context.call_id || context.request_id || context.tool_id || '',
50
+ tool: toolName,
51
+ args: args || {},
52
+ tier,
53
+ reason: why,
54
+ prompt,
55
+ options: options.map((option) => ({
56
+ key: option.key,
57
+ label: option.label,
58
+ value: option.value,
59
+ hint: option.hint || '',
60
+ })),
61
+ });
62
+
63
+ return await new Promise((resolve) => {
64
+ const timer = setTimeout(() => {
65
+ this.pending.delete(approvalId);
66
+ const reason = `Approval timed out for ${toolName}`;
67
+ this.history.push({ tool: toolName, decision: 'timeout', tier, time: Date.now(), reason });
68
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'timeout', scope: 'once', reason, prompt });
69
+ this._rememberRejection({ tool: toolName, args, tier, decision: 'reject', reason, note: '' });
70
+ this.emit('agent_approval_resolved', {
71
+ approval_id: approvalId,
72
+ tool_id: context.tool_id || context.call_id || context.request_id || '',
73
+ call_id: context.call_id || context.request_id || context.tool_id || '',
74
+ tool: toolName,
75
+ approved: false,
76
+ reason,
77
+ });
78
+ resolve({ approved: false, tier, reason });
79
+ }, this.timeoutMs);
80
+
81
+ this.pending.set(approvalId, {
82
+ toolName,
83
+ args: args || {},
84
+ tier,
85
+ prompt,
86
+ options,
87
+ resolve: (decision) => {
88
+ clearTimeout(timer);
89
+ resolve(this._decisionResult(decision, { approvalId, toolName, args: args || {}, tier, prompt, context }));
90
+ },
91
+ });
92
+ });
93
+ }
94
+
95
+ decide(approvalId, decision = {}) {
96
+ const pending = this.pending.get(approvalId);
97
+ if (!pending) {
98
+ const err = new Error(`Approval request not found or already resolved: ${approvalId}`);
99
+ err.code = 'NOT_FOUND';
100
+ throw err;
101
+ }
102
+ this.pending.delete(approvalId);
103
+ pending.resolve(decision);
104
+ return { ok: true, approval_id: approvalId };
105
+ }
106
+
107
+ rejectAll(reason = 'Local workspace session closed') {
108
+ for (const [approvalId, pending] of this.pending.entries()) {
109
+ this.pending.delete(approvalId);
110
+ pending.resolve({ decision: 'reject', reason });
111
+ }
112
+ }
113
+
114
+ _decisionResult(decision, { approvalId, toolName, args, tier, prompt, context = {} }) {
115
+ const value = normalizeDecision(decision);
116
+ const note = String(decision?.reason || decision?.note || '').trim();
117
+
118
+ switch (value) {
119
+ case 'approve':
120
+ this.history.push({ tool: toolName, decision: 'yes', tier, time: Date.now() });
121
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve', scope: 'once', prompt });
122
+ this.emitResolved(approvalId, toolName, true, 'once', '', context);
123
+ return { approved: true, tier, scope: 'once' };
124
+
125
+ case 'allow-session': {
126
+ if (!this.policy.hitl?.allowSessionTrust) {
127
+ this.history.push({ tool: toolName, decision: 'yes', tier, time: Date.now() });
128
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve', scope: 'once', prompt });
129
+ this.emitResolved(approvalId, toolName, true, 'once', '', context);
130
+ return { approved: true, tier, scope: 'once' };
131
+ }
132
+ const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'SESSION' });
133
+ this.history.push({ tool: toolName, decision: 'session-trust', tier, time: Date.now(), rule_id: rule.id });
134
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'SESSION', rule_id: rule.id, prompt });
135
+ this.emitResolved(approvalId, toolName, true, 'SESSION', '', context);
136
+ return { approved: true, tier, scope: 'SESSION', rule_id: rule.id };
137
+ }
138
+
139
+ case 'allow-project': {
140
+ if (!this.policy.hitl?.allowProjectTrust) {
141
+ this.history.push({ tool: toolName, decision: 'yes', tier, time: Date.now() });
142
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve', scope: 'once', prompt });
143
+ this.emitResolved(approvalId, toolName, true, 'once', '', context);
144
+ return { approved: true, tier, scope: 'once' };
145
+ }
146
+ const rule = this.trustStore.add({ tool: toolName, args, tier, scope: 'PROJECT' });
147
+ this.history.push({ tool: toolName, decision: 'project-trust', tier, time: Date.now(), rule_id: rule.id });
148
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve_trusted', scope: 'PROJECT', rule_id: rule.id, prompt });
149
+ this.emitResolved(approvalId, toolName, true, 'PROJECT', '', context);
150
+ return { approved: true, tier, scope: 'PROJECT', rule_id: rule.id };
151
+ }
152
+
153
+ case 'allow-type':
154
+ this.approvedToolTypes.add(toolName);
155
+ this.history.push({ tool: toolName, decision: 'type-approve', tier, time: Date.now() });
156
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'type-approve', scope: 'session', prompt });
157
+ this.emitResolved(approvalId, toolName, true, 'type', '', context);
158
+ return { approved: true, tier, scope: 'type' };
159
+
160
+ case 'allow-all':
161
+ this.approveAll = true;
162
+ this.history.push({ tool: toolName, decision: 'approve-all', tier, time: Date.now() });
163
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'approve-all', scope: 'session', prompt });
164
+ this.emitResolved(approvalId, toolName, true, 'all', '', context);
165
+ return { approved: true, tier, scope: 'all' };
166
+
167
+ case 'reject':
168
+ default: {
169
+ const reason = note || 'User stopped the command';
170
+ this.history.push({ tool: toolName, decision: 'no', tier, time: Date.now(), reason });
171
+ this.approvalLog.append({ tool: toolName, args, tier, decision: 'reject', scope: 'once', reason, prompt });
172
+ this._rememberRejection({ tool: toolName, args, tier, decision: 'reject', reason, note });
173
+ this.emitResolved(approvalId, toolName, false, 'once', reason, context);
174
+ return { approved: false, tier, reason };
175
+ }
176
+ }
177
+ }
178
+
179
+ emitResolved(approvalId, toolName, approved, scope, reason = '', context = {}) {
180
+ this.emit('agent_approval_resolved', {
181
+ approval_id: approvalId,
182
+ tool_id: context.tool_id || context.call_id || context.request_id || '',
183
+ call_id: context.call_id || context.request_id || context.tool_id || '',
184
+ tool: toolName,
185
+ approved,
186
+ scope,
187
+ reason,
188
+ });
189
+ }
190
+ }
191
+
192
+ function normalizeDecision(decision = {}) {
193
+ const value = String(decision.decision || decision.value || '').trim();
194
+ switch (value) {
195
+ case 'approve':
196
+ case 'allow-session':
197
+ case 'allow-project':
198
+ case 'allow-type':
199
+ case 'allow-all':
200
+ case 'reject':
201
+ return value;
202
+ case 'grant':
203
+ return 'approve';
204
+ case 'deny':
205
+ default:
206
+ return 'reject';
207
+ }
208
+ }
209
+
210
+ function promptForBrowser(tool, args, tier, why) {
211
+ return {
212
+ title: `Approval · ${String(tier || '').toUpperCase()} · ${tool || 'tool'}`,
213
+ subject: approvalSubject(tool, args),
214
+ reason: why || '',
215
+ };
216
+ }
217
+
218
+ function approvalSubject(tool, args = {}) {
219
+ if (tool === 'shell') return args.command || args.cmd || 'shell command';
220
+ const target = args.path || args.file_path || args.file || args.target || '';
221
+ return target ? `${tool} ${target}` : String(tool || 'tool');
222
+ }
@@ -0,0 +1,24 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ export function openLocalBrowser(url) {
4
+ if (!url) return;
5
+ let command;
6
+ let args;
7
+
8
+ if (process.platform === 'darwin') {
9
+ command = 'open';
10
+ args = [url];
11
+ } else if (process.platform === 'win32') {
12
+ command = 'cmd';
13
+ args = ['/c', 'start', '', url];
14
+ } else {
15
+ command = 'xdg-open';
16
+ args = [url];
17
+ }
18
+
19
+ const child = spawn(command, args, {
20
+ stdio: 'ignore',
21
+ detached: true,
22
+ });
23
+ child.unref();
24
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * CLI command group for Bahulam local workspaces.
3
+ */
4
+
5
+ import {
6
+ createLocalWorkspaceSession,
7
+ listLocalWorkspaceSessions,
8
+ } from './session-store.mjs';
9
+ import { startLocalWorkspaceService } from './server.mjs';
10
+ import { openLocalBrowser } from './browser.mjs';
11
+
12
+ const RESET = '\x1b[0m';
13
+ const BOLD = '\x1b[1m';
14
+ const DIM = '\x1b[2m';
15
+ const CYAN = '\x1b[36m';
16
+ const GREEN = '\x1b[32m';
17
+ const YELLOW = '\x1b[33m';
18
+
19
+ export async function runLocalWorkspaceCommand(argv = [], { cwd = process.cwd() } = {}) {
20
+ const args = parseLocalWorkspaceArgs(argv);
21
+
22
+ if (args.help || args.command === 'help') {
23
+ printLocalWorkspaceHelp();
24
+ return;
25
+ }
26
+
27
+ if (args.command === 'list' || args.command === 'status') {
28
+ printLocalWorkspaceSessions();
29
+ return;
30
+ }
31
+
32
+ if (!['open', 'start', 'serve'].includes(args.command)) {
33
+ process.stderr.write(`${YELLOW}! Unknown workspace command: ${args.command || '(empty)'}${RESET}\n\n`);
34
+ printLocalWorkspaceHelp();
35
+ process.exitCode = 2;
36
+ return;
37
+ }
38
+
39
+ const { session, token } = createLocalWorkspaceSession({
40
+ targetPath: args.targetPath || cwd,
41
+ cwd,
42
+ kind: args.kind,
43
+ });
44
+
45
+ const service = await startLocalWorkspaceService({
46
+ session,
47
+ token,
48
+ port: args.port,
49
+ });
50
+
51
+ if (args.json) {
52
+ process.stdout.write(`${JSON.stringify({ ok: true, session, url: service.url, port: service.port }, null, 2)}\n`);
53
+ } else {
54
+ process.stderr.write(`\n${BOLD}${CYAN}Bahulam Local Workspace${RESET}\n`);
55
+ process.stderr.write(` ${DIM}session${RESET} ${session.id}\n`);
56
+ process.stderr.write(` ${DIM}kind${RESET} ${session.kind}\n`);
57
+ process.stderr.write(` ${DIM}root${RESET} ${session.root_path}\n`);
58
+ if (session.focus_path) process.stderr.write(` ${DIM}focus${RESET} ${session.focus_path}\n`);
59
+ process.stderr.write(` ${DIM}url${RESET} ${CYAN}${service.url}${RESET}\n\n`);
60
+ }
61
+
62
+ if (args.open) {
63
+ openLocalBrowser(service.url);
64
+ }
65
+
66
+ process.stderr.write(`${GREEN}ready${RESET} ${DIM}local service is bound to 127.0.0.1:${service.port}. Press Ctrl+C to stop.${RESET}\n`);
67
+
68
+ await waitForShutdown(service);
69
+ }
70
+
71
+ export function parseLocalWorkspaceArgs(argv = []) {
72
+ const out = {
73
+ command: argv[0] && !argv[0].startsWith('-') ? argv[0] : 'open',
74
+ targetPath: null,
75
+ port: 0,
76
+ kind: null,
77
+ open: true,
78
+ json: false,
79
+ help: false,
80
+ };
81
+
82
+ const startIndex = out.command === argv[0] ? 1 : 0;
83
+ for (let i = startIndex; i < argv.length; i++) {
84
+ const arg = argv[i];
85
+ switch (arg) {
86
+ case '--help':
87
+ case '-h':
88
+ out.help = true;
89
+ break;
90
+ case '--port':
91
+ out.port = Number(argv[++i]) || 0;
92
+ break;
93
+ case '--kind':
94
+ out.kind = argv[++i] || null;
95
+ break;
96
+ case '--no-open':
97
+ out.open = false;
98
+ break;
99
+ case '--json':
100
+ out.json = true;
101
+ out.open = false;
102
+ break;
103
+ default:
104
+ if (!arg.startsWith('-') && !out.targetPath) out.targetPath = arg;
105
+ break;
106
+ }
107
+ }
108
+
109
+ return out;
110
+ }
111
+
112
+ function printLocalWorkspaceHelp() {
113
+ process.stderr.write(`
114
+ ${BOLD}${CYAN}Bahulam Local Workspace${RESET}
115
+
116
+ ${BOLD}Usage:${RESET}
117
+ bahulam workspace open [path] Open a local coding/document workspace
118
+ bahulam workspace start [path] Alias for open
119
+ bahulam workspace list List recent local workspace sessions
120
+ bahulam local open [path] Same command group, shorter alias
121
+
122
+ ${BOLD}Options:${RESET}
123
+ --port <n> Bind a specific localhost port
124
+ --kind <name> Override inferred kind: coding, documents, workspace
125
+ --no-open Start service without opening the browser
126
+ --json Print session JSON and do not open the browser
127
+
128
+ ${DIM}The service binds to 127.0.0.1 and grants only the selected file or folder root.${RESET}
129
+ `);
130
+ }
131
+
132
+ function printLocalWorkspaceSessions() {
133
+ const sessions = listLocalWorkspaceSessions({ limit: 20 });
134
+ if (!sessions.length) {
135
+ process.stderr.write(`${DIM}No local workspace sessions.${RESET}\n`);
136
+ return;
137
+ }
138
+ for (const s of sessions) {
139
+ process.stderr.write(`${CYAN}${s.id}${RESET} ${s.kind} ${s.status} ${DIM}${s.root_path}${s.focus_path ? `/${s.focus_path}` : ''}${RESET}\n`);
140
+ }
141
+ }
142
+
143
+ function waitForShutdown(service) {
144
+ return new Promise((resolve) => {
145
+ let done = false;
146
+ const stop = async () => {
147
+ if (done) return;
148
+ done = true;
149
+ await service.close();
150
+ resolve();
151
+ };
152
+ process.once('SIGINT', stop);
153
+ process.once('SIGTERM', stop);
154
+ });
155
+ }