gm-skill 2.0.1114 → 2.0.1116

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 CHANGED
@@ -28,7 +28,7 @@ npx gm-skill-bootstrap
28
28
 
29
29
  ## Version
30
30
 
31
- `2.0.1114` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` republishes this package alongside all 15 platform packages.
31
+ `2.0.1116` — auto-bumped from the canonical `gm` repo. Every push to `AnEntrypoint/gm` republishes this package alongside all 15 platform packages.
32
32
 
33
33
  ## Source of truth
34
34
 
@@ -1 +1 @@
1
- 0.1.393
1
+ 0.1.394
package/bin/plugkit.wasm CHANGED
Binary file
@@ -1 +1 @@
1
- e35333def0786a3741b90f6cdf33b3748870d9891e0e9a4bbbc681c41ef0033f plugkit.wasm
1
+ c682ec59373cc9cc7962d438c6f189844dc8b1998d53c7da14cddc49662b37f4 plugkit.wasm
@@ -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);
package/gm.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm",
3
- "version": "2.0.1114",
3
+ "version": "2.0.1116",
4
4
  "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -17,5 +17,5 @@
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
20
- "plugkitVersion": "0.1.393"
20
+ "plugkitVersion": "0.1.394"
21
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-skill",
3
- "version": "2.0.1114",
3
+ "version": "2.0.1116",
4
4
  "description": "Canonical universal harness — AI-native software engineering via skill-driven orchestration; bootstraps plugkit for task execution and session isolation. Install in any AI coding agent host.",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -39,7 +39,7 @@
39
39
  "gm.json"
40
40
  ],
41
41
  "dependencies": {
42
- "gm-plugkit": "^2.0.1114"
42
+ "gm-plugkit": "^2.0.1116"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=16.0.0"
@@ -30,10 +30,18 @@ Dispatch `instruction` (empty body for current phase; `phase=<NAME>` line, `{"ph
30
30
 
31
31
  ## Host verbs
32
32
 
33
- `fs_read`, `fs_write`, `fs_stat`, `fs_readdir`, `kv_get`, `kv_put`, `kv_query`, `fetch`, `exec_js`, `env_get`, `recall`, `codesearch`, `memorize`, `health`, `status`, `wait`, `sleep`, `close`, `kill-port`, `forget`, `feedback`, `learn-status`, `learn-debug`, `learn-build`, `discipline`, `pause`, `runner`, `inference`.
33
+ `fs_read`, `fs_write`, `fs_stat`, `fs_readdir`, `kv_get`, `kv_put`, `kv_query`, `fetch`, `exec_js`, `env_get`, `recall`, `codesearch`, `memorize`, `health`, `status`, `wait`, `sleep`, `close`, `kill-port`, `forget`, `feedback`, `learn-status`, `learn-debug`, `learn-build`, `discipline`, `pause`, `runner`, `inference`, `browser`.
34
34
 
35
35
  ## Language verbs
36
36
 
37
37
  `nodejs`, `python`, `bash`, `powershell`, `ssh`, `go`, `rust`, `c`, `cpp`, `java`, `deno` — write raw code as the request body.
38
38
 
39
+ ### Browser
40
+
41
+ Dispatch `.gm/exec-spool/in/browser/<N>.txt` with raw JavaScript as the body. The wrapper spawns Chrome (managed profile at `<cwd>/.plugkit-browser-profile/`) and runs the JS via playwriter. Globals available inside the body: `page` (playwright Page), `snapshot` (accessibility snapshot), `screenshotWithAccessibilityLabels` (screenshot helper), `state` (per-session state object).
42
+
43
+ Special commands (body starts with `session `): `session new`, `session list`, `session close <id>` pass through to playwriter directly.
44
+
45
+ Chrome is detected from system install paths; profile dir is project-scoped so cookies/login persist per project. Add `.plugkit-browser-profile/` to your repo's `.gitignore` — the wrapper does this automatically.
46
+
39
47
  Plugkit serves what prior skills (`gm:planning`, `gm:gm-execute`) used to serve, on demand, per phase. There is no other skill.