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/bin/gm-validate.js
DELETED
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const cp = require('child_process');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
|
|
9
|
-
const ROOT = process.cwd();
|
|
10
|
-
const ARGS = new Set(process.argv.slice(2));
|
|
11
|
-
const REQUIRE_NEW_EMBED = ARGS.has('--require-new-embed');
|
|
12
|
-
const SPOOL = path.join(ROOT, '.gm', 'exec-spool');
|
|
13
|
-
const IN = path.join(SPOOL, 'in');
|
|
14
|
-
const OUT = path.join(SPOOL, 'out');
|
|
15
|
-
const PROFILE = path.join(ROOT, '.gm', 'browser-profile');
|
|
16
|
-
const PORTS = path.join(SPOOL, 'browser-ports.json');
|
|
17
|
-
const SESSIONS = path.join(SPOOL, 'browser-sessions.json');
|
|
18
|
-
const STATUS = path.join(SPOOL, '.status.json');
|
|
19
|
-
const WATCHER_LOG = path.join(SPOOL, '.watcher.log');
|
|
20
|
-
const WITNESS_DIR = path.join(ROOT, '.gm', 'witness');
|
|
21
|
-
|
|
22
|
-
function log(...a) { process.stderr.write('[gm-validate] ' + a.join(' ') + '\n'); }
|
|
23
|
-
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
24
|
-
function rmrf(p) { try { fs.rmSync(p, { recursive: true, force: true }); } catch (_) {} }
|
|
25
|
-
function readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return null; } }
|
|
26
|
-
|
|
27
|
-
let DISPATCH_SEQ = 0;
|
|
28
|
-
function nextTask(tag) { DISPATCH_SEQ++; return `validate-${process.pid}-${DISPATCH_SEQ}-${tag}`; }
|
|
29
|
-
|
|
30
|
-
async function dispatch(verb, body, timeoutMs = 60000) {
|
|
31
|
-
const task = nextTask(verb.replace(/[^a-z0-9-]/gi, ''));
|
|
32
|
-
const inDir = path.join(IN, verb);
|
|
33
|
-
fs.mkdirSync(inDir, { recursive: true });
|
|
34
|
-
const inFile = path.join(inDir, `${task}.txt`);
|
|
35
|
-
const outFile = path.join(OUT, `${verb}-${task}.json`);
|
|
36
|
-
const payload = typeof body === 'string' ? body : JSON.stringify(body);
|
|
37
|
-
const tmp = inFile + '.tmp.' + process.pid;
|
|
38
|
-
fs.writeFileSync(tmp, payload);
|
|
39
|
-
fs.renameSync(tmp, inFile);
|
|
40
|
-
const t0 = Date.now();
|
|
41
|
-
while (Date.now() - t0 < timeoutMs) {
|
|
42
|
-
if (fs.existsSync(outFile)) {
|
|
43
|
-
const txt = fs.readFileSync(outFile, 'utf8');
|
|
44
|
-
try { return { ok: true, latency_ms: Date.now() - t0, response: JSON.parse(txt) }; }
|
|
45
|
-
catch (e) { return { ok: false, latency_ms: Date.now() - t0, error: 'parse: ' + e.message, raw: txt }; }
|
|
46
|
-
}
|
|
47
|
-
await sleep(100);
|
|
48
|
-
}
|
|
49
|
-
return { ok: false, latency_ms: timeoutMs, error: 'timeout' };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function ensureWatcher() {
|
|
53
|
-
let st = readJson(STATUS);
|
|
54
|
-
const now = Date.now();
|
|
55
|
-
if (st && st.pid && (now - (st.ts || 0)) < 15000) {
|
|
56
|
-
try { process.kill(st.pid, 0); log('watcher already up pid=' + st.pid); return st.pid; } catch (_) {}
|
|
57
|
-
}
|
|
58
|
-
log('booting watcher via bun x gm-plugkit@latest spool');
|
|
59
|
-
const child = cp.spawn('bun', ['x', 'gm-plugkit@latest', 'spool'], {
|
|
60
|
-
cwd: ROOT, detached: true, stdio: ['ignore', 'ignore', 'ignore'], windowsHide: true, shell: true,
|
|
61
|
-
});
|
|
62
|
-
child.unref();
|
|
63
|
-
const t0 = Date.now();
|
|
64
|
-
while (Date.now() - t0 < 60000) {
|
|
65
|
-
st = readJson(STATUS);
|
|
66
|
-
if (st && st.pid && (Date.now() - (st.ts || 0)) < 10000) {
|
|
67
|
-
try { process.kill(st.pid, 0); log('watcher up pid=' + st.pid); return st.pid; } catch (_) {}
|
|
68
|
-
}
|
|
69
|
-
await sleep(500);
|
|
70
|
-
}
|
|
71
|
-
throw new Error('watcher boot timed out');
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function findChromiumProcs() {
|
|
75
|
-
try {
|
|
76
|
-
const ps = `Get-CimInstance Win32_Process | Where-Object { $_.Name -match 'chrome|chromium|msedge' } | Select-Object ProcessId, Name, CommandLine | ConvertTo-Json -Compress -Depth 3`;
|
|
77
|
-
const out = cp.execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps], { encoding: 'utf8', windowsHide: true, maxBuffer: 8 * 1024 * 1024 });
|
|
78
|
-
if (!out.trim()) return [];
|
|
79
|
-
const j = JSON.parse(out);
|
|
80
|
-
return Array.isArray(j) ? j : [j];
|
|
81
|
-
} catch (e) { return []; }
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function findChromiumMainWindowTitleForPids(pids) {
|
|
85
|
-
try {
|
|
86
|
-
const list = pids.map(p => String(p)).join(',');
|
|
87
|
-
const ps = `$ids = @(${list}); Get-Process -Id $ids -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowTitle } | Select-Object Id, MainWindowTitle | ConvertTo-Json -Compress`;
|
|
88
|
-
const out = cp.execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps], { encoding: 'utf8', windowsHide: true, maxBuffer: 4 * 1024 * 1024 });
|
|
89
|
-
if (!out.trim()) return [];
|
|
90
|
-
const j = JSON.parse(out);
|
|
91
|
-
return Array.isArray(j) ? j : [j];
|
|
92
|
-
} catch (e) { return []; }
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function validateChromium() {
|
|
96
|
-
const v = { ok: false, session_id: '', headed: false, window_title: '', errors: [] };
|
|
97
|
-
log('Validation 1: chromium headed');
|
|
98
|
-
rmrf(PROFILE);
|
|
99
|
-
try { fs.unlinkSync(PORTS); } catch (_) {}
|
|
100
|
-
try { fs.unlinkSync(SESSIONS); } catch (_) {}
|
|
101
|
-
|
|
102
|
-
await dispatch('instruction', '{}', 30000).catch(() => {});
|
|
103
|
-
const r = await dispatch('browser', 'session new', 90000);
|
|
104
|
-
if (!r.ok) { v.errors.push('dispatch: ' + r.error); return v; }
|
|
105
|
-
const resp = r.response || {};
|
|
106
|
-
const data = resp.data || {};
|
|
107
|
-
const stdout = data.stdout || '';
|
|
108
|
-
const m = stdout.match(/Session\s+(\S+)\s+created/i) || stdout.match(/session_id["':\s]+([A-Za-z0-9_-]+)/i);
|
|
109
|
-
v.session_id = (resp.session_id || data.session_id || (m && m[1]) || '').toString();
|
|
110
|
-
if (!resp.ok && !data.ok) { v.errors.push('browser session new not ok: ' + JSON.stringify(resp).slice(0, 400)); }
|
|
111
|
-
|
|
112
|
-
const t0 = Date.now();
|
|
113
|
-
let matched = null;
|
|
114
|
-
while (Date.now() - t0 < 30000) {
|
|
115
|
-
const procs = findChromiumProcs();
|
|
116
|
-
matched = procs.filter(p => p && typeof p.CommandLine === 'string' && p.CommandLine.toLowerCase().includes(PROFILE.toLowerCase().replace(/\\/g, '\\').toLowerCase()));
|
|
117
|
-
if (matched.length === 0) {
|
|
118
|
-
matched = procs.filter(p => p && typeof p.CommandLine === 'string' && /browser-profile/i.test(p.CommandLine) && p.CommandLine.includes(ROOT));
|
|
119
|
-
}
|
|
120
|
-
if (matched.length > 0) break;
|
|
121
|
-
await sleep(500);
|
|
122
|
-
}
|
|
123
|
-
if (!matched || matched.length === 0) { v.errors.push('no chromium process with cwd browser-profile cmdline within 30s'); return v; }
|
|
124
|
-
const pids = matched.map(p => p.ProcessId);
|
|
125
|
-
log('chromium pids matching profile: ' + pids.join(','));
|
|
126
|
-
|
|
127
|
-
const titles = findChromiumMainWindowTitleForPids(pids);
|
|
128
|
-
if (titles.length > 0) {
|
|
129
|
-
v.window_title = titles[0].MainWindowTitle || '';
|
|
130
|
-
}
|
|
131
|
-
if (!v.window_title) {
|
|
132
|
-
await sleep(3000);
|
|
133
|
-
const t2 = findChromiumMainWindowTitleForPids(pids);
|
|
134
|
-
if (t2.length > 0) v.window_title = t2[0].MainWindowTitle || '';
|
|
135
|
-
}
|
|
136
|
-
v.headed = !!v.window_title;
|
|
137
|
-
if (!v.headed) v.errors.push('no MainWindowTitle on any matched chromium pid (headless?)');
|
|
138
|
-
|
|
139
|
-
const ports = readJson(PORTS) || {};
|
|
140
|
-
let portMatch = false;
|
|
141
|
-
for (const k of Object.keys(ports)) {
|
|
142
|
-
const e = ports[k];
|
|
143
|
-
if (e && pids.includes(e.pid)) { portMatch = true; break; }
|
|
144
|
-
}
|
|
145
|
-
if (!portMatch) v.errors.push('browser-ports.json has no entry whose pid matches running chromium');
|
|
146
|
-
|
|
147
|
-
if (v.session_id) {
|
|
148
|
-
try {
|
|
149
|
-
fs.mkdirSync(WITNESS_DIR, { recursive: true });
|
|
150
|
-
const shot = path.join(WITNESS_DIR, 'chromium-headed.png');
|
|
151
|
-
const script = `await page.goto('about:blank'); const buf = await page.screenshot(); require('fs').writeFileSync(${JSON.stringify(shot)}, buf);`;
|
|
152
|
-
await dispatch('browser', `session -s ${v.session_id} -e ${JSON.stringify(script)}`, 30000);
|
|
153
|
-
v.screenshot = fs.existsSync(shot) ? shot : '';
|
|
154
|
-
} catch (e) { v.errors.push('screenshot: ' + e.message); }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
v.ok = v.headed && portMatch;
|
|
158
|
-
return v;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function readWatcherLogTail(bytes = 64 * 1024) {
|
|
162
|
-
try {
|
|
163
|
-
const stat = fs.statSync(WATCHER_LOG);
|
|
164
|
-
const start = Math.max(0, stat.size - bytes);
|
|
165
|
-
const fd = fs.openSync(WATCHER_LOG, 'r');
|
|
166
|
-
const buf = Buffer.alloc(stat.size - start);
|
|
167
|
-
fs.readSync(fd, buf, 0, buf.length, start);
|
|
168
|
-
fs.closeSync(fd);
|
|
169
|
-
return buf.toString('utf8');
|
|
170
|
-
} catch (_) { return ''; }
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async function validateEmbed() {
|
|
174
|
-
const v = { ok: false, calls: [], p50_ms: 0, p95_ms: 0, crashed: false, recall_mode: '', recall_top_text: '', errors: [] };
|
|
175
|
-
log('Validation 2: embed end-to-end (node)');
|
|
176
|
-
const before = readWatcherLogTail();
|
|
177
|
-
const beforeLen = before.length;
|
|
178
|
-
|
|
179
|
-
const texts = [
|
|
180
|
-
'gm-validate witness one ' + Date.now(),
|
|
181
|
-
'gm-validate witness two ' + Date.now(),
|
|
182
|
-
'gm-validate witness three ' + Date.now(),
|
|
183
|
-
];
|
|
184
|
-
for (const text of texts) {
|
|
185
|
-
const r = await dispatch('memorize-fire', { text, namespace: 'validate' }, 60000);
|
|
186
|
-
const d = r.response && (r.response.data || r.response) || {};
|
|
187
|
-
const ok = !!(r.ok && (r.response && r.response.ok) && (d.embedded === true || d.embedded === undefined) && (d.memorized !== false));
|
|
188
|
-
v.calls.push({ text, latency_ms: r.latency_ms, ok, embedded: d.embedded === true, memorized: d.memorized !== false, error: r.error || (r.response && r.response.error) || null });
|
|
189
|
-
}
|
|
190
|
-
const lats = v.calls.map(c => c.latency_ms).sort((a, b) => a - b);
|
|
191
|
-
v.p50_ms = lats[Math.floor(lats.length * 0.5)] || 0;
|
|
192
|
-
v.p95_ms = lats[Math.max(0, Math.ceil(lats.length * 0.95) - 1)] || 0;
|
|
193
|
-
|
|
194
|
-
const r = await dispatch('recall', { query: 'gm-validate witness', namespace: 'validate', limit: 3 }, 60000);
|
|
195
|
-
const rd = (r.response && (r.response.data || r.response)) || {};
|
|
196
|
-
v.recall_mode = rd.mode || '';
|
|
197
|
-
const rows = rd.rows || rd.hits || [];
|
|
198
|
-
v.recall_top_text = (rows[0] && (rows[0].text || rows[0].content)) || '';
|
|
199
|
-
|
|
200
|
-
const after = readWatcherLogTail(256 * 1024);
|
|
201
|
-
const delta = after.slice(Math.max(0, after.length - (after.length - beforeLen) - 2048));
|
|
202
|
-
if (/proc_exit|panicked|wasm trap|RuntimeError/i.test(delta)) {
|
|
203
|
-
v.crashed = true;
|
|
204
|
-
v.errors.push('watcher log shows wasm crash');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const allOk = v.calls.every(c => c.ok);
|
|
208
|
-
const recallOk = v.recall_top_text.includes('gm-validate witness');
|
|
209
|
-
v.ok = allOk && recallOk && !v.crashed;
|
|
210
|
-
if (!allOk) v.errors.push('not all memorize calls ok');
|
|
211
|
-
if (!recallOk) v.errors.push('recall top hit did not contain witness text');
|
|
212
|
-
if (REQUIRE_NEW_EMBED && v.recall_mode !== 'vector_top_k') v.errors.push('recall mode != vector_top_k (require-new-embed)');
|
|
213
|
-
return v;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function which(cmd) {
|
|
217
|
-
try {
|
|
218
|
-
const out = cp.execFileSync(process.platform === 'win32' ? 'where.exe' : 'which', [cmd], { encoding: 'utf8', windowsHide: true }).split(/\r?\n/).filter(Boolean);
|
|
219
|
-
return out[0] || '';
|
|
220
|
-
} catch (_) { return ''; }
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async function validateBrowserEmbed() {
|
|
224
|
-
const v = { ok: false, calls: [], p50_ms: 0, p95_ms: 0, recall_top_text: '', skipped: false, errors: [] };
|
|
225
|
-
log('Validation 3: embed in browser via playwriter');
|
|
226
|
-
const pw = which('playwriter') || which('playwriter.cmd');
|
|
227
|
-
if (!pw) { v.skipped = true; v.errors.push('playwriter not found'); return v; }
|
|
228
|
-
|
|
229
|
-
const tbDir = path.resolve(__dirname, '..');
|
|
230
|
-
if (!fs.existsSync(path.join(tbDir, 'docs'))) { v.skipped = true; v.errors.push('repo docs/ missing -- cannot serve a fixture page'); return v; }
|
|
231
|
-
|
|
232
|
-
let serveProc = null;
|
|
233
|
-
let port;
|
|
234
|
-
try {
|
|
235
|
-
const r = cp.spawnSync(process.execPath, ['-e', "const net=require('net');const s=net.createServer();s.listen(0,'127.0.0.1',()=>{const p=s.address().port;s.close(()=>process.stdout.write(String(p)));});s.on('error',e=>{process.stderr.write(e.message);process.exit(1);});"], { encoding: 'utf-8', timeout: 5000 });
|
|
236
|
-
if (r.status !== 0) throw new Error('could not allocate free port: ' + (r.stderr || 'unknown'));
|
|
237
|
-
port = parseInt(r.stdout.trim(), 10);
|
|
238
|
-
} catch (e) { v.errors.push('free-port alloc: ' + e.message); return v; }
|
|
239
|
-
try {
|
|
240
|
-
serveProc = cp.spawn(process.platform === 'win32' ? 'npx.cmd' : 'npx', ['serve', 'docs', '-l', String(port)], {
|
|
241
|
-
cwd: tbDir, detached: true, stdio: 'ignore', windowsHide: true, shell: process.platform === 'win32',
|
|
242
|
-
});
|
|
243
|
-
serveProc.unref();
|
|
244
|
-
} catch (e) { v.errors.push('serve spawn: ' + e.message); return v; }
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
const t0 = Date.now();
|
|
248
|
-
let ready = false;
|
|
249
|
-
while (Date.now() - t0 < 20000) {
|
|
250
|
-
try {
|
|
251
|
-
await new Promise((res, rej) => {
|
|
252
|
-
const req = require('http').get(`http://127.0.0.1:${port}/`, r => { r.resume(); res(); });
|
|
253
|
-
req.on('error', rej);
|
|
254
|
-
req.setTimeout(1500, () => req.destroy(new Error('timeout')));
|
|
255
|
-
});
|
|
256
|
-
ready = true; break;
|
|
257
|
-
} catch (_) { await sleep(500); }
|
|
258
|
-
}
|
|
259
|
-
if (!ready) { v.errors.push('docs serve never came up on ' + port); return v; }
|
|
260
|
-
|
|
261
|
-
let sessionId = '';
|
|
262
|
-
try {
|
|
263
|
-
const out = cp.execSync('playwriter session new', { encoding: 'utf8', windowsHide: true });
|
|
264
|
-
const m = out.match(/Session\s+(\S+)\s+created/i) || out.match(/([A-Za-z0-9_-]{1,40})/);
|
|
265
|
-
sessionId = (m && m[1]) || '';
|
|
266
|
-
} catch (e) { v.errors.push('playwriter session new: ' + e.message); return v; }
|
|
267
|
-
if (!sessionId) { v.errors.push('no session id from playwriter'); return v; }
|
|
268
|
-
|
|
269
|
-
const script = `
|
|
270
|
-
await page.goto('http://127.0.0.1:${port}/');
|
|
271
|
-
await page.waitForLoadState('domcontentloaded');
|
|
272
|
-
const has = await page.evaluate(() => !!(window.__debug && window.__debug.gm));
|
|
273
|
-
if (!has) { return { skipped: true, reason: 'no window.__debug.gm' }; }
|
|
274
|
-
const out = { mems: [], recall: null };
|
|
275
|
-
for (const t of ['bw one ${Date.now()}', 'bw two ${Date.now()}', 'bw three ${Date.now()}']) {
|
|
276
|
-
const t0 = performance.now();
|
|
277
|
-
const r = await window.__debug.gm.memorize({ text: t, namespace: 'validate-browser' });
|
|
278
|
-
out.mems.push({ ok: !!(r && r.ok), latency_ms: performance.now() - t0 });
|
|
279
|
-
}
|
|
280
|
-
out.recall = await window.__debug.gm.recall({ query: 'bw', namespace: 'validate-browser', limit: 3 });
|
|
281
|
-
return out;
|
|
282
|
-
`;
|
|
283
|
-
let res = null;
|
|
284
|
-
try {
|
|
285
|
-
const tmpScript = path.join(os.tmpdir(), 'gm-validate-' + Date.now() + '.js');
|
|
286
|
-
fs.writeFileSync(tmpScript, script);
|
|
287
|
-
const out = cp.execFileSync(pw, ['-s', sessionId, '--timeout', '60000', '-f', tmpScript], { encoding: 'utf8', windowsHide: true, maxBuffer: 4 * 1024 * 1024 });
|
|
288
|
-
try { fs.unlinkSync(tmpScript); } catch (_) {}
|
|
289
|
-
const m = out.match(/\{[\s\S]*\}\s*$/);
|
|
290
|
-
if (m) { try { res = JSON.parse(m[0]); } catch (_) {} }
|
|
291
|
-
if (!res) v.errors.push('could not parse playwriter eval output');
|
|
292
|
-
} catch (e) { v.errors.push('playwriter -e: ' + e.message); }
|
|
293
|
-
|
|
294
|
-
if (res && res.skipped) { v.skipped = true; v.errors.push(res.reason || 'browser debug surface missing'); return v; }
|
|
295
|
-
if (res && Array.isArray(res.mems)) {
|
|
296
|
-
v.calls = res.mems;
|
|
297
|
-
const lats = res.mems.map(c => c.latency_ms).sort((a, b) => a - b);
|
|
298
|
-
v.p50_ms = Math.round(lats[Math.floor(lats.length * 0.5)] || 0);
|
|
299
|
-
v.p95_ms = Math.round(lats[Math.max(0, Math.ceil(lats.length * 0.95) - 1)] || 0);
|
|
300
|
-
const rows = (res.recall && (res.recall.rows || res.recall.hits)) || [];
|
|
301
|
-
v.recall_top_text = (rows[0] && (rows[0].text || rows[0].content)) || '';
|
|
302
|
-
const allOk = v.calls.every(c => c.ok);
|
|
303
|
-
v.ok = allOk && v.recall_top_text.startsWith('bw');
|
|
304
|
-
if (!allOk) v.errors.push('not all browser memorize calls ok');
|
|
305
|
-
}
|
|
306
|
-
return v;
|
|
307
|
-
} finally {
|
|
308
|
-
if (serveProc && serveProc.pid) {
|
|
309
|
-
try {
|
|
310
|
-
if (process.platform === 'win32') cp.execFileSync('taskkill', ['/F', '/T', '/PID', String(serveProc.pid)], { stdio: 'ignore', windowsHide: true });
|
|
311
|
-
else process.kill(serveProc.pid, 'SIGTERM');
|
|
312
|
-
} catch (_) {}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
(async () => {
|
|
318
|
-
const final = { chromium_headed: false, chromium_window_title: '', embed_node: { ok: false, p50_ms: 0, p95_ms: 0, crashed: false }, embed_browser: { ok: false, p50_ms: 0, p95_ms: 0, skipped: false } };
|
|
319
|
-
let exit = 0;
|
|
320
|
-
try {
|
|
321
|
-
await ensureWatcher();
|
|
322
|
-
const v1 = await validateChromium();
|
|
323
|
-
final.chromium_headed = !!v1.ok;
|
|
324
|
-
final.chromium_window_title = v1.window_title || '';
|
|
325
|
-
final._v1 = v1;
|
|
326
|
-
if (!v1.ok) exit = 1;
|
|
327
|
-
|
|
328
|
-
const v2 = await validateEmbed();
|
|
329
|
-
final.embed_node = { ok: v2.ok, p50_ms: v2.p50_ms, p95_ms: v2.p95_ms, crashed: v2.crashed, recall_mode: v2.recall_mode, errors: v2.errors };
|
|
330
|
-
final._v2 = v2;
|
|
331
|
-
if (!v2.ok) exit = 1;
|
|
332
|
-
|
|
333
|
-
const v3 = await validateBrowserEmbed();
|
|
334
|
-
final.embed_browser = { ok: v3.ok, p50_ms: v3.p50_ms, p95_ms: v3.p95_ms, skipped: !!v3.skipped, errors: v3.errors };
|
|
335
|
-
final._v3 = v3;
|
|
336
|
-
if (!v3.ok && !v3.skipped) exit = 1;
|
|
337
|
-
} catch (e) {
|
|
338
|
-
final.error = e.message;
|
|
339
|
-
exit = 1;
|
|
340
|
-
}
|
|
341
|
-
process.stdout.write(JSON.stringify(final, null, 2) + '\n');
|
|
342
|
-
process.exit(exit);
|
|
343
|
-
})();
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
|
|
6
|
-
async function main() {
|
|
7
|
-
const [, , projectDir, command, codeB64] = process.argv;
|
|
8
|
-
if (!projectDir || !command || codeB64 === undefined) {
|
|
9
|
-
console.log(JSON.stringify({ ok: false, error: 'usage: lang-host-runner <projectDir> <command> <code-base64>' }));
|
|
10
|
-
process.exit(2);
|
|
11
|
-
}
|
|
12
|
-
const code = Buffer.from(codeB64, 'base64').toString('utf8');
|
|
13
|
-
const langDir = path.join(projectDir, 'lang');
|
|
14
|
-
if (!fs.existsSync(langDir)) {
|
|
15
|
-
console.log(JSON.stringify({ ok: false, error: 'no-lang-dir', langDir }));
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
const files = fs.readdirSync(langDir).filter(f => f.endsWith('.js') && f !== 'loader.js');
|
|
19
|
-
const plugins = files.reduce((acc, f) => {
|
|
20
|
-
try {
|
|
21
|
-
const p = require(path.join(langDir, f));
|
|
22
|
-
if (p && typeof p.id === 'string' && p.exec && p.exec.match instanceof RegExp && typeof p.exec.run === 'function') {
|
|
23
|
-
acc.push(p);
|
|
24
|
-
}
|
|
25
|
-
} catch (_) {}
|
|
26
|
-
return acc;
|
|
27
|
-
}, []);
|
|
28
|
-
const plugin = plugins.find(p => p.exec.match.test(command));
|
|
29
|
-
if (!plugin) {
|
|
30
|
-
console.log(JSON.stringify({ ok: false, error: 'no-plugin-matched', command, available: plugins.map(p => p.id) }));
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
const t0 = Date.now();
|
|
34
|
-
const timer = setTimeout(() => {
|
|
35
|
-
console.log(JSON.stringify({ ok: false, error: 'timeout', plugin_id: plugin.id, ms: Date.now() - t0 }));
|
|
36
|
-
process.exit(0);
|
|
37
|
-
}, 30000);
|
|
38
|
-
try {
|
|
39
|
-
const out = await plugin.exec.run(code, projectDir);
|
|
40
|
-
clearTimeout(timer);
|
|
41
|
-
console.log(JSON.stringify({ ok: true, plugin_id: plugin.id, output: String(out), ms: Date.now() - t0 }));
|
|
42
|
-
} catch (e) {
|
|
43
|
-
clearTimeout(timer);
|
|
44
|
-
console.log(JSON.stringify({ ok: false, error: String(e && e.message || e), plugin_id: plugin.id, ms: Date.now() - t0 }));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
main();
|
package/lang/ssh.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const fsSync = require('fs');
|
|
5
|
-
const http = require('http');
|
|
6
|
-
|
|
7
|
-
function loadTarget(targetName) {
|
|
8
|
-
const candidatesCfg = [
|
|
9
|
-
path.join(os.homedir(), '.gm-tools', 'ssh-targets.json'),
|
|
10
|
-
path.join(os.homedir(), '.claude', 'ssh-targets.json'),
|
|
11
|
-
];
|
|
12
|
-
const cfgPath = candidatesCfg.find(p => fsSync.existsSync(p));
|
|
13
|
-
if (!cfgPath) throw new Error('No ssh-targets.json found at ' + candidatesCfg.join(' or '));
|
|
14
|
-
if (process.platform !== 'win32') {
|
|
15
|
-
try {
|
|
16
|
-
const mode = fsSync.statSync(cfgPath).mode & 0o777;
|
|
17
|
-
if (mode !== 0o600) fsSync.chmodSync(cfgPath, 0o600);
|
|
18
|
-
} catch (_) {}
|
|
19
|
-
}
|
|
20
|
-
const cfg = JSON.parse(fsSync.readFileSync(cfgPath, 'utf8'));
|
|
21
|
-
const name = targetName || 'default';
|
|
22
|
-
if (!cfg[name]) throw new Error('No target \'' + name + '\' in ssh-targets.json. Available: ' + Object.keys(cfg).join(', '));
|
|
23
|
-
return cfg[name];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function parseCommand(code) {
|
|
27
|
-
const lines = code.trim().split('\n');
|
|
28
|
-
let target = 'default';
|
|
29
|
-
let cmd = code.trim();
|
|
30
|
-
if (lines[0].trim().startsWith('@')) {
|
|
31
|
-
target = lines[0].trim().slice(1);
|
|
32
|
-
cmd = lines.slice(1).join('\n').trim();
|
|
33
|
-
}
|
|
34
|
-
return { target, cmd };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function resolveSsh2() {
|
|
38
|
-
const candidates = [
|
|
39
|
-
path.join(os.homedir(), '.gm-tools', 'node_modules', 'ssh2'),
|
|
40
|
-
path.join(os.homedir(), '.claude', 'gm-tools', 'node_modules', 'ssh2'),
|
|
41
|
-
path.join(os.homedir(), '.claude', 'plugins', 'node_modules', 'ssh2'),
|
|
42
|
-
'ssh2',
|
|
43
|
-
];
|
|
44
|
-
for (const p of candidates) {
|
|
45
|
-
try { return require(p); } catch (_) {}
|
|
46
|
-
}
|
|
47
|
-
throw new Error('ssh2 not found. Install into ~/.gm-tools/ with: mkdir -p ~/.gm-tools && cd ~/.gm-tools && npm install ssh2');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function getRunnerPort() {
|
|
51
|
-
const portFile = path.join(os.tmpdir(), 'glootie-runner.port');
|
|
52
|
-
try { return parseInt(fsSync.readFileSync(portFile, 'utf8').trim(), 10); } catch { return null; }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function rpcCall(port, method, params) {
|
|
56
|
-
return new Promise((resolve, reject) => {
|
|
57
|
-
const body = JSON.stringify({ method, params });
|
|
58
|
-
const req = http.request(
|
|
59
|
-
{ hostname: '127.0.0.1', port, path: '/rpc', method: 'POST',
|
|
60
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },
|
|
61
|
-
(res) => {
|
|
62
|
-
let data = '';
|
|
63
|
-
res.on('data', c => { data += c; });
|
|
64
|
-
res.on('end', () => {
|
|
65
|
-
try {
|
|
66
|
-
const p = JSON.parse(data);
|
|
67
|
-
if (p.error) return reject(new Error(p.error.message || String(p.error)));
|
|
68
|
-
resolve(p.result);
|
|
69
|
-
} catch { reject(new Error('RPC parse error: ' + data)); }
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
);
|
|
73
|
-
req.setTimeout(5000, () => { req.destroy(); reject(new Error('RPC timeout')); });
|
|
74
|
-
req.on('error', reject);
|
|
75
|
-
req.write(body);
|
|
76
|
-
req.end();
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function runSsh(target, cmd, onData) {
|
|
81
|
-
return new Promise((resolve, reject) => {
|
|
82
|
-
const { Client } = resolveSsh2();
|
|
83
|
-
const ssh = new Client();
|
|
84
|
-
let stdout = '';
|
|
85
|
-
let stderr = '';
|
|
86
|
-
let done = false;
|
|
87
|
-
|
|
88
|
-
const finish = (extra) => {
|
|
89
|
-
if (!done) {
|
|
90
|
-
done = true;
|
|
91
|
-
ssh.end();
|
|
92
|
-
resolve({ stdout: stdout.trimEnd(), stderr: stderr.trimEnd(), timedOut: false, ...extra });
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
const timeout = setTimeout(() => {
|
|
97
|
-
if (!done) {
|
|
98
|
-
done = true;
|
|
99
|
-
try { ssh.end(); } catch (_) {}
|
|
100
|
-
resolve({ stdout: stdout.trimEnd(), stderr: (stderr + '\n[ssh timed out after 55s; output is partial]').trimEnd(), timedOut: true });
|
|
101
|
-
}
|
|
102
|
-
}, 55000);
|
|
103
|
-
|
|
104
|
-
ssh.on('ready', () => {
|
|
105
|
-
ssh.exec(cmd, { pty: false }, (err, stream) => {
|
|
106
|
-
if (err) { clearTimeout(timeout); ssh.end(); reject(err); return; }
|
|
107
|
-
stream.on('data', d => {
|
|
108
|
-
const s = d.toString();
|
|
109
|
-
stdout += s;
|
|
110
|
-
if (onData) onData(s, 'stdout');
|
|
111
|
-
});
|
|
112
|
-
stream.stderr.on('data', d => {
|
|
113
|
-
const s = d.toString();
|
|
114
|
-
stderr += s;
|
|
115
|
-
if (onData) onData(s, 'stderr');
|
|
116
|
-
});
|
|
117
|
-
stream.on('close', () => { clearTimeout(timeout); finish(); });
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
ssh.on('error', err => { clearTimeout(timeout); if (!done) { done = true; reject(err); } });
|
|
122
|
-
|
|
123
|
-
const connOpts = { host: target.host, port: target.port || 22, username: target.username, readyTimeout: 15000 };
|
|
124
|
-
if (target.password) connOpts.password = target.password;
|
|
125
|
-
if (target.keyPath) connOpts.privateKey = fsSync.readFileSync(target.keyPath);
|
|
126
|
-
if (target.passphrase) connOpts.passphrase = target.passphrase;
|
|
127
|
-
ssh.connect(connOpts);
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async function runBackground(target, cmd) {
|
|
132
|
-
const port = getRunnerPort();
|
|
133
|
-
if (!port) return null;
|
|
134
|
-
|
|
135
|
-
let taskId;
|
|
136
|
-
try {
|
|
137
|
-
const r = await rpcCall(port, 'createTask', { code: '', runtime: 'ssh', workingDirectory: process.cwd() });
|
|
138
|
-
taskId = r?.taskId ?? r;
|
|
139
|
-
await rpcCall(port, 'startTask', { taskId });
|
|
140
|
-
} catch { return null; }
|
|
141
|
-
|
|
142
|
-
const onData = (data, type) => {
|
|
143
|
-
rpcCall(port, 'appendOutput', { taskId, type, data }).catch(() => {});
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
runSsh(target, cmd, onData).then(r => {
|
|
147
|
-
rpcCall(port, 'completeTask', { taskId, result: { success: !r.timedOut, exitCode: r.timedOut ? 124 : 0, stdout: r.stdout, stderr: r.stderr, error: r.timedOut ? 'ssh timed out after 55s' : null } }).catch(() => {});
|
|
148
|
-
}).catch(err => {
|
|
149
|
-
rpcCall(port, 'completeTask', { taskId, result: { success: false, exitCode: 1, stdout: '', stderr: err.message, error: err.message } }).catch(() => {});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
return taskId;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
module.exports = {
|
|
156
|
-
id: 'ssh',
|
|
157
|
-
exec: {
|
|
158
|
-
match: /^exec:ssh/,
|
|
159
|
-
async run(code) {
|
|
160
|
-
const { target: targetName, cmd } = parseCommand(code);
|
|
161
|
-
if (!cmd) return '[no command provided]';
|
|
162
|
-
const target = loadTarget(targetName);
|
|
163
|
-
|
|
164
|
-
const isBackground = /(&\s*$|^\s*(nohup|systemd-run|setsid)\s)/m.test(cmd);
|
|
165
|
-
|
|
166
|
-
if (isBackground) {
|
|
167
|
-
const taskId = await runBackground(target, cmd);
|
|
168
|
-
if (taskId != null) {
|
|
169
|
-
return 'Backgrounded on remote host. Local task_' + taskId + ' streams output.\n\n' +
|
|
170
|
-
' exec:sleep\n task_' + taskId + '\n\n' +
|
|
171
|
-
' exec:status\n task_' + taskId + '\n\n' +
|
|
172
|
-
' exec:close\n task_' + taskId;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const r = await runSsh(target, cmd, null);
|
|
177
|
-
const combined = [r.stdout, r.stderr].filter(Boolean).join('\n');
|
|
178
|
-
return combined || (r.timedOut ? '[ssh timed out after 55s; no output]' : '');
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
context: `=== exec:ssh ===
|
|
182
|
-
exec:ssh
|
|
183
|
-
[@target]
|
|
184
|
-
<shell command>
|
|
185
|
-
|
|
186
|
-
Runs shell command on remote SSH host. Target from ~/.claude/ssh-targets.json ("default" if no @name). Supports multi-line scripts. Password or key auth. Returns combined stdout+stderr. Commands ending with & or using nohup/systemd-run are backgrounded -- use exec:sleep/status/close to follow.`
|
|
187
|
-
};
|