gm-skill 2.0.1867 → 2.0.1868
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/README.md +11 -10
- package/gm-plugkit/instructions/entry.md +1 -1
- package/gm-plugkit/package.json +1 -2
- package/gm-plugkit/plugkit-wasm-wrapper.js +34 -31
- package/gm.json +1 -1
- package/package.json +1 -7
- package/skills/gm/SKILL.md +1 -1
- package/agents/gm.md +0 -22
- package/agents/memorize.md +0 -100
- package/agents/research-worker.md +0 -36
- package/agents/textprocessing.md +0 -47
- package/bin/gm-shell-validate.js +0 -143
- package/bin/gm-validate.js +0 -343
- package/gm-plugkit/lang-host-runner.js +0 -48
- package/lang/ssh.js +0 -187
- package/lib/skill-bootstrap.js +0 -935
- package/lib/spool.js +0 -170
- package/scripts/run-hook.sh +0 -6
package/lib/skill-bootstrap.js
DELETED
|
@@ -1,935 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const https = require('https');
|
|
4
|
-
const { execSync, execFileSync, spawn } = require('child_process');
|
|
5
|
-
const crypto = require('crypto');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const spool = require('./spool.js');
|
|
8
|
-
|
|
9
|
-
function resolveToolsDir() {
|
|
10
|
-
const primary = path.join(os.homedir(), '.gm-tools');
|
|
11
|
-
const fallback = path.join(os.homedir(), '.claude', 'gm-tools');
|
|
12
|
-
if (fs.existsSync(primary)) return primary;
|
|
13
|
-
if (fs.existsSync(fallback)) return fallback;
|
|
14
|
-
return primary;
|
|
15
|
-
}
|
|
16
|
-
const PLUGKIT_TOOLS_DIR = resolveToolsDir();
|
|
17
|
-
const PLUGKIT_VERSION_FILE = path.join(PLUGKIT_TOOLS_DIR, 'plugkit.version');
|
|
18
|
-
const PLUGKIT_WASM_PATH = path.join(PLUGKIT_TOOLS_DIR, 'plugkit.wasm');
|
|
19
|
-
const PLUGKIT_WASM_WRAPPER = path.join(PLUGKIT_TOOLS_DIR, 'plugkit-wasm-wrapper.js');
|
|
20
|
-
const PLUGKIT_SUPERVISOR = path.join(PLUGKIT_TOOLS_DIR, 'plugkit-supervisor.js');
|
|
21
|
-
const BOOTSTRAP_STATUS_FILE = path.join(os.homedir(), '.gm', 'bootstrap-status.json');
|
|
22
|
-
const BOOTSTRAP_ERROR_FILE = path.join(os.homedir(), '.gm', 'bootstrap-error.json');
|
|
23
|
-
const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
|
|
24
|
-
|
|
25
|
-
function getPlugkitPath() {
|
|
26
|
-
return PLUGKIT_WASM_PATH;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function emitBootstrapEvent(severity, message, details) {
|
|
30
|
-
try {
|
|
31
|
-
const date = new Date().toISOString().split('T')[0];
|
|
32
|
-
const logDir = path.join(LOG_DIR, date);
|
|
33
|
-
if (!fs.existsSync(logDir)) {
|
|
34
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
35
|
-
}
|
|
36
|
-
const logFile = path.join(logDir, 'bootstrap.jsonl');
|
|
37
|
-
const entry = {
|
|
38
|
-
ts: new Date().toISOString(),
|
|
39
|
-
severity,
|
|
40
|
-
message,
|
|
41
|
-
...details,
|
|
42
|
-
};
|
|
43
|
-
fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
|
|
44
|
-
} catch (e) {
|
|
45
|
-
console.error(`[bootstrap] Failed to emit event: ${e.message}`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function resolveFromCandidates(candidates, requireResolveId) {
|
|
50
|
-
for (const c of candidates) {
|
|
51
|
-
if (c && fs.existsSync(c)) return c;
|
|
52
|
-
}
|
|
53
|
-
if (requireResolveId) {
|
|
54
|
-
try {
|
|
55
|
-
const resolved = require.resolve(requireResolveId);
|
|
56
|
-
if (fs.existsSync(resolved)) return resolved;
|
|
57
|
-
} catch (e) {
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function readManifest() {
|
|
64
|
-
try {
|
|
65
|
-
const gmJsonPath = resolveFromCandidates([
|
|
66
|
-
path.join(__dirname, '..', 'gm.json'),
|
|
67
|
-
path.join(__dirname, '..', '..', 'gm.json'),
|
|
68
|
-
], 'gm-skill/gm.json');
|
|
69
|
-
if (!gmJsonPath) {
|
|
70
|
-
throw new Error('gm.json not found relative to skill-bootstrap.js');
|
|
71
|
-
}
|
|
72
|
-
const gm = JSON.parse(fs.readFileSync(gmJsonPath, 'utf8'));
|
|
73
|
-
const version = gm.plugkitVersion;
|
|
74
|
-
|
|
75
|
-
const sha256Path = resolveFromCandidates([
|
|
76
|
-
path.join(__dirname, '..', 'bin', 'plugkit.wasm.sha256'),
|
|
77
|
-
path.join(__dirname, '..', '..', 'bin', 'plugkit.wasm.sha256'),
|
|
78
|
-
], 'gm-skill/bin/plugkit.wasm.sha256');
|
|
79
|
-
if (!sha256Path) {
|
|
80
|
-
throw new Error('bin/plugkit.wasm.sha256 not found relative to skill-bootstrap.js');
|
|
81
|
-
}
|
|
82
|
-
const sha256Content = fs.readFileSync(sha256Path, 'utf8').trim();
|
|
83
|
-
const expectedHash = sha256Content.split(/\s+/)[0];
|
|
84
|
-
|
|
85
|
-
return { version, expectedHash };
|
|
86
|
-
} catch (e) {
|
|
87
|
-
emitBootstrapEvent('error', 'Failed to read manifest', { error: e.message });
|
|
88
|
-
throw e;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function getInstalledVersion() {
|
|
93
|
-
try {
|
|
94
|
-
if (fs.existsSync(PLUGKIT_VERSION_FILE)) {
|
|
95
|
-
return fs.readFileSync(PLUGKIT_VERSION_FILE, 'utf8').trim();
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
98
|
-
} catch (e) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function computeFileHash(filePath) {
|
|
104
|
-
const content = fs.readFileSync(filePath);
|
|
105
|
-
return crypto.createHash('sha256').update(content).digest('hex');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function httpGet(url, timeoutMs, redirectsLeft = 5) {
|
|
109
|
-
return new Promise((resolve, reject) => {
|
|
110
|
-
const req = https.get(url, { timeout: timeoutMs, headers: { 'accept': 'application/json', 'user-agent': 'gm-skill-bootstrap' } }, (res) => {
|
|
111
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
112
|
-
res.resume();
|
|
113
|
-
if (redirectsLeft <= 0) { reject(new Error(`too many redirects ${url}`)); return; }
|
|
114
|
-
httpGet(res.headers.location, timeoutMs, redirectsLeft - 1).then(resolve, reject);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
if (res.statusCode !== 200) {
|
|
118
|
-
res.resume();
|
|
119
|
-
reject(new Error(`HTTP ${res.statusCode} ${url}`));
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const chunks = [];
|
|
123
|
-
res.on('data', (c) => chunks.push(c));
|
|
124
|
-
res.on('end', () => resolve(Buffer.concat(chunks)));
|
|
125
|
-
res.on('error', reject);
|
|
126
|
-
});
|
|
127
|
-
req.on('timeout', () => { req.destroy(new Error(`timeout ${timeoutMs}ms ${url}`)); });
|
|
128
|
-
req.on('error', reject);
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function getLatestRemoteVersion() {
|
|
133
|
-
let version = null;
|
|
134
|
-
let source = null;
|
|
135
|
-
try {
|
|
136
|
-
const buf = await httpGet('https://registry.npmjs.org/plugkit-wasm/latest', 3000);
|
|
137
|
-
const pkg = JSON.parse(buf.toString('utf-8'));
|
|
138
|
-
if (pkg && pkg.version) {
|
|
139
|
-
version = pkg.version;
|
|
140
|
-
source = 'npm-plugkit-wasm';
|
|
141
|
-
}
|
|
142
|
-
} catch (e) {
|
|
143
|
-
emitBootstrapEvent('warn', 'npm plugkit-wasm lookup failed', { error: e.message });
|
|
144
|
-
}
|
|
145
|
-
if (!version) {
|
|
146
|
-
try {
|
|
147
|
-
const buf = await httpGet('https://registry.npmjs.org/gm-plugkit/latest', 3000);
|
|
148
|
-
const pkg = JSON.parse(buf.toString('utf-8'));
|
|
149
|
-
if (pkg && pkg.plugkitVersion) {
|
|
150
|
-
version = pkg.plugkitVersion;
|
|
151
|
-
source = 'npm-gm-plugkit';
|
|
152
|
-
} else if (pkg && pkg.version) {
|
|
153
|
-
version = pkg.version;
|
|
154
|
-
source = 'npm-gm-plugkit-fallback';
|
|
155
|
-
}
|
|
156
|
-
} catch (e) {
|
|
157
|
-
emitBootstrapEvent('warn', 'npm gm-plugkit fallback lookup failed', { error: e.message });
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (!version) {
|
|
161
|
-
emitBootstrapEvent('warn', 'All latest-version lookups failed; falling back to manifest');
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
let sha = '';
|
|
165
|
-
try {
|
|
166
|
-
const shaBuf = await httpGet(`https://github.com/AnEntrypoint/plugkit-bin/releases/download/v${version}/plugkit.wasm.sha256`, 3000);
|
|
167
|
-
sha = shaBuf.toString('utf-8').trim().split(/\s+/)[0];
|
|
168
|
-
} catch (e) {
|
|
169
|
-
emitBootstrapEvent('warn', 'sha fetch failed; will verify after download', { error: e.message, version });
|
|
170
|
-
}
|
|
171
|
-
emitBootstrapEvent('info', 'Resolved latest plugkit version', { version, source, hasSha: Boolean(sha) });
|
|
172
|
-
return { version, sha, source };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function detectBuildToolConfigs(cwd) {
|
|
176
|
-
const detectors = [
|
|
177
|
-
{ tool: 'vite', patterns: ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.cjs'] },
|
|
178
|
-
{ tool: 'vitest', patterns: ['vitest.config.js', 'vitest.config.ts', 'vitest.config.mjs'] },
|
|
179
|
-
{ tool: 'astro', patterns: ['astro.config.js', 'astro.config.ts', 'astro.config.mjs'] },
|
|
180
|
-
{ tool: 'next', patterns: ['next.config.js', 'next.config.ts', 'next.config.mjs', 'next.config.cjs'] },
|
|
181
|
-
{ tool: 'jest', patterns: ['jest.config.js', 'jest.config.ts', 'jest.config.mjs', 'jest.config.cjs', 'jest.config.json'] },
|
|
182
|
-
{ tool: 'webpack', patterns: ['webpack.config.js', 'webpack.config.ts', 'webpack.config.cjs', 'webpack.config.mjs'] },
|
|
183
|
-
{ tool: 'rollup', patterns: ['rollup.config.js', 'rollup.config.ts', 'rollup.config.mjs'] },
|
|
184
|
-
{ tool: 'nuxt', patterns: ['nuxt.config.js', 'nuxt.config.ts', 'nuxt.config.mjs'] },
|
|
185
|
-
];
|
|
186
|
-
const found = [];
|
|
187
|
-
for (const d of detectors) {
|
|
188
|
-
for (const p of d.patterns) {
|
|
189
|
-
const fp = path.join(cwd, p);
|
|
190
|
-
if (fs.existsSync(fp)) {
|
|
191
|
-
found.push({ tool: d.tool, file: p, fullPath: fp });
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return found;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function patchJestJsonConfig(filePath) {
|
|
200
|
-
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
201
|
-
const cfg = JSON.parse(raw);
|
|
202
|
-
const want = '<rootDir>/.gm/';
|
|
203
|
-
const existing = Array.isArray(cfg.watchPathIgnorePatterns) ? cfg.watchPathIgnorePatterns : [];
|
|
204
|
-
if (existing.includes(want)) return false;
|
|
205
|
-
cfg.watchPathIgnorePatterns = [...existing, want];
|
|
206
|
-
fs.writeFileSync(filePath, JSON.stringify(cfg, null, 2) + '\n');
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function patchPackageJsonJest(cwd) {
|
|
211
|
-
const pkgPath = path.join(cwd, 'package.json');
|
|
212
|
-
if (!fs.existsSync(pkgPath)) return null;
|
|
213
|
-
const raw = fs.readFileSync(pkgPath, 'utf-8');
|
|
214
|
-
let pkg;
|
|
215
|
-
try { pkg = JSON.parse(raw); } catch (_) { return null; }
|
|
216
|
-
if (!pkg.jest || typeof pkg.jest !== 'object') return null;
|
|
217
|
-
const want = '<rootDir>/.gm/';
|
|
218
|
-
const existing = Array.isArray(pkg.jest.watchPathIgnorePatterns) ? pkg.jest.watchPathIgnorePatterns : [];
|
|
219
|
-
if (existing.includes(want)) return false;
|
|
220
|
-
pkg.jest.watchPathIgnorePatterns = [...existing, want];
|
|
221
|
-
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
222
|
-
return true;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function ensureBuildToolIgnores(cwd) {
|
|
226
|
-
try {
|
|
227
|
-
const found = detectBuildToolConfigs(cwd);
|
|
228
|
-
const automated = [];
|
|
229
|
-
const advisory = [];
|
|
230
|
-
|
|
231
|
-
for (const f of found) {
|
|
232
|
-
if (f.tool === 'jest' && f.file.endsWith('.json')) {
|
|
233
|
-
try {
|
|
234
|
-
const changed = patchJestJsonConfig(f.fullPath);
|
|
235
|
-
automated.push({ tool: 'jest', file: f.file, changed });
|
|
236
|
-
} catch (e) {
|
|
237
|
-
advisory.push({ ...f, reason: `patch failed: ${e.message}` });
|
|
238
|
-
}
|
|
239
|
-
} else {
|
|
240
|
-
advisory.push(f);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
const pkgChanged = patchPackageJsonJest(cwd);
|
|
246
|
-
if (pkgChanged !== null) automated.push({ tool: 'jest', file: 'package.json#jest', changed: pkgChanged });
|
|
247
|
-
} catch (_) {}
|
|
248
|
-
|
|
249
|
-
const snippets = {
|
|
250
|
-
vite: "server: { watch: { ignored: ['**/.gm/**'] } }",
|
|
251
|
-
vitest: "test: { watchExclude: ['**/.gm/**'] }",
|
|
252
|
-
astro: "vite: { server: { watch: { ignored: ['**/.gm/**'] } } }",
|
|
253
|
-
next: "webpack: (config) => { config.watchOptions = { ...config.watchOptions, ignored: ['**/.gm/**', ...(config.watchOptions?.ignored || [])] }; return config; }",
|
|
254
|
-
jest: "watchPathIgnorePatterns: ['<rootDir>/.gm/']",
|
|
255
|
-
webpack: "watchOptions: { ignored: ['**/.gm/**', '**/node_modules/**'] }",
|
|
256
|
-
rollup: "watch: { exclude: ['.gm/**'] }",
|
|
257
|
-
nuxt: "vite: { server: { watch: { ignored: ['**/.gm/**'] } } }",
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
const gmDir = path.join(cwd, '.gm');
|
|
261
|
-
const advisoryPath = path.join(gmDir, 'build-tool-ignores.md');
|
|
262
|
-
if (advisory.length === 0) {
|
|
263
|
-
try { if (fs.existsSync(advisoryPath)) fs.unlinkSync(advisoryPath); } catch (_) {}
|
|
264
|
-
} else {
|
|
265
|
-
fs.mkdirSync(gmDir, { recursive: true });
|
|
266
|
-
const lines = [
|
|
267
|
-
'# Build-tool ignore advisories',
|
|
268
|
-
'',
|
|
269
|
-
'`.gitignore` already excludes `.gm/`. Tools whose watchers honor `.gitignore` (vite, vitest, astro, nuxt with chokidar defaults) need no further action.',
|
|
270
|
-
'',
|
|
271
|
-
'For watchers that do NOT honor `.gitignore` automatically, paste these snippets into the corresponding config file:',
|
|
272
|
-
'',
|
|
273
|
-
];
|
|
274
|
-
for (const f of advisory) {
|
|
275
|
-
const snip = snippets[f.tool];
|
|
276
|
-
if (!snip) continue;
|
|
277
|
-
lines.push(`## ${f.tool} (\`${f.file}\`)`, '', '```', snip, '```', '');
|
|
278
|
-
}
|
|
279
|
-
lines.push('---', '', 'Regenerated each bootstrap. Delete after applying -- file is gitignored.');
|
|
280
|
-
fs.writeFileSync(advisoryPath, lines.join('\n'));
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
emitBootstrapEvent('info', 'Build-tool ignore handling complete', {
|
|
284
|
-
automated: automated.map(a => ({ tool: a.tool, file: a.file, changed: a.changed })),
|
|
285
|
-
advisory: advisory.map(a => ({ tool: a.tool, file: a.file })),
|
|
286
|
-
});
|
|
287
|
-
} catch (e) {
|
|
288
|
-
emitBootstrapEvent('warn', 'ensureBuildToolIgnores failed', { error: e.message });
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
function discoverBundledSkills() {
|
|
294
|
-
const roots = [
|
|
295
|
-
path.join(__dirname, '..', 'skills'),
|
|
296
|
-
path.join(__dirname, '..', '..', 'skills'),
|
|
297
|
-
];
|
|
298
|
-
const root = roots.find(r => { try { return fs.existsSync(r) && fs.statSync(r).isDirectory(); } catch (_) { return false; } });
|
|
299
|
-
if (!root) return [];
|
|
300
|
-
try {
|
|
301
|
-
return fs.readdirSync(root, { withFileTypes: true })
|
|
302
|
-
.filter(e => e.isDirectory())
|
|
303
|
-
.map(e => e.name)
|
|
304
|
-
.filter(name => { try { return fs.existsSync(path.join(root, name, 'SKILL.md')); } catch (_) { return false; } })
|
|
305
|
-
.sort();
|
|
306
|
-
} catch (_) { return []; }
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function ensureSkillMdCurrent() {
|
|
310
|
-
const allRefreshed = [];
|
|
311
|
-
let anySkipped = false;
|
|
312
|
-
for (const skillName of discoverBundledSkills()) {
|
|
313
|
-
try {
|
|
314
|
-
const bundledPath = resolveFromCandidates([
|
|
315
|
-
path.join(__dirname, '..', 'skills', skillName, 'SKILL.md'),
|
|
316
|
-
path.join(__dirname, '..', '..', 'skills', skillName, 'SKILL.md'),
|
|
317
|
-
], `gm-skill/skills/${skillName}/SKILL.md`);
|
|
318
|
-
if (!bundledPath || !fs.existsSync(bundledPath)) {
|
|
319
|
-
emitBootstrapEvent('warn', 'bundled SKILL.md not found; skipping refresh', { skillName });
|
|
320
|
-
anySkipped = true;
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
const bundled = fs.readFileSync(bundledPath, 'utf8');
|
|
324
|
-
const _norm = s => s.replace(/\r\n/g, '\n');
|
|
325
|
-
const bundledHash = crypto.createHash('sha256').update(_norm(bundled)).digest('hex');
|
|
326
|
-
const targets = [
|
|
327
|
-
path.join(os.homedir(), '.agents', 'skills', skillName, 'SKILL.md'),
|
|
328
|
-
path.join(os.homedir(), '.claude', 'skills', skillName, 'SKILL.md'),
|
|
329
|
-
];
|
|
330
|
-
if (skillName === 'gm') {
|
|
331
|
-
for (const legacy of [
|
|
332
|
-
path.join(os.homedir(), '.agents', 'skills', 'gm-skill'),
|
|
333
|
-
path.join(os.homedir(), '.claude', 'skills', 'gm-skill'),
|
|
334
|
-
]) {
|
|
335
|
-
try { if (fs.existsSync(legacy)) fs.rmSync(legacy, { recursive: true, force: true }); } catch (_) {}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
for (const target of targets) {
|
|
339
|
-
try {
|
|
340
|
-
let needsWrite = true;
|
|
341
|
-
if (fs.existsSync(target)) {
|
|
342
|
-
const existing = fs.readFileSync(target, 'utf8');
|
|
343
|
-
const existingHash = crypto.createHash('sha256').update(_norm(existing)).digest('hex');
|
|
344
|
-
if (existingHash === bundledHash) needsWrite = false;
|
|
345
|
-
}
|
|
346
|
-
if (needsWrite) {
|
|
347
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
348
|
-
const tmp = target + '.tmp';
|
|
349
|
-
fs.writeFileSync(tmp, bundled);
|
|
350
|
-
fs.renameSync(tmp, target);
|
|
351
|
-
allRefreshed.push(target);
|
|
352
|
-
}
|
|
353
|
-
} catch (e) {
|
|
354
|
-
emitBootstrapEvent('warn', 'SKILL.md refresh failed for target', { target, error: e.message });
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
} catch (e) {
|
|
358
|
-
emitBootstrapEvent('warn', 'ensureSkillMdCurrent failed for skill', { skillName, error: e.message });
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (allRefreshed.length > 0) {
|
|
362
|
-
emitBootstrapEvent('info', 'SKILL.md refreshed', { targets: allRefreshed });
|
|
363
|
-
}
|
|
364
|
-
return { refreshed: allRefreshed, skipped: anySkipped };
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function writeSessionSidecar(sessionId) {
|
|
368
|
-
try {
|
|
369
|
-
const spool = path.join(process.cwd(), '.gm', 'exec-spool');
|
|
370
|
-
fs.mkdirSync(spool, { recursive: true });
|
|
371
|
-
let sess = sessionId || process.env.CLAUDE_SESSION_ID || process.env.GM_SESSION_ID || '';
|
|
372
|
-
if (!sess) {
|
|
373
|
-
const fallbackFile = path.join(spool, '.session-fallback');
|
|
374
|
-
try { sess = fs.readFileSync(fallbackFile, 'utf8').trim(); } catch (_) {}
|
|
375
|
-
if (!sess) {
|
|
376
|
-
const cwdHash = require('crypto').createHash('sha1').update(process.cwd()).digest('hex').slice(0, 8);
|
|
377
|
-
sess = `machine-${cwdHash}-${Date.now().toString(36)}`;
|
|
378
|
-
try { fs.writeFileSync(fallbackFile, sess); } catch (_) {}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
fs.writeFileSync(path.join(spool, '.session-current'), sess);
|
|
382
|
-
} catch (_) {}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
async function downloadPlugkitBinary(version) {
|
|
386
|
-
const binaryName = 'plugkit.wasm';
|
|
387
|
-
const url = `https://github.com/AnEntrypoint/plugkit-bin/releases/download/v${version}/${binaryName}`;
|
|
388
|
-
emitBootstrapEvent('info', 'Starting WASM download', { version, url });
|
|
389
|
-
try {
|
|
390
|
-
const buf = await httpGet(url, 30000);
|
|
391
|
-
emitBootstrapEvent('info', 'WASM download complete', { bytes: buf.length });
|
|
392
|
-
return buf;
|
|
393
|
-
} catch (e) {
|
|
394
|
-
emitBootstrapEvent('error', 'Download failed', { error: e.message });
|
|
395
|
-
throw e;
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function repoStatePidsForCwd() {
|
|
400
|
-
const pids = [];
|
|
401
|
-
const cwd = process.cwd();
|
|
402
|
-
for (const rel of [path.join('.gm', 'exec-spool', '.supervisor.pid'), path.join('.gm', 'exec-spool', '.watcher.pid')]) {
|
|
403
|
-
try {
|
|
404
|
-
const p = path.join(cwd, rel);
|
|
405
|
-
if (!fs.existsSync(p)) continue;
|
|
406
|
-
const pid = parseInt(fs.readFileSync(p, 'utf8').trim(), 10);
|
|
407
|
-
if (Number.isFinite(pid) && pid > 0) pids.push(String(pid));
|
|
408
|
-
} catch (_) {}
|
|
409
|
-
}
|
|
410
|
-
try {
|
|
411
|
-
const statusPath = path.join(cwd, '.gm', 'exec-spool', '.status.json');
|
|
412
|
-
if (fs.existsSync(statusPath)) {
|
|
413
|
-
const status = JSON.parse(fs.readFileSync(statusPath, 'utf-8'));
|
|
414
|
-
if (Number.isFinite(status.pid) && status.pid > 0) pids.push(String(status.pid));
|
|
415
|
-
}
|
|
416
|
-
} catch (_) {}
|
|
417
|
-
return Array.from(new Set(pids));
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function findPlugkitWasmPids() {
|
|
421
|
-
return repoStatePidsForCwd().filter(pid => {
|
|
422
|
-
try { process.kill(parseInt(pid, 10), 0); return true; } catch (_) { return false; }
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function isProcessRunning() {
|
|
427
|
-
return findPlugkitWasmPids().length > 0;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function pidCommandLineForKillGuard(pid) {
|
|
431
|
-
try {
|
|
432
|
-
if (process.platform === 'win32') {
|
|
433
|
-
return execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId=${Number(pid)}").CommandLine`], { encoding: 'utf8', windowsHide: true, timeout: 5000 }) || '';
|
|
434
|
-
}
|
|
435
|
-
return execFileSync('ps', ['-p', String(pid), '-o', 'args='], { encoding: 'utf8', timeout: 5000 }) || '';
|
|
436
|
-
} catch (_) { return ''; }
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function pidIsPlugkitProcess(pid) {
|
|
440
|
-
return /plugkit-wasm-wrapper\.js|plugkit-supervisor\.js|gm-plugkit[\\\/]supervisor\.js/i.test(pidCommandLineForKillGuard(pid));
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function writeKillAttribution(targetSpoolDir, info) {
|
|
444
|
-
try {
|
|
445
|
-
fs.mkdirSync(targetSpoolDir, { recursive: true });
|
|
446
|
-
fs.writeFileSync(path.join(targetSpoolDir, '.kill-attribution.json'), JSON.stringify({ killer_pid: process.pid, killer_cwd: process.cwd(), killer_script: __filename, ts: Date.now(), ...info }, null, 2));
|
|
447
|
-
} catch (_) {}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function killExistingPlugkit() {
|
|
451
|
-
try {
|
|
452
|
-
const pids = findPlugkitWasmPids();
|
|
453
|
-
if (pids.length === 0) {
|
|
454
|
-
emitBootstrapEvent('info', 'No existing plugkit WASM watcher to kill');
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
const killed = [];
|
|
458
|
-
for (const pid of pids) {
|
|
459
|
-
if (!pidIsPlugkitProcess(pid)) {
|
|
460
|
-
emitBootstrapEvent('warn', 'Kill skipped: pid is not a plugkit process (stale or reused pid)', { pid });
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
writeKillAttribution(path.join(process.cwd(), '.gm', 'exec-spool'), { reason: 'bootstrap-refresh', target_pid: Number(pid), via: 'killExistingPlugkit' });
|
|
464
|
-
try {
|
|
465
|
-
if (process.platform === 'win32') {
|
|
466
|
-
execFileSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true });
|
|
467
|
-
} else {
|
|
468
|
-
execFileSync('kill', ['-9', String(pid)], { stdio: 'ignore' });
|
|
469
|
-
}
|
|
470
|
-
killed.push(pid);
|
|
471
|
-
} catch (e) {
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
emitBootstrapEvent('info', 'Killed existing plugkit WASM watcher', { pids: killed });
|
|
475
|
-
} catch (e) {
|
|
476
|
-
emitBootstrapEvent('warn', 'Failed to kill existing plugkit', { error: e.message });
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
async function ensureBinaryWritable(filePath) {
|
|
481
|
-
try {
|
|
482
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
483
|
-
if (fs.existsSync(filePath)) {
|
|
484
|
-
fs.unlinkSync(filePath);
|
|
485
|
-
}
|
|
486
|
-
} catch (e) {
|
|
487
|
-
throw new Error(`Cannot write to ${filePath}: ${e.message}`);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
async function writeBinaryWithRetry(filePath, data, maxRetries = 3) {
|
|
492
|
-
let lastErr;
|
|
493
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
494
|
-
try {
|
|
495
|
-
await ensureBinaryWritable(filePath);
|
|
496
|
-
fs.writeFileSync(filePath, data);
|
|
497
|
-
fs.chmodSync(filePath, 0o755);
|
|
498
|
-
emitBootstrapEvent('info', 'Binary written successfully', { path: filePath });
|
|
499
|
-
return;
|
|
500
|
-
} catch (e) {
|
|
501
|
-
lastErr = e;
|
|
502
|
-
emitBootstrapEvent('warn', `Write attempt ${attempt + 1} failed`, { error: e.message });
|
|
503
|
-
if (attempt < maxRetries - 1) {
|
|
504
|
-
await new Promise(r => setTimeout(r, 50 * Math.pow(2, attempt)));
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
throw lastErr;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
async function verifyBinaryHealth(filePath) {
|
|
512
|
-
try {
|
|
513
|
-
if (!fs.existsSync(filePath)) {
|
|
514
|
-
throw new Error(`File not found: ${filePath}`);
|
|
515
|
-
}
|
|
516
|
-
const stat = fs.statSync(filePath);
|
|
517
|
-
if (stat.size < 1024) {
|
|
518
|
-
throw new Error(`File too small: ${stat.size} bytes`);
|
|
519
|
-
}
|
|
520
|
-
emitBootstrapEvent('info', 'Binary health check passed', { size: stat.size });
|
|
521
|
-
return true;
|
|
522
|
-
} catch (e) {
|
|
523
|
-
emitBootstrapEvent('warn', 'Binary health check failed', { error: e.message });
|
|
524
|
-
return false;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function openWatcherLog(projectDir) {
|
|
529
|
-
const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
|
|
530
|
-
fs.mkdirSync(spoolDir, { recursive: true });
|
|
531
|
-
const logPath = path.join(spoolDir, '.watcher.log');
|
|
532
|
-
try {
|
|
533
|
-
const stat = fs.statSync(logPath);
|
|
534
|
-
if (stat.size > 10 * 1024 * 1024) {
|
|
535
|
-
const rotated = path.join(spoolDir, '.watcher.log.1');
|
|
536
|
-
try { fs.unlinkSync(rotated); } catch (_) {}
|
|
537
|
-
fs.renameSync(logPath, rotated);
|
|
538
|
-
}
|
|
539
|
-
} catch (_) {}
|
|
540
|
-
const fd = fs.openSync(logPath, 'a');
|
|
541
|
-
const header = `\n--- watcher boot ${new Date().toISOString()} pid=${process.pid} ---\n`;
|
|
542
|
-
try { fs.writeSync(fd, header); } catch (_) {}
|
|
543
|
-
return fd;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function ensureSupervisorInstalled() {
|
|
547
|
-
try {
|
|
548
|
-
let src = null;
|
|
549
|
-
try {
|
|
550
|
-
const gmPlugkit = require('gm-plugkit');
|
|
551
|
-
const base = path.dirname(gmPlugkit.getPath ? gmPlugkit.getPath() : require.resolve('gm-plugkit'));
|
|
552
|
-
const cand = path.join(base, 'supervisor.js');
|
|
553
|
-
if (fs.existsSync(cand)) src = cand;
|
|
554
|
-
} catch (_) {}
|
|
555
|
-
if (!src) {
|
|
556
|
-
src = resolveFromCandidates([
|
|
557
|
-
path.join(__dirname, '..', 'gm-plugkit', 'supervisor.js'),
|
|
558
|
-
path.join(__dirname, '..', '..', 'gm-plugkit', 'supervisor.js'),
|
|
559
|
-
], 'gm-skill/gm-plugkit/supervisor.js');
|
|
560
|
-
}
|
|
561
|
-
if (!src || !fs.existsSync(src)) {
|
|
562
|
-
emitBootstrapEvent('warn', 'bundled plugkit-supervisor.js not found; supervisor unavailable');
|
|
563
|
-
return null;
|
|
564
|
-
}
|
|
565
|
-
fs.mkdirSync(PLUGKIT_TOOLS_DIR, { recursive: true });
|
|
566
|
-
let needsWrite = true;
|
|
567
|
-
if (fs.existsSync(PLUGKIT_SUPERVISOR)) {
|
|
568
|
-
try {
|
|
569
|
-
const a = crypto.createHash('sha256').update(fs.readFileSync(src)).digest('hex');
|
|
570
|
-
const b = crypto.createHash('sha256').update(fs.readFileSync(PLUGKIT_SUPERVISOR)).digest('hex');
|
|
571
|
-
if (a === b) needsWrite = false;
|
|
572
|
-
} catch (_) {}
|
|
573
|
-
}
|
|
574
|
-
if (needsWrite) {
|
|
575
|
-
const tmp = PLUGKIT_SUPERVISOR + '.tmp';
|
|
576
|
-
fs.copyFileSync(src, tmp);
|
|
577
|
-
fs.renameSync(tmp, PLUGKIT_SUPERVISOR);
|
|
578
|
-
emitBootstrapEvent('info', 'plugkit-supervisor.js installed', { target: PLUGKIT_SUPERVISOR });
|
|
579
|
-
}
|
|
580
|
-
return PLUGKIT_SUPERVISOR;
|
|
581
|
-
} catch (e) {
|
|
582
|
-
emitBootstrapEvent('warn', 'ensureSupervisorInstalled failed', { error: e.message });
|
|
583
|
-
return null;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function ensureWrapperInstalled() {
|
|
588
|
-
try {
|
|
589
|
-
let src = null;
|
|
590
|
-
try {
|
|
591
|
-
const gmPlugkit = require('gm-plugkit');
|
|
592
|
-
const base = path.dirname(gmPlugkit.getPath ? gmPlugkit.getPath() : require.resolve('gm-plugkit'));
|
|
593
|
-
const cand = path.join(base, 'plugkit-wasm-wrapper.js');
|
|
594
|
-
if (fs.existsSync(cand)) src = cand;
|
|
595
|
-
} catch (_) {}
|
|
596
|
-
if (!src) {
|
|
597
|
-
src = resolveFromCandidates([
|
|
598
|
-
path.join(__dirname, '..', 'gm-plugkit', 'plugkit-wasm-wrapper.js'),
|
|
599
|
-
path.join(__dirname, '..', '..', 'gm-plugkit', 'plugkit-wasm-wrapper.js'),
|
|
600
|
-
], 'gm-skill/gm-plugkit/plugkit-wasm-wrapper.js');
|
|
601
|
-
}
|
|
602
|
-
if (!src || !fs.existsSync(src)) {
|
|
603
|
-
emitBootstrapEvent('warn', 'bundled plugkit-wasm-wrapper.js not found; wrapper refresh skipped');
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
606
|
-
fs.mkdirSync(PLUGKIT_TOOLS_DIR, { recursive: true });
|
|
607
|
-
let needsWrite = true;
|
|
608
|
-
if (fs.existsSync(PLUGKIT_WASM_WRAPPER)) {
|
|
609
|
-
try {
|
|
610
|
-
const a = crypto.createHash('sha256').update(fs.readFileSync(src)).digest('hex');
|
|
611
|
-
const b = crypto.createHash('sha256').update(fs.readFileSync(PLUGKIT_WASM_WRAPPER)).digest('hex');
|
|
612
|
-
if (a === b) needsWrite = false;
|
|
613
|
-
} catch (_) {}
|
|
614
|
-
}
|
|
615
|
-
if (needsWrite) {
|
|
616
|
-
const tmp = PLUGKIT_WASM_WRAPPER + '.tmp';
|
|
617
|
-
fs.copyFileSync(src, tmp);
|
|
618
|
-
fs.renameSync(tmp, PLUGKIT_WASM_WRAPPER);
|
|
619
|
-
emitBootstrapEvent('info', 'plugkit-wasm-wrapper.js refreshed', { target: PLUGKIT_WASM_WRAPPER });
|
|
620
|
-
}
|
|
621
|
-
return PLUGKIT_WASM_WRAPPER;
|
|
622
|
-
} catch (e) {
|
|
623
|
-
emitBootstrapEvent('warn', 'ensureWrapperInstalled failed', { error: e.message });
|
|
624
|
-
return null;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function findSupervisorPid() {
|
|
629
|
-
try {
|
|
630
|
-
const supervisorPidFile = path.join(process.cwd(), '.gm', 'exec-spool', '.supervisor.pid');
|
|
631
|
-
if (!fs.existsSync(supervisorPidFile)) return null;
|
|
632
|
-
const pid = parseInt(fs.readFileSync(supervisorPidFile, 'utf8').trim(), 10);
|
|
633
|
-
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
634
|
-
try { process.kill(pid, 0); return pid; } catch (_) { return null; }
|
|
635
|
-
} catch (_) { return null; }
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
async function spawnPlugkitWatcher(wasmPath) {
|
|
639
|
-
try {
|
|
640
|
-
emitBootstrapEvent('info', 'Spawning plugkit supervisor');
|
|
641
|
-
|
|
642
|
-
let wrapperPath;
|
|
643
|
-
try {
|
|
644
|
-
const gmPlugkit = require('gm-plugkit');
|
|
645
|
-
wrapperPath = path.join(path.dirname(gmPlugkit.getPath ? gmPlugkit.getPath() : require.resolve('gm-plugkit')), 'plugkit-wasm-wrapper.js');
|
|
646
|
-
} catch (e) {
|
|
647
|
-
emitBootstrapEvent('warn', 'gm-plugkit npm not available, using bundled wrapper', { error: e.message });
|
|
648
|
-
wrapperPath = path.join(path.dirname(wasmPath), 'plugkit-wasm-wrapper.js');
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (!fs.existsSync(wrapperPath)) {
|
|
652
|
-
throw new Error(`WASM wrapper not found at ${wrapperPath}`);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const supervisorPath = ensureSupervisorInstalled();
|
|
656
|
-
ensureWrapperInstalled();
|
|
657
|
-
const projectDir = process.cwd();
|
|
658
|
-
|
|
659
|
-
const existingSupervisor = findSupervisorPid();
|
|
660
|
-
if (existingSupervisor) {
|
|
661
|
-
emitBootstrapEvent('info', 'Plugkit supervisor already running', { pid: existingSupervisor });
|
|
662
|
-
return existingSupervisor;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
if (!supervisorPath) {
|
|
666
|
-
emitBootstrapEvent('warn', 'falling back to direct watcher spawn (no supervisor)');
|
|
667
|
-
const logFd = openWatcherLog(projectDir);
|
|
668
|
-
const proc = spawn(process.execPath, [wrapperPath, 'spool'], {
|
|
669
|
-
detached: true,
|
|
670
|
-
stdio: ['ignore', logFd, logFd],
|
|
671
|
-
windowsHide: true,
|
|
672
|
-
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir },
|
|
673
|
-
...(process.platform === 'win32' ? { creationFlags: 0x08000000 | 0x00000008 } : {}),
|
|
674
|
-
});
|
|
675
|
-
try { fs.closeSync(logFd); } catch (_) {}
|
|
676
|
-
const pid = proc.pid;
|
|
677
|
-
proc.unref();
|
|
678
|
-
emitBootstrapEvent('info', 'Plugkit watcher spawned (unsupervised)', { pid });
|
|
679
|
-
return pid;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const logFd = openWatcherLog(projectDir);
|
|
683
|
-
try { fs.writeSync(logFd, `\n--- supervisor spawn ${new Date().toISOString()} parent=${process.pid} ---\n`); } catch (_) {}
|
|
684
|
-
|
|
685
|
-
const proc = spawn(process.execPath, [supervisorPath], {
|
|
686
|
-
detached: true,
|
|
687
|
-
stdio: ['ignore', logFd, logFd],
|
|
688
|
-
windowsHide: true,
|
|
689
|
-
env: { ...process.env, CLAUDE_PROJECT_DIR: projectDir },
|
|
690
|
-
...(process.platform === 'win32' ? { creationFlags: 0x08000000 | 0x00000008 } : {}),
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
try { fs.closeSync(logFd); } catch (_) {}
|
|
694
|
-
|
|
695
|
-
const pid = proc.pid;
|
|
696
|
-
proc.unref();
|
|
697
|
-
|
|
698
|
-
emitBootstrapEvent('info', 'Plugkit supervisor spawned', { pid, supervisor: supervisorPath, logPath: path.join(projectDir, '.gm', 'exec-spool', '.watcher.log') });
|
|
699
|
-
return pid;
|
|
700
|
-
} catch (e) {
|
|
701
|
-
emitBootstrapEvent('error', 'Failed to spawn plugkit supervisor', { error: e.message });
|
|
702
|
-
throw e;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function ensureNextStepWiring(cwd) {
|
|
707
|
-
const gmDir = path.join(cwd, '.gm');
|
|
708
|
-
try { fs.mkdirSync(gmDir, { recursive: true }); } catch (_) {}
|
|
709
|
-
|
|
710
|
-
const nextStepPath = path.join(gmDir, 'next-step.md');
|
|
711
|
-
const nextStepBody = '# Next step\n\nDispatch the `instruction` verb to plugkit by writing `.gm/exec-spool/in/instruction/<N>.txt` (any unique N) with body `{}` (or `{"prompt":"<user request>"}` on the first dispatch of the turn). Read the response from `.gm/exec-spool/out/<N>.json` and follow the imperative in the `instruction` field.\n\nThis file is auto-rewritten by plugkit on every instruction dispatch.\n';
|
|
712
|
-
try {
|
|
713
|
-
if (!fs.existsSync(nextStepPath)) {
|
|
714
|
-
fs.writeFileSync(nextStepPath, nextStepBody);
|
|
715
|
-
emitBootstrapEvent('info', 'Seeded .gm/next-step.md', { path: nextStepPath });
|
|
716
|
-
}
|
|
717
|
-
} catch (e) {
|
|
718
|
-
emitBootstrapEvent('warn', 'Failed to seed .gm/next-step.md', { error: e.message });
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
722
|
-
try {
|
|
723
|
-
if (!fs.existsSync(claudeMdPath)) {
|
|
724
|
-
fs.writeFileSync(claudeMdPath, '@AGENTS.md\n');
|
|
725
|
-
} else {
|
|
726
|
-
const cur = fs.readFileSync(claudeMdPath, 'utf8');
|
|
727
|
-
const hasLine = cur.split(/\r?\n/).some(l => l.trim() === '@AGENTS.md');
|
|
728
|
-
if (!hasLine) fs.writeFileSync(claudeMdPath, '@AGENTS.md\n' + cur);
|
|
729
|
-
}
|
|
730
|
-
} catch (_) {}
|
|
731
|
-
|
|
732
|
-
const agentsMdPath = path.join(cwd, 'AGENTS.md');
|
|
733
|
-
try {
|
|
734
|
-
if (fs.existsSync(agentsMdPath)) {
|
|
735
|
-
const cur = fs.readFileSync(agentsMdPath, 'utf8');
|
|
736
|
-
const hasLine = cur.split(/\r?\n/).some(l => l.trim() === '@.gm/next-step.md');
|
|
737
|
-
if (!hasLine) {
|
|
738
|
-
const sep = cur.endsWith('\n') ? '' : '\n';
|
|
739
|
-
fs.writeFileSync(agentsMdPath, cur + sep + '\n@.gm/next-step.md\n');
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
} catch (_) {}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
async function bootstrapPlugkit(sessionId, options) {
|
|
746
|
-
const startTime = Date.now();
|
|
747
|
-
const opts = options || {};
|
|
748
|
-
const forceLatest = Boolean(opts.latest);
|
|
749
|
-
|
|
750
|
-
try {
|
|
751
|
-
emitBootstrapEvent('info', 'Bootstrap started', { forceLatest });
|
|
752
|
-
|
|
753
|
-
writeSessionSidecar(sessionId);
|
|
754
|
-
ensureBuildToolIgnores(process.cwd());
|
|
755
|
-
ensureNextStepWiring(process.cwd());
|
|
756
|
-
ensureSkillMdCurrent();
|
|
757
|
-
ensureWrapperInstalled();
|
|
758
|
-
|
|
759
|
-
const manifest = readManifest();
|
|
760
|
-
let targetVersion = manifest.version;
|
|
761
|
-
let expectedHash = manifest.expectedHash;
|
|
762
|
-
|
|
763
|
-
if (forceLatest) {
|
|
764
|
-
const latest = await getLatestRemoteVersion();
|
|
765
|
-
if (latest && latest.version) {
|
|
766
|
-
if (latest.version === manifest.version) {
|
|
767
|
-
expectedHash = latest.sha || expectedHash;
|
|
768
|
-
} else if (latest.sha) {
|
|
769
|
-
targetVersion = latest.version;
|
|
770
|
-
expectedHash = latest.sha;
|
|
771
|
-
emitBootstrapEvent('info', 'forceLatest: using newer remote version', { latest: latest.version, manifest: manifest.version });
|
|
772
|
-
} else {
|
|
773
|
-
emitBootstrapEvent('warn', 'forceLatest: no sha for newer version; staying on pinned manifest version', { latest: latest.version, manifest: manifest.version });
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
const installedVersion = getInstalledVersion();
|
|
779
|
-
const plugkitPath = getPlugkitPath();
|
|
780
|
-
|
|
781
|
-
const manifestVersion = targetVersion;
|
|
782
|
-
const versionMismatch = installedVersion !== targetVersion;
|
|
783
|
-
const binaryMissing = !fs.existsSync(plugkitPath);
|
|
784
|
-
|
|
785
|
-
if (!binaryMissing && !versionMismatch) {
|
|
786
|
-
emitBootstrapEvent('info', 'Binary up-to-date', { version: installedVersion });
|
|
787
|
-
|
|
788
|
-
if (isProcessRunning()) {
|
|
789
|
-
emitBootstrapEvent('info', 'Plugkit watcher already running');
|
|
790
|
-
const statusPayload = {
|
|
791
|
-
ok: true,
|
|
792
|
-
version: installedVersion,
|
|
793
|
-
status: 'running',
|
|
794
|
-
sessionId: sessionId || null,
|
|
795
|
-
timestamp: new Date().toISOString(),
|
|
796
|
-
durationMs: Date.now() - startTime,
|
|
797
|
-
};
|
|
798
|
-
fs.mkdirSync(path.dirname(BOOTSTRAP_STATUS_FILE), { recursive: true });
|
|
799
|
-
fs.writeFileSync(BOOTSTRAP_STATUS_FILE, JSON.stringify(statusPayload, null, 2));
|
|
800
|
-
return { ok: true, binaryPath: PLUGKIT_WASM_PATH };
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
if (binaryMissing || versionMismatch) {
|
|
805
|
-
emitBootstrapEvent('info', 'Downloading binary', {
|
|
806
|
-
reason: binaryMissing ? 'missing' : 'version-mismatch',
|
|
807
|
-
version: manifestVersion,
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
let binaryData;
|
|
811
|
-
try {
|
|
812
|
-
binaryData = await downloadPlugkitBinary(manifestVersion);
|
|
813
|
-
} catch (downloadErr) {
|
|
814
|
-
emitBootstrapEvent('error', 'Download failed, checking for cached binary', {
|
|
815
|
-
error: downloadErr.message,
|
|
816
|
-
fallback: fs.existsSync(plugkitPath),
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
if (!fs.existsSync(plugkitPath)) {
|
|
820
|
-
throw downloadErr;
|
|
821
|
-
}
|
|
822
|
-
emitBootstrapEvent('info', 'Using cached binary as fallback');
|
|
823
|
-
binaryData = null;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
if (binaryData) {
|
|
827
|
-
const downloadedHash = crypto.createHash('sha256').update(binaryData).digest('hex');
|
|
828
|
-
if (expectedHash && downloadedHash !== expectedHash) {
|
|
829
|
-
throw new Error(`Hash mismatch: got ${downloadedHash}, expected ${expectedHash}`);
|
|
830
|
-
}
|
|
831
|
-
if (!expectedHash) {
|
|
832
|
-
emitBootstrapEvent('warn', 'No expected hash; trusting npm-resolved download', { sha: downloadedHash, version: manifestVersion });
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
killExistingPlugkit();
|
|
836
|
-
await writeBinaryWithRetry(plugkitPath, binaryData);
|
|
837
|
-
|
|
838
|
-
fs.mkdirSync(path.dirname(PLUGKIT_VERSION_FILE), { recursive: true });
|
|
839
|
-
fs.writeFileSync(PLUGKIT_VERSION_FILE, manifestVersion + '\n');
|
|
840
|
-
emitBootstrapEvent('info', 'Binary installed', { version: manifestVersion });
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
const isHealthy = await verifyBinaryHealth(plugkitPath);
|
|
845
|
-
if (!isHealthy) {
|
|
846
|
-
emitBootstrapEvent('warn', 'Binary health check failed, but proceeding');
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
const watcherRunning = isProcessRunning();
|
|
850
|
-
let watcherPid;
|
|
851
|
-
if (!watcherRunning) {
|
|
852
|
-
watcherPid = await spawnPlugkitWatcher(PLUGKIT_WASM_PATH);
|
|
853
|
-
} else {
|
|
854
|
-
watcherPid = 'already-running';
|
|
855
|
-
emitBootstrapEvent('info', 'Watcher already running');
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
const currentVersion = getInstalledVersion() || manifestVersion;
|
|
859
|
-
const statusPayload = {
|
|
860
|
-
ok: true,
|
|
861
|
-
version: currentVersion,
|
|
862
|
-
watcherPid,
|
|
863
|
-
sessionId: sessionId || null,
|
|
864
|
-
timestamp: new Date().toISOString(),
|
|
865
|
-
durationMs: Date.now() - startTime,
|
|
866
|
-
};
|
|
867
|
-
|
|
868
|
-
fs.mkdirSync(path.dirname(BOOTSTRAP_STATUS_FILE), { recursive: true });
|
|
869
|
-
fs.writeFileSync(BOOTSTRAP_STATUS_FILE, JSON.stringify(statusPayload, null, 2));
|
|
870
|
-
|
|
871
|
-
emitBootstrapEvent('info', 'Bootstrap completed successfully', statusPayload);
|
|
872
|
-
return { ok: true, binaryPath: PLUGKIT_WASM_PATH };
|
|
873
|
-
} catch (err) {
|
|
874
|
-
const errorPayload = {
|
|
875
|
-
ok: false,
|
|
876
|
-
error: err.message,
|
|
877
|
-
timestamp: new Date().toISOString(),
|
|
878
|
-
durationMs: Date.now() - startTime,
|
|
879
|
-
stack: err.stack,
|
|
880
|
-
};
|
|
881
|
-
|
|
882
|
-
fs.mkdirSync(path.dirname(BOOTSTRAP_ERROR_FILE), { recursive: true });
|
|
883
|
-
fs.writeFileSync(BOOTSTRAP_ERROR_FILE, JSON.stringify(errorPayload, null, 2));
|
|
884
|
-
|
|
885
|
-
emitBootstrapEvent('error', 'Bootstrap failed', errorPayload);
|
|
886
|
-
console.error(`[skill-bootstrap] ${err.message}`);
|
|
887
|
-
|
|
888
|
-
return { ok: false, error: err.message };
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
async function checkPortReachable(host, port, timeoutMs = 500) {
|
|
893
|
-
const net = require('net');
|
|
894
|
-
return new Promise((resolve) => {
|
|
895
|
-
const socket = new net.Socket();
|
|
896
|
-
let done = false;
|
|
897
|
-
const finish = (ok) => {
|
|
898
|
-
if (done) return;
|
|
899
|
-
done = true;
|
|
900
|
-
try { socket.destroy(); } catch (e) {}
|
|
901
|
-
resolve(ok);
|
|
902
|
-
};
|
|
903
|
-
socket.setTimeout(timeoutMs);
|
|
904
|
-
socket.once('connect', () => finish(true));
|
|
905
|
-
socket.once('timeout', () => finish(false));
|
|
906
|
-
socket.once('error', () => finish(false));
|
|
907
|
-
try {
|
|
908
|
-
socket.connect(port, host);
|
|
909
|
-
} catch (e) {
|
|
910
|
-
finish(false);
|
|
911
|
-
}
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
async function getSnapshot(sessionId, cwd) {
|
|
916
|
-
try {
|
|
917
|
-
const sid = sessionId || process.env.CLAUDE_SESSION_ID || 'default';
|
|
918
|
-
const c = cwd || process.cwd();
|
|
919
|
-
const result = await spool.execSpool('snapshot', 'snapshot', { sessionId: sid, cwd: c, timeoutMs: 5000 });
|
|
920
|
-
if (result && typeof result === 'object') return result;
|
|
921
|
-
return { git: { ok: false }, tasks: [], error: 'no snapshot result' };
|
|
922
|
-
} catch (e) {
|
|
923
|
-
emitBootstrapEvent('warn', 'Failed to get snapshot', { error: e.message });
|
|
924
|
-
return { git: { ok: false }, tasks: [], error: e.message };
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
module.exports = {
|
|
929
|
-
bootstrapPlugkit,
|
|
930
|
-
getSnapshot,
|
|
931
|
-
checkPortReachable,
|
|
932
|
-
ensureBuildToolIgnores,
|
|
933
|
-
detectBuildToolConfigs,
|
|
934
|
-
ensureSkillMdCurrent,
|
|
935
|
-
};
|