gm-oc 2.0.336 → 2.0.338
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/cli.js +2 -0
- package/gm-oc.mjs +24 -0
- package/lang/ssh.js +166 -0
- 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.
|
|
3
|
+
"version": "2.0.338",
|
|
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",
|