@openagents-org/agent-launcher 0.2.142 → 0.2.145
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/package.json +1 -1
- package/registry.json +274 -66
- package/src/adapters/aider.js +634 -0
- package/src/adapters/amp.js +477 -0
- package/src/adapters/base.js +108 -7
- package/src/adapters/claude.js +50 -34
- package/src/adapters/cline-stream.js +632 -0
- package/src/adapters/cline.js +914 -0
- package/src/adapters/codex.js +14 -16
- package/src/adapters/copilot-stream-parser.js +380 -0
- package/src/adapters/copilot.js +796 -0
- package/src/adapters/cursor.js +9 -15
- package/src/adapters/gemini.js +10 -12
- package/src/adapters/goose-stream.js +268 -0
- package/src/adapters/goose.js +649 -0
- package/src/adapters/health-status.js +166 -0
- package/src/adapters/hermes.js +8 -17
- package/src/adapters/index.js +16 -1
- package/src/adapters/nanoclaw-bridge.js +264 -0
- package/src/adapters/nanoclaw-channel-install.js +411 -0
- package/src/adapters/nanoclaw-control.js +491 -0
- package/src/adapters/nanoclaw-delivery-store.js +0 -0
- package/src/adapters/nanoclaw-protocol.js +349 -0
- package/src/adapters/nanoclaw.js +661 -9
- package/src/adapters/opencode.js +665 -79
- package/src/agent-rows.js +13 -1
- package/src/auth-guidance.js +98 -0
- package/src/cli.js +29 -7
- package/src/config.js +8 -2
- package/src/daemon.js +108 -6
- package/src/installer.js +509 -10
- package/src/paths.js +125 -2
- package/src/tui.js +29 -2
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NanoClaw control-plane client + environment detection.
|
|
3
|
+
*
|
|
4
|
+
* Management/detection uses NanoClaw's official host control socket
|
|
5
|
+
* `<home>/data/ncl.sock` (the same surface the `ncl` binary speaks). One
|
|
6
|
+
* line-delimited JSON RequestFrame `{id,command,args}` per connection; the
|
|
7
|
+
* host replies with one ResponseFrame `{id,ok,data}` / `{id,ok,false,error}`
|
|
8
|
+
* and closes. We use ONLY `open`-access read commands (`*-list`, `*-get`) —
|
|
9
|
+
* NanoClaw gates create/update/delete behind human approval, so the bridge
|
|
10
|
+
* never silently mutates groups, messaging groups, or wirings.
|
|
11
|
+
*
|
|
12
|
+
* See [[nanoclaw-facts-and-arch]] for the verified contract.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const net = require('net');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const { execFile, execFileSync } = require('child_process');
|
|
22
|
+
|
|
23
|
+
const { redactSecrets } = require('./nanoclaw-protocol');
|
|
24
|
+
|
|
25
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Path helpers (DATA_DIR = <home>/data, per NanoClaw src/config.ts)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function dataDir(home) {
|
|
32
|
+
return path.join(home, 'data');
|
|
33
|
+
}
|
|
34
|
+
function nclSocketPath(home) {
|
|
35
|
+
return path.join(home, 'data', 'ncl.sock');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Dedicated local-IPC dir for the `openagents` channel. Kept SEPARATE from
|
|
39
|
+
* NanoClaw's `data/` so we can lock it to 0700 without touching NanoClaw's own
|
|
40
|
+
* files. Holds the bridge socket (0600) and the handshake secret (0600).
|
|
41
|
+
*/
|
|
42
|
+
function bridgeSocketDir(home) {
|
|
43
|
+
return path.join(home, 'data', 'openagents');
|
|
44
|
+
}
|
|
45
|
+
/** Local IPC socket owned by the native `openagents` channel we ship. */
|
|
46
|
+
function bridgeSocketPath(home) {
|
|
47
|
+
return path.join(bridgeSocketDir(home), 'bridge.sock');
|
|
48
|
+
}
|
|
49
|
+
/** File holding the random per-host handshake secret (channel writes, bridge reads). */
|
|
50
|
+
function bridgeSecretPath(home) {
|
|
51
|
+
return path.join(bridgeSocketDir(home), 'secret');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Local-IPC security helpers (shared by the bridge; the channel mirrors these
|
|
56
|
+
// in openagents.ts). All best-effort + cross-platform: Unix enforces via file
|
|
57
|
+
// mode + ownership, every platform enforces via the random secret.
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/** lstat without following symlinks; null if missing. */
|
|
61
|
+
function _lstat(p) {
|
|
62
|
+
try {
|
|
63
|
+
return fs.lstatSync(p);
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Is `p` safe to use as a local-trust path? It must NOT be a symlink (blocks
|
|
71
|
+
* path-escape / redirection). Returns {ok, reason}.
|
|
72
|
+
*/
|
|
73
|
+
function isPathSafe(p) {
|
|
74
|
+
const st = _lstat(p);
|
|
75
|
+
if (st && st.isSymbolicLink()) return { ok: false, reason: 'path is a symlink' };
|
|
76
|
+
return { ok: true };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Ensure the IPC dir exists, is a real directory we own, is not a symlink, and
|
|
81
|
+
* is not group/other-accessible. Creates it 0700 if missing. Best-effort on
|
|
82
|
+
* Windows (mode bits are advisory there — the secret is the real guard).
|
|
83
|
+
* @returns {{ok:boolean, reason?:string}}
|
|
84
|
+
*/
|
|
85
|
+
function ensureSecureDir(dir) {
|
|
86
|
+
const st = _lstat(dir);
|
|
87
|
+
if (st && st.isSymbolicLink()) return { ok: false, reason: 'ipc dir is a symlink' };
|
|
88
|
+
if (st && !st.isDirectory()) return { ok: false, reason: 'ipc dir path is not a directory' };
|
|
89
|
+
if (!st) {
|
|
90
|
+
try {
|
|
91
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
92
|
+
} catch (e) {
|
|
93
|
+
return { ok: false, reason: `mkdir failed: ${e.message}` };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
fs.chmodSync(dir, 0o700);
|
|
98
|
+
} catch {
|
|
99
|
+
/* best-effort (Windows) */
|
|
100
|
+
}
|
|
101
|
+
if (!IS_WINDOWS) {
|
|
102
|
+
const after = _lstat(dir);
|
|
103
|
+
if (after) {
|
|
104
|
+
if (typeof process.getuid === 'function' && after.uid !== process.getuid()) {
|
|
105
|
+
return { ok: false, reason: 'ipc dir is owned by another user' };
|
|
106
|
+
}
|
|
107
|
+
if (after.mode & 0o077) return { ok: false, reason: 'ipc dir is group/other-accessible' };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { ok: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Read the handshake secret. The file must be a regular file (not a symlink)
|
|
115
|
+
* and, on Unix, not group/other-readable. Returns the secret string or null.
|
|
116
|
+
*/
|
|
117
|
+
function readBridgeSecret(home) {
|
|
118
|
+
const p = bridgeSecretPath(home);
|
|
119
|
+
const st = _lstat(p);
|
|
120
|
+
if (!st || st.isSymbolicLink() || !st.isFile()) return null;
|
|
121
|
+
if (!IS_WINDOWS && st.mode & 0o077) return null; // refuse a world/group-readable secret
|
|
122
|
+
try {
|
|
123
|
+
const s = fs.readFileSync(p, 'utf-8').trim();
|
|
124
|
+
return s || null;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Unlink `p` ONLY if it is an actual socket file — never a regular file,
|
|
132
|
+
* directory, or symlink. Prevents stale-cleanup from destroying user data.
|
|
133
|
+
* @returns {{removed:boolean, reason?:string}}
|
|
134
|
+
*/
|
|
135
|
+
function safeUnlinkSocket(p) {
|
|
136
|
+
const st = _lstat(p);
|
|
137
|
+
if (!st) return { removed: false };
|
|
138
|
+
if (st.isSymbolicLink()) return { removed: false, reason: 'refusing to unlink a symlink' };
|
|
139
|
+
if (!st.isSocket()) return { removed: false, reason: 'refusing to unlink a non-socket file' };
|
|
140
|
+
try {
|
|
141
|
+
fs.unlinkSync(p);
|
|
142
|
+
return { removed: true };
|
|
143
|
+
} catch (e) {
|
|
144
|
+
return { removed: false, reason: e.message };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Detection
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
/** Does `dir` look like a NanoClaw checkout? */
|
|
153
|
+
function looksLikeNanoclaw(dir) {
|
|
154
|
+
try {
|
|
155
|
+
if (!dir || !fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return false;
|
|
156
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
157
|
+
if (fs.existsSync(pkgPath)) {
|
|
158
|
+
try {
|
|
159
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
160
|
+
if (pkg && pkg.name === 'nanoclaw') return true;
|
|
161
|
+
} catch {
|
|
162
|
+
/* fall through */
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Structural fallback: bin/ncl + src/index.ts (+ src/channels)
|
|
166
|
+
return (
|
|
167
|
+
fs.existsSync(path.join(dir, 'bin', 'ncl')) &&
|
|
168
|
+
fs.existsSync(path.join(dir, 'src', 'index.ts')) &&
|
|
169
|
+
fs.existsSync(path.join(dir, 'src', 'channels'))
|
|
170
|
+
);
|
|
171
|
+
} catch {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Follow `ncl` on PATH back to the checkout root (bin/ncl → <root>). */
|
|
177
|
+
function homeFromNclBinary() {
|
|
178
|
+
try {
|
|
179
|
+
const which = IS_WINDOWS ? 'where' : 'which';
|
|
180
|
+
const out = execFileSync(which, ['ncl'], {
|
|
181
|
+
encoding: 'utf-8',
|
|
182
|
+
timeout: 5000,
|
|
183
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
184
|
+
})
|
|
185
|
+
.split(/\r?\n/)[0]
|
|
186
|
+
.trim();
|
|
187
|
+
if (!out) return null;
|
|
188
|
+
let real = out;
|
|
189
|
+
try {
|
|
190
|
+
real = fs.realpathSync(out);
|
|
191
|
+
} catch {
|
|
192
|
+
/* use as-is */
|
|
193
|
+
}
|
|
194
|
+
// bin/ncl → dirname(bin) → root
|
|
195
|
+
const root = path.dirname(path.dirname(real));
|
|
196
|
+
return looksLikeNanoclaw(root) ? root : null;
|
|
197
|
+
} catch {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Resolve the NanoClaw checkout directory.
|
|
204
|
+
* Priority: $NANOCLAW_HOME → `ncl` on PATH → common locations.
|
|
205
|
+
* @returns {{home:string, source:string}|null}
|
|
206
|
+
*/
|
|
207
|
+
function findNanoclawHome(env = process.env) {
|
|
208
|
+
const explicit = env.NANOCLAW_HOME && env.NANOCLAW_HOME.trim();
|
|
209
|
+
if (explicit) {
|
|
210
|
+
const resolved = path.resolve(explicit);
|
|
211
|
+
if (looksLikeNanoclaw(resolved)) return { home: resolved, source: 'NANOCLAW_HOME' };
|
|
212
|
+
// Honour the user's intent even if structure check is imperfect, but flag it.
|
|
213
|
+
return { home: resolved, source: 'NANOCLAW_HOME', unverified: true };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const fromBin = homeFromNclBinary();
|
|
217
|
+
if (fromBin) return { home: fromBin, source: 'ncl-on-path' };
|
|
218
|
+
|
|
219
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
220
|
+
const candidates = [
|
|
221
|
+
path.join(home, 'nanoclaw'),
|
|
222
|
+
path.join(home, '.nanoclaw'),
|
|
223
|
+
path.join(home, 'src', 'nanoclaw'),
|
|
224
|
+
path.join(home, 'code', 'nanoclaw'),
|
|
225
|
+
path.join(home, 'projects', 'nanoclaw'),
|
|
226
|
+
path.join(home, 'dev', 'nanoclaw'),
|
|
227
|
+
];
|
|
228
|
+
for (const c of candidates) {
|
|
229
|
+
if (looksLikeNanoclaw(c)) return { home: c, source: 'discovered' };
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function whichSafe(bin) {
|
|
235
|
+
try {
|
|
236
|
+
const which = IS_WINDOWS ? 'where' : 'which';
|
|
237
|
+
const out = execFileSync(which, [bin], {
|
|
238
|
+
encoding: 'utf-8',
|
|
239
|
+
timeout: 5000,
|
|
240
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
241
|
+
})
|
|
242
|
+
.split(/\r?\n/)[0]
|
|
243
|
+
.trim();
|
|
244
|
+
return out || null;
|
|
245
|
+
} catch {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function checkNode() {
|
|
251
|
+
const bin = whichSafe('node');
|
|
252
|
+
if (!bin) return { present: false };
|
|
253
|
+
try {
|
|
254
|
+
const v = execFileSync(bin, ['--version'], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
255
|
+
return { present: true, version: v };
|
|
256
|
+
} catch {
|
|
257
|
+
return { present: true };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function checkPackageManager() {
|
|
262
|
+
const pnpm = whichSafe('pnpm');
|
|
263
|
+
if (pnpm) return { present: true, manager: 'pnpm', path: pnpm };
|
|
264
|
+
const npm = whichSafe('npm');
|
|
265
|
+
if (npm) return { present: true, manager: 'npm', path: npm };
|
|
266
|
+
return { present: false };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Check Docker: installed AND daemon reachable. `docker info` exit 0 ⇒ running.
|
|
271
|
+
* `runner` is injectable for tests.
|
|
272
|
+
* @returns {Promise<{installed:boolean, running:boolean, detail:string}>}
|
|
273
|
+
*/
|
|
274
|
+
function checkDocker(runner) {
|
|
275
|
+
const run =
|
|
276
|
+
runner ||
|
|
277
|
+
((cmd, args, cb) =>
|
|
278
|
+
execFile(cmd, args, { timeout: 8000, encoding: 'utf-8' }, (err, stdout, stderr) =>
|
|
279
|
+
cb(err, stdout, stderr),
|
|
280
|
+
));
|
|
281
|
+
return new Promise((resolve) => {
|
|
282
|
+
const dockerBin = whichSafe('docker');
|
|
283
|
+
if (!dockerBin) {
|
|
284
|
+
resolve({ installed: false, running: false, detail: 'docker binary not found' });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
run('docker', ['info', '--format', '{{.ServerVersion}}'], (err, stdout, stderr) => {
|
|
288
|
+
if (err) {
|
|
289
|
+
resolve({
|
|
290
|
+
installed: true,
|
|
291
|
+
running: false,
|
|
292
|
+
detail: redactSecrets(((stderr || '') + (err.message || '')).slice(0, 200)),
|
|
293
|
+
});
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
resolve({ installed: true, running: true, detail: String(stdout || '').trim() });
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Control socket client
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
|
|
305
|
+
class NclControlError extends Error {
|
|
306
|
+
constructor(message, code) {
|
|
307
|
+
super(message);
|
|
308
|
+
this.name = 'NclControlError';
|
|
309
|
+
this.nclCode = code || 'transport-error';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
class NclControl {
|
|
314
|
+
/**
|
|
315
|
+
* @param {string} socketPath path to data/ncl.sock
|
|
316
|
+
* @param {{timeoutMs?:number}} [opts]
|
|
317
|
+
*/
|
|
318
|
+
constructor(socketPath, opts = {}) {
|
|
319
|
+
this.socketPath = socketPath;
|
|
320
|
+
this.timeoutMs = opts.timeoutMs || 8000;
|
|
321
|
+
this._seq = 0;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
_nextId() {
|
|
325
|
+
this._seq += 1;
|
|
326
|
+
return `oa-${Date.now().toString(36)}-${this._seq}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Send one command, await one response. Resolves with `data`, rejects with
|
|
331
|
+
* an NclControlError carrying `.nclCode` on host-reported failure.
|
|
332
|
+
* @param {string} command e.g. 'groups-list'
|
|
333
|
+
* @param {object} [args]
|
|
334
|
+
* @returns {Promise<unknown>}
|
|
335
|
+
*/
|
|
336
|
+
request(command, args = {}) {
|
|
337
|
+
const id = this._nextId();
|
|
338
|
+
const frame = JSON.stringify({ id, command, args }) + '\n';
|
|
339
|
+
return new Promise((resolve, reject) => {
|
|
340
|
+
let settled = false;
|
|
341
|
+
let buffer = '';
|
|
342
|
+
const client = net.createConnection(this.socketPath);
|
|
343
|
+
|
|
344
|
+
const done = (fn, val) => {
|
|
345
|
+
if (settled) return;
|
|
346
|
+
settled = true;
|
|
347
|
+
clearTimeout(timer);
|
|
348
|
+
try {
|
|
349
|
+
client.end();
|
|
350
|
+
} catch {
|
|
351
|
+
/* best-effort */
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
client.destroy();
|
|
355
|
+
} catch {
|
|
356
|
+
/* best-effort */
|
|
357
|
+
}
|
|
358
|
+
fn(val);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const timer = setTimeout(
|
|
362
|
+
() => done(reject, new NclControlError(`ncl control request timed out: ${command}`, 'timeout')),
|
|
363
|
+
this.timeoutMs,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
client.on('connect', () => client.write(frame));
|
|
367
|
+
client.on('data', (chunk) => {
|
|
368
|
+
buffer += chunk.toString('utf8');
|
|
369
|
+
const idx = buffer.indexOf('\n');
|
|
370
|
+
if (idx < 0) {
|
|
371
|
+
// Bound the buffer: a peer streaming without a newline would otherwise
|
|
372
|
+
// grow memory unbounded. Control responses are small JSON lines.
|
|
373
|
+
if (buffer.length > 8 * 1024 * 1024) {
|
|
374
|
+
done(reject, new NclControlError('ncl control response exceeded size limit without a newline', 'transport-error'));
|
|
375
|
+
}
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const line = buffer.slice(0, idx);
|
|
379
|
+
let resp;
|
|
380
|
+
try {
|
|
381
|
+
resp = JSON.parse(line);
|
|
382
|
+
} catch (e) {
|
|
383
|
+
done(reject, new NclControlError(`malformed ncl response: ${e.message}`, 'transport-error'));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (resp && resp.ok) {
|
|
387
|
+
done(resolve, resp.data);
|
|
388
|
+
} else {
|
|
389
|
+
const err = (resp && resp.error) || {};
|
|
390
|
+
done(reject, new NclControlError(redactSecrets(err.message || 'ncl error'), err.code || 'handler-error'));
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
client.on('error', (err) => {
|
|
394
|
+
const code = err && err.code === 'ENOENT' ? 'host-not-running' : err && err.code === 'ECONNREFUSED' ? 'host-not-running' : 'transport-error';
|
|
395
|
+
done(reject, new NclControlError(redactSecrets(err.message || 'socket error'), code));
|
|
396
|
+
});
|
|
397
|
+
client.on('close', () => {
|
|
398
|
+
if (!settled) done(reject, new NclControlError('host closed connection before responding', 'transport-error'));
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Liveness probe — `groups-list` is an `open` command, safe to call. */
|
|
404
|
+
async ping() {
|
|
405
|
+
await this.request('groups-list', {});
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
_asArray(data) {
|
|
410
|
+
if (Array.isArray(data)) return data;
|
|
411
|
+
if (data && Array.isArray(data.rows)) return data.rows;
|
|
412
|
+
if (data && Array.isArray(data.items)) return data.items;
|
|
413
|
+
return data == null ? [] : [data];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async listGroups() {
|
|
417
|
+
return this._asArray(await this.request('groups-list', {}));
|
|
418
|
+
}
|
|
419
|
+
async getGroup(id) {
|
|
420
|
+
return this.request('groups-get', { id });
|
|
421
|
+
}
|
|
422
|
+
async listMessagingGroups(filters = {}) {
|
|
423
|
+
return this._asArray(await this.request('messaging-groups-list', filters));
|
|
424
|
+
}
|
|
425
|
+
async listWirings(filters = {}) {
|
|
426
|
+
return this._asArray(await this.request('wirings-list', filters));
|
|
427
|
+
}
|
|
428
|
+
async listSessions(filters = {}) {
|
|
429
|
+
return this._asArray(await this.request('sessions-list', filters));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* One-shot aggregate environment probe used at adapter start and for the
|
|
435
|
+
* launcher/workspace status surface.
|
|
436
|
+
* @returns {Promise<object>}
|
|
437
|
+
*/
|
|
438
|
+
async function detectEnvironment(env = process.env, opts = {}) {
|
|
439
|
+
const homeInfo = findNanoclawHome(env);
|
|
440
|
+
const node = checkNode();
|
|
441
|
+
const pm = checkPackageManager();
|
|
442
|
+
const docker = await checkDocker(opts.dockerRunner);
|
|
443
|
+
|
|
444
|
+
const result = {
|
|
445
|
+
installed: !!homeInfo,
|
|
446
|
+
home: homeInfo ? homeInfo.home : null,
|
|
447
|
+
homeSource: homeInfo ? homeInfo.source : null,
|
|
448
|
+
node,
|
|
449
|
+
packageManager: pm,
|
|
450
|
+
docker,
|
|
451
|
+
hostRunning: false,
|
|
452
|
+
nclSocket: null,
|
|
453
|
+
bridgeSocket: null,
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
if (homeInfo) {
|
|
457
|
+
result.nclSocket = nclSocketPath(homeInfo.home);
|
|
458
|
+
result.bridgeSocket = bridgeSocketPath(homeInfo.home);
|
|
459
|
+
// Host running iff the control socket exists AND answers.
|
|
460
|
+
if (fs.existsSync(result.nclSocket)) {
|
|
461
|
+
try {
|
|
462
|
+
const ctl = new NclControl(result.nclSocket, { timeoutMs: opts.pingTimeoutMs || 4000 });
|
|
463
|
+
await ctl.ping();
|
|
464
|
+
result.hostRunning = true;
|
|
465
|
+
} catch {
|
|
466
|
+
result.hostRunning = false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return result;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
module.exports = {
|
|
474
|
+
dataDir,
|
|
475
|
+
nclSocketPath,
|
|
476
|
+
bridgeSocketDir,
|
|
477
|
+
bridgeSocketPath,
|
|
478
|
+
bridgeSecretPath,
|
|
479
|
+
isPathSafe,
|
|
480
|
+
ensureSecureDir,
|
|
481
|
+
readBridgeSecret,
|
|
482
|
+
safeUnlinkSocket,
|
|
483
|
+
looksLikeNanoclaw,
|
|
484
|
+
findNanoclawHome,
|
|
485
|
+
checkNode,
|
|
486
|
+
checkPackageManager,
|
|
487
|
+
checkDocker,
|
|
488
|
+
detectEnvironment,
|
|
489
|
+
NclControl,
|
|
490
|
+
NclControlError,
|
|
491
|
+
};
|
|
Binary file
|