gm-kilo 2.0.337 → 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.
Files changed (3) hide show
  1. package/cli.js +2 -0
  2. package/lang/ssh.js +166 -0
  3. package/package.json +2 -1
package/cli.js CHANGED
@@ -15,6 +15,7 @@ try {
15
15
  fs.mkdirSync(path.join(kiloConfigDir, 'plugins'), { recursive: true });
16
16
  fs.mkdirSync(path.join(kiloConfigDir, 'agents'), { recursive: true });
17
17
  fs.mkdirSync(path.join(kiloConfigDir, 'skills'), { recursive: true });
18
+ fs.mkdirSync(path.join(kiloConfigDir, '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-kilo.mjs'), path.join(kiloConfigDir, 'plugins', 'gm-kilo.mjs'));
30
31
  copyRecursive(path.join(srcDir, 'agents'), path.join(kiloConfigDir, 'agents'));
31
32
  copyRecursive(path.join(srcDir, 'skills'), path.join(kiloConfigDir, 'skills'));
33
+ copyRecursive(path.join(srcDir, 'lang'), path.join(kiloConfigDir, 'lang'));
32
34
 
33
35
  const kiloJsonPath = path.join(kiloConfigDir, 'kilocode.json');
34
36
  let kiloConfig = {};
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-kilo",
3
- "version": "2.0.337",
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-kilo.mjs",
43
44
  "index.js",