@bahulam/code 0.1.6 → 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 (178) 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 +9 -4
  21. package/src/core/tool-executor.mjs +105 -29
  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 +231 -53
  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/registry.mjs +4 -0
  51. package/src/ui/icons.mjs +2 -0
  52. package/src/ui/palette.mjs +1 -1
  53. package/src/ui/slash-commands.mjs +4 -6
  54. package/src/ui/tool-card.mjs +7 -3
  55. package/pulse/app/activity/page.tsx +0 -190
  56. package/pulse/app/api/activity/route.ts +0 -138
  57. package/pulse/app/api/benchmark/route.ts +0 -113
  58. package/pulse/app/api/benchmarks/route.ts +0 -195
  59. package/pulse/app/api/costs/route.ts +0 -88
  60. package/pulse/app/api/export/route.ts +0 -77
  61. package/pulse/app/api/history/route.ts +0 -11
  62. package/pulse/app/api/import/route.ts +0 -31
  63. package/pulse/app/api/memory/route.ts +0 -50
  64. package/pulse/app/api/plans/route.ts +0 -9
  65. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  66. package/pulse/app/api/projects/route.ts +0 -121
  67. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  68. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  69. package/pulse/app/api/sessions/route.ts +0 -112
  70. package/pulse/app/api/settings/route.ts +0 -14
  71. package/pulse/app/api/stats/route.ts +0 -143
  72. package/pulse/app/api/todos/route.ts +0 -9
  73. package/pulse/app/api/tools/route.ts +0 -160
  74. package/pulse/app/benchmarks/page.tsx +0 -224
  75. package/pulse/app/costs/page.tsx +0 -179
  76. package/pulse/app/export/page.tsx +0 -465
  77. package/pulse/app/favicon.ico +0 -0
  78. package/pulse/app/globals.css +0 -263
  79. package/pulse/app/help/page.tsx +0 -143
  80. package/pulse/app/history/page.tsx +0 -157
  81. package/pulse/app/layout.tsx +0 -46
  82. package/pulse/app/memory/page.tsx +0 -365
  83. package/pulse/app/overview-client.tsx +0 -393
  84. package/pulse/app/page.tsx +0 -14
  85. package/pulse/app/plans/page.tsx +0 -308
  86. package/pulse/app/projects/[slug]/page.tsx +0 -390
  87. package/pulse/app/projects/page.tsx +0 -110
  88. package/pulse/app/sessions/[id]/page.tsx +0 -243
  89. package/pulse/app/sessions/page.tsx +0 -39
  90. package/pulse/app/settings/page.tsx +0 -188
  91. package/pulse/app/todos/page.tsx +0 -211
  92. package/pulse/app/tools/page.tsx +0 -249
  93. package/pulse/cli.js +0 -164
  94. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  95. package/pulse/components/activity/streak-card.tsx +0 -36
  96. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  97. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  98. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  99. package/pulse/components/costs/model-token-table.tsx +0 -60
  100. package/pulse/components/global-search.tsx +0 -193
  101. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  102. package/pulse/components/layout/bottom-nav.tsx +0 -53
  103. package/pulse/components/layout/client-layout.tsx +0 -31
  104. package/pulse/components/layout/sidebar-context.tsx +0 -50
  105. package/pulse/components/layout/sidebar.tsx +0 -183
  106. package/pulse/components/layout/top-bar.tsx +0 -121
  107. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  108. package/pulse/components/overview/conversation-table.tsx +0 -148
  109. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  110. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  111. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  112. package/pulse/components/overview/stat-card.tsx +0 -102
  113. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  114. package/pulse/components/projects/project-card.tsx +0 -175
  115. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  116. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  117. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  118. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  119. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  120. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  121. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  122. package/pulse/components/sessions/session-badges.tsx +0 -49
  123. package/pulse/components/sessions/session-table.tsx +0 -299
  124. package/pulse/components/theme-provider.tsx +0 -44
  125. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  126. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  127. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  128. package/pulse/components/tools/version-history-table.tsx +0 -32
  129. package/pulse/components/ui/alert.tsx +0 -66
  130. package/pulse/components/ui/badge.tsx +0 -48
  131. package/pulse/components/ui/breadcrumb.tsx +0 -109
  132. package/pulse/components/ui/button.tsx +0 -64
  133. package/pulse/components/ui/calendar.tsx +0 -220
  134. package/pulse/components/ui/card.tsx +0 -92
  135. package/pulse/components/ui/command.tsx +0 -158
  136. package/pulse/components/ui/dialog.tsx +0 -158
  137. package/pulse/components/ui/input.tsx +0 -21
  138. package/pulse/components/ui/popover.tsx +0 -89
  139. package/pulse/components/ui/progress.tsx +0 -31
  140. package/pulse/components/ui/select.tsx +0 -190
  141. package/pulse/components/ui/separator.tsx +0 -28
  142. package/pulse/components/ui/sheet.tsx +0 -143
  143. package/pulse/components/ui/skeleton.tsx +0 -13
  144. package/pulse/components/ui/table.tsx +0 -116
  145. package/pulse/components/ui/tabs.tsx +0 -91
  146. package/pulse/components/ui/tooltip.tsx +0 -57
  147. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  148. package/pulse/components.json +0 -23
  149. package/pulse/eslint.config.mjs +0 -18
  150. package/pulse/lib/bahulam-paths.ts +0 -23
  151. package/pulse/lib/claude-reader.ts +0 -592
  152. package/pulse/lib/decode.ts +0 -129
  153. package/pulse/lib/pricing.ts +0 -102
  154. package/pulse/lib/replay-parser.ts +0 -165
  155. package/pulse/lib/tool-categories.ts +0 -140
  156. package/pulse/lib/utils.ts +0 -6
  157. package/pulse/next-env.d.ts +0 -6
  158. package/pulse/next.config.ts +0 -16
  159. package/pulse/package.json +0 -45
  160. package/pulse/postcss.config.mjs +0 -7
  161. package/pulse/public/activity.png +0 -0
  162. package/pulse/public/cc-lens.png +0 -0
  163. package/pulse/public/command-k.png +0 -0
  164. package/pulse/public/costs.png +0 -0
  165. package/pulse/public/dashboard-dark.png +0 -0
  166. package/pulse/public/dashboard-white.png +0 -0
  167. package/pulse/public/export.png +0 -0
  168. package/pulse/public/file.svg +0 -1
  169. package/pulse/public/globe.svg +0 -1
  170. package/pulse/public/next.svg +0 -1
  171. package/pulse/public/projects.png +0 -0
  172. package/pulse/public/session-chat.png +0 -0
  173. package/pulse/public/todos.png +0 -0
  174. package/pulse/public/tools.png +0 -0
  175. package/pulse/public/vercel.svg +0 -1
  176. package/pulse/public/window.svg +0 -1
  177. package/pulse/tsconfig.json +0 -34
  178. 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
+ }