gm-copilot-cli 2.0.1066 → 2.0.1068

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.
@@ -0,0 +1,435 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const { spawn, execSync } = require('child_process');
5
+ const os = require('os');
6
+ const spool = require('./spool.js');
7
+
8
+ const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
9
+ const GM_STATE_DIR = path.join(os.homedir(), '.gm');
10
+
11
+ function emitDaemonEvent(daemon, severity, message, details) {
12
+ try {
13
+ const date = new Date().toISOString().split('T')[0];
14
+ const logDir = path.join(LOG_DIR, date);
15
+ if (!fs.existsSync(logDir)) {
16
+ fs.mkdirSync(logDir, { recursive: true });
17
+ }
18
+ const logFile = path.join(logDir, 'daemon.jsonl');
19
+ const entry = {
20
+ ts: new Date().toISOString(),
21
+ daemon,
22
+ severity,
23
+ message,
24
+ ...details,
25
+ };
26
+ fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
27
+ } catch (e) {
28
+ console.error(`[daemon-bootstrap] Failed to emit event: ${e.message}`);
29
+ }
30
+ }
31
+
32
+ function getPlatformKey() {
33
+ const plat = process.platform;
34
+ if (plat === 'win32') return plat;
35
+ if (plat === 'darwin') return plat;
36
+ if (plat === 'linux') return plat;
37
+ throw new Error(`Unsupported platform: ${plat}`);
38
+ }
39
+
40
+ function getSessionId() {
41
+ return process.env.CLAUDE_SESSION_ID || 'unknown';
42
+ }
43
+
44
+ function isDaemonRunning(daemonName) {
45
+ try {
46
+ const plat = getPlatformKey();
47
+ if (plat === 'win32') {
48
+ const output = execSync('tasklist /FO CSV /NH', { encoding: 'utf8' });
49
+ const lines = output.split('\n').filter(Boolean);
50
+ return lines.some(line => {
51
+ const parts = line.split(',').map(p => p.trim().replace(/^"/, '').replace(/"$/, ''));
52
+ return parts[0].includes(daemonName);
53
+ });
54
+ } else {
55
+ try {
56
+ execSync(`pgrep -f "${daemonName}" > /dev/null 2>&1`);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+ } catch (e) {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ function checkPortReachable(host, port, timeoutMs = 500) {
68
+ return spool.execSpool('health', 'health', { timeoutMs, sessionId: getSessionId() })
69
+ .then((r) => !!(r && r.ok))
70
+ .catch(() => false);
71
+ }
72
+
73
+ function computeIndexDigest(cwd = process.cwd()) {
74
+ try {
75
+ let mtimeSum = 0;
76
+ const walkDir = (dir) => {
77
+ try {
78
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
79
+ for (const entry of entries) {
80
+ if (entry.isDirectory()) {
81
+ if (!entry.name.startsWith('.') && entry.name !== 'node_modules') {
82
+ walkDir(path.join(dir, entry.name));
83
+ }
84
+ } else {
85
+ const fullPath = path.join(dir, entry.name);
86
+ const stat = fs.statSync(fullPath);
87
+ mtimeSum += stat.mtimeMs;
88
+ }
89
+ }
90
+ } catch (e) {
91
+ return;
92
+ }
93
+ };
94
+
95
+ walkDir(cwd);
96
+
97
+ let gitHead = '';
98
+ try {
99
+ gitHead = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8' }).trim();
100
+ } catch {
101
+ gitHead = 'unknown';
102
+ }
103
+
104
+ let dirtyStatus = 'clean';
105
+ try {
106
+ const porcelain = execSync('git status --porcelain', { cwd, encoding: 'utf8' }).trim();
107
+ if (porcelain.length > 0) {
108
+ dirtyStatus = 'dirty';
109
+ }
110
+ } catch {
111
+ dirtyStatus = 'unknown';
112
+ }
113
+
114
+ const digestInput = `${mtimeSum}:${gitHead}:${dirtyStatus}`;
115
+ const digest = crypto.createHash('sha256').update(digestInput).digest('hex');
116
+ return `v1:${digest}:files=${mtimeSum}`;
117
+ } catch (e) {
118
+ emitDaemonEvent('digest', 'error', 'Failed to compute digest', { error: e.message });
119
+ return '';
120
+ }
121
+ }
122
+
123
+ function writeStatusFile(daemonName, status, sessionId) {
124
+ try {
125
+ fs.mkdirSync(GM_STATE_DIR, { recursive: true });
126
+ const statusFile = path.join(GM_STATE_DIR, `${daemonName}-status.json`);
127
+ const payload = {
128
+ daemon: daemonName,
129
+ status,
130
+ sessionId,
131
+ timestamp: new Date().toISOString(),
132
+ pid: process.pid,
133
+ };
134
+ fs.writeFileSync(statusFile, JSON.stringify(payload, null, 2));
135
+ emitDaemonEvent(daemonName, 'info', 'Status written', { file: statusFile });
136
+ } catch (e) {
137
+ emitDaemonEvent(daemonName, 'warn', 'Failed to write status file', { error: e.message });
138
+ }
139
+ }
140
+
141
+ async function ensureRsLearningDaemonRunning() {
142
+ const daemonName = 'rs-learn';
143
+ const sessionId = getSessionId();
144
+ const startTime = Date.now();
145
+
146
+ try {
147
+ emitDaemonEvent(daemonName, 'info', 'Daemon startup check initiated', { sessionId });
148
+
149
+ if (isDaemonRunning(daemonName)) {
150
+ emitDaemonEvent(daemonName, 'info', 'Daemon already running', { sessionId });
151
+ writeStatusFile(daemonName, 'running', sessionId);
152
+ return { ok: true, already_running: true };
153
+ }
154
+
155
+ emitDaemonEvent(daemonName, 'info', 'Spawning daemon', { sessionId });
156
+
157
+ const env = Object.assign({}, process.env, {
158
+ CLAUDE_SESSION_ID: sessionId,
159
+ });
160
+
161
+ const proc = spawn('bun', ['x', 'rs-learn@latest'], {
162
+ detached: true,
163
+ stdio: 'ignore',
164
+ windowsHide: true,
165
+ env,
166
+ });
167
+
168
+ const pid = proc.pid;
169
+ proc.unref();
170
+
171
+ emitDaemonEvent(daemonName, 'info', 'Daemon spawned successfully', { pid, sessionId });
172
+ writeStatusFile(daemonName, 'started', sessionId);
173
+
174
+ return {
175
+ ok: true,
176
+ pid,
177
+ sessionId,
178
+ durationMs: Date.now() - startTime,
179
+ };
180
+ } catch (e) {
181
+ emitDaemonEvent(daemonName, 'error', 'Daemon spawn failed', {
182
+ error: e.message,
183
+ sessionId,
184
+ durationMs: Date.now() - startTime,
185
+ });
186
+ writeStatusFile(daemonName, 'error', sessionId);
187
+ throw e;
188
+ }
189
+ }
190
+
191
+ async function ensureAcptoapiRunning() {
192
+ const sessionId = getSessionId();
193
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
194
+ const statusPath = path.join(projectDir, '.gm', 'acptoapi-status.json');
195
+
196
+ const host = '127.0.0.1';
197
+ const port = 4800;
198
+
199
+ try {
200
+ const reachable = await checkPortReachable(host, port);
201
+
202
+ if (reachable) {
203
+ emitDaemonEvent('acptoapi', 'info', 'Already running', { port, sessionId });
204
+ writeStatusFile('acptoapi', 'running', sessionId);
205
+ return { ok: true, message: 'acptoapi already running' };
206
+ }
207
+
208
+ emitDaemonEvent('acptoapi', 'info', 'Spawning daemon', { port, sessionId });
209
+
210
+ const env = Object.assign({}, process.env, {
211
+ CLAUDE_SESSION_ID: sessionId,
212
+ });
213
+
214
+ try {
215
+ const child = spawn('bun', ['x', 'acptoapi@latest'], {
216
+ detached: true,
217
+ stdio: 'ignore',
218
+ windowsHide: true,
219
+ env,
220
+ });
221
+ child.unref();
222
+ emitDaemonEvent('acptoapi', 'info', 'Daemon spawned', { pid: child.pid, port, sessionId });
223
+ writeStatusFile('acptoapi', 'spawned', sessionId);
224
+ return { ok: true, message: 'acptoapi spawned', pid: child.pid };
225
+ } catch (spawnErr) {
226
+ emitDaemonEvent('acptoapi', 'warn', 'Spawn failed, fallback to SDK', {
227
+ error: spawnErr.message,
228
+ sessionId,
229
+ });
230
+ writeStatusFile('acptoapi', 'spawn_failed', sessionId);
231
+ return { ok: false, message: 'acptoapi spawn failed, fallback to Anthropic SDK', error: spawnErr.message };
232
+ }
233
+ } catch (err) {
234
+ emitDaemonEvent('acptoapi', 'error', 'Check failed', {
235
+ error: err.message,
236
+ sessionId,
237
+ });
238
+ writeStatusFile('acptoapi', 'check_error', sessionId);
239
+ return { ok: false, message: 'acptoapi check failed, fallback to Anthropic SDK', error: err.message };
240
+ }
241
+ }
242
+
243
+ async function ensureRsCodeinsightDaemonRunning() {
244
+ const daemonName = 'rs-codeinsight';
245
+ const sessionId = getSessionId();
246
+ const startTime = Date.now();
247
+
248
+ try {
249
+ emitDaemonEvent(daemonName, 'info', 'Daemon startup check initiated', { sessionId });
250
+
251
+ if (isDaemonRunning(daemonName)) {
252
+ emitDaemonEvent(daemonName, 'info', 'Daemon already running', { sessionId });
253
+ writeStatusFile(daemonName, 'running', sessionId);
254
+ return { ok: true, already_running: true };
255
+ }
256
+
257
+ emitDaemonEvent(daemonName, 'info', 'Spawning daemon', { sessionId });
258
+
259
+ const env = Object.assign({}, process.env, {
260
+ CLAUDE_SESSION_ID: sessionId,
261
+ });
262
+
263
+ const proc = spawn('bun', ['x', 'rs-codeinsight@latest'], {
264
+ detached: true,
265
+ stdio: 'ignore',
266
+ windowsHide: true,
267
+ env,
268
+ });
269
+
270
+ const pid = proc.pid;
271
+ proc.unref();
272
+
273
+ emitDaemonEvent(daemonName, 'info', 'Daemon spawned successfully', { pid, sessionId });
274
+ writeStatusFile(daemonName, 'started', sessionId);
275
+
276
+ return { ok: true, pid, sessionId, durationMs: Date.now() - startTime };
277
+ } catch (e) {
278
+ emitDaemonEvent(daemonName, 'error', 'Daemon spawn failed', {
279
+ error: e.message,
280
+ sessionId,
281
+ durationMs: Date.now() - startTime,
282
+ });
283
+ writeStatusFile(daemonName, 'error', sessionId);
284
+ throw e;
285
+ }
286
+ }
287
+
288
+ async function ensureRsSearchDaemonRunning() {
289
+ const daemonName = 'rs-search';
290
+ const sessionId = getSessionId();
291
+ const startTime = Date.now();
292
+
293
+ try {
294
+ emitDaemonEvent(daemonName, 'info', 'Daemon startup check initiated', { sessionId });
295
+
296
+ if (isDaemonRunning(daemonName)) {
297
+ emitDaemonEvent(daemonName, 'info', 'Daemon already running', { sessionId });
298
+ writeStatusFile(daemonName, 'running', sessionId);
299
+ return { ok: true, already_running: true };
300
+ }
301
+
302
+ emitDaemonEvent(daemonName, 'info', 'Spawning daemon', { sessionId });
303
+
304
+ const env = Object.assign({}, process.env, {
305
+ CLAUDE_SESSION_ID: sessionId,
306
+ });
307
+
308
+ const proc = spawn('bun', ['x', 'rs-search@latest'], {
309
+ detached: true,
310
+ stdio: 'ignore',
311
+ windowsHide: true,
312
+ env,
313
+ });
314
+
315
+ const pid = proc.pid;
316
+ proc.unref();
317
+
318
+ emitDaemonEvent(daemonName, 'info', 'Daemon spawned successfully', { pid, sessionId });
319
+ writeStatusFile(daemonName, 'started', sessionId);
320
+
321
+ return { ok: true, pid, sessionId, durationMs: Date.now() - startTime };
322
+ } catch (e) {
323
+ emitDaemonEvent(daemonName, 'error', 'Daemon spawn failed', {
324
+ error: e.message,
325
+ sessionId,
326
+ durationMs: Date.now() - startTime,
327
+ });
328
+ writeStatusFile(daemonName, 'error', sessionId);
329
+ throw e;
330
+ }
331
+ }
332
+
333
+ async function ensureRsCodeinsightReady(sessionId = getSessionId()) {
334
+ const startTime = Date.now();
335
+ const daemonName = 'rs-codeinsight';
336
+ const host = '127.0.0.1';
337
+ const port = 4802;
338
+
339
+ try {
340
+ emitDaemonEvent(daemonName, 'info', 'Ensuring daemon readiness', { sessionId, host, port });
341
+
342
+ await ensureRsCodeinsightDaemonRunning();
343
+
344
+ const maxWaitMs = 30000;
345
+ const pollIntervalMs = 500;
346
+ const deadline = Date.now() + maxWaitMs;
347
+
348
+ while (Date.now() < deadline) {
349
+ const reachable = await checkPortReachable(host, port, 1000);
350
+ if (reachable) {
351
+ emitDaemonEvent(daemonName, 'info', 'Daemon ready', {
352
+ host,
353
+ port,
354
+ elapsedMs: Date.now() - startTime,
355
+ sessionId,
356
+ });
357
+ writeStatusFile(daemonName, 'ready', sessionId);
358
+ return { ok: true, host, port, sessionId, durationMs: Date.now() - startTime };
359
+ }
360
+
361
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
362
+ }
363
+
364
+ emitDaemonEvent(daemonName, 'warn', 'Timeout waiting for readiness', {
365
+ host,
366
+ port,
367
+ maxWaitMs,
368
+ sessionId,
369
+ elapsedMs: Date.now() - startTime,
370
+ });
371
+ writeStatusFile(daemonName, 'timeout', sessionId);
372
+ return { ok: false, error: 'Timeout waiting for codeinsight daemon', durationMs: Date.now() - startTime };
373
+ } catch (e) {
374
+ emitDaemonEvent(daemonName, 'error', 'Failed to ensure readiness', {
375
+ error: e.message,
376
+ sessionId,
377
+ durationMs: Date.now() - startTime,
378
+ });
379
+ writeStatusFile(daemonName, 'error', sessionId);
380
+ return { ok: false, error: e.message, durationMs: Date.now() - startTime };
381
+ }
382
+ }
383
+
384
+ async function ensureBrowserReady(sessionId = getSessionId()) {
385
+ const startTime = Date.now();
386
+ const host = '127.0.0.1';
387
+ const port = 5000;
388
+
389
+ try {
390
+ emitDaemonEvent('browser', 'info', 'Checking browser readiness', { sessionId, host, port });
391
+
392
+ const maxWaitMs = 10000;
393
+ const pollIntervalMs = 250;
394
+ const deadline = Date.now() + maxWaitMs;
395
+
396
+ while (Date.now() < deadline) {
397
+ const reachable = await checkPortReachable(host, port, 1000);
398
+ if (reachable) {
399
+ emitDaemonEvent('browser', 'info', 'Browser ready', {
400
+ host,
401
+ port,
402
+ elapsedMs: Date.now() - startTime,
403
+ sessionId,
404
+ });
405
+ return { ok: true, host, port, sessionId, durationMs: Date.now() - startTime };
406
+ }
407
+
408
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
409
+ }
410
+
411
+ emitDaemonEvent('browser', 'warn', 'Browser not available', {
412
+ host,
413
+ port,
414
+ maxWaitMs,
415
+ sessionId,
416
+ });
417
+ return { ok: false, error: 'Browser API not available at 127.0.0.1:5000', durationMs: Date.now() - startTime };
418
+ } catch (e) {
419
+ emitDaemonEvent('browser', 'error', 'Failed to check browser', {
420
+ error: e.message,
421
+ sessionId,
422
+ });
423
+ return { ok: false, error: e.message, durationMs: Date.now() - startTime };
424
+ }
425
+ }
426
+
427
+ module.exports = {
428
+ ensureAcptoapiRunning,
429
+ ensureRsCodeinsightDaemonRunning,
430
+ ensureRsCodeinsightReady,
431
+ ensureRsSearchDaemonRunning,
432
+ ensureRsLearningDaemonRunning,
433
+ ensureBrowserReady,
434
+ checkPortReachable,
435
+ };