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 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 (~149MB) + embedded-model (offline in-wasm embeddings) mechanics in rs-learn (`recall: WASM-only plugkit size mechanics`).
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 { const { spawnSync } = require('child_process'); spawnSync(process.execPath, ['-e', 'setTimeout(()=>{}, 2000)'], { timeout: 2500, killSignal: 'SIGKILL', stdio: 'ignore', windowsHide: true }); } catch (_) {}
311
+ try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000); } catch (_) {}
312
312
  }
313
313
  }
314
314
  }
@@ -1 +1 @@
1
- 0.1.674
1
+ 0.1.675
@@ -1 +1 @@
1
- 4b5920d31272fa0371440b50d174ac5688053e9db5d689e412871c30f706a1f4 plugkit.wasm
1
+ da9194797af0203623d02bb4fc972f6562ad7057fa7334a7c3f68af4aa8ba7f1 plugkit.wasm
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1591",
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.1591",
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.674"
20
+ "plugkitVersion": "0.1.675"
21
21
  }
@@ -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
- httpGet(res.headers.location, timeoutMs).then(resolve, reject);
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 runtime = process.platform === 'win32' ? 'bun.exe' : 'bun';
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,
@@ -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('status: unknown')) {
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.1591",
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;