gm-oc 2.0.336 → 2.0.337

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 (4) hide show
  1. package/cli.js +2 -0
  2. package/gm-oc.mjs +24 -0
  3. package/lang/ssh.js +166 -0
  4. package/package.json +2 -1
package/cli.js CHANGED
@@ -15,6 +15,7 @@ try {
15
15
  fs.mkdirSync(path.join(ocConfigDir, 'plugins'), { recursive: true });
16
16
  fs.mkdirSync(path.join(ocConfigDir, 'agents'), { recursive: true });
17
17
  fs.mkdirSync(path.join(ocConfigDir, 'skills'), { recursive: true });
18
+ fs.mkdirSync(path.join(ocConfigDir, 'lang'), { recursive: true });
18
19
 
19
20
  function copyRecursive(src, dst) {
20
21
  if (!fs.existsSync(src)) return;
@@ -29,6 +30,7 @@ try {
29
30
  fs.copyFileSync(path.join(srcDir, 'gm-oc.mjs'), path.join(ocConfigDir, 'plugins', 'gm-oc.mjs'));
30
31
  copyRecursive(path.join(srcDir, 'agents'), path.join(ocConfigDir, 'agents'));
31
32
  copyRecursive(path.join(srcDir, 'skills'), path.join(ocConfigDir, 'skills'));
33
+ copyRecursive(path.join(srcDir, 'lang'), path.join(ocConfigDir, 'lang'));
32
34
 
33
35
  const ocJsonPath = path.join(ocConfigDir, 'opencode.json');
34
36
  let ocConfig = {};
package/gm-oc.mjs CHANGED
@@ -19,10 +19,26 @@ function detectLang(src) {
19
19
 
20
20
  function stripFooter(s) { return s ? s.replace(/\n\[Running tools\][\s\S]*$/, '').trimEnd() : ''; }
21
21
 
22
+ function tryLangPlugin(lang, code, cwd) {
23
+ const langPluginDir = join(__dirname, '..', 'lang');
24
+ const langPluginFile = join(langPluginDir, lang+'.js');
25
+ if (!existsSync(langPluginFile)) return null;
26
+ try {
27
+ const plugin = require(langPluginFile);
28
+ if (plugin && plugin.exec && plugin.exec.run) {
29
+ const result = plugin.exec.run(code, cwd || process.cwd());
30
+ return String(result === undefined ? '' : result);
31
+ }
32
+ } catch(e) {}
33
+ return null;
34
+ }
35
+
22
36
  function runExec(rawLang, code, cwd) {
23
37
  const lang = LANG_ALIASES[rawLang] || (rawLang || detectLang(code));
24
38
  const opts = { encoding: 'utf-8', timeout: 30000, ...(cwd && { cwd }) };
25
39
  const out = (r) => { const o = (r.stdout||'').trimEnd(), e = stripFooter(r.stderr||'').trimEnd(); return o && e ? o+'\n[stderr]\n'+e : o||e||'(no output)'; };
40
+ const pluginResult = tryLangPlugin(lang, code, cwd);
41
+ if (pluginResult !== null) return pluginResult;
26
42
  if (lang === 'python') return out(spawnSync('python3',['-c',code],opts));
27
43
  const ext = lang === 'typescript' ? 'ts' : lang === 'bash' ? 'sh' : 'mjs';
28
44
  const tmp = join(tmpdir(),'gm-plugin-'+Date.now()+'.'+ext);
@@ -94,6 +110,14 @@ export async function GmPlugin({ directory }) {
94
110
  output.args = { ...output.args, command: safePrintf('Use the code-search skill for codebase exploration instead of '+input.tool+'. Describe what you need in plain language.') };
95
111
  return;
96
112
  }
113
+ if (input.tool === 'EnterPlanMode') {
114
+ output.args = { ...output.args, command: safePrintf('Plan mode is disabled. Use the gm skill (PLAN→EXECUTE→EMIT→VERIFY→COMPLETE state machine) instead.') };
115
+ return;
116
+ }
117
+ if (input.tool === 'Task' && input.args?.subagent_type === 'Explore') {
118
+ output.args = { ...output.args, command: safePrintf('The Explore agent is blocked. Use exec:codesearch in the Bash tool instead.\n\nexec:codesearch\n<natural language description of what to find>') };
119
+ return;
120
+ }
97
121
  if (input.tool === 'write' || input.tool === 'Write') {
98
122
  const fp = (output.args && output.args.file_path) || '';
99
123
  const base = basename(fp).toLowerCase();
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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-oc",
3
- "version": "2.0.336",
3
+ "version": "2.0.337",
4
4
  "description": "State machine agent with hooks, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",
@@ -38,6 +38,7 @@
38
38
  "hooks/",
39
39
  "scripts/",
40
40
  "skills/",
41
+ "lang/",
41
42
  "gm.js",
42
43
  "gm-oc.mjs",
43
44
  "index.js",