gm-skill 0.1.0 → 0.1.1
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.
- package/index.js +1 -1
- package/lib/daemon-bootstrap.js +314 -0
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const net = require('net');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { spawn, execSync } = require('child_process');
|
|
6
|
+
const os = require('os');
|
|
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 emitEvent(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] emit failed: ${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 new Promise((resolve) => {
|
|
69
|
+
const socket = new net.Socket();
|
|
70
|
+
const timeoutHandle = setTimeout(() => {
|
|
71
|
+
socket.destroy();
|
|
72
|
+
resolve(false);
|
|
73
|
+
}, timeoutMs);
|
|
74
|
+
|
|
75
|
+
socket.connect(port, host, () => {
|
|
76
|
+
clearTimeout(timeoutHandle);
|
|
77
|
+
socket.destroy();
|
|
78
|
+
resolve(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
socket.on('error', () => {
|
|
82
|
+
clearTimeout(timeoutHandle);
|
|
83
|
+
resolve(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function writeStatusFile(daemonName, status, details = {}) {
|
|
89
|
+
try {
|
|
90
|
+
fs.mkdirSync(GM_STATE_DIR, { recursive: true });
|
|
91
|
+
const statusFile = path.join(GM_STATE_DIR, `${daemonName}-status.json`);
|
|
92
|
+
const payload = {
|
|
93
|
+
daemon: daemonName,
|
|
94
|
+
status,
|
|
95
|
+
sessionId: getSessionId(),
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
pid: process.pid,
|
|
98
|
+
...details,
|
|
99
|
+
};
|
|
100
|
+
fs.writeFileSync(statusFile, JSON.stringify(payload, null, 2));
|
|
101
|
+
emitEvent(daemonName, 'info', 'Status written', { file: statusFile });
|
|
102
|
+
} catch (e) {
|
|
103
|
+
emitEvent(daemonName, 'warn', 'Failed to write status file', { error: e.message });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function checkState(daemonName) {
|
|
108
|
+
const sessionId = getSessionId();
|
|
109
|
+
const startTime = Date.now();
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
emitEvent(daemonName, 'info', 'checkState initiated', { sessionId });
|
|
113
|
+
|
|
114
|
+
const running = isDaemonRunning(daemonName);
|
|
115
|
+
if (!running) {
|
|
116
|
+
emitEvent(daemonName, 'info', 'Daemon not running', { sessionId });
|
|
117
|
+
writeStatusFile(daemonName, 'not_running', { sessionId });
|
|
118
|
+
return { ok: true, running: false, durationMs: Date.now() - startTime };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
emitEvent(daemonName, 'info', 'Daemon running', { sessionId });
|
|
122
|
+
writeStatusFile(daemonName, 'running', { sessionId });
|
|
123
|
+
return { ok: true, running: true, durationMs: Date.now() - startTime };
|
|
124
|
+
} catch (e) {
|
|
125
|
+
emitEvent(daemonName, 'error', 'checkState failed', {
|
|
126
|
+
error: e.message,
|
|
127
|
+
sessionId,
|
|
128
|
+
durationMs: Date.now() - startTime,
|
|
129
|
+
});
|
|
130
|
+
return { ok: false, error: e.message, durationMs: Date.now() - startTime };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function spawn(daemonName, cmd) {
|
|
135
|
+
const sessionId = getSessionId();
|
|
136
|
+
const startTime = Date.now();
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
emitEvent(daemonName, 'info', 'spawn initiated', { cmd, sessionId });
|
|
140
|
+
|
|
141
|
+
if (isDaemonRunning(daemonName)) {
|
|
142
|
+
emitEvent(daemonName, 'info', 'Already running, skipping spawn', { sessionId });
|
|
143
|
+
writeStatusFile(daemonName, 'running', { sessionId });
|
|
144
|
+
return { ok: true, already_running: true, durationMs: Date.now() - startTime };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
emitEvent(daemonName, 'info', 'Spawning daemon', { cmd, sessionId });
|
|
148
|
+
|
|
149
|
+
const env = Object.assign({}, process.env, {
|
|
150
|
+
CLAUDE_SESSION_ID: sessionId,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const proc = spawn('bun', ['x', cmd], {
|
|
154
|
+
detached: true,
|
|
155
|
+
stdio: 'ignore',
|
|
156
|
+
windowsHide: true,
|
|
157
|
+
env,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const pid = proc.pid;
|
|
161
|
+
proc.unref();
|
|
162
|
+
|
|
163
|
+
emitEvent(daemonName, 'info', 'Daemon spawned', { pid, cmd, sessionId });
|
|
164
|
+
writeStatusFile(daemonName, 'spawned', { pid, sessionId });
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
pid,
|
|
169
|
+
cmd,
|
|
170
|
+
sessionId,
|
|
171
|
+
durationMs: Date.now() - startTime,
|
|
172
|
+
};
|
|
173
|
+
} catch (e) {
|
|
174
|
+
emitEvent(daemonName, 'error', 'spawn failed', {
|
|
175
|
+
error: e.message,
|
|
176
|
+
cmd,
|
|
177
|
+
sessionId,
|
|
178
|
+
durationMs: Date.now() - startTime,
|
|
179
|
+
});
|
|
180
|
+
writeStatusFile(daemonName, 'spawn_error', { error: e.message, sessionId });
|
|
181
|
+
return { ok: false, error: e.message, durationMs: Date.now() - startTime };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function waitForReady(daemonName, host, port, timeoutMs = 30000) {
|
|
186
|
+
const sessionId = getSessionId();
|
|
187
|
+
const startTime = Date.now();
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
emitEvent(daemonName, 'info', 'waitForReady initiated', {
|
|
191
|
+
host,
|
|
192
|
+
port,
|
|
193
|
+
timeoutMs,
|
|
194
|
+
sessionId,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const deadline = startTime + timeoutMs;
|
|
198
|
+
const pollIntervalMs = 500;
|
|
199
|
+
|
|
200
|
+
while (Date.now() < deadline) {
|
|
201
|
+
const reachable = await checkPortReachable(host, port, 1000);
|
|
202
|
+
if (reachable) {
|
|
203
|
+
emitEvent(daemonName, 'info', 'Ready', {
|
|
204
|
+
host,
|
|
205
|
+
port,
|
|
206
|
+
elapsedMs: Date.now() - startTime,
|
|
207
|
+
sessionId,
|
|
208
|
+
});
|
|
209
|
+
writeStatusFile(daemonName, 'ready', { host, port, sessionId });
|
|
210
|
+
return { ok: true, host, port, elapsedMs: Date.now() - startTime };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
emitEvent(daemonName, 'warn', 'Timeout waiting for readiness', {
|
|
217
|
+
host,
|
|
218
|
+
port,
|
|
219
|
+
timeoutMs,
|
|
220
|
+
sessionId,
|
|
221
|
+
elapsedMs: Date.now() - startTime,
|
|
222
|
+
});
|
|
223
|
+
writeStatusFile(daemonName, 'timeout', { host, port, timeoutMs, sessionId });
|
|
224
|
+
return { ok: false, error: 'Timeout', timeoutMs, elapsedMs: Date.now() - startTime };
|
|
225
|
+
} catch (e) {
|
|
226
|
+
emitEvent(daemonName, 'error', 'waitForReady failed', {
|
|
227
|
+
error: e.message,
|
|
228
|
+
host,
|
|
229
|
+
port,
|
|
230
|
+
sessionId,
|
|
231
|
+
elapsedMs: Date.now() - startTime,
|
|
232
|
+
});
|
|
233
|
+
return { ok: false, error: e.message, elapsedMs: Date.now() - startTime };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function getSocket(daemonName) {
|
|
238
|
+
try {
|
|
239
|
+
emitEvent(daemonName, 'info', 'getSocket initiated', { sessionId: getSessionId() });
|
|
240
|
+
|
|
241
|
+
const statusFile = path.join(GM_STATE_DIR, `${daemonName}-status.json`);
|
|
242
|
+
if (!fs.existsSync(statusFile)) {
|
|
243
|
+
emitEvent(daemonName, 'warn', 'No status file found', { statusFile });
|
|
244
|
+
return { ok: false, error: 'No status file found' };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const status = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
248
|
+
const socket = `${status.host || '127.0.0.1'}:${status.port || 'unknown'}`;
|
|
249
|
+
|
|
250
|
+
emitEvent(daemonName, 'info', 'Socket retrieved', { socket });
|
|
251
|
+
return { ok: true, socket, ...status };
|
|
252
|
+
} catch (e) {
|
|
253
|
+
emitEvent(daemonName, 'error', 'getSocket failed', {
|
|
254
|
+
error: e.message,
|
|
255
|
+
sessionId: getSessionId(),
|
|
256
|
+
});
|
|
257
|
+
return { ok: false, error: e.message };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function shutdown(daemonName) {
|
|
262
|
+
const sessionId = getSessionId();
|
|
263
|
+
const startTime = Date.now();
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
emitEvent(daemonName, 'info', 'shutdown initiated', { sessionId });
|
|
267
|
+
|
|
268
|
+
const plat = getPlatformKey();
|
|
269
|
+
let killed = false;
|
|
270
|
+
|
|
271
|
+
if (plat === 'win32') {
|
|
272
|
+
try {
|
|
273
|
+
execSync(`taskkill /F /IM ${daemonName}* /T`, { stdio: 'ignore' });
|
|
274
|
+
killed = true;
|
|
275
|
+
} catch {
|
|
276
|
+
killed = false;
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
try {
|
|
280
|
+
execSync(`pkill -9 -f "${daemonName}"`, { stdio: 'ignore' });
|
|
281
|
+
killed = true;
|
|
282
|
+
} catch {
|
|
283
|
+
killed = false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
emitEvent(daemonName, 'info', 'shutdown completed', {
|
|
288
|
+
killed,
|
|
289
|
+
sessionId,
|
|
290
|
+
durationMs: Date.now() - startTime,
|
|
291
|
+
});
|
|
292
|
+
writeStatusFile(daemonName, 'shutdown', { killed, sessionId });
|
|
293
|
+
|
|
294
|
+
return { ok: true, killed, durationMs: Date.now() - startTime };
|
|
295
|
+
} catch (e) {
|
|
296
|
+
emitEvent(daemonName, 'error', 'shutdown failed', {
|
|
297
|
+
error: e.message,
|
|
298
|
+
sessionId,
|
|
299
|
+
durationMs: Date.now() - startTime,
|
|
300
|
+
});
|
|
301
|
+
return { ok: false, error: e.message, durationMs: Date.now() - startTime };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = {
|
|
306
|
+
checkState,
|
|
307
|
+
spawn,
|
|
308
|
+
waitForReady,
|
|
309
|
+
getSocket,
|
|
310
|
+
shutdown,
|
|
311
|
+
emitEvent,
|
|
312
|
+
isDaemonRunning,
|
|
313
|
+
checkPortReachable,
|
|
314
|
+
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Unified skill library for gm platform implementations",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./index.js",
|
|
8
|
-
"./daemon-bootstrap": "./
|
|
8
|
+
"./daemon-bootstrap": "./lib/daemon-bootstrap.js",
|
|
9
9
|
"./manifest": "./lib/manifest.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|