gm-copilot-cli 2.0.1066 → 2.0.1067

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,410 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const https = require('https');
4
+ const { execSync, spawn } = require('child_process');
5
+ const crypto = require('crypto');
6
+ const os = require('os');
7
+ const spool = require('./spool.js');
8
+
9
+ const PLUGKIT_TOOLS_DIR = path.join(os.homedir(), '.claude', 'gm-tools');
10
+ const PLUGKIT_VERSION_FILE = path.join(PLUGKIT_TOOLS_DIR, 'plugkit.version');
11
+ const BOOTSTRAP_STATUS_FILE = path.join(os.homedir(), '.gm', 'bootstrap-status.json');
12
+ const BOOTSTRAP_ERROR_FILE = path.join(os.homedir(), '.gm', 'bootstrap-error.json');
13
+ const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
14
+ const PLATFORM_MAP = {
15
+ win32: { suffix: '-win32-x64.exe', altSuffix: '-win32-arm64.exe' },
16
+ darwin: { suffix: '-darwin-x64', altSuffix: '-darwin-arm64' },
17
+ linux: { suffix: '-linux-x64', altSuffix: '-linux-arm64' },
18
+ };
19
+
20
+ function getPlatformKey() {
21
+ const plat = process.platform;
22
+ if (plat === 'win32') return plat;
23
+ if (plat === 'darwin') return plat;
24
+ if (plat === 'linux') return plat;
25
+ throw new Error(`Unsupported platform: ${plat}`);
26
+ }
27
+
28
+ function getExpectedBinaryName() {
29
+ const plat = getPlatformKey();
30
+ const suffix = PLATFORM_MAP[plat].suffix;
31
+ return `plugkit${suffix}`;
32
+ }
33
+
34
+ function getPlugkitPath() {
35
+ const name = getExpectedBinaryName();
36
+ return path.join(PLUGKIT_TOOLS_DIR, name);
37
+ }
38
+
39
+ function emitBootstrapEvent(severity, message, details) {
40
+ try {
41
+ const date = new Date().toISOString().split('T')[0];
42
+ const logDir = path.join(LOG_DIR, date);
43
+ if (!fs.existsSync(logDir)) {
44
+ fs.mkdirSync(logDir, { recursive: true });
45
+ }
46
+ const logFile = path.join(logDir, 'bootstrap.jsonl');
47
+ const entry = {
48
+ ts: new Date().toISOString(),
49
+ severity,
50
+ message,
51
+ ...details,
52
+ };
53
+ fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
54
+ } catch (e) {
55
+ console.error(`[bootstrap] Failed to emit event: ${e.message}`);
56
+ }
57
+ }
58
+
59
+ function readManifest() {
60
+ try {
61
+ const gmJsonPath = path.join(process.cwd(), 'gm-starter', 'gm.json');
62
+ if (!fs.existsSync(gmJsonPath)) {
63
+ throw new Error('gm-starter/gm.json not found');
64
+ }
65
+ const gm = JSON.parse(fs.readFileSync(gmJsonPath, 'utf8'));
66
+ const version = gm.plugkitVersion;
67
+
68
+ const sha256Path = path.join(process.cwd(), 'gm-starter', 'bin', 'plugkit.sha256');
69
+ if (!fs.existsSync(sha256Path)) {
70
+ throw new Error('gm-starter/bin/plugkit.sha256 not found');
71
+ }
72
+ const sha256Lines = fs.readFileSync(sha256Path, 'utf8').split('\n').filter(Boolean);
73
+ const binaryName = getExpectedBinaryName();
74
+ const hashLine = sha256Lines.find(line => line.includes(binaryName));
75
+ if (!hashLine) {
76
+ throw new Error(`No hash found for ${binaryName}`);
77
+ }
78
+ const expectedHash = hashLine.split(/\s+/)[0];
79
+
80
+ return { version, expectedHash };
81
+ } catch (e) {
82
+ emitBootstrapEvent('error', 'Failed to read manifest', { error: e.message });
83
+ throw e;
84
+ }
85
+ }
86
+
87
+ function getInstalledVersion() {
88
+ try {
89
+ if (fs.existsSync(PLUGKIT_VERSION_FILE)) {
90
+ return fs.readFileSync(PLUGKIT_VERSION_FILE, 'utf8').trim();
91
+ }
92
+ return null;
93
+ } catch (e) {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ function computeFileHash(filePath) {
99
+ const content = fs.readFileSync(filePath);
100
+ return crypto.createHash('sha256').update(content).digest('hex');
101
+ }
102
+
103
+ async function downloadPlugkitBinary(version) {
104
+ const binaryName = getExpectedBinaryName();
105
+ const url = `https://github.com/AnEntrypoint/plugkit-bin/releases/download/${version}/${binaryName}`;
106
+
107
+ emitBootstrapEvent('info', 'Starting binary download', { version, binaryName, url });
108
+
109
+ return new Promise((resolve, reject) => {
110
+ https
111
+ .get(url, { timeout: 30000 }, (res) => {
112
+ if (res.statusCode === 404) {
113
+ reject(new Error(`Binary not found: ${binaryName} v${version}`));
114
+ return;
115
+ }
116
+ if (res.statusCode !== 200) {
117
+ reject(new Error(`HTTP ${res.statusCode} downloading ${binaryName}`));
118
+ return;
119
+ }
120
+
121
+ const chunks = [];
122
+ res.on('data', (chunk) => chunks.push(chunk));
123
+ res.on('end', () => {
124
+ const data = Buffer.concat(chunks);
125
+ emitBootstrapEvent('info', 'Binary download complete', { bytes: data.length });
126
+ resolve(data);
127
+ });
128
+ })
129
+ .on('error', (e) => {
130
+ emitBootstrapEvent('error', 'Download failed', { error: e.message });
131
+ reject(e);
132
+ });
133
+ });
134
+ }
135
+
136
+ function isProcessRunning(pidOrName) {
137
+ try {
138
+ const plat = getPlatformKey();
139
+ if (plat === 'win32') {
140
+ const output = execSync('tasklist /FO CSV /NH', { encoding: 'utf8' });
141
+ const lines = output.split('\n').filter(Boolean);
142
+ return lines.some(line => {
143
+ const parts = line.split(',').map(p => p.trim().replace(/^"/, '').replace(/"$/, ''));
144
+ return parts[0] === 'plugkit.exe' || parts[0] === pidOrName;
145
+ });
146
+ } else {
147
+ try {
148
+ execSync(`ps -p ${pidOrName} > /dev/null 2>&1`);
149
+ return true;
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+ } catch (e) {
155
+ return false;
156
+ }
157
+ }
158
+
159
+ function killExistingPlugkit() {
160
+ try {
161
+ const plat = getPlatformKey();
162
+ if (plat === 'win32') {
163
+ execSync('taskkill /IM plugkit.exe /F 2>nul || true', { shell: true });
164
+ emitBootstrapEvent('info', 'Killed existing plugkit process on Windows');
165
+ } else {
166
+ execSync('pkill -f "plugkit" || true', { shell: true });
167
+ emitBootstrapEvent('info', 'Killed existing plugkit process on Unix');
168
+ }
169
+ } catch (e) {
170
+ emitBootstrapEvent('warn', 'Failed to kill existing plugkit', { error: e.message });
171
+ }
172
+ }
173
+
174
+ async function ensureBinaryWritable(filePath) {
175
+ try {
176
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
177
+ if (fs.existsSync(filePath)) {
178
+ fs.unlinkSync(filePath);
179
+ }
180
+ } catch (e) {
181
+ throw new Error(`Cannot write to ${filePath}: ${e.message}`);
182
+ }
183
+ }
184
+
185
+ async function writeBinaryWithRetry(filePath, data, maxRetries = 3) {
186
+ let lastErr;
187
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
188
+ try {
189
+ await ensureBinaryWritable(filePath);
190
+ fs.writeFileSync(filePath, data);
191
+ fs.chmodSync(filePath, 0o755);
192
+ emitBootstrapEvent('info', 'Binary written successfully', { path: filePath });
193
+ return;
194
+ } catch (e) {
195
+ lastErr = e;
196
+ emitBootstrapEvent('warn', `Write attempt ${attempt + 1} failed`, { error: e.message });
197
+ if (attempt < maxRetries - 1) {
198
+ await new Promise(r => setTimeout(r, 50 * Math.pow(2, attempt)));
199
+ }
200
+ }
201
+ }
202
+ throw lastErr;
203
+ }
204
+
205
+ async function verifyBinaryHealth(filePath) {
206
+ try {
207
+ if (process.platform === 'win32') {
208
+ execSync(`"${filePath}" health > nul 2>&1`, { timeout: 5000, shell: true });
209
+ } else {
210
+ execSync(`"${filePath}" health > /dev/null 2>&1`, { timeout: 5000 });
211
+ }
212
+ emitBootstrapEvent('info', 'Binary health check passed');
213
+ return true;
214
+ } catch (e) {
215
+ emitBootstrapEvent('warn', 'Binary health check failed', { error: e.message });
216
+ return false;
217
+ }
218
+ }
219
+
220
+ async function spawnPlugkitWatcher(filePath) {
221
+ try {
222
+ emitBootstrapEvent('info', 'Spawning plugkit watcher daemon');
223
+
224
+ const cmd = process.platform === 'win32' ? filePath : filePath;
225
+ const proc = spawn(cmd, ['watch', '--once=false'], {
226
+ detached: true,
227
+ stdio: 'ignore',
228
+ windowsHide: true,
229
+ });
230
+
231
+ const pid = proc.pid;
232
+ proc.unref();
233
+
234
+ emitBootstrapEvent('info', 'Plugkit watcher spawned', { pid });
235
+ return pid;
236
+ } catch (e) {
237
+ emitBootstrapEvent('error', 'Failed to spawn plugkit watcher', { error: e.message });
238
+ throw e;
239
+ }
240
+ }
241
+
242
+ async function bootstrapPlugkit() {
243
+ const startTime = Date.now();
244
+
245
+ try {
246
+ emitBootstrapEvent('info', 'Bootstrap started');
247
+
248
+ const { version: manifestVersion, expectedHash } = readManifest();
249
+ const installedVersion = getInstalledVersion();
250
+ const plugkitPath = getPlugkitPath();
251
+
252
+ const versionMismatch = installedVersion !== manifestVersion;
253
+ const binaryMissing = !fs.existsSync(plugkitPath);
254
+
255
+ if (!binaryMissing && !versionMismatch) {
256
+ emitBootstrapEvent('info', 'Binary up-to-date', { version: installedVersion });
257
+
258
+ if (isProcessRunning('plugkit')) {
259
+ emitBootstrapEvent('info', 'Plugkit watcher already running');
260
+ const statusPayload = {
261
+ ok: true,
262
+ version: installedVersion,
263
+ status: 'running',
264
+ timestamp: new Date().toISOString(),
265
+ durationMs: Date.now() - startTime,
266
+ };
267
+ fs.mkdirSync(path.dirname(BOOTSTRAP_STATUS_FILE), { recursive: true });
268
+ fs.writeFileSync(BOOTSTRAP_STATUS_FILE, JSON.stringify(statusPayload, null, 2));
269
+ return { ok: true };
270
+ }
271
+ }
272
+
273
+ if (binaryMissing || versionMismatch) {
274
+ emitBootstrapEvent('info', 'Downloading binary', {
275
+ reason: binaryMissing ? 'missing' : 'version-mismatch',
276
+ version: manifestVersion,
277
+ });
278
+
279
+ let binaryData;
280
+ try {
281
+ binaryData = await downloadPlugkitBinary(manifestVersion);
282
+ } catch (downloadErr) {
283
+ emitBootstrapEvent('error', 'Download failed, checking for cached binary', {
284
+ error: downloadErr.message,
285
+ fallback: fs.existsSync(plugkitPath),
286
+ });
287
+
288
+ if (!fs.existsSync(plugkitPath)) {
289
+ throw downloadErr;
290
+ }
291
+ emitBootstrapEvent('info', 'Using cached binary as fallback');
292
+ binaryData = null;
293
+ }
294
+
295
+ if (binaryData) {
296
+ const downloadedHash = crypto.createHash('sha256').update(binaryData).digest('hex');
297
+ if (downloadedHash !== expectedHash) {
298
+ throw new Error(`Hash mismatch: got ${downloadedHash}, expected ${expectedHash}`);
299
+ }
300
+
301
+ killExistingPlugkit();
302
+ await writeBinaryWithRetry(plugkitPath, binaryData);
303
+
304
+ fs.mkdirSync(path.dirname(PLUGKIT_VERSION_FILE), { recursive: true });
305
+ fs.writeFileSync(PLUGKIT_VERSION_FILE, manifestVersion + '\n');
306
+ emitBootstrapEvent('info', 'Binary installed', { version: manifestVersion });
307
+ }
308
+ }
309
+
310
+ const isHealthy = await verifyBinaryHealth(plugkitPath);
311
+ if (!isHealthy) {
312
+ emitBootstrapEvent('warn', 'Binary health check failed, but proceeding');
313
+ }
314
+
315
+ const watcherRunning = isProcessRunning('plugkit');
316
+ let watcherPid;
317
+ if (!watcherRunning) {
318
+ watcherPid = await spawnPlugkitWatcher(plugkitPath);
319
+ } else {
320
+ watcherPid = 'already-running';
321
+ emitBootstrapEvent('info', 'Watcher already running');
322
+ }
323
+
324
+ const currentVersion = getInstalledVersion() || manifestVersion;
325
+ const statusPayload = {
326
+ ok: true,
327
+ version: currentVersion,
328
+ watcherPid,
329
+ timestamp: new Date().toISOString(),
330
+ durationMs: Date.now() - startTime,
331
+ };
332
+
333
+ fs.mkdirSync(path.dirname(BOOTSTRAP_STATUS_FILE), { recursive: true });
334
+ fs.writeFileSync(BOOTSTRAP_STATUS_FILE, JSON.stringify(statusPayload, null, 2));
335
+
336
+ emitBootstrapEvent('info', 'Bootstrap completed successfully', statusPayload);
337
+ return { ok: true };
338
+ } catch (err) {
339
+ const errorPayload = {
340
+ ok: false,
341
+ error: err.message,
342
+ timestamp: new Date().toISOString(),
343
+ durationMs: Date.now() - startTime,
344
+ stack: err.stack,
345
+ };
346
+
347
+ fs.mkdirSync(path.dirname(BOOTSTRAP_ERROR_FILE), { recursive: true });
348
+ fs.writeFileSync(BOOTSTRAP_ERROR_FILE, JSON.stringify(errorPayload, null, 2));
349
+
350
+ emitBootstrapEvent('error', 'Bootstrap failed', errorPayload);
351
+ console.error(`[skill-bootstrap] ${err.message}`);
352
+
353
+ return { ok: false, error: err.message };
354
+ }
355
+ }
356
+
357
+ async function checkPortReachable(host, port, timeoutMs = 500) {
358
+ try {
359
+ const result = await spool.execSpool('health', 'health', { timeoutMs, sessionId: process.env.CLAUDE_SESSION_ID || 'unknown' });
360
+ return !!(result && result.ok);
361
+ } catch (e) {
362
+ return false;
363
+ }
364
+ }
365
+
366
+ async function bootstrapAcptoapi() {
367
+ const port = 4800;
368
+ const running = await checkPortReachable('127.0.0.1', port);
369
+ if (running) return { ok: true, status: 'already-running' };
370
+
371
+ emitBootstrapEvent('info', 'Spawning acptoapi daemon');
372
+ try {
373
+ const child = spawn('bun', ['x', 'acptoapi@latest'], {
374
+ detached: true,
375
+ stdio: 'ignore',
376
+ windowsHide: true,
377
+ });
378
+ child.unref();
379
+ emitBootstrapEvent('info', 'acptoapi spawned', { pid: child.pid });
380
+ return { ok: true, status: 'spawned', pid: child.pid };
381
+ } catch (e) {
382
+ emitBootstrapEvent('error', 'Failed to spawn acptoapi', { error: e.message });
383
+ return { ok: false, error: e.message };
384
+ }
385
+ }
386
+
387
+ async function getSnapshot(sessionId, cwd) {
388
+ const plugkitPath = getPlugkitPath();
389
+ if (!fs.existsSync(plugkitPath)) {
390
+ return { git: { ok: false }, tasks: [], error: 'plugkit not found' };
391
+ }
392
+
393
+ try {
394
+ const sid = sessionId || process.env.CLAUDE_SESSION_ID || 'default';
395
+ const c = cwd || process.cwd();
396
+ const cmd = `"${plugkitPath}" snapshot --session "${sid}" --cwd "${c}"`;
397
+ const output = execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
398
+ return JSON.parse(output);
399
+ } catch (e) {
400
+ emitBootstrapEvent('warn', 'Failed to get snapshot', { error: e.message });
401
+ return { git: { ok: false }, tasks: [], error: e.message };
402
+ }
403
+ }
404
+
405
+ module.exports = {
406
+ bootstrapPlugkit,
407
+ bootstrapAcptoapi,
408
+ getSnapshot,
409
+ checkPortReachable
410
+ };
@@ -0,0 +1,75 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ async function dispatchSpool(cmd, lang, body, timeoutMs, sessionId) {
6
+ const taskId = `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
7
+ const langDir = lang.match(/^(nodejs|python|bash|typescript|go|rust|c|cpp|java|deno)$/) ? lang : 'nodejs';
8
+ const ext = {
9
+ nodejs: 'js',
10
+ python: 'py',
11
+ bash: 'sh',
12
+ typescript: 'ts',
13
+ go: 'go',
14
+ rust: 'rs',
15
+ c: 'c',
16
+ cpp: 'cpp',
17
+ java: 'java',
18
+ deno: 'ts'
19
+ }[langDir] || 'js';
20
+
21
+ const inDir = path.join(process.cwd(), '.gm', 'exec-spool', 'in', langDir);
22
+ const outDir = path.join(process.cwd(), '.gm', 'exec-spool', 'out');
23
+ const inFile = path.join(inDir, `${taskId}.${ext}`);
24
+ const jsonFile = path.join(outDir, `${taskId}.json`);
25
+
26
+ fs.mkdirSync(inDir, { recursive: true });
27
+ fs.mkdirSync(outDir, { recursive: true });
28
+
29
+ const code = sessionId ? `const SESSION_ID = '${sessionId}';\n${body}` : body;
30
+ fs.writeFileSync(inFile, code, 'utf8');
31
+
32
+ return pollForCompletion(jsonFile, timeoutMs, taskId);
33
+ }
34
+
35
+ async function pollForCompletion(jsonFile, timeoutMs, taskId) {
36
+ const start = Date.now();
37
+ const interval = 50;
38
+
39
+ while (Date.now() - start < timeoutMs) {
40
+ if (fs.existsSync(jsonFile)) {
41
+ try {
42
+ const metadata = JSON.parse(fs.readFileSync(jsonFile, 'utf8'));
43
+ const outFile = jsonFile.replace(/\.json$/, '.out');
44
+ const errFile = jsonFile.replace(/\.json$/, '.err');
45
+ const stdout = fs.existsSync(outFile) ? fs.readFileSync(outFile, 'utf8') : '';
46
+ const stderr = fs.existsSync(errFile) ? fs.readFileSync(errFile, 'utf8') : '';
47
+ return {
48
+ ok: metadata.exitCode === 0 && !metadata.timedOut,
49
+ exitCode: metadata.exitCode,
50
+ stdout,
51
+ stderr,
52
+ durationMs: metadata.durationMs,
53
+ taskId,
54
+ timedOut: metadata.timedOut || false
55
+ };
56
+ } catch (e) {
57
+ await new Promise(r => setTimeout(r, interval));
58
+ }
59
+ } else {
60
+ await new Promise(r => setTimeout(r, interval));
61
+ }
62
+ }
63
+
64
+ return {
65
+ ok: false,
66
+ exitCode: -1,
67
+ stdout: '',
68
+ stderr: `[spool dispatch timeout after ${timeoutMs}ms]`,
69
+ durationMs: Date.now() - start,
70
+ taskId,
71
+ timedOut: true
72
+ };
73
+ }
74
+
75
+ module.exports = { dispatchSpool };
package/lib/spool.js ADDED
@@ -0,0 +1,201 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ function getSpoolBaseDir() {
6
+ const cwd = process.cwd();
7
+ return path.join(cwd, '.gm', 'exec-spool');
8
+ }
9
+
10
+ function generateTaskId() {
11
+ return `${Date.now()}-${Math.random().toString(16).slice(2, 8)}`;
12
+ }
13
+
14
+ function validateLang(lang) {
15
+ const valid = ['nodejs', 'python', 'bash', 'typescript', 'go', 'rust', 'c', 'cpp', 'java', 'deno'];
16
+ return valid.includes(lang) ? lang : 'nodejs';
17
+ }
18
+
19
+ function getExtForLang(lang) {
20
+ const langExt = {
21
+ nodejs: 'js',
22
+ python: 'py',
23
+ bash: 'sh',
24
+ typescript: 'ts',
25
+ go: 'go',
26
+ rust: 'rs',
27
+ c: 'c',
28
+ cpp: 'cpp',
29
+ java: 'java',
30
+ deno: 'ts'
31
+ };
32
+ return langExt[lang] || 'js';
33
+ }
34
+
35
+ function validateVerb(verb) {
36
+ const valid = ['codesearch', 'recall', 'memorize', 'wait', 'sleep', 'status', 'close', 'browser', 'runner', 'type', 'kill-port', 'forget', 'feedback', 'learn-status', 'learn-debug', 'learn-build', 'discipline', 'pause', 'health'];
37
+ return valid.includes(verb) ? verb : 'status';
38
+ }
39
+
40
+ function writeSpool(body, lang = 'nodejs', options = {}) {
41
+ const validLang = validateLang(lang);
42
+ const ext = getExtForLang(validLang);
43
+ const taskId = options.taskId || generateTaskId();
44
+
45
+ const baseDir = getSpoolBaseDir();
46
+ const inDir = path.join(baseDir, 'in', validLang);
47
+ const inFile = path.join(inDir, `${taskId}.${ext}`);
48
+
49
+ fs.mkdirSync(inDir, { recursive: true });
50
+
51
+ const sessionId = options.sessionId || process.env.CLAUDE_SESSION_ID;
52
+ const code = sessionId ? `const SESSION_ID = '${sessionId}';\n${body}` : body;
53
+
54
+ fs.writeFileSync(inFile, code, 'utf8');
55
+
56
+ return {
57
+ id: taskId,
58
+ path: inFile,
59
+ lang: validLang,
60
+ ext
61
+ };
62
+ }
63
+
64
+ function writeSpoolVerb(body, verb, options = {}) {
65
+ const validVerb = validateVerb(verb);
66
+ const taskId = options.taskId || generateTaskId();
67
+ const baseDir = getSpoolBaseDir();
68
+ const inDir = path.join(baseDir, 'in', validVerb);
69
+ const inFile = path.join(inDir, `${taskId}.txt`);
70
+ fs.mkdirSync(inDir, { recursive: true });
71
+ fs.writeFileSync(inFile, body, 'utf8');
72
+ return { id: taskId, path: inFile, verb: validVerb };
73
+ }
74
+
75
+ function readSpoolOutput(id) {
76
+ const baseDir = getSpoolBaseDir();
77
+ const outDir = path.join(baseDir, 'out');
78
+
79
+ const outFile = path.join(outDir, `${id}.out`);
80
+ const errFile = path.join(outDir, `${id}.err`);
81
+ const jsonFile = path.join(outDir, `${id}.json`);
82
+
83
+ const stdout = fs.existsSync(outFile) ? fs.readFileSync(outFile, 'utf8') : '';
84
+ const stderr = fs.existsSync(errFile) ? fs.readFileSync(errFile, 'utf8') : '';
85
+
86
+ let metadata = {};
87
+ if (fs.existsSync(jsonFile)) {
88
+ try {
89
+ metadata = JSON.parse(fs.readFileSync(jsonFile, 'utf8'));
90
+ } catch (e) {
91
+ metadata = { error: 'Failed to parse metadata' };
92
+ }
93
+ }
94
+
95
+ return {
96
+ id,
97
+ stdout,
98
+ stderr,
99
+ metadata,
100
+ exitCode: metadata.exitCode,
101
+ durationMs: metadata.durationMs,
102
+ timedOut: metadata.timedOut || false
103
+ };
104
+ }
105
+
106
+ async function waitForCompletion(id, timeoutMs = 30000) {
107
+ const baseDir = getSpoolBaseDir();
108
+ const outDir = path.join(baseDir, 'out');
109
+ const jsonFile = path.join(outDir, `${id}.json`);
110
+
111
+ const start = Date.now();
112
+ const interval = 50;
113
+
114
+ while (Date.now() - start < timeoutMs) {
115
+ if (fs.existsSync(jsonFile)) {
116
+ try {
117
+ const metadata = JSON.parse(fs.readFileSync(jsonFile, 'utf8'));
118
+ const output = readSpoolOutput(id);
119
+ return {
120
+ ok: metadata.exitCode === 0 && !metadata.timedOut,
121
+ ...output
122
+ };
123
+ } catch (e) {
124
+ await new Promise(r => setTimeout(r, interval));
125
+ }
126
+ } else {
127
+ await new Promise(r => setTimeout(r, interval));
128
+ }
129
+ }
130
+
131
+ const output = readSpoolOutput(id);
132
+ return {
133
+ ok: false,
134
+ ...output,
135
+ timedOut: true,
136
+ stderr: output.stderr + `\n[spool timeout after ${timeoutMs}ms]`
137
+ };
138
+ }
139
+
140
+ async function execSpool(body, lang, options = {}) {
141
+ const timeoutMs = options.timeoutMs || 30000;
142
+ const sessionId = options.sessionId || process.env.CLAUDE_SESSION_ID;
143
+ const task = lang === 'nodejs' || lang === 'bash' ? writeSpool(body, lang, { sessionId }) : writeSpoolVerb(body, lang, {});
144
+ const result = await waitForCompletion(task.id, timeoutMs);
145
+ if (options.cleanup !== false) {
146
+ try { fs.unlinkSync(task.path); } catch (e) {}
147
+ }
148
+ return result;
149
+ }
150
+
151
+ async function execCodesearch(query, options = {}) { return execSpool(query, 'codesearch', options); }
152
+ async function execRecall(query, options = {}) { return execSpool(query, 'recall', options); }
153
+ async function execMemorize(fact, options = {}) { return execSpool(fact, 'memorize', options); }
154
+
155
+ function getAllOutputs() {
156
+ const baseDir = getSpoolBaseDir();
157
+ const outDir = path.join(baseDir, 'out');
158
+
159
+ if (!fs.existsSync(outDir)) {
160
+ return [];
161
+ }
162
+
163
+ const files = fs.readdirSync(outDir);
164
+ const taskIds = new Set();
165
+
166
+ files.forEach(file => {
167
+ const match = file.match(/^(.+?)\.(out|err|json)$/);
168
+ if (match) {
169
+ taskIds.add(match[1]);
170
+ }
171
+ });
172
+
173
+ return Array.from(taskIds).map(id => readSpoolOutput(id));
174
+ }
175
+
176
+ async function getEvents(sessionId, cwd) {
177
+ try {
178
+ const { getSnapshot } = require('./skill-bootstrap');
179
+ return await getSnapshot(sessionId, cwd);
180
+ } catch (e) {
181
+ return { error: e.message };
182
+ }
183
+ }
184
+
185
+ module.exports = {
186
+ writeSpool,
187
+ writeSpoolVerb,
188
+ readSpoolOutput,
189
+ waitForCompletion,
190
+ execSpool,
191
+ execCodesearch,
192
+ execRecall,
193
+ execMemorize,
194
+ getAllOutputs,
195
+ getSpoolBaseDir,
196
+ generateTaskId,
197
+ validateLang,
198
+ getExtForLang,
199
+ validateVerb,
200
+ getEvents
201
+ };
package/manifest.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  name: gm
2
- version: 2.0.1066
2
+ version: 2.0.1067
3
3
  description: State machine agent with hooks, skills, and automated git enforcement
4
4
  author: AnEntrypoint
5
5