gm-skill 2.0.1591 → 2.0.1593
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/AGENTS.md +1 -1
- package/bin/bootstrap.js +1 -1
- package/bin/plugkit.version +1 -1
- package/bin/plugkit.wasm.sha256 +1 -1
- package/gm-plugkit/package.json +1 -1
- package/gm.json +2 -2
- package/lib/skill-bootstrap.js +4 -4
- package/lib/spool-dispatch.js +1 -1
- package/package.json +1 -1
- package/bin/plugkit.js +0 -119
- package/lang/browser.js +0 -40
- package/lib/browser-spool-handler.js +0 -130
- package/lib/browser.js +0 -136
- package/lib/wasm-host.js +0 -236
package/AGENTS.md
CHANGED
|
@@ -26,7 +26,7 @@ This repo IS the published `gm-skill` npm package: repo root = package root, no
|
|
|
26
26
|
|
|
27
27
|
## WASM-only
|
|
28
28
|
|
|
29
|
-
The plugkit stack runs as a wasm cdylib loaded by `plugkit-wasm-wrapper.js` under Node/bun -- no native binaries built, downloaded, or published. The shipped `plugkit.wasm` is fetched at bootstrap from `plugkit-wasm` npm / `plugkit-bin` gh-releases, sha256-pinned. Size
|
|
29
|
+
The plugkit stack runs as a wasm cdylib loaded by `plugkit-wasm-wrapper.js` under Node/bun -- no native binaries built, downloaded, or published. The shipped `plugkit.wasm` is fetched at bootstrap from `plugkit-wasm` npm / `plugkit-bin` gh-releases, sha256-pinned. Size + embedded-model (offline in-wasm embeddings) mechanics in rs-learn (`recall: WASM-only plugkit size mechanics`).
|
|
30
30
|
|
|
31
31
|
**Every wasm host-import `extern "C"` block carries `#[link(wasm_import_module = "env")]`** -- in rs-plugkit AND every dep crate linked into the cdylib (rs-learn) AND any sibling building wasm (rs-exec, rs-search); miss it anywhere and the cascade goes dark (local builds stay green, only Linux CI link fails). Incident + host-fn enumeration in rs-learn (`recall: cascade outage wasm import module link`, `recall: wasm host-import link-module trap`).
|
|
32
32
|
|
package/bin/bootstrap.js
CHANGED
|
@@ -308,7 +308,7 @@ function acquireLock(lockPath) {
|
|
|
308
308
|
continue;
|
|
309
309
|
}
|
|
310
310
|
if (Date.now() - start > ATTEMPT_TIMEOUT_MS) throw new Error(`lock wait timeout: ${lockPath}`);
|
|
311
|
-
try {
|
|
311
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000); } catch (_) {}
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
}
|
package/bin/plugkit.version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.675
|
package/bin/plugkit.wasm.sha256
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
da9194797af0203623d02bb4fc972f6562ad7057fa7334a7c3f68af4aa8ba7f1 plugkit.wasm
|
package/gm-plugkit/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-plugkit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1593",
|
|
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": {
|
package/gm.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1593",
|
|
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.
|
|
20
|
+
"plugkitVersion": "0.1.675"
|
|
21
21
|
}
|
package/lib/skill-bootstrap.js
CHANGED
|
@@ -105,12 +105,13 @@ function computeFileHash(filePath) {
|
|
|
105
105
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
function httpGet(url, timeoutMs) {
|
|
108
|
+
function httpGet(url, timeoutMs, redirectsLeft = 5) {
|
|
109
109
|
return new Promise((resolve, reject) => {
|
|
110
110
|
const req = https.get(url, { timeout: timeoutMs, headers: { 'accept': 'application/json', 'user-agent': 'gm-skill-bootstrap' } }, (res) => {
|
|
111
111
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
112
112
|
res.resume();
|
|
113
|
-
|
|
113
|
+
if (redirectsLeft <= 0) { reject(new Error(`too many redirects ${url}`)); return; }
|
|
114
|
+
httpGet(res.headers.location, timeoutMs, redirectsLeft - 1).then(resolve, reject);
|
|
114
115
|
return;
|
|
115
116
|
}
|
|
116
117
|
if (res.statusCode !== 200) {
|
|
@@ -623,8 +624,7 @@ async function spawnPlugkitWatcher(wasmPath) {
|
|
|
623
624
|
if (!supervisorPath) {
|
|
624
625
|
emitBootstrapEvent('warn', 'falling back to direct watcher spawn (no supervisor)');
|
|
625
626
|
const logFd = openWatcherLog(projectDir);
|
|
626
|
-
const
|
|
627
|
-
const proc = spawn(runtime, [wrapperPath, 'spool'], {
|
|
627
|
+
const proc = spawn(process.execPath, [wrapperPath, 'spool'], {
|
|
628
628
|
detached: true,
|
|
629
629
|
stdio: ['ignore', logFd, logFd],
|
|
630
630
|
windowsHide: true,
|
package/lib/spool-dispatch.js
CHANGED
|
@@ -433,7 +433,7 @@ function checkDispatchGates(sessionId, operation, extra) {
|
|
|
433
433
|
if (fs.existsSync(mutsPath)) {
|
|
434
434
|
try {
|
|
435
435
|
const content = fs.readFileSync(mutsPath, 'utf8');
|
|
436
|
-
if (content.includes('
|
|
436
|
+
if (yamlStatusValues(content).includes('unknown')) {
|
|
437
437
|
logDeviation('deviation.gate-deny', { operation, reason: 'unresolved mutables' });
|
|
438
438
|
return { allowed: false, reason: 'unresolved mutables block tool execution; resolve all mutables before proceeding' };
|
|
439
439
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-skill",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1593",
|
|
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",
|
package/bin/plugkit.js
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
const { spawnSync } = require('child_process');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
|
|
8
|
-
const wrapperDir = __dirname;
|
|
9
|
-
|
|
10
|
-
function sha256OfFileSync(filePath) {
|
|
11
|
-
try {
|
|
12
|
-
const crypto = require('crypto');
|
|
13
|
-
const h = crypto.createHash('sha256');
|
|
14
|
-
const fd = fs.openSync(filePath, 'r');
|
|
15
|
-
try {
|
|
16
|
-
const buf = Buffer.alloc(1 << 20);
|
|
17
|
-
let n;
|
|
18
|
-
while ((n = fs.readSync(fd, buf, 0, buf.length, null)) > 0) h.update(buf.subarray(0, n));
|
|
19
|
-
} finally { fs.closeSync(fd); }
|
|
20
|
-
return h.digest('hex');
|
|
21
|
-
} catch (_) { return null; }
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function readPinnedVersion() {
|
|
25
|
-
try { return fs.readFileSync(path.join(wrapperDir, 'plugkit.version'), 'utf8').trim(); } catch (_) { return null; }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function readExpectedSha() {
|
|
29
|
-
try {
|
|
30
|
-
const manifest = fs.readFileSync(path.join(wrapperDir, 'plugkit.sha256'), 'utf8');
|
|
31
|
-
for (const line of manifest.split(/\r?\n/)) {
|
|
32
|
-
const parts = line.trim().split(/\s+/);
|
|
33
|
-
if (parts.length >= 2 && parts[parts.length - 1].replace(/^\*/, '') === 'plugkit.wasm') {
|
|
34
|
-
return parts[0].toLowerCase();
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
} catch (_) {}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function isReady() {
|
|
42
|
-
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
43
|
-
const primaryWasm = path.join(home, '.gm-tools', 'plugkit.wasm');
|
|
44
|
-
const fallbackWasm = path.join(home, '.claude', 'gm-tools', 'plugkit.wasm');
|
|
45
|
-
const wasmBin = fs.existsSync(primaryWasm) ? primaryWasm : fallbackWasm;
|
|
46
|
-
if (!fs.existsSync(wasmBin)) return false;
|
|
47
|
-
const expected = readExpectedSha();
|
|
48
|
-
if (!expected) return true;
|
|
49
|
-
const actual = sha256OfFileSync(wasmBin);
|
|
50
|
-
return actual && actual.toLowerCase() === expected;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function ensureReady(silent) {
|
|
54
|
-
if (isReady()) return true;
|
|
55
|
-
const bootstrap = path.join(wrapperDir, 'bootstrap.js');
|
|
56
|
-
const r = spawnSync(process.execPath, [bootstrap], {
|
|
57
|
-
stdio: silent ? ['ignore', 'pipe', 'pipe'] : ['ignore', 'inherit', 'inherit'],
|
|
58
|
-
windowsHide: true,
|
|
59
|
-
});
|
|
60
|
-
return r.status === 0 && isReady();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function wasmPath() {
|
|
64
|
-
return path.join(wrapperDir, 'plugkit.wasm');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function shouldUseWasm() {
|
|
68
|
-
if (process.env.GM_USE_WASM === '1') return true;
|
|
69
|
-
if (fs.existsSync(wasmPath())) return true;
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function runWasm(args) {
|
|
74
|
-
try {
|
|
75
|
-
const WasmHost = require('../lib/wasm-host');
|
|
76
|
-
const host = new WasmHost(wasmPath());
|
|
77
|
-
const verb = args[0] || 'health';
|
|
78
|
-
const body = args.slice(1).join(' ') || '{}';
|
|
79
|
-
const result = await host.dispatch(verb, body);
|
|
80
|
-
console.log(JSON.stringify(result, null, 2));
|
|
81
|
-
process.exit(result.ok ? 0 : 1);
|
|
82
|
-
} catch (err) {
|
|
83
|
-
console.error('[plugkit wasm]', err.message);
|
|
84
|
-
process.exit(1);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function main() {
|
|
89
|
-
const args = process.argv.slice(2);
|
|
90
|
-
const isHook = args[0] === 'hook';
|
|
91
|
-
const hookSubcmd = isHook ? (args[1] || '') : '';
|
|
92
|
-
|
|
93
|
-
if (shouldUseWasm()) {
|
|
94
|
-
return runWasm(args);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const blocksUntilReady = hookSubcmd === 'session-start' || hookSubcmd === 'prompt-submit';
|
|
98
|
-
|
|
99
|
-
if (blocksUntilReady) {
|
|
100
|
-
if (!ensureReady(false)) {
|
|
101
|
-
process.stderr.write('[plugkit] bootstrap failed; aborting hook\n');
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
return runWasm(args);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const home = process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
108
|
-
const primaryWasm = path.join(home, '.gm-tools', 'plugkit.wasm');
|
|
109
|
-
const fallbackWasm = path.join(home, '.claude', 'gm-tools', 'plugkit.wasm');
|
|
110
|
-
const wasmBin = fs.existsSync(primaryWasm) ? primaryWasm : fallbackWasm;
|
|
111
|
-
if (!fs.existsSync(wasmBin)) {
|
|
112
|
-
if (isHook) process.exit(0);
|
|
113
|
-
process.exit(1);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
runWasm(args);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
main();
|
package/lang/browser.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const { spawnSync } = require('child_process');
|
|
5
|
-
const fsSync = require('fs');
|
|
6
|
-
|
|
7
|
-
function findPlugkit() {
|
|
8
|
-
if (process.env.PLUGKIT_BIN && fsSync.existsSync(process.env.PLUGKIT_BIN)) return process.env.PLUGKIT_BIN;
|
|
9
|
-
const home = os.homedir();
|
|
10
|
-
const candidates = [
|
|
11
|
-
path.join(__dirname, '..', 'bin', 'plugkit.js'),
|
|
12
|
-
path.join(home, '.gm-tools', 'plugkit.js'),
|
|
13
|
-
path.join(home, '.claude', 'gm-tools', 'plugkit.js'),
|
|
14
|
-
];
|
|
15
|
-
for (const p of candidates) {
|
|
16
|
-
if (fsSync.existsSync(p)) return p;
|
|
17
|
-
}
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
module.exports = {
|
|
22
|
-
id: 'browser',
|
|
23
|
-
exec: {
|
|
24
|
-
run(code, cwd) {
|
|
25
|
-
const tmp = path.join(os.tmpdir(), 'gm-browser-' + Date.now() + '.js');
|
|
26
|
-
try {
|
|
27
|
-
fsSync.writeFileSync(tmp, code, 'utf-8');
|
|
28
|
-
const plugkit = findPlugkit();
|
|
29
|
-
if (!plugkit) return '[plugkit wrapper not found]';
|
|
30
|
-
const opts = { encoding: 'utf-8', timeout: 120000, windowsHide: true, ...(cwd && { cwd }) };
|
|
31
|
-
const r = spawnSync(process.execPath, [plugkit, 'exec', '--lang', 'browser', '--file', tmp], opts);
|
|
32
|
-
const out = (r.stdout || '').trimEnd();
|
|
33
|
-
const err = (r.stderr || '').trimEnd();
|
|
34
|
-
return out && err ? out + '\n[stderr]\n' + err : out || err || '(no output)';
|
|
35
|
-
} finally {
|
|
36
|
-
try { fsSync.unlinkSync(tmp); } catch (_) {}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
};
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const {
|
|
5
|
-
createSession,
|
|
6
|
-
sendCommand,
|
|
7
|
-
getScreenshot,
|
|
8
|
-
closeSession,
|
|
9
|
-
isBrowserAvailable,
|
|
10
|
-
} = require('./browser');
|
|
11
|
-
|
|
12
|
-
const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
|
|
13
|
-
|
|
14
|
-
function emitHandlerEvent(severity, message, details) {
|
|
15
|
-
try {
|
|
16
|
-
const date = new Date().toISOString().split('T')[0];
|
|
17
|
-
const logDir = path.join(LOG_DIR, date);
|
|
18
|
-
if (!fs.existsSync(logDir)) {
|
|
19
|
-
fs.mkdirSync(logDir, { recursive: true });
|
|
20
|
-
}
|
|
21
|
-
const logFile = path.join(logDir, 'browser-handler.jsonl');
|
|
22
|
-
const entry = {
|
|
23
|
-
ts: new Date().toISOString(),
|
|
24
|
-
severity,
|
|
25
|
-
message,
|
|
26
|
-
...details,
|
|
27
|
-
};
|
|
28
|
-
fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
|
|
29
|
-
} catch (e) {
|
|
30
|
-
console.error(`[browser-handler] Failed to emit event: ${e.message}`);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function handleBrowserVerb(body, sessionId) {
|
|
35
|
-
const lines = body.trim().split('\n');
|
|
36
|
-
const action = lines[0]?.trim();
|
|
37
|
-
const args = lines.slice(1).join('\n').trim();
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
emitHandlerEvent('info', 'Browser verb dispatched', {
|
|
41
|
-
sessionId,
|
|
42
|
-
action,
|
|
43
|
-
argsLength: args.length,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const available = await isBrowserAvailable();
|
|
47
|
-
if (!available) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
'Browser unavailable: the plugkit health probe did not respond. Check the watcher is alive (.gm/exec-spool/.status.json ts within 15s); if dead, boot it with `bun x gm-plugkit@latest spool`.'
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
switch (action) {
|
|
54
|
-
case 'start': {
|
|
55
|
-
const result = await createSession(sessionId);
|
|
56
|
-
console.log(JSON.stringify(result));
|
|
57
|
-
return result;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
case 'stop': {
|
|
61
|
-
const result = await closeSession(sessionId);
|
|
62
|
-
console.log(JSON.stringify(result));
|
|
63
|
-
return result;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
case 'screenshot': {
|
|
67
|
-
const result = await getScreenshot(sessionId);
|
|
68
|
-
console.log(JSON.stringify({
|
|
69
|
-
ok: result.ok,
|
|
70
|
-
mimeType: result.mimeType,
|
|
71
|
-
screenshotLength: result.screenshot?.length || 0,
|
|
72
|
-
screenshot: result.screenshot,
|
|
73
|
-
}));
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
case 'click':
|
|
78
|
-
case 'type':
|
|
79
|
-
case 'navigate':
|
|
80
|
-
case 'execute': {
|
|
81
|
-
let commandArgs = {};
|
|
82
|
-
if (args) {
|
|
83
|
-
try {
|
|
84
|
-
commandArgs = JSON.parse(args);
|
|
85
|
-
} catch (e) {
|
|
86
|
-
commandArgs = { value: args };
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const result = await sendCommand(sessionId, action, commandArgs);
|
|
91
|
-
console.log(JSON.stringify(result));
|
|
92
|
-
return result;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
default: {
|
|
96
|
-
let commandArgs = {};
|
|
97
|
-
if (args) {
|
|
98
|
-
try {
|
|
99
|
-
commandArgs = JSON.parse(args);
|
|
100
|
-
} catch (e) {
|
|
101
|
-
commandArgs = { value: args };
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
const result = await sendCommand(sessionId, action, commandArgs);
|
|
105
|
-
console.log(JSON.stringify(result));
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} catch (e) {
|
|
110
|
-
emitHandlerEvent('error', 'Browser verb failed', {
|
|
111
|
-
sessionId,
|
|
112
|
-
action,
|
|
113
|
-
error: e.message,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const errorResponse = {
|
|
117
|
-
ok: false,
|
|
118
|
-
error: e.message,
|
|
119
|
-
action,
|
|
120
|
-
sessionId,
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
console.log(JSON.stringify(errorResponse));
|
|
124
|
-
throw e;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
module.exports = {
|
|
129
|
-
handleBrowserVerb,
|
|
130
|
-
};
|
package/lib/browser.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const spool = require('./spool.js');
|
|
5
|
-
|
|
6
|
-
const LOG_DIR = path.join(os.homedir(), '.claude', 'gm-log');
|
|
7
|
-
const SESSION_STATE_DIR = path.join(os.homedir(), '.gm', 'browser-sessions');
|
|
8
|
-
|
|
9
|
-
function emitBrowserEvent(severity, message, details) {
|
|
10
|
-
try {
|
|
11
|
-
const date = new Date().toISOString().split('T')[0];
|
|
12
|
-
const logDir = path.join(LOG_DIR, date);
|
|
13
|
-
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
|
14
|
-
fs.appendFileSync(path.join(logDir, 'browser.jsonl'), JSON.stringify({ ts: new Date().toISOString(), severity, message, ...details }) + '\n');
|
|
15
|
-
} catch (e) {
|
|
16
|
-
console.error(`[browser] Failed to emit event: ${e.message}`);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function loadSessionState(sessionId) {
|
|
21
|
-
try {
|
|
22
|
-
fs.mkdirSync(SESSION_STATE_DIR, { recursive: true });
|
|
23
|
-
const stateFile = path.join(SESSION_STATE_DIR, `${sessionId}.json`);
|
|
24
|
-
if (fs.existsSync(stateFile)) return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
25
|
-
} catch (e) {
|
|
26
|
-
emitBrowserEvent('warn', 'Failed to load session state', { sessionId, error: e.message });
|
|
27
|
-
}
|
|
28
|
-
return { sessionId, createdAt: new Date().toISOString(), commands: [] };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function saveSessionState(sessionId, state) {
|
|
32
|
-
try {
|
|
33
|
-
fs.mkdirSync(SESSION_STATE_DIR, { recursive: true });
|
|
34
|
-
fs.writeFileSync(path.join(SESSION_STATE_DIR, `${sessionId}.json`), JSON.stringify(state, null, 2));
|
|
35
|
-
} catch (e) {
|
|
36
|
-
emitBrowserEvent('warn', 'Failed to save session state', { sessionId, error: e.message });
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parseJsonFromStdout(stdout) {
|
|
41
|
-
const trimmed = (stdout || '').trim();
|
|
42
|
-
if (!trimmed) return null;
|
|
43
|
-
const lines = trimmed.split(/\r?\n/).filter(Boolean);
|
|
44
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
45
|
-
try {
|
|
46
|
-
return JSON.parse(lines[i]);
|
|
47
|
-
} catch (e) {}
|
|
48
|
-
}
|
|
49
|
-
try {
|
|
50
|
-
return JSON.parse(trimmed);
|
|
51
|
-
} catch (e) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function runBrowserVerb(action, args, sessionId, timeoutMs = 30000) {
|
|
57
|
-
const payload = args && Object.keys(args).length > 0 ? `${action}\n${JSON.stringify(args)}` : action;
|
|
58
|
-
const result = await spool.execSpool(payload, 'browser', { timeoutMs, sessionId });
|
|
59
|
-
if (!result.ok) throw new Error(result.stderr || result.stdout || `browser verb failed: ${action}`);
|
|
60
|
-
const parsed = parseJsonFromStdout(result.stdout);
|
|
61
|
-
return parsed || { ok: true };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function isBrowserAvailable(sessionId = process.env.CLAUDE_SESSION_ID || 'unknown') {
|
|
65
|
-
try {
|
|
66
|
-
const result = await spool.execSpool('health', 'health', { timeoutMs: 1000, sessionId });
|
|
67
|
-
return !!(result && result.ok);
|
|
68
|
-
} catch (e) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function createSession(sessionId) {
|
|
74
|
-
if (!sessionId) throw new Error('sessionId is required');
|
|
75
|
-
const result = await runBrowserVerb('start', {}, sessionId, 30000);
|
|
76
|
-
const state = loadSessionState(sessionId);
|
|
77
|
-
state.browserSessionId = result.browserSessionId || result.sessionId || sessionId;
|
|
78
|
-
state.status = 'active';
|
|
79
|
-
state.createdAt = new Date().toISOString();
|
|
80
|
-
saveSessionState(sessionId, state);
|
|
81
|
-
return { ok: true, sessionId, browserSessionId: state.browserSessionId };
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function sendCommand(sessionId, commandType, args) {
|
|
85
|
-
if (!sessionId) throw new Error('sessionId is required');
|
|
86
|
-
const result = await runBrowserVerb(commandType, args || {}, sessionId, 30000);
|
|
87
|
-
const state = loadSessionState(sessionId);
|
|
88
|
-
state.commands = state.commands || [];
|
|
89
|
-
state.commands.push({ type: commandType, args: args || {}, timestamp: new Date().toISOString() });
|
|
90
|
-
if (state.commands.length > 1000) state.commands = state.commands.slice(-500);
|
|
91
|
-
saveSessionState(sessionId, state);
|
|
92
|
-
return { ok: true, result: result.result || result.data || result };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function executeScript(sessionId, code, options = {}) {
|
|
96
|
-
if (!sessionId) throw new Error('sessionId is required');
|
|
97
|
-
if (!code || typeof code !== 'string') throw new Error('code must be a non-empty string');
|
|
98
|
-
const payload = { code, ...options };
|
|
99
|
-
return sendCommand(sessionId, 'execute', payload);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async function getScreenshot(sessionId) {
|
|
103
|
-
if (!sessionId) throw new Error('sessionId is required');
|
|
104
|
-
const result = await runBrowserVerb('screenshot', {}, sessionId, 30000);
|
|
105
|
-
let screenshotData = result.screenshot || result.data || '';
|
|
106
|
-
if (screenshotData && !screenshotData.startsWith('data:image')) screenshotData = `data:image/png;base64,${screenshotData}`;
|
|
107
|
-
return { ok: true, screenshot: screenshotData, mimeType: 'image/png' };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function closeSession(sessionId) {
|
|
111
|
-
if (!sessionId) throw new Error('sessionId is required');
|
|
112
|
-
try { await runBrowserVerb('stop', {}, sessionId, 10000); } catch (e) {}
|
|
113
|
-
const stateFile = path.join(SESSION_STATE_DIR, `${sessionId}.json`);
|
|
114
|
-
if (fs.existsSync(stateFile)) fs.unlinkSync(stateFile);
|
|
115
|
-
return { ok: true, sessionId };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function closeAllSessions(opts = {}) {
|
|
119
|
-
if (!fs.existsSync(SESSION_STATE_DIR)) return { ok: true, closed: 0 };
|
|
120
|
-
const all = opts && opts.all === true;
|
|
121
|
-
const only = opts && typeof opts.sessionId === 'string' ? opts.sessionId : null;
|
|
122
|
-
if (!all && !only) {
|
|
123
|
-
return { ok: false, error: 'closeAllSessions is session-scoped: pass {sessionId} to close one, or {all:true} to close every session in this state dir. The per-session idle timer auto-closes idle browsers and they re-open seamlessly on next use, so a blanket close-all is no longer routine cleanup.', closed: 0 };
|
|
124
|
-
}
|
|
125
|
-
const files = fs.readdirSync(SESSION_STATE_DIR);
|
|
126
|
-
let closed = 0;
|
|
127
|
-
for (const file of files) {
|
|
128
|
-
if (!file.endsWith('.json')) continue;
|
|
129
|
-
const sessionId = file.replace('.json', '');
|
|
130
|
-
if (only && sessionId !== only) continue;
|
|
131
|
-
try { await closeSession(sessionId); closed++; } catch (e) {}
|
|
132
|
-
}
|
|
133
|
-
return { ok: true, closed, scope: only ? 'session' : 'all' };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
module.exports = { createSession, sendCommand, executeScript, getScreenshot, closeSession, closeAllSessions, isBrowserAvailable, loadSessionState, saveSessionState };
|
package/lib/wasm-host.js
DELETED
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { execSync, spawnSync } = require('child_process');
|
|
4
|
-
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
|
|
5
|
-
|
|
6
|
-
class WasmHost {
|
|
7
|
-
constructor(wasmPath) {
|
|
8
|
-
this.wasmPath = wasmPath;
|
|
9
|
-
this.instance = null;
|
|
10
|
-
this.memory = null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async init() {
|
|
14
|
-
try {
|
|
15
|
-
const wasmBuffer = fs.readFileSync(this.wasmPath);
|
|
16
|
-
const wasmModule = new WebAssembly.Module(wasmBuffer);
|
|
17
|
-
|
|
18
|
-
const importObject = {
|
|
19
|
-
host: {
|
|
20
|
-
host_fs_read: this.hostFsRead.bind(this),
|
|
21
|
-
host_fs_write: this.hostFsWrite.bind(this),
|
|
22
|
-
host_fs_readdir: this.hostFsReaddir.bind(this),
|
|
23
|
-
host_fs_stat: this.hostFsStat.bind(this),
|
|
24
|
-
host_kv_get: this.hostKvGet.bind(this),
|
|
25
|
-
host_kv_put: this.hostKvPut.bind(this),
|
|
26
|
-
host_kv_query: this.hostKvQuery.bind(this),
|
|
27
|
-
host_fetch: this.hostFetch.bind(this),
|
|
28
|
-
host_vec_search: this.hostVecSearch.bind(this),
|
|
29
|
-
host_browser_spawn: this.hostBrowserSpawn.bind(this),
|
|
30
|
-
host_browser_eval: this.hostBrowserEval.bind(this),
|
|
31
|
-
host_browser_close: this.hostBrowserClose.bind(this),
|
|
32
|
-
host_exec_js: this.hostExecJs.bind(this),
|
|
33
|
-
host_log: this.hostLog.bind(this),
|
|
34
|
-
host_now_ms: this.hostNowMs.bind(this),
|
|
35
|
-
host_env_get: this.hostEnvGet.bind(this),
|
|
36
|
-
},
|
|
37
|
-
env: {
|
|
38
|
-
memory: new WebAssembly.Memory({ initial: 256, maximum: 512 }),
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
this.instance = new WebAssembly.Instance(wasmModule, importObject);
|
|
43
|
-
this.memory = importObject.env.memory;
|
|
44
|
-
return { ok: true };
|
|
45
|
-
} catch (err) {
|
|
46
|
-
return { ok: false, error: err.message };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
readString(offset, len) {
|
|
51
|
-
const buf = new Uint8Array(this.memory.buffer, offset, len);
|
|
52
|
-
return new TextDecoder().decode(buf);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
writeString(str) {
|
|
56
|
-
const encoder = new TextEncoder();
|
|
57
|
-
const encoded = encoder.encode(str);
|
|
58
|
-
const len = encoded.length;
|
|
59
|
-
const offset = this.instance.exports.plugkit_alloc(len);
|
|
60
|
-
const buf = new Uint8Array(this.memory.buffer, offset, len);
|
|
61
|
-
buf.set(encoded);
|
|
62
|
-
return [offset, len];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
hostFsRead(pathPtr, pathLen) {
|
|
66
|
-
try {
|
|
67
|
-
const pathStr = this.readString(pathPtr, pathLen);
|
|
68
|
-
const content = fs.readFileSync(pathStr, 'utf8');
|
|
69
|
-
const [offset, len] = this.writeString(content);
|
|
70
|
-
return offset;
|
|
71
|
-
} catch (err) {
|
|
72
|
-
return 0;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
hostFsWrite(pathPtr, pathLen, dataPtr, dataLen) {
|
|
77
|
-
try {
|
|
78
|
-
const pathStr = this.readString(pathPtr, pathLen);
|
|
79
|
-
const data = this.readString(dataPtr, dataLen);
|
|
80
|
-
fs.writeFileSync(pathStr, data, 'utf8');
|
|
81
|
-
return 1;
|
|
82
|
-
} catch (err) {
|
|
83
|
-
return 0;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
hostFsReaddir(pathPtr, pathLen) {
|
|
88
|
-
try {
|
|
89
|
-
const pathStr = this.readString(pathPtr, pathLen);
|
|
90
|
-
const entries = fs.readdirSync(pathStr);
|
|
91
|
-
const result = JSON.stringify(entries);
|
|
92
|
-
const [offset] = this.writeString(result);
|
|
93
|
-
return offset;
|
|
94
|
-
} catch (err) {
|
|
95
|
-
return 0;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
hostFsStat(pathPtr, pathLen) {
|
|
100
|
-
try {
|
|
101
|
-
const pathStr = this.readString(pathPtr, pathLen);
|
|
102
|
-
const stat = fs.statSync(pathStr);
|
|
103
|
-
const result = JSON.stringify({
|
|
104
|
-
isFile: stat.isFile(),
|
|
105
|
-
isDirectory: stat.isDirectory(),
|
|
106
|
-
size: stat.size,
|
|
107
|
-
mtime: stat.mtime.getTime(),
|
|
108
|
-
});
|
|
109
|
-
const [offset] = this.writeString(result);
|
|
110
|
-
return offset;
|
|
111
|
-
} catch (err) {
|
|
112
|
-
return 0;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
hostKvGet(keyPtr, keyLen) {
|
|
117
|
-
return 0;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
hostKvPut(keyPtr, keyLen, valPtr, valLen) {
|
|
121
|
-
return 1;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
hostKvQuery(queryPtr, queryLen) {
|
|
125
|
-
return 0;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
hostFetch(urlPtr, urlLen, optsPtr, optsLen) {
|
|
129
|
-
return 0;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
hostVecSearch(queryPtr, queryLen) {
|
|
133
|
-
return 0;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
hostBrowserSpawn(urlPtr, urlLen) {
|
|
137
|
-
return 0;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
hostBrowserEval(sessionPtr, sessionLen, jsPtr, jsLen) {
|
|
141
|
-
return 0;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
hostBrowserClose(sessionPtr, sessionLen) {
|
|
145
|
-
return 1;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
hostExecJs(codePtr, codeLen) {
|
|
149
|
-
try {
|
|
150
|
-
const code = this.readString(codePtr, codeLen);
|
|
151
|
-
const result = eval(`(${code})`);
|
|
152
|
-
const [offset] = this.writeString(JSON.stringify(result));
|
|
153
|
-
return offset;
|
|
154
|
-
} catch (err) {
|
|
155
|
-
return 0;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
hostLog(msgPtr, msgLen) {
|
|
160
|
-
const msg = this.readString(msgPtr, msgLen);
|
|
161
|
-
console.log(msg);
|
|
162
|
-
return 1;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
hostNowMs() {
|
|
166
|
-
return Date.now();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
hostEnvGet(keyPtr, keyLen) {
|
|
170
|
-
const key = this.readString(keyPtr, keyLen);
|
|
171
|
-
const val = process.env[key] || '';
|
|
172
|
-
const [offset] = this.writeString(val);
|
|
173
|
-
return offset;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async dispatch(verb, body) {
|
|
177
|
-
if (!this.instance) {
|
|
178
|
-
const initResult = await this.init();
|
|
179
|
-
if (!initResult.ok) return initResult;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
|
184
|
-
const [verbOffset, verbLen] = this.writeString(verb);
|
|
185
|
-
const [bodyOffset, bodyLen] = this.writeString(bodyStr);
|
|
186
|
-
|
|
187
|
-
const resultPtr = this.instance.exports.dispatch_verb(verbOffset, verbLen, bodyOffset, bodyLen);
|
|
188
|
-
if (resultPtr === 0) {
|
|
189
|
-
return { ok: false, error: 'dispatch_verb returned null' };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const resultStr = this.readString(resultPtr, 1024);
|
|
193
|
-
try {
|
|
194
|
-
return JSON.parse(resultStr);
|
|
195
|
-
} catch {
|
|
196
|
-
return { ok: true, output: resultStr };
|
|
197
|
-
}
|
|
198
|
-
} catch (err) {
|
|
199
|
-
return { ok: false, error: err.message };
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async hook(name, body) {
|
|
204
|
-
if (!this.instance) {
|
|
205
|
-
const initResult = await this.init();
|
|
206
|
-
if (!initResult.ok) return initResult;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
try {
|
|
210
|
-
const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
|
211
|
-
const [nameOffset, nameLen] = this.writeString(name);
|
|
212
|
-
const [bodyOffset, bodyLen] = this.writeString(bodyStr);
|
|
213
|
-
|
|
214
|
-
const hookFn = this.instance.exports[`hook_${name}`];
|
|
215
|
-
if (!hookFn) {
|
|
216
|
-
return { ok: false, error: `hook_${name} not found` };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const resultPtr = hookFn(bodyOffset, bodyLen);
|
|
220
|
-
if (resultPtr === 0) {
|
|
221
|
-
return { ok: false, error: `hook_${name} returned null` };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const resultStr = this.readString(resultPtr, 1024);
|
|
225
|
-
try {
|
|
226
|
-
return JSON.parse(resultStr);
|
|
227
|
-
} catch {
|
|
228
|
-
return { ok: true, output: resultStr };
|
|
229
|
-
}
|
|
230
|
-
} catch (err) {
|
|
231
|
-
return { ok: false, error: err.message };
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
module.exports = WasmHost;
|