gm-plugkit 2.0.1114 → 2.0.1115

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1114",
3
+ "version": "2.0.1115",
4
4
  "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -4,10 +4,182 @@ import os from 'os';
4
4
  import crypto from 'crypto';
5
5
  import { watch } from 'fs';
6
6
  import { spawn, spawnSync } from 'child_process';
7
+ import net from 'net';
7
8
 
8
9
  const KV_DIR = path.join(os.homedir(), '.claude', 'gm-tools', 'kv');
9
10
  fs.mkdirSync(KV_DIR, { recursive: true });
10
11
 
12
+ const TMP_DIR = os.tmpdir();
13
+ const BROWSER_PORTS_FILE = path.join(TMP_DIR, 'plugkit-browser-ports.json');
14
+ const BROWSER_SESSIONS_FILE = path.join(TMP_DIR, 'plugkit-browser-sessions.json');
15
+
16
+ function readJsonFile(fp, fallback) {
17
+ try { return JSON.parse(fs.readFileSync(fp, 'utf-8')); } catch (_) { return fallback; }
18
+ }
19
+ function writeJsonFile(fp, value) {
20
+ try { fs.writeFileSync(fp, JSON.stringify(value, null, 2)); } catch (_) {}
21
+ }
22
+
23
+ function findChrome() {
24
+ if (process.platform === 'win32') {
25
+ const candidates = [
26
+ path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'Google', 'Chrome', 'Application', 'chrome.exe'),
27
+ path.join(process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)', 'Google', 'Chrome', 'Application', 'chrome.exe'),
28
+ path.join(process.env.LOCALAPPDATA || '', 'Google', 'Chrome', 'Application', 'chrome.exe'),
29
+ ];
30
+ for (const c of candidates) { if (c && fs.existsSync(c)) return c; }
31
+ return null;
32
+ }
33
+ if (process.platform === 'darwin') {
34
+ const mac = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
35
+ if (fs.existsSync(mac)) return mac;
36
+ return null;
37
+ }
38
+ for (const bin of ['google-chrome', 'chromium', 'chromium-browser']) {
39
+ const r = spawnSync('which', [bin], { encoding: 'utf-8' });
40
+ if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
41
+ }
42
+ return null;
43
+ }
44
+
45
+ function findPlaywriter() {
46
+ const npmR = spawnSync('npm', ['root', '-g'], { encoding: 'utf-8', shell: true });
47
+ if (npmR.status === 0 && npmR.stdout.trim()) {
48
+ const root = npmR.stdout.trim().split(/\r?\n/).pop();
49
+ const binJs = path.join(root, 'playwriter', 'bin.js');
50
+ if (fs.existsSync(binJs)) return { cmd: process.execPath, baseArgs: [binJs], shell: false };
51
+ }
52
+ const whichCmd = process.platform === 'win32' ? 'where' : 'which';
53
+ const r = spawnSync(whichCmd, ['playwriter'], { encoding: 'utf-8', shell: true });
54
+ if (r.status === 0 && r.stdout.trim()) {
55
+ const candidates = r.stdout.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
56
+ const cmd = candidates.find(c => c.toLowerCase().endsWith('.cmd')) || candidates.find(c => !c.toLowerCase().endsWith('.ps1')) || candidates[0];
57
+ if (cmd) return { cmd, baseArgs: [], shell: process.platform === 'win32' };
58
+ }
59
+ const bunR = spawnSync(whichCmd, ['bun'], { encoding: 'utf-8', shell: true });
60
+ if (bunR.status === 0 && bunR.stdout.trim()) {
61
+ return { cmd: 'bun', baseArgs: ['x', 'playwriter@latest'], shell: true };
62
+ }
63
+ const npxR = spawnSync(whichCmd, ['npx'], { encoding: 'utf-8', shell: true });
64
+ if (npxR.status === 0 && npxR.stdout.trim()) {
65
+ return { cmd: 'npx', baseArgs: ['-y', 'playwriter'], shell: true };
66
+ }
67
+ return null;
68
+ }
69
+
70
+ function ensureGitignored(cwd, entry) {
71
+ try {
72
+ const gi = path.join(cwd, '.gitignore');
73
+ let content = '';
74
+ if (fs.existsSync(gi)) content = fs.readFileSync(gi, 'utf-8');
75
+ const lines = content.split(/\r?\n/);
76
+ if (lines.some(l => l.trim() === entry)) return;
77
+ const updated = (content && !content.endsWith('\n') ? content + '\n' : content) + entry + '\n';
78
+ fs.writeFileSync(gi, updated);
79
+ } catch (_) {}
80
+ }
81
+
82
+ function isProfileLocked(profileDir) {
83
+ const lock = path.join(profileDir, 'SingletonLock');
84
+ return fs.existsSync(lock);
85
+ }
86
+
87
+ function acquireProfileDir(cwd) {
88
+ const primary = path.join(cwd, '.plugkit-browser-profile');
89
+ ensureGitignored(cwd, '.plugkit-browser-profile/');
90
+ ensureGitignored(cwd, '.plugkit-browser-profile-*/');
91
+ try { fs.mkdirSync(primary, { recursive: true }); } catch (_) {}
92
+ if (!isProfileLocked(primary)) return primary;
93
+ const fallback = path.join(cwd, `.plugkit-browser-profile-${process.pid}`);
94
+ try { fs.mkdirSync(fallback, { recursive: true }); } catch (_) {}
95
+ return fallback;
96
+ }
97
+
98
+ function findFreePortSync() {
99
+ const r = spawnSync(process.execPath, ['-e', `
100
+ const net = require('net');
101
+ const srv = net.createServer();
102
+ srv.listen(0, '127.0.0.1', () => { const p = srv.address().port; srv.close(() => { process.stdout.write(String(p)); }); });
103
+ srv.on('error', e => { process.stderr.write(e.message); process.exit(1); });
104
+ `], { encoding: 'utf-8', timeout: 5000 });
105
+ if (r.status !== 0) throw new Error('could not allocate free port');
106
+ return parseInt(r.stdout.trim(), 10);
107
+ }
108
+
109
+ function isPortAliveSync(port) {
110
+ const r = spawnSync(process.execPath, ['-e', `
111
+ const net = require('net');
112
+ const s = net.connect({ port: ${port}, host: '127.0.0.1' });
113
+ s.on('connect', () => { s.destroy(); process.exit(0); });
114
+ s.on('error', () => process.exit(1));
115
+ setTimeout(() => process.exit(1), 800);
116
+ `], { timeout: 2000 });
117
+ return r.status === 0;
118
+ }
119
+
120
+ function sleepSync(ms) {
121
+ spawnSync(process.execPath, ['-e', `setTimeout(()=>{}, ${ms})`], { timeout: ms + 2000 });
122
+ }
123
+
124
+ function runPlaywriter(pw, args, timeoutMs) {
125
+ return spawnSync(pw.cmd, [...pw.baseArgs, ...args], {
126
+ encoding: 'utf-8',
127
+ timeout: timeoutMs,
128
+ shell: pw.shell,
129
+ env: process.env,
130
+ });
131
+ }
132
+
133
+ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
134
+ const ports = readJsonFile(BROWSER_PORTS_FILE, {});
135
+ const sessions = readJsonFile(BROWSER_SESSIONS_FILE, {});
136
+ const existing = ports[claudeSessionId];
137
+ if (existing && existing.port && isPortAliveSync(existing.port)) {
138
+ const pwIds = sessions[claudeSessionId] || [];
139
+ if (pwIds.length > 0) return pwIds[0];
140
+ }
141
+ const chrome = findChrome();
142
+ if (!chrome) throw new Error('Chrome not found. Please install Google Chrome.');
143
+ const profileDir = acquireProfileDir(cwd);
144
+ const port = findFreePortSync();
145
+ const chromeArgs = [
146
+ `--remote-debugging-port=${port}`,
147
+ `--user-data-dir=${profileDir}`,
148
+ '--no-first-run',
149
+ '--no-default-browser-check',
150
+ '--disable-features=Translate',
151
+ ];
152
+ const child = spawn(chrome, chromeArgs, { detached: true, stdio: 'ignore' });
153
+ child.unref();
154
+ const deadline = Date.now() + 10000;
155
+ let alive = false;
156
+ while (Date.now() < deadline) {
157
+ if (isPortAliveSync(port)) { alive = true; break; }
158
+ sleepSync(300);
159
+ }
160
+ if (!alive) throw new Error(`Chrome failed to open debug port ${port}`);
161
+ const newR = runPlaywriter(pw, ['session', 'new', `--direct=localhost:${port}`], 30000);
162
+ if (newR.status !== 0) throw new Error(`playwriter session new failed: ${newR.stderr || newR.stdout || 'unknown'}`);
163
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
164
+ const out = stripAnsi(newR.stdout || '').trim();
165
+ let pwSessionId = null;
166
+ const created = out.match(/Session\s+(\S+)\s+created/i);
167
+ if (created) pwSessionId = created[1];
168
+ if (!pwSessionId) {
169
+ const hex = out.match(/\b([a-f0-9-]{8,})\b/i);
170
+ if (hex) pwSessionId = hex[1];
171
+ }
172
+ if (!pwSessionId) {
173
+ try { const j = JSON.parse(out); pwSessionId = j.id || j.session_id || j.session; } catch (_) {}
174
+ }
175
+ if (!pwSessionId) throw new Error(`could not parse playwriter session id from: ${out}`);
176
+ ports[claudeSessionId] = { port, profileDir };
177
+ sessions[claudeSessionId] = [pwSessionId];
178
+ writeJsonFile(BROWSER_PORTS_FILE, ports);
179
+ writeJsonFile(BROWSER_SESSIONS_FILE, sessions);
180
+ return pwSessionId;
181
+ }
182
+
11
183
  const ACPTOAPI_URL = process.env.ACPTOAPI_URL || 'http://127.0.0.1:4800';
12
184
  const VEC_K_DEFAULT = 10;
13
185
  const EMBED_MODEL_DEFAULT = process.env.EMBED_MODEL || 'mistral/mistral-embed';
@@ -355,6 +527,37 @@ function makeHostFunctions(instanceRef) {
355
527
 
356
528
  host_now_ms: () => BigInt(Date.now()),
357
529
 
530
+ host_browser_exec: (bodyPtr, bodyLen, cwdPtr, cwdLen, sidPtr, sidLen) => {
531
+ try {
532
+ const body = readWasmStr(instanceRef.value, bodyPtr, bodyLen);
533
+ const cwd = readWasmStr(instanceRef.value, cwdPtr, cwdLen) || process.cwd();
534
+ const sessionId = readWasmStr(instanceRef.value, sidPtr, sidLen) || 'default';
535
+ const pw = findPlaywriter();
536
+ if (!pw) return writeWasmJson(instanceRef.value, { ok: false, error: 'playwriter not found. Install via: npm i -g playwriter' });
537
+ if (body.startsWith('session ')) {
538
+ const parts = body.slice(8).trim().split(/\s+/);
539
+ const r = runPlaywriter(pw, ['session', ...parts], 30000);
540
+ return writeWasmJson(instanceRef.value, {
541
+ ok: r.status === 0,
542
+ stdout: r.stdout || '',
543
+ stderr: r.stderr || '',
544
+ exit_code: r.status === null ? -1 : r.status,
545
+ });
546
+ }
547
+ const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
548
+ const r = runPlaywriter(pw, ['-s', pwSessionId, '--timeout', '14000', '-e', body], 60000);
549
+ return writeWasmJson(instanceRef.value, {
550
+ ok: r.status === 0,
551
+ stdout: r.stdout || '',
552
+ stderr: r.stderr || '',
553
+ exit_code: r.status === null ? -1 : r.status,
554
+ session_id: pwSessionId,
555
+ });
556
+ } catch (e) {
557
+ return writeWasmJson(instanceRef.value, { ok: false, error: e.message });
558
+ }
559
+ },
560
+
358
561
  host_env_get: (keyPtr, keyLen) => {
359
562
  try {
360
563
  const key = readWasmStr(instanceRef.value, keyPtr, keyLen);