gm-skill 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/README.md +1 -1
- package/gm-plugkit/plugkit-wasm-wrapper.js +203 -0
- package/gm.json +1 -1
- package/package.json +2 -2
- package/skills/gm-skill/SKILL.md +9 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ npx gm-skill-bootstrap
|
|
|
28
28
|
|
|
29
29
|
## Version
|
|
30
30
|
|
|
31
|
-
`2.0.
|
|
31
|
+
`2.0.1115` — 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
|
|
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1115",
|
|
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.
|
|
42
|
+
"gm-plugkit": "^2.0.1115"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=16.0.0"
|
package/skills/gm-skill/SKILL.md
CHANGED
|
@@ -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.
|