@clearlysh/beehaven 0.8.16

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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/dist/__tests__/_cli-switch.js +105 -0
  4. package/dist/__tests__/agent-wrap-probe.js +136 -0
  5. package/dist/agent-attribution.js +192 -0
  6. package/dist/agent-identity-daemon.js +135 -0
  7. package/dist/agent-onboard.js +156 -0
  8. package/dist/agent-scope.js +79 -0
  9. package/dist/agent-sessions.js +370 -0
  10. package/dist/agent-state.js +482 -0
  11. package/dist/app-manifest.js +221 -0
  12. package/dist/app-signer.js +54 -0
  13. package/dist/app-signing.js +127 -0
  14. package/dist/audio-play.js +98 -0
  15. package/dist/avatar-protocol.js +25 -0
  16. package/dist/avatar-route.js +79 -0
  17. package/dist/bee/cloud-proxy.js +130 -0
  18. package/dist/blocked-actions.js +120 -0
  19. package/dist/build-channel.js +101 -0
  20. package/dist/buildings/daemon-workspace.js +475 -0
  21. package/dist/claude-bridge.js +310 -0
  22. package/dist/claude-credential-store.js +633 -0
  23. package/dist/clearly-sync.js +518 -0
  24. package/dist/clearly.js +616 -0
  25. package/dist/cli-state.js +334 -0
  26. package/dist/cli.js +6973 -0
  27. package/dist/commands/agent-skills.js +302 -0
  28. package/dist/commands/agent.js +1310 -0
  29. package/dist/commands/app.js +1123 -0
  30. package/dist/commands/auth.js +171 -0
  31. package/dist/commands/building-shell.js +392 -0
  32. package/dist/commands/canvas.js +932 -0
  33. package/dist/commands/claude.js +218 -0
  34. package/dist/commands/compliance.js +387 -0
  35. package/dist/commands/compose.js +507 -0
  36. package/dist/commands/composition.js +95 -0
  37. package/dist/commands/connect-session.js +409 -0
  38. package/dist/commands/conversations.js +160 -0
  39. package/dist/commands/costs.js +443 -0
  40. package/dist/commands/d1.js +253 -0
  41. package/dist/commands/daemon-cmd.js +162 -0
  42. package/dist/commands/deploy.js +164 -0
  43. package/dist/commands/design.js +617 -0
  44. package/dist/commands/dev-generate.js +125 -0
  45. package/dist/commands/dev.js +109 -0
  46. package/dist/commands/devices.js +135 -0
  47. package/dist/commands/explain-router.js +24 -0
  48. package/dist/commands/explain.js +882 -0
  49. package/dist/commands/explore.js +264 -0
  50. package/dist/commands/help.js +2418 -0
  51. package/dist/commands/init.js +408 -0
  52. package/dist/commands/install.js +129 -0
  53. package/dist/commands/library.js +229 -0
  54. package/dist/commands/marketplace.js +138 -0
  55. package/dist/commands/mcp-install.js +476 -0
  56. package/dist/commands/mention.js +257 -0
  57. package/dist/commands/omni.js +529 -0
  58. package/dist/commands/secrets.js +323 -0
  59. package/dist/commands/self-update.js +206 -0
  60. package/dist/commands/sheet.js +264 -0
  61. package/dist/commands/shell-output.js +309 -0
  62. package/dist/commands/shell.js +1048 -0
  63. package/dist/commands/shop.js +368 -0
  64. package/dist/commands/skills.js +311 -0
  65. package/dist/commands/slides.js +418 -0
  66. package/dist/commands/swarm.js +341 -0
  67. package/dist/commands/ticket.js +1301 -0
  68. package/dist/commands/tool.js +312 -0
  69. package/dist/commands/trash.js +106 -0
  70. package/dist/commands/treehouse.js +120 -0
  71. package/dist/commands/user.js +240 -0
  72. package/dist/commands/voice-tts.js +319 -0
  73. package/dist/commands/voice.js +166 -0
  74. package/dist/commands/watch-git.js +401 -0
  75. package/dist/commands/webhook.js +159 -0
  76. package/dist/commands/workspace.js +3354 -0
  77. package/dist/composition-repos.js +91 -0
  78. package/dist/daemon-avatar.js +130 -0
  79. package/dist/daemon-claude-code-auto-install.js +52 -0
  80. package/dist/daemon-client.js +363 -0
  81. package/dist/daemon-codex.js +349 -0
  82. package/dist/daemon-entry.js +101 -0
  83. package/dist/daemon-fetch-allowlist.js +194 -0
  84. package/dist/daemon-gemini.js +224 -0
  85. package/dist/daemon-mcp-clearly.js +353 -0
  86. package/dist/daemon-mcp-sandbox.js +127 -0
  87. package/dist/daemon-omni-company.js +338 -0
  88. package/dist/daemon-omni-hooks.js +375 -0
  89. package/dist/daemon-omni-identity.js +229 -0
  90. package/dist/daemon-omni-pipeline.js +251 -0
  91. package/dist/daemon-omni-policies.js +346 -0
  92. package/dist/daemon-omni-principal.js +580 -0
  93. package/dist/daemon-omni-tool-gate.js +116 -0
  94. package/dist/daemon-omni.js +1442 -0
  95. package/dist/daemon-path-transform.js +120 -0
  96. package/dist/daemon-write-sandbox.js +191 -0
  97. package/dist/daemon.js +4471 -0
  98. package/dist/dashboard.js +846 -0
  99. package/dist/dep-installer.js +286 -0
  100. package/dist/dev-mode.js +67 -0
  101. package/dist/doc-indexer.js +169 -0
  102. package/dist/e2e-crypto.js +94 -0
  103. package/dist/feature-flags.js +154 -0
  104. package/dist/file-browser.js +88 -0
  105. package/dist/first-party-hives.js +211 -0
  106. package/dist/folder-sync.js +226 -0
  107. package/dist/health.js +56 -0
  108. package/dist/ipc-auth.js +299 -0
  109. package/dist/ipc-client.js +495 -0
  110. package/dist/ipc-server.js +684 -0
  111. package/dist/keychain.js +224 -0
  112. package/dist/lib/admin-client.js +73 -0
  113. package/dist/lib/mobile-build-parser.js +156 -0
  114. package/dist/local-app-manager.js +660 -0
  115. package/dist/log-redaction.js +89 -0
  116. package/dist/logger.js +108 -0
  117. package/dist/manifest.js +143 -0
  118. package/dist/marketplace.js +793 -0
  119. package/dist/min-version-copy.js +51 -0
  120. package/dist/min-version.js +214 -0
  121. package/dist/public-surface.js +23 -0
  122. package/dist/relay-presets.js +89 -0
  123. package/dist/relay.js +2723 -0
  124. package/dist/repl.js +619 -0
  125. package/dist/setup-relay.js +363 -0
  126. package/dist/setup-service.js +536 -0
  127. package/dist/shared/bee-memory/index.js +6 -0
  128. package/dist/shared/bee-memory/types.js +682 -0
  129. package/dist/shared/claude-code/daemon-types.js +112 -0
  130. package/dist/shared/claude-code/index.js +2 -0
  131. package/dist/shared/claude-code/types.js +1 -0
  132. package/dist/shared/status/schema.js +133 -0
  133. package/dist/shell-session.js +198 -0
  134. package/dist/speak-lock.js +97 -0
  135. package/dist/spinner.js +57 -0
  136. package/dist/telemetry.js +166 -0
  137. package/dist/terminal/block-parser.js +207 -0
  138. package/dist/terminal/shell-integration.js +108 -0
  139. package/dist/transcript-tailer.js +215 -0
  140. package/dist/tts-qwen3.js +547 -0
  141. package/dist/types.js +4 -0
  142. package/dist/utm.js +38 -0
  143. package/dist/verb-surface.js +331 -0
  144. package/dist/version-utils.js +85 -0
  145. package/dist/watcher.js +207 -0
  146. package/package.json +88 -0
  147. package/python/qwen3_tts.py +230 -0
  148. package/scripts/stamp-build-channel.mjs +115 -0
  149. package/scripts/sync-installed.mjs +159 -0
  150. package/scripts/sync-skills.mjs +112 -0
  151. package/skills/README.md +24 -0
  152. package/skills/agent-doodle-space/SKILL.md +118 -0
  153. package/skills/agent-home/SKILL.md +98 -0
  154. package/skills/beehaven-cli/SKILL.md +144 -0
  155. package/skills/claude-shopper/README.md +90 -0
  156. package/skills/claude-shopper/SKILL.md +151 -0
  157. package/skills/clearly-agent/SKILL.md +183 -0
  158. package/skills/clearly-canvas/SKILL.md +222 -0
  159. package/skills/clearly-canvas/authoring.md +160 -0
  160. package/skills/clearly-canvas/communications.md +32 -0
  161. package/skills/clearly-canvas/composition-patterns.md +84 -0
  162. package/skills/clearly-canvas/example.mjs +78 -0
  163. package/skills/clearly-canvas/project-plans.md +156 -0
  164. package/skills/clearly-canvas/review.mjs +68 -0
  165. package/skills/clearly-workspace/SKILL.md +79 -0
  166. package/skills/clearly-workspace/survey.mjs +101 -0
  167. package/skills/codebase-map/SKILL.md +142 -0
  168. package/skills/design-craft/SKILL.md +300 -0
  169. package/skills/index.json +79 -0
  170. package/skills/layout-systems/SKILL.md +184 -0
  171. package/skills/pair-on-canvas/SKILL.md +92 -0
@@ -0,0 +1,475 @@
1
+ /**
2
+ * DaemonWorkspace — The local Workspace that IS the beehaven daemon.
3
+ *
4
+ * Runs inside the local app server on the user's machine. Exposes filesystem,
5
+ * shell, metrics, transcripts, and events as proper Workspace primitives so:
6
+ * - Electron app connects via ws://localhost:9876/cell/daemon/main/ws
7
+ * - Same useFolder(), useRPC() hooks work against local data (zero latency)
8
+ * - Cloud DO bridges to this via relay.onLocalAppFetch
9
+ * - Claude Code agent calls actions for tool use
10
+ *
11
+ * Replaces: bespoke relay handlers, IPC server, .clearly/ YAML sync,
12
+ * in-memory AgentMetrics, TranscriptTailer JSONL, file-browser.ts.
13
+ */
14
+ import { readFileSync, writeFileSync, existsSync, statSync, readdirSync, mkdirSync } from 'fs';
15
+ import { execSync, execFileSync, spawn } from 'child_process';
16
+ import { dirname, join } from 'path';
17
+ import { homedir } from 'os';
18
+ // ── Workspace Class ─────────────────────────────────────────────────
19
+ // The actual base class is injected at registration time by LocalHive.
20
+ // For authoring: we declare the shape and cast in the factory export.
21
+ /**
22
+ * Build the DaemonWorkspace class dynamically.
23
+ * Called once from daemon.ts after dynamic-importing the SDK.
24
+ *
25
+ * This factory pattern avoids a top-level import of @clearly-sh/beehaven-core
26
+ * which would fail if the package isn't installed (it's a peer dep).
27
+ */
28
+ export function createDaemonWorkspaceClass(Workspace) {
29
+ return class DaemonWorkspace extends Workspace {
30
+ // ════════════════════════════════════════════════════════════════
31
+ // TABLES — Persistent, queryable, auto-synced to connected clients
32
+ // ════════════════════════════════════════════════════════════════
33
+ /** Claude Code hook events (sessionStart, toolUse, etc.) */
34
+ events = this.folder('events', {
35
+ session_id: 'string',
36
+ event_type: 'string', // SessionStart, PostToolUse, etc.
37
+ tool_name: 'string',
38
+ summary: 'string',
39
+ cwd: 'string',
40
+ timestamp: 'date',
41
+ }, {
42
+ indexes: [['timestamp'], ['session_id'], ['event_type']],
43
+ sync: 'full',
44
+ clientWrite: false,
45
+ });
46
+ /** Agent metrics snapshots */
47
+ metrics = this.folder('metrics', {
48
+ session_id: 'string',
49
+ tool_calls: 'int',
50
+ files_changed: 'int',
51
+ errors: 'int',
52
+ velocity: 'number',
53
+ timestamp: 'date',
54
+ }, {
55
+ indexes: [['timestamp'], ['session_id']],
56
+ sync: 'full',
57
+ clientWrite: false,
58
+ });
59
+ /** Full conversation transcript entries */
60
+ transcript = this.folder('transcript', {
61
+ session_id: 'string',
62
+ entry_type: 'string', // user, assistant, tool_use, tool_result, thinking
63
+ role: 'string',
64
+ content: 'string',
65
+ tool_name: 'string',
66
+ tool_input: 'json',
67
+ tool_response: 'string',
68
+ uuid: 'string',
69
+ timestamp: 'date',
70
+ }, {
71
+ indexes: [['session_id'], ['timestamp'], ['entry_type']],
72
+ sync: 'lazy', // Large — don't hydrate all at once
73
+ clientWrite: false,
74
+ });
75
+ /** Active tunnel URLs */
76
+ tunnels = this.folder('tunnels', {
77
+ session_id: 'string',
78
+ port: 'int',
79
+ url: 'string',
80
+ started_at: 'date',
81
+ }, {
82
+ sync: 'full',
83
+ clientWrite: false,
84
+ });
85
+ /** Daemon status / presence */
86
+ status = this.folder('status', {
87
+ key: 'string',
88
+ value: 'json',
89
+ updated_at: 'date',
90
+ }, {
91
+ sync: 'full',
92
+ clientWrite: false,
93
+ });
94
+ // ════════════════════════════════════════════════════════════════
95
+ // RING BUFFERS — Ephemeral, capacity-limited, real-time
96
+ // ════════════════════════════════════════════════════════════════
97
+ /** Live activity log (last 200 events for REPL/avatar display) */
98
+ activityLog = this.ringBuffer('activity', {
99
+ schema: {
100
+ type: 'string',
101
+ summary: 'string',
102
+ session_id: 'string',
103
+ timestamp: 'date',
104
+ },
105
+ capacity: 200,
106
+ });
107
+ // ════════════════════════════════════════════════════════════════
108
+ // ACTIONS — Filesystem
109
+ // ════════════════════════════════════════════════════════════════
110
+ /** Read a file with optional line range */
111
+ fsRead = this.install('fs:read', {
112
+ input: { path: 'string', offset: 'int', limit: 'int' },
113
+ run: async (input) => {
114
+ const filePath = input.path;
115
+ if (!filePath)
116
+ throw new Error('path is required');
117
+ if (!existsSync(filePath))
118
+ throw new Error(`File not found: ${filePath}`);
119
+ const stat = statSync(filePath);
120
+ if (stat.isDirectory())
121
+ throw new Error(`${filePath} is a directory, not a file`);
122
+ if (stat.size > 5 * 1024 * 1024)
123
+ throw new Error(`File too large (${Math.round(stat.size / 1024)}KB). Max 5MB.`);
124
+ const text = readFileSync(filePath, 'utf8');
125
+ const lines = text.split('\n');
126
+ const offset = Math.max(0, (input.offset || 1) - 1);
127
+ const limit = input.limit || lines.length;
128
+ const slice = lines.slice(offset, offset + limit);
129
+ const numbered = slice
130
+ .map((line, i) => {
131
+ const lineNum = String(offset + i + 1).padStart(6, ' ');
132
+ return `${lineNum}\t${line}`;
133
+ })
134
+ .join('\n');
135
+ const content = numbered.length > 50_000 ? numbered.slice(0, 50_000) + '\n... (truncated)' : numbered;
136
+ return { content };
137
+ },
138
+ });
139
+ /** Write content to a file */
140
+ fsWrite = this.install('fs:write', {
141
+ input: { path: 'string', content: 'string' },
142
+ run: async (input) => {
143
+ const { path: filePath, content } = input;
144
+ if (!filePath || content === undefined)
145
+ throw new Error('path and content are required');
146
+ const dir = dirname(filePath);
147
+ if (!existsSync(dir))
148
+ mkdirSync(dir, { recursive: true });
149
+ writeFileSync(filePath, content, 'utf8');
150
+ return { path: filePath, bytes: content.length };
151
+ },
152
+ });
153
+ /** Edit a file with string replacement */
154
+ fsEdit = this.install('fs:edit', {
155
+ input: { path: 'string', old_string: 'string', new_string: 'string' },
156
+ run: async (input) => {
157
+ const { path: filePath, old_string: oldStr, new_string: newStr } = input;
158
+ if (!filePath || !oldStr || newStr === undefined)
159
+ throw new Error('path, old_string, and new_string are required');
160
+ if (!existsSync(filePath))
161
+ throw new Error(`File not found: ${filePath}`);
162
+ const text = readFileSync(filePath, 'utf8');
163
+ const occurrences = text.split(oldStr).length - 1;
164
+ if (occurrences === 0)
165
+ throw new Error(`old_string not found in ${filePath}`);
166
+ if (occurrences > 1)
167
+ throw new Error(`old_string appears ${occurrences} times — must be unique`);
168
+ writeFileSync(filePath, text.replace(oldStr, newStr), 'utf8');
169
+ return { path: filePath, replacements: 1 };
170
+ },
171
+ });
172
+ /** Glob pattern matching */
173
+ fsGlob = this.install('fs:glob', {
174
+ input: { pattern: 'string' },
175
+ run: async (input) => {
176
+ let pattern = input.pattern;
177
+ if (!pattern)
178
+ throw new Error('pattern is required');
179
+ pattern = pattern.replace(/\/\//g, '/');
180
+ const globStart = pattern.search(/[*?{[]/);
181
+ const baseDir = globStart >= 0
182
+ ? pattern.slice(0, pattern.lastIndexOf('/', globStart) + 1) || '.'
183
+ : pattern;
184
+ if (!existsSync(baseDir))
185
+ throw new Error(`Directory not found: ${baseDir}`);
186
+ const matches = [];
187
+ const regex = patternToRegex(pattern);
188
+ walkDir(baseDir, matches, regex, 500, 8);
189
+ return { matches, count: matches.length };
190
+ },
191
+ });
192
+ /** List directory contents */
193
+ fsLs = this.install('fs:ls', {
194
+ input: { path: 'string' },
195
+ run: async (input) => {
196
+ const dirPath = input.path || homedir();
197
+ if (!existsSync(dirPath))
198
+ throw new Error(`Directory not found: ${dirPath}`);
199
+ const stat = statSync(dirPath);
200
+ if (!stat.isDirectory())
201
+ throw new Error(`Not a directory: ${dirPath}`);
202
+ const entries = readdirSync(dirPath, { withFileTypes: true });
203
+ const items = [];
204
+ for (const entry of entries) {
205
+ if (entry.name.startsWith('.'))
206
+ continue;
207
+ const fullPath = join(dirPath, entry.name);
208
+ if (entry.isDirectory()) {
209
+ items.push({ name: entry.name, type: 'dir' });
210
+ }
211
+ else {
212
+ try {
213
+ const fstat = statSync(fullPath);
214
+ items.push({ name: entry.name, type: 'file', size: fstat.size });
215
+ }
216
+ catch {
217
+ items.push({ name: entry.name, type: 'file', size: 0 });
218
+ }
219
+ }
220
+ }
221
+ return { path: dirPath, entries: items };
222
+ },
223
+ });
224
+ /** Search file contents with regex */
225
+ fsGrep = this.install('fs:grep', {
226
+ input: { pattern: 'string', path: 'string' },
227
+ run: async (input) => {
228
+ const { pattern, path: searchPath } = input;
229
+ if (!pattern)
230
+ throw new Error('pattern is required');
231
+ const dir = searchPath || homedir();
232
+ try {
233
+ const output = execFileSync('grep', ['-rn', '--include=*', pattern, dir], {
234
+ cwd: dir,
235
+ timeout: 30_000,
236
+ maxBuffer: 1024 * 1024,
237
+ encoding: 'utf8',
238
+ stdio: ['pipe', 'pipe', 'pipe'],
239
+ });
240
+ const content = output.length > 50_000 ? output.slice(0, 50_000) + '\n... (truncated)' : output;
241
+ return { content, truncated: output.length > 50_000 };
242
+ }
243
+ catch (err) {
244
+ if (err.status === 1)
245
+ return { content: '', truncated: false };
246
+ throw new Error(err.stderr || err.message || 'grep failed');
247
+ }
248
+ },
249
+ });
250
+ // ════════════════════════════════════════════════════════════════
251
+ // ACTIONS — Shell
252
+ // ════════════════════════════════════════════════════════════════
253
+ /** Execute a shell command */
254
+ shell = this.install('shell', {
255
+ input: { command: 'string', cwd: 'string', timeout: 'int' },
256
+ run: async (input) => {
257
+ const command = input.command;
258
+ if (!command)
259
+ throw new Error('command is required');
260
+ let cwd = input.cwd || homedir();
261
+ // Resolve virtual /local/ prefix from cloud agent
262
+ if (cwd.startsWith('/local/'))
263
+ cwd = cwd.slice('/local'.length);
264
+ if (!existsSync(cwd) && cwd.startsWith('/')) {
265
+ const asHome = join(homedir(), cwd);
266
+ if (existsSync(asHome))
267
+ cwd = asHome;
268
+ }
269
+ if (!existsSync(cwd))
270
+ throw new Error(`Working directory does not exist: ${cwd}`);
271
+ const timeout = Math.min(input.timeout || 30_000, 120_000);
272
+ try {
273
+ const output = execSync(command, {
274
+ cwd,
275
+ timeout,
276
+ maxBuffer: 1024 * 1024,
277
+ encoding: 'utf8',
278
+ stdio: ['pipe', 'pipe', 'pipe'],
279
+ env: { ...process.env, TERM: 'dumb' },
280
+ });
281
+ const stdout = output.length > 50_000 ? output.slice(0, 50_000) + '\n... (truncated)' : output;
282
+ return { stdout, stderr: '', exitCode: 0 };
283
+ }
284
+ catch (err) {
285
+ return {
286
+ stdout: String(err.stdout || '').slice(0, 2000),
287
+ stderr: String(err.stderr || '').slice(0, 2000),
288
+ exitCode: err.status || 1,
289
+ };
290
+ }
291
+ },
292
+ });
293
+ // ════════════════════════════════════════════════════════════════
294
+ // ACTIONS — Background Processes
295
+ // ════════════════════════════════════════════════════════════════
296
+ _bgProcesses = new Map();
297
+ /** Start a long-running background process (dev server, watcher) */
298
+ processStart = this.install('process:start', {
299
+ input: { command: 'string', cwd: 'string' },
300
+ run: async (input) => {
301
+ const command = input.command;
302
+ if (!command)
303
+ throw new Error('command is required');
304
+ let cwd = input.cwd || homedir();
305
+ if (cwd.startsWith('/local/'))
306
+ cwd = cwd.slice('/local'.length);
307
+ if (!existsSync(cwd))
308
+ cwd = homedir();
309
+ const id = `proc_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
310
+ const proc = spawn('sh', ['-c', command], {
311
+ cwd,
312
+ stdio: ['ignore', 'pipe', 'pipe'],
313
+ env: { ...process.env, TERM: 'dumb' },
314
+ });
315
+ const entry = { proc, output: [], startedAt: Date.now(), command, cwd };
316
+ this._bgProcesses.set(id, entry);
317
+ proc.stdout?.on('data', (d) => {
318
+ const line = d.toString();
319
+ entry.output.push(line);
320
+ if (entry.output.length > 200)
321
+ entry.output.shift();
322
+ });
323
+ proc.stderr?.on('data', (d) => {
324
+ const line = d.toString();
325
+ entry.output.push(line);
326
+ if (entry.output.length > 200)
327
+ entry.output.shift();
328
+ });
329
+ proc.on('exit', () => {
330
+ // Keep for 5 min so output can be read
331
+ setTimeout(() => this._bgProcesses.delete(id), 300_000);
332
+ });
333
+ return { id, pid: proc.pid, command, cwd };
334
+ },
335
+ });
336
+ /** Check a background process */
337
+ processCheck = this.install('process:check', {
338
+ input: { id: 'string' },
339
+ run: async (input) => {
340
+ const entry = this._bgProcesses.get(input.id);
341
+ if (!entry)
342
+ throw new Error(`Process not found: ${input.id}`);
343
+ const running = !entry.proc.killed && entry.proc.exitCode === null;
344
+ return {
345
+ id: input.id,
346
+ running,
347
+ exitCode: entry.proc.exitCode,
348
+ output: entry.output.slice(-50).join(''),
349
+ command: entry.command,
350
+ cwd: entry.cwd,
351
+ uptime: Date.now() - entry.startedAt,
352
+ };
353
+ },
354
+ });
355
+ /** Kill a background process */
356
+ processKill = this.install('process:kill', {
357
+ input: { id: 'string' },
358
+ run: async (input) => {
359
+ const entry = this._bgProcesses.get(input.id);
360
+ if (!entry)
361
+ throw new Error(`Process not found: ${input.id}`);
362
+ entry.proc.kill('SIGTERM');
363
+ return { id: input.id, killed: true };
364
+ },
365
+ });
366
+ /** List all background processes */
367
+ processList = this.install('process:list', {
368
+ run: async () => {
369
+ const processes = [];
370
+ for (const [id, entry] of this._bgProcesses) {
371
+ const running = !entry.proc.killed && entry.proc.exitCode === null;
372
+ processes.push({
373
+ id,
374
+ command: entry.command,
375
+ cwd: entry.cwd,
376
+ running,
377
+ pid: entry.proc.pid,
378
+ uptime: Date.now() - entry.startedAt,
379
+ });
380
+ }
381
+ return { processes };
382
+ },
383
+ });
384
+ // ════════════════════════════════════════════════════════════════
385
+ // LIFECYCLE
386
+ // ════════════════════════════════════════════════════════════════
387
+ /** Called when a client connects — send initial status */
388
+ async onConnect(userId, ws) {
389
+ this.status.upsert({
390
+ id: 'daemon',
391
+ key: 'daemon',
392
+ value: JSON.stringify({
393
+ pid: process.pid,
394
+ hostname: (await import('os')).hostname(),
395
+ platform: process.platform,
396
+ arch: process.arch,
397
+ nodeVersion: process.version,
398
+ startedAt: Date.now(),
399
+ }),
400
+ updated_at: Date.now(),
401
+ });
402
+ }
403
+ };
404
+ }
405
+ // ── Utility Functions (extracted from relay.ts) ──────────────────────
406
+ /** Convert a glob pattern to a regex */
407
+ function patternToRegex(pattern) {
408
+ let regexStr = '^';
409
+ let i = 0;
410
+ while (i < pattern.length) {
411
+ const c = pattern[i];
412
+ if (c === '*') {
413
+ if (pattern[i + 1] === '*') {
414
+ regexStr += '.*';
415
+ i += 2;
416
+ if (pattern[i] === '/') {
417
+ i++;
418
+ } // skip trailing /
419
+ continue;
420
+ }
421
+ regexStr += '[^/]*';
422
+ }
423
+ else if (c === '?') {
424
+ regexStr += '[^/]';
425
+ }
426
+ else if (c === '{') {
427
+ const end = pattern.indexOf('}', i);
428
+ if (end > i) {
429
+ const alts = pattern
430
+ .slice(i + 1, end)
431
+ .split(',')
432
+ .map((a) => a.replace(/[.*+?^$|()[\]\\]/g, '\\$&'));
433
+ regexStr += `(?:${alts.join('|')})`;
434
+ i = end;
435
+ }
436
+ else {
437
+ regexStr += '\\{';
438
+ }
439
+ }
440
+ else if ('[.*+?^$|()\\]'.includes(c)) {
441
+ regexStr += '\\' + c;
442
+ }
443
+ else {
444
+ regexStr += c;
445
+ }
446
+ i++;
447
+ }
448
+ regexStr += '$';
449
+ return new RegExp(regexStr);
450
+ }
451
+ /** Walk a directory tree, collecting matching paths */
452
+ function walkDir(dir, matches, regex, maxMatches, maxDepth, depth = 0) {
453
+ if (depth > maxDepth || matches.length >= maxMatches)
454
+ return;
455
+ let entries;
456
+ try {
457
+ entries = readdirSync(dir, { withFileTypes: true });
458
+ }
459
+ catch {
460
+ return;
461
+ }
462
+ for (const entry of entries) {
463
+ if (matches.length >= maxMatches)
464
+ return;
465
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
466
+ continue;
467
+ const fullPath = join(dir, entry.name);
468
+ if (entry.isDirectory()) {
469
+ walkDir(fullPath, matches, regex, maxMatches, maxDepth, depth + 1);
470
+ }
471
+ else if (regex.test(fullPath)) {
472
+ matches.push(fullPath);
473
+ }
474
+ }
475
+ }