@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,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
+ }