gm-skill 0.1.2 → 2.0.1080

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.
Files changed (85) hide show
  1. package/AGENTS.md +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +20 -84
  4. package/agents/gm.md +22 -0
  5. package/agents/memorize.md +100 -0
  6. package/agents/research-worker.md +36 -0
  7. package/agents/textprocessing.md +47 -0
  8. package/bin/bootstrap.js +702 -0
  9. package/bin/plugkit.js +136 -0
  10. package/bin/plugkit.sha256 +7 -0
  11. package/bin/plugkit.version +1 -0
  12. package/bin/plugkit.wasm +0 -0
  13. package/bin/plugkit.wasm.sha256 +1 -0
  14. package/bin/rtk.sha256 +6 -0
  15. package/bin/rtk.version +1 -0
  16. package/gm-plugkit/bootstrap.js +694 -0
  17. package/gm-plugkit/cli.js +48 -0
  18. package/gm-plugkit/index.js +12 -0
  19. package/gm-plugkit/package.json +26 -0
  20. package/gm-plugkit/plugkit-wasm-wrapper.js +190 -0
  21. package/gm-plugkit/plugkit.sha256 +6 -0
  22. package/gm-plugkit/plugkit.version +1 -0
  23. package/gm.json +27 -0
  24. package/lang/browser.js +45 -0
  25. package/lang/ssh.js +166 -0
  26. package/lib/browser-spool-handler.js +130 -0
  27. package/lib/browser.js +131 -0
  28. package/lib/codeinsight.js +109 -0
  29. package/lib/daemon-bootstrap.js +253 -132
  30. package/lib/git.js +0 -1
  31. package/lib/learning.js +169 -0
  32. package/lib/skill-bootstrap.js +406 -0
  33. package/lib/spool-dispatch.js +100 -0
  34. package/lib/spool.js +87 -49
  35. package/lib/wasm-host.js +241 -0
  36. package/package.json +38 -20
  37. package/prompts/bash-deny.txt +22 -0
  38. package/prompts/pre-compact.txt +21 -0
  39. package/prompts/prompt-submit.txt +83 -0
  40. package/prompts/session-start.txt +15 -0
  41. package/scripts/run-hook.sh +7 -0
  42. package/scripts/watch-cascade.js +166 -0
  43. package/skills/browser/SKILL.md +80 -0
  44. package/skills/code-search/SKILL.md +48 -0
  45. package/skills/create-lang-plugin/SKILL.md +121 -0
  46. package/skills/gm/SKILL.md +10 -49
  47. package/skills/gm-complete/SKILL.md +16 -87
  48. package/skills/gm-emit/SKILL.md +17 -50
  49. package/skills/gm-execute/SKILL.md +18 -69
  50. package/skills/gm-skill/SKILL.md +43 -0
  51. package/skills/gm-skill/index.js +21 -0
  52. package/skills/governance/SKILL.md +97 -0
  53. package/skills/pages/SKILL.md +208 -0
  54. package/skills/planning/SKILL.md +21 -97
  55. package/skills/research/SKILL.md +43 -0
  56. package/skills/ssh/SKILL.md +71 -0
  57. package/skills/textprocessing/SKILL.md +40 -0
  58. package/skills/update-docs/SKILL.md +24 -43
  59. package/gm-complete.SKILL.md +0 -106
  60. package/gm-emit.SKILL.md +0 -70
  61. package/gm-execute.SKILL.md +0 -88
  62. package/gm.SKILL.md +0 -63
  63. package/index.js +0 -1
  64. package/lib/index.js +0 -37
  65. package/lib/loader.js +0 -66
  66. package/lib/manifest.js +0 -99
  67. package/lib/prepare.js +0 -14
  68. package/planning.SKILL.md +0 -118
  69. package/skills/gm/index.js +0 -113
  70. package/skills/gm-complete/index.js +0 -118
  71. package/skills/gm-complete.SKILL.md +0 -106
  72. package/skills/gm-emit/index.js +0 -90
  73. package/skills/gm-emit.SKILL.md +0 -70
  74. package/skills/gm-execute/index.js +0 -91
  75. package/skills/gm-execute.SKILL.md +0 -88
  76. package/skills/gm.SKILL.md +0 -63
  77. package/skills/planning/index.js +0 -107
  78. package/skills/planning.SKILL.md +0 -118
  79. package/skills/update-docs/index.js +0 -108
  80. package/skills/update-docs.SKILL.md +0 -66
  81. package/test-build.js +0 -29
  82. package/test-e2e.js +0 -117
  83. package/test-unified.js +0 -24
  84. package/test.js +0 -89
  85. package/update-docs.SKILL.md +0 -66
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { ensureReady, startSpoolDaemon, getBinaryPath, isReady } = require('./bootstrap');
5
+
6
+ const usage = `gm-plugkit — Bootstrap and daemon-spawn for gm plugkit binary.
7
+
8
+ Usage:
9
+ bun x gm-plugkit@latest Bootstrap + start spool daemon
10
+ bun x gm-plugkit@latest --daemon Same as default
11
+ bun x gm-plugkit@latest --binary Print binary path only
12
+ bun x gm-plugkit@latest --status JSON status check
13
+ bun x gm-plugkit@latest --help Show this help
14
+ `;
15
+
16
+ (async () => {
17
+ const args = process.argv.slice(2);
18
+
19
+ if (args.includes('--help') || args.includes('-h')) {
20
+ console.log(usage);
21
+ process.exit(0);
22
+ }
23
+
24
+ try {
25
+ const result = await ensureReady();
26
+ if (!result.ok) {
27
+ console.error('Bootstrap failed:', result.error);
28
+ process.exit(1);
29
+ }
30
+
31
+ const daemon = startSpoolDaemon();
32
+ if (!daemon.ok) {
33
+ console.error('Daemon start failed:', daemon.error);
34
+ process.exit(1);
35
+ }
36
+
37
+ console.log(JSON.stringify({
38
+ ok: true,
39
+ binary: result.binaryPath,
40
+ daemon: daemon,
41
+ message: 'plugkit ready, spool watcher running'
42
+ }));
43
+ process.exit(0);
44
+ } catch (err) {
45
+ console.error('gm-plugkit failed:', err.message);
46
+ process.exit(1);
47
+ }
48
+ })();
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { ensureReady, startSpoolDaemon, getBinaryPath, isReady, bootstrap } = require('./bootstrap');
5
+
6
+ module.exports = {
7
+ ensureReady,
8
+ startSpoolDaemon,
9
+ getBinaryPath,
10
+ isReady,
11
+ bootstrap,
12
+ };
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "gm-plugkit",
3
+ "version": "0.1.0",
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
+ "main": "index.js",
6
+ "bin": {
7
+ "gm-plugkit": "./cli.js",
8
+ "plugkit-wasm-wrapper": "./plugkit-wasm-wrapper.js"
9
+ },
10
+ "files": [
11
+ "cli.js",
12
+ "index.js",
13
+ "bootstrap.js",
14
+ "plugkit-wasm-wrapper.js",
15
+ "plugkit.version",
16
+ "plugkit.sha256"
17
+ ],
18
+ "keywords": ["gm", "plugkit", "bootstrap", "daemon", "spool", "wasm"],
19
+ "author": "AnEntrypoint",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://github.com/AnEntrypoint/gm.git",
24
+ "directory": "gm-starter/gm-plugkit"
25
+ }
26
+ }
@@ -0,0 +1,190 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { watch } from 'fs';
5
+
6
+ function createWasiShim() {
7
+ const shim = new Proxy({}, {
8
+ get(target, prop) {
9
+ if (prop === 'proc_exit') return (code) => process.exit(code);
10
+ return () => 0;
11
+ }
12
+ });
13
+ return shim;
14
+ }
15
+
16
+ function memWriteStr(memory, ptr, len, str) {
17
+ const bytes = new TextEncoder().encode(str);
18
+ const buf = new Uint8Array(memory.buffer, ptr, len);
19
+ buf.set(bytes.slice(0, len));
20
+ return Math.min(bytes.length, len);
21
+ }
22
+
23
+ function memReadStr(memory, ptr, len) {
24
+ const buf = new Uint8Array(memory.buffer, ptr, len);
25
+ return new TextDecoder().decode(buf);
26
+ }
27
+
28
+ let nextMemPtr = 0x10000;
29
+ function allocInMemory(memory, data) {
30
+ const ptr = nextMemPtr;
31
+ const buf = new Uint8Array(memory.buffer, ptr, data.length);
32
+ buf.set(data);
33
+ nextMemPtr += data.length;
34
+ if (nextMemPtr > memory.buffer.byteLength - 4096) nextMemPtr = 0x10000;
35
+ return { ptr, len: data.length };
36
+ }
37
+
38
+ function packResult(str) {
39
+ const bytes = new TextEncoder().encode(str);
40
+ const ptr = Math.random() * 0x100000 | 0;
41
+ const len = bytes.length;
42
+ return (BigInt(ptr) & 0xffffffffn) | (BigInt(len) << 32n);
43
+ }
44
+
45
+ async function runSpoolWatcher(instance, spoolDir) {
46
+ const inDir = path.join(spoolDir, 'in');
47
+ const outDir = path.join(spoolDir, 'out');
48
+ fs.mkdirSync(inDir, { recursive: true });
49
+ fs.mkdirSync(outDir, { recursive: true });
50
+
51
+ console.log(`[plugkit-wasm] watching ${inDir}`);
52
+
53
+ const processed = new Set();
54
+ const dispatch = instance.exports.dispatch_verb;
55
+ if (!dispatch) throw new Error('dispatch_verb not exported');
56
+
57
+ const processFile = async (filePath) => {
58
+ const key = path.relative(inDir, filePath);
59
+ if (processed.has(key)) return;
60
+ processed.add(key);
61
+
62
+ try {
63
+ const content = fs.readFileSync(filePath, 'utf8');
64
+ const relPath = path.relative(inDir, filePath);
65
+ const dir = path.dirname(relPath);
66
+ const verb = dir === '.' ? path.basename(filePath, path.extname(filePath)) : dir;
67
+ const body = content.trim() || '{}';
68
+
69
+ const verbBytes = new TextEncoder().encode(verb);
70
+ const bodyBytes = new TextEncoder().encode(body);
71
+
72
+ const { ptr: verbPtr, len: verbLen } = allocInMemory(instance.exports.memory, verbBytes);
73
+ const { ptr: bodyPtr, len: bodyLen } = allocInMemory(instance.exports.memory, bodyBytes);
74
+
75
+ const result = dispatch(verbPtr, verbLen, bodyPtr, bodyLen);
76
+
77
+ const ptr = Number(result & 0xffffffffn);
78
+ const len = Number(result >> 32n);
79
+ const resultStr = memReadStr(instance.exports.memory, ptr, len);
80
+
81
+ const taskId = Math.random().toString(36).slice(2);
82
+ fs.writeFileSync(path.join(outDir, `${taskId}.json`), resultStr);
83
+
84
+ fs.unlinkSync(filePath);
85
+ processed.delete(key);
86
+ } catch (e) {
87
+ console.error(`[plugkit-wasm] error processing ${key}: ${e.message}`);
88
+ try { fs.unlinkSync(filePath); } catch (_) {}
89
+ processed.delete(key);
90
+ }
91
+ };
92
+
93
+ function walkDir(dir) {
94
+ const files = [];
95
+ try {
96
+ for (const entry of fs.readdirSync(dir)) {
97
+ const fullPath = path.join(dir, entry);
98
+ const stat = fs.statSync(fullPath);
99
+ if (stat.isFile()) {
100
+ files.push(fullPath);
101
+ } else if (stat.isDirectory()) {
102
+ files.push(...walkDir(fullPath));
103
+ }
104
+ }
105
+ } catch (e) {
106
+ console.error(`[plugkit-wasm] error walking ${dir}: ${e.message}`);
107
+ }
108
+ return files;
109
+ }
110
+
111
+ const existing = walkDir(inDir);
112
+ for (const fullPath of existing) {
113
+ await processFile(fullPath);
114
+ }
115
+
116
+ let debounce = {};
117
+ watch(inDir, { recursive: true }, (eventType, filename) => {
118
+ if (!filename) return;
119
+ const fullPath = path.join(inDir, filename);
120
+
121
+ clearTimeout(debounce[fullPath]);
122
+ debounce[fullPath] = setTimeout(async () => {
123
+ try {
124
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
125
+ await processFile(fullPath);
126
+ }
127
+ } catch (_) {}
128
+ delete debounce[fullPath];
129
+ }, 50);
130
+ });
131
+
132
+ console.log('[plugkit-wasm] spool watcher running');
133
+ await new Promise(() => {});
134
+ }
135
+
136
+ (async () => {
137
+ try {
138
+ const wasmPath = path.join(os.homedir(), '.claude', 'gm-tools', 'plugkit.wasm');
139
+ const wasmBuffer = fs.readFileSync(wasmPath);
140
+ const wasmModule = new WebAssembly.Module(wasmBuffer);
141
+
142
+ const memory = new WebAssembly.Memory({ initial: 256, maximum: 512 });
143
+
144
+ const hostFunctions = {
145
+ host_fs_read: () => 0,
146
+ host_fs_write: () => 0,
147
+ host_fs_readdir: () => 0,
148
+ host_fs_stat: () => 0,
149
+ host_kv_get: () => 0,
150
+ host_kv_put: () => 0,
151
+ host_kv_query: () => 0,
152
+ host_fetch: () => 0,
153
+ host_vec_search: () => 0,
154
+ host_vec_embed: () => 0,
155
+ host_browser_spawn: () => 0,
156
+ host_browser_eval: () => 0,
157
+ host_browser_close: () => 0,
158
+ host_exec_js: () => 0,
159
+ host_log: (ptr, len) => { console.log('[host_log]'); return 0; },
160
+ host_now_ms: () => BigInt(Date.now()),
161
+ host_env_get: () => 0,
162
+ };
163
+
164
+ const importObject = {
165
+ env: { memory, ...hostFunctions },
166
+ wasi_snapshot_preview1: createWasiShim(),
167
+ };
168
+
169
+ const instance = new WebAssembly.Instance(wasmModule, importObject);
170
+
171
+ const args = process.argv.slice(2);
172
+ if (args.includes('--version')) {
173
+ console.log('plugkit v0.1.366 (wasm)');
174
+ process.exit(0);
175
+ }
176
+
177
+ if (args[0] === 'spool') {
178
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
179
+ const spoolDir = path.join(projectDir, '.gm', 'exec-spool');
180
+ await runSpoolWatcher(instance, spoolDir);
181
+ } else {
182
+ console.log('[plugkit-wasm] args:', args.join(' '));
183
+ process.exit(0);
184
+ }
185
+ } catch (e) {
186
+ console.error('[plugkit-wasm] fatal:', e.message);
187
+ if (process.env.DEBUG) console.error(e.stack);
188
+ process.exit(1);
189
+ }
190
+ })();
@@ -0,0 +1,6 @@
1
+ e5569efe81e4ef06c8349678253ca2845571495e619babb0a79be9268ea83c2a plugkit-win32-x64.exe
2
+ ce2f09f8ea0dd522345a9d9c3e5b04ba44bc37b2046d311d7ff1737f1b3fbf1a plugkit-win32-arm64.exe
3
+ a1a1d376986551828e5a39e4ae931accf66f00663aceac1439b2778cb4fffd27 plugkit-darwin-x64
4
+ 7c36d730edab5cddf678211146ca670c9ce1def17d8b454234ce4bc04a4d7e85 plugkit-darwin-arm64
5
+ c9db60a399caf53c490dc08705713c7d83a1f62db057585a3950d64ab8fa449a plugkit-linux-x64
6
+ b9ebabaace995b1768d1d96ae13ca18a6dc5e2ca65b774fcdd457f069a7d115c plugkit-linux-arm64
@@ -0,0 +1 @@
1
+ 0.1.366
package/gm.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "gm",
3
+ "version": "2.0.1080",
4
+ "description": "Spool-dispatch orchestration engine with unified state machine, skills, and automated git enforcement",
5
+ "author": "AnEntrypoint",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "claude-code",
9
+ "claude-plugin",
10
+ "wfgy",
11
+ "automation"
12
+ ],
13
+ "homepage": "https://github.com/AnEntrypoint/gm",
14
+ "agents": [
15
+ {
16
+ "name": "gm",
17
+ "description": "Agent (not skill) - immutable programming state machine. Always invoke for all work coordination."
18
+ }
19
+ ],
20
+ "engines": {
21
+ "node": ">=16.0.0"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "plugkitVersion": "0.1.366"
27
+ }
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const { spawnSync, execSync } = 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 isWin = process.platform === 'win32';
11
+ const exe = isWin ? 'plugkit.exe' : 'plugkit';
12
+ const candidates = [
13
+ path.join(home, '.claude', 'gm-tools', exe),
14
+ path.join(home, '.local', 'bin', exe),
15
+ path.join(home, '.claude', 'plugins', 'marketplaces', 'gm-cc', 'bin', exe),
16
+ ];
17
+ if (process.env.CLAUDE_PLUGIN_ROOT) {
18
+ candidates.push(path.join(process.env.CLAUDE_PLUGIN_ROOT, 'bin', exe));
19
+ }
20
+ for (const p of candidates) {
21
+ if (fsSync.existsSync(p)) return p;
22
+ }
23
+ try { execSync('plugkit --version', { encoding: 'utf-8', timeout: 3000 }); return 'plugkit'; } catch (_) {}
24
+ return 'plugkit';
25
+ }
26
+
27
+ module.exports = {
28
+ id: 'browser',
29
+ exec: {
30
+ run(code, cwd) {
31
+ const tmp = path.join(os.tmpdir(), 'gm-browser-' + Date.now() + '.js');
32
+ try {
33
+ fsSync.writeFileSync(tmp, code, 'utf-8');
34
+ const plugkit = findPlugkit();
35
+ const opts = { encoding: 'utf-8', timeout: 120000, windowsHide: true, ...(cwd && { cwd }) };
36
+ const r = spawnSync(plugkit, ['exec', '--lang', 'browser', '--file', tmp], opts);
37
+ const out = (r.stdout || '').trimEnd();
38
+ const err = (r.stderr || '').trimEnd();
39
+ return out && err ? out + '\n[stderr]\n' + err : out || err || '(no output)';
40
+ } finally {
41
+ try { fsSync.unlinkSync(tmp); } catch (_) {}
42
+ }
43
+ }
44
+ }
45
+ };
package/lang/ssh.js ADDED
@@ -0,0 +1,166 @@
1
+ 'use strict';
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const fsSync = require('fs');
5
+ const http = require('http');
6
+
7
+ function loadTarget(targetName) {
8
+ const cfgPath = path.join(os.homedir(), '.claude', 'ssh-targets.json');
9
+ if (!fsSync.existsSync(cfgPath)) throw new Error('No ssh-targets.json found at ' + cfgPath);
10
+ const cfg = JSON.parse(fsSync.readFileSync(cfgPath, 'utf8'));
11
+ const name = targetName || 'default';
12
+ if (!cfg[name]) throw new Error('No target \'' + name + '\' in ssh-targets.json. Available: ' + Object.keys(cfg).join(', '));
13
+ return cfg[name];
14
+ }
15
+
16
+ function parseCommand(code) {
17
+ const lines = code.trim().split('\n');
18
+ let target = 'default';
19
+ let cmd = code.trim();
20
+ if (lines[0].trim().startsWith('@')) {
21
+ target = lines[0].trim().slice(1);
22
+ cmd = lines.slice(1).join('\n').trim();
23
+ }
24
+ return { target, cmd };
25
+ }
26
+
27
+ function resolveSsh2() {
28
+ const candidates = [
29
+ path.join(os.homedir(), '.claude', 'gm-tools', 'node_modules', 'ssh2'),
30
+ path.join(os.homedir(), '.claude', 'plugins', 'node_modules', 'ssh2'),
31
+ 'ssh2',
32
+ ];
33
+ for (const p of candidates) {
34
+ try { return require(p); } catch (_) {}
35
+ }
36
+ throw new Error('ssh2 not found. Run: cd ~/.claude/gm-tools && npm install ssh2');
37
+ }
38
+
39
+ function getRunnerPort() {
40
+ const portFile = path.join(os.tmpdir(), 'glootie-runner.port');
41
+ try { return parseInt(fsSync.readFileSync(portFile, 'utf8').trim(), 10); } catch { return null; }
42
+ }
43
+
44
+ function rpcCall(port, method, params) {
45
+ return new Promise((resolve, reject) => {
46
+ const body = JSON.stringify({ method, params });
47
+ const req = http.request(
48
+ { hostname: '127.0.0.1', port, path: '/rpc', method: 'POST',
49
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } },
50
+ (res) => {
51
+ let data = '';
52
+ res.on('data', c => { data += c; });
53
+ res.on('end', () => {
54
+ try {
55
+ const p = JSON.parse(data);
56
+ if (p.error) return reject(new Error(p.error.message || String(p.error)));
57
+ resolve(p.result);
58
+ } catch { reject(new Error('RPC parse error: ' + data)); }
59
+ });
60
+ }
61
+ );
62
+ req.setTimeout(5000, () => { req.destroy(); reject(new Error('RPC timeout')); });
63
+ req.on('error', reject);
64
+ req.write(body);
65
+ req.end();
66
+ });
67
+ }
68
+
69
+ function runSsh(target, cmd, onData) {
70
+ return new Promise((resolve, reject) => {
71
+ const { Client } = resolveSsh2();
72
+ const ssh = new Client();
73
+ let out = '';
74
+ let done = false;
75
+
76
+ const finish = (text) => {
77
+ if (!done) { done = true; ssh.end(); resolve(text != null ? text : out.trimEnd()); }
78
+ };
79
+
80
+ const timeout = setTimeout(() => {
81
+ if (!done) { done = true; try { ssh.end(); } catch (_) {} resolve(out.trimEnd() || '[timeout after 55s]'); }
82
+ }, 55000);
83
+
84
+ ssh.on('ready', () => {
85
+ ssh.exec(cmd, { pty: false }, (err, stream) => {
86
+ if (err) { clearTimeout(timeout); ssh.end(); reject(err); return; }
87
+ stream.on('data', d => {
88
+ const s = d.toString();
89
+ out += s;
90
+ if (onData) onData(s, 'stdout');
91
+ });
92
+ stream.stderr.on('data', d => {
93
+ const s = d.toString();
94
+ out += s;
95
+ if (onData) onData(s, 'stderr');
96
+ });
97
+ stream.on('close', () => { clearTimeout(timeout); finish(); });
98
+ });
99
+ });
100
+
101
+ ssh.on('error', err => { clearTimeout(timeout); if (!done) { done = true; reject(err); } });
102
+
103
+ const connOpts = { host: target.host, port: target.port || 22, username: target.username, readyTimeout: 15000 };
104
+ if (target.password) connOpts.password = target.password;
105
+ if (target.keyPath) connOpts.privateKey = fsSync.readFileSync(target.keyPath);
106
+ if (target.passphrase) connOpts.passphrase = target.passphrase;
107
+ ssh.connect(connOpts);
108
+ });
109
+ }
110
+
111
+ async function runBackground(target, cmd) {
112
+ const port = getRunnerPort();
113
+ if (!port) return null;
114
+
115
+ let taskId;
116
+ try {
117
+ const r = await rpcCall(port, 'createTask', { code: '', runtime: 'ssh', workingDirectory: process.cwd() });
118
+ taskId = r?.taskId ?? r;
119
+ await rpcCall(port, 'startTask', { taskId });
120
+ } catch { return null; }
121
+
122
+ const onData = (data, type) => {
123
+ rpcCall(port, 'appendOutput', { taskId, type, data }).catch(() => {});
124
+ };
125
+
126
+ runSsh(target, cmd, onData).then(out => {
127
+ rpcCall(port, 'completeTask', { taskId, result: { success: true, exitCode: 0, stdout: out, stderr: '', error: null } }).catch(() => {});
128
+ }).catch(err => {
129
+ rpcCall(port, 'completeTask', { taskId, result: { success: false, exitCode: 1, stdout: '', stderr: err.message, error: err.message } }).catch(() => {});
130
+ });
131
+
132
+ return taskId;
133
+ }
134
+
135
+ module.exports = {
136
+ id: 'ssh',
137
+ exec: {
138
+ match: /^exec:ssh/,
139
+ async run(code) {
140
+ const { target: targetName, cmd } = parseCommand(code);
141
+ if (!cmd) return '[no command provided]';
142
+ const target = loadTarget(targetName);
143
+
144
+ // Detect background-only commands (fire-and-forget: ends with & or uses nohup/systemd-run)
145
+ const isBackground = /(&\s*$|^\s*(nohup|systemd-run|setsid)\s)/m.test(cmd);
146
+
147
+ if (isBackground) {
148
+ const taskId = await runBackground(target, cmd);
149
+ if (taskId != null) {
150
+ return 'Backgrounded on remote host. Local task_' + taskId + ' streams output.\n\n' +
151
+ ' exec:sleep\n task_' + taskId + '\n\n' +
152
+ ' exec:status\n task_' + taskId + '\n\n' +
153
+ ' exec:close\n task_' + taskId;
154
+ }
155
+ }
156
+
157
+ return await runSsh(target, cmd, null);
158
+ }
159
+ },
160
+ context: `=== exec:ssh ===
161
+ exec:ssh
162
+ [@target]
163
+ <shell command>
164
+
165
+ Runs shell command on remote SSH host. Target from ~/.claude/ssh-targets.json ("default" if no @name). Supports multi-line scripts. Password or key auth. Returns combined stdout+stderr. Commands ending with & or using nohup/systemd-run are backgrounded — use exec:sleep/status/close to follow.`
166
+ };
@@ -0,0 +1,130 @@
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 API unavailable at 127.0.0.1:5000. Ensure rs-exec is running with browser support enabled.'
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
+ };