pllla-connect 0.1.0
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/dist/account.js +24 -0
- package/dist/catalog.js +58 -0
- package/dist/detect.js +176 -0
- package/dist/exec.js +161 -0
- package/dist/main.js +397 -0
- package/dist/nodeProvision.js +269 -0
- package/dist/nodeResolve.js +84 -0
- package/dist/openclaw.js +338 -0
- package/dist/progress.js +74 -0
- package/dist/version.js +105 -0
- package/package.json +22 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Node runs the runtime CLI — "private first + verified", the opposite
|
|
3
|
+
* of the desktop's agentManager resolver (docs/agent/EXTERNAL_RUNTIME.md §6.1).
|
|
4
|
+
* System-first would grab an older system node (an nvm 22.17, say) and fail
|
|
5
|
+
* the runtime's floor; private-first picks the copy PLLLA already verified,
|
|
6
|
+
* then any system node that passes `nodeRange`, and only then downloads.
|
|
7
|
+
*
|
|
8
|
+
* Used for an existing install too: the detected `openclaw` is a
|
|
9
|
+
* `#!/usr/bin/env node` script, so bridging it needs a qualifying `node` on
|
|
10
|
+
* the PATH we hand it — without ever touching the user's own Node.
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from "node:child_process";
|
|
13
|
+
import { existsSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { candidateBinDirs, findExecutable } from "./detect.js";
|
|
17
|
+
import { defaultNodeRoot, ensurePrivateNode, pickPrivateNode, scanPrivateNodes, } from "./nodeProvision.js";
|
|
18
|
+
import { parseVersion, formatVersion, satisfiesNodeRange } from "./version.js";
|
|
19
|
+
export function probeNodeVersion(nodeBin) {
|
|
20
|
+
try {
|
|
21
|
+
const output = execFileSync(nodeBin, ["--version"], {
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
timeout: 15_000,
|
|
24
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
25
|
+
windowsHide: true,
|
|
26
|
+
});
|
|
27
|
+
const parsed = parseVersion(output);
|
|
28
|
+
return parsed ? formatVersion(parsed) : null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function resolveNodeForRuntime(params) {
|
|
35
|
+
const home = params.home ?? homedir();
|
|
36
|
+
const env = params.env ?? process.env;
|
|
37
|
+
const platform = params.platform ?? process.platform;
|
|
38
|
+
const root = params.root ?? defaultNodeRoot(home);
|
|
39
|
+
const privateNode = pickPrivateNode(scanPrivateNodes(root), params.nodeRange);
|
|
40
|
+
if (privateNode) {
|
|
41
|
+
params.log(`Using private Node ${privateNode.version}.`);
|
|
42
|
+
return { ...privateNode, source: "private" };
|
|
43
|
+
}
|
|
44
|
+
const dirs = [
|
|
45
|
+
...(params.preferredDirs ?? []),
|
|
46
|
+
...candidateBinDirs({
|
|
47
|
+
home,
|
|
48
|
+
pathEnv: env.PATH,
|
|
49
|
+
runtimeId: "openclaw",
|
|
50
|
+
platform,
|
|
51
|
+
}),
|
|
52
|
+
];
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
for (const dir of dirs) {
|
|
55
|
+
if (seen.has(dir))
|
|
56
|
+
continue;
|
|
57
|
+
seen.add(dir);
|
|
58
|
+
const nodeBin = findExecutable("node", [dir], platform);
|
|
59
|
+
if (!nodeBin)
|
|
60
|
+
continue;
|
|
61
|
+
const version = probeNodeVersion(nodeBin);
|
|
62
|
+
if (!version || !satisfiesNodeRange(version, params.nodeRange))
|
|
63
|
+
continue;
|
|
64
|
+
const npmCli = join(dir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
|
|
65
|
+
params.log(`Using system Node ${version} at ${nodeBin} (untouched).`);
|
|
66
|
+
return {
|
|
67
|
+
version,
|
|
68
|
+
nodeBin,
|
|
69
|
+
nodeBinDir: dir,
|
|
70
|
+
// npm-cli.js runs as `node npm-cli.js` — existence, not an exec bit.
|
|
71
|
+
npmCli: existsSync(npmCli) ? npmCli : null,
|
|
72
|
+
source: "system",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const provisioned = await ensurePrivateNode({
|
|
76
|
+
nodeRange: params.nodeRange,
|
|
77
|
+
provisionNodeMajor: params.provisionNodeMajor,
|
|
78
|
+
root,
|
|
79
|
+
platform,
|
|
80
|
+
log: params.log,
|
|
81
|
+
progress: params.progress,
|
|
82
|
+
});
|
|
83
|
+
return { ...provisioned, source: "provisioned" };
|
|
84
|
+
}
|
package/dist/openclaw.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw adapter — the connector's per-runtime knowledge lives here; the
|
|
3
|
+
* shared flow (catalog, detection, Node, progress) stays runtime-agnostic.
|
|
4
|
+
*
|
|
5
|
+
* Two entry situations (docs/agent/EXTERNAL_RUNTIME.md §6):
|
|
6
|
+
* - existing install → `bridgeExisting` only: plugin + one account under
|
|
7
|
+
* `channels.pllla.accounts.<accountId>` + gateway restart. Other accounts'
|
|
8
|
+
* keys, the user's Node, and their brain are never touched.
|
|
9
|
+
* - nothing installed → `installFresh` into the private prefix on the
|
|
10
|
+
* private Node, `resolveAuthChoice` (brain ladder §6.5), `onboardFresh`,
|
|
11
|
+
* then the same `bridgeExisting`.
|
|
12
|
+
*
|
|
13
|
+
* Every call runs the detected/installed `binaryPath` (never a bare
|
|
14
|
+
* `openclaw`) with a PATH that starts with the Node we resolved — the CLI is
|
|
15
|
+
* a `#!/usr/bin/env node` script. The bridge plugin consumes the pairing
|
|
16
|
+
* token at gateway start, so the rt- runtime key never crosses argv here.
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
21
|
+
import { candidateBinDirs, findExecutable, privateRuntimePrefix, } from "./detect.js";
|
|
22
|
+
import { describeCommandFailure, prependPath, runCommand, shellQuote, } from "./exec.js";
|
|
23
|
+
import { ConnectError } from "./progress.js";
|
|
24
|
+
const BRIDGE_TIMEOUT_MS = 3 * 60_000;
|
|
25
|
+
const INSTALL_TIMEOUT_MS = 20 * 60_000;
|
|
26
|
+
const ONBOARD_TIMEOUT_MS = 10 * 60_000;
|
|
27
|
+
const GATEWAY_TIMEOUT_MS = 2 * 60_000;
|
|
28
|
+
/**
|
|
29
|
+
* PATH for running the runtime CLI: resolved Node first (§6.1 private first),
|
|
30
|
+
* then the CLI's own dir (an nvm install pairs with its sibling node), then
|
|
31
|
+
* every §6.2 candidate dir so tools the runtime shells out to are reachable
|
|
32
|
+
* from a GUI-launched process.
|
|
33
|
+
*/
|
|
34
|
+
export function buildRuntimeEnv(params) {
|
|
35
|
+
const home = params.home ?? homedir();
|
|
36
|
+
const baseEnv = params.baseEnv ?? process.env;
|
|
37
|
+
return prependPath(baseEnv, [
|
|
38
|
+
params.nodeBinDir,
|
|
39
|
+
dirname(params.binaryPath),
|
|
40
|
+
...candidateBinDirs({
|
|
41
|
+
home,
|
|
42
|
+
pathEnv: baseEnv.PATH,
|
|
43
|
+
runtimeId: "openclaw",
|
|
44
|
+
platform: params.platform,
|
|
45
|
+
}),
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
/** The command a person runs in a terminal to finish onboarding themselves. */
|
|
49
|
+
export function buildManualOnboardCommand(params) {
|
|
50
|
+
return `PATH=${shellQuote(params.nodeBinDir)}:"$PATH" ${shellQuote(params.binaryPath)} onboard`;
|
|
51
|
+
}
|
|
52
|
+
async function runOpenClaw(params) {
|
|
53
|
+
const result = await runCommand({
|
|
54
|
+
file: params.binaryPath,
|
|
55
|
+
args: params.args,
|
|
56
|
+
env: params.env,
|
|
57
|
+
timeoutMs: params.timeoutMs,
|
|
58
|
+
onLine: params.log,
|
|
59
|
+
});
|
|
60
|
+
return result.stdout;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Wires one PLLLA account into an OpenClaw that already exists. Idempotent:
|
|
64
|
+
* re-running with the same account overwrites only that account's two keys.
|
|
65
|
+
*/
|
|
66
|
+
export async function bridgeExisting(params) {
|
|
67
|
+
const { binaryPath, accountId, pairingToken, serverOrigin, bridgePackage, log, } = params;
|
|
68
|
+
const env = buildRuntimeEnv({
|
|
69
|
+
nodeBinDir: params.nodeBinDir,
|
|
70
|
+
binaryPath,
|
|
71
|
+
home: params.home,
|
|
72
|
+
});
|
|
73
|
+
const run = (args, timeoutMs = BRIDGE_TIMEOUT_MS) => runOpenClaw({ binaryPath, args, env, timeoutMs, log });
|
|
74
|
+
// 설치 여부는 에러 문구가 아니라 *상태* 로 판정한다 — 이미 설치된 플러그인
|
|
75
|
+
// (로컬 tgz 설치 포함)은 npm 이 404/불통이어도 설치 단계를 건너뛰어야 한다.
|
|
76
|
+
let bridgeInstalled = false;
|
|
77
|
+
try {
|
|
78
|
+
await run(["plugins", "inspect", "pllla"]);
|
|
79
|
+
bridgeInstalled = true;
|
|
80
|
+
log("PLLLA bridge plugin already installed — skipping install.");
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
bridgeInstalled = false;
|
|
84
|
+
}
|
|
85
|
+
if (!bridgeInstalled) {
|
|
86
|
+
log(`Installing the PLLLA bridge plugin (${bridgePackage})…`);
|
|
87
|
+
try {
|
|
88
|
+
await run(["plugins", "install", `npm:${bridgePackage}`]);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
const failure = describeCommandFailure(error);
|
|
92
|
+
// "이미 설치됨"만 관용한다. 그 밖(npm 404, 네트워크 등)을 넘기면 뒤 config
|
|
93
|
+
// set 이 "unknown channel id: pllla" 로 터져 진짜 원인이 가려진다(실기계 재현).
|
|
94
|
+
if (!/already (installed|exists|enabled|up.to.date)/i.test(failure)) {
|
|
95
|
+
throw new ConnectError({
|
|
96
|
+
phase: "bridge",
|
|
97
|
+
code: "bridge_failed",
|
|
98
|
+
message: `Could not install the PLLLA bridge plugin (${bridgePackage}): ${failure}${/404|not found|E404/i.test(failure)
|
|
99
|
+
? " — the bridge package is not available on npm yet."
|
|
100
|
+
: ""}`,
|
|
101
|
+
cause: error,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
log(`Plugin already present (${failure}) — continuing.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
log(`Configuring PLLLA account "${accountId}"…`);
|
|
108
|
+
const accountKey = `channels.pllla.accounts.${accountId}`;
|
|
109
|
+
try {
|
|
110
|
+
await run(["config", "set", `${accountKey}.serverOrigin`, serverOrigin]);
|
|
111
|
+
await run(["config", "set", `${accountKey}.pairingToken`, pairingToken]);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
const failure = describeCommandFailure(error);
|
|
115
|
+
// 채널 id 미등록 = 플러그인이 로드되지 않은 것 — 설정 문법이 아니라 플러그인
|
|
116
|
+
// 부재/비활성이 원인임을 그대로 말한다.
|
|
117
|
+
const hint = /unknown channel id/i.test(failure)
|
|
118
|
+
? " OpenClaw has no 'pllla' channel registered — the bridge plugin is not installed or not enabled."
|
|
119
|
+
: "";
|
|
120
|
+
throw new ConnectError({
|
|
121
|
+
phase: "bridge",
|
|
122
|
+
code: "bridge_failed",
|
|
123
|
+
message: `Could not write the PLLLA account into OpenClaw's config: ${failure}${hint}`,
|
|
124
|
+
cause: error,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
await run(["plugins", "enable", "pllla"]);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* bundled auto-enable — enable is best-effort */
|
|
132
|
+
}
|
|
133
|
+
log("Restarting the OpenClaw gateway so the bridge pairs and connects…");
|
|
134
|
+
try {
|
|
135
|
+
await run(["gateway", "restart"], GATEWAY_TIMEOUT_MS);
|
|
136
|
+
return { gatewayRestarted: true };
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
log(`Could not restart the gateway automatically (${describeCommandFailure(error)}). Start it yourself — e.g. \`${shellQuote(binaryPath)} gateway restart\` — the bridge pairs on startup.`);
|
|
140
|
+
return { gatewayRestarted: false };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* `npm install -g openclaw@latest --prefix ~/.pllla/runtimes/openclaw` on the
|
|
145
|
+
* private Node. `--prefix` keeps the user's global npm tree untouched.
|
|
146
|
+
*/
|
|
147
|
+
export async function installFresh(params) {
|
|
148
|
+
const home = params.home ?? homedir();
|
|
149
|
+
const prefix = params.prefix ?? privateRuntimePrefix(home, "openclaw");
|
|
150
|
+
const nodeBinDir = dirname(params.nodeBin);
|
|
151
|
+
const env = {
|
|
152
|
+
...prependPath(process.env, [nodeBinDir]),
|
|
153
|
+
npm_config_update_notifier: "false",
|
|
154
|
+
npm_config_fund: "false",
|
|
155
|
+
npm_config_audit: "false",
|
|
156
|
+
};
|
|
157
|
+
params.log(`Installing OpenClaw into ${prefix} (private Node)…`);
|
|
158
|
+
try {
|
|
159
|
+
await runCommand({
|
|
160
|
+
file: params.nodeBin,
|
|
161
|
+
args: [
|
|
162
|
+
params.npmCli,
|
|
163
|
+
"install",
|
|
164
|
+
"-g",
|
|
165
|
+
"openclaw@latest",
|
|
166
|
+
"--prefix",
|
|
167
|
+
prefix,
|
|
168
|
+
"--allow-scripts=openclaw",
|
|
169
|
+
],
|
|
170
|
+
env,
|
|
171
|
+
cwd: home,
|
|
172
|
+
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
173
|
+
onLine: params.log,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
throw new ConnectError({
|
|
178
|
+
phase: "install",
|
|
179
|
+
code: "install_failed",
|
|
180
|
+
message: `OpenClaw install failed: ${describeCommandFailure(error)}`,
|
|
181
|
+
cause: error,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const binaryPath = join(prefix, "bin", "openclaw");
|
|
185
|
+
if (!existsSync(binaryPath)) {
|
|
186
|
+
throw new ConnectError({
|
|
187
|
+
phase: "install",
|
|
188
|
+
code: "install_failed",
|
|
189
|
+
message: `npm finished but ${binaryPath} does not exist.`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return { binaryPath, prefix };
|
|
193
|
+
}
|
|
194
|
+
function parseJsonObject(text) {
|
|
195
|
+
const start = text.indexOf("{");
|
|
196
|
+
if (start < 0)
|
|
197
|
+
return null;
|
|
198
|
+
try {
|
|
199
|
+
const parsed = JSON.parse(text.slice(start));
|
|
200
|
+
return typeof parsed === "object" && parsed !== null
|
|
201
|
+
? parsed
|
|
202
|
+
: null;
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/** `ollama list` table → model names (header row dropped). */
|
|
209
|
+
export function parseOllamaModels(output) {
|
|
210
|
+
return output
|
|
211
|
+
.split("\n")
|
|
212
|
+
.map((line) => line.trim())
|
|
213
|
+
.filter((line) => line.length > 0 && !/^NAME\s/i.test(line))
|
|
214
|
+
.map((line) => line.split(/\s+/)[0])
|
|
215
|
+
.filter((name) => name.length > 0);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Brain ladder (§6.5): a signed-in Claude Code → `anthropic-cli`; else a
|
|
219
|
+
* local Ollama with at least one model → `ollama` (with a caveat); else null
|
|
220
|
+
* — the caller stops with `needs_model_auth`. No starter brain is lent.
|
|
221
|
+
*/
|
|
222
|
+
export async function resolveAuthChoice(params) {
|
|
223
|
+
const home = params.home ?? homedir();
|
|
224
|
+
const baseEnv = params.env ?? process.env;
|
|
225
|
+
const platform = params.platform ?? process.platform;
|
|
226
|
+
const dirs = [
|
|
227
|
+
join(home, ".claude", "local"),
|
|
228
|
+
...candidateBinDirs({
|
|
229
|
+
home,
|
|
230
|
+
pathEnv: baseEnv.PATH,
|
|
231
|
+
runtimeId: "openclaw",
|
|
232
|
+
platform,
|
|
233
|
+
}),
|
|
234
|
+
];
|
|
235
|
+
const env = prependPath(baseEnv, [
|
|
236
|
+
...(params.nodeBinDir ? [params.nodeBinDir] : []),
|
|
237
|
+
...dirs,
|
|
238
|
+
]);
|
|
239
|
+
const claudeBin = findExecutable("claude", dirs, platform);
|
|
240
|
+
if (claudeBin) {
|
|
241
|
+
try {
|
|
242
|
+
const { stdout } = await runCommand({
|
|
243
|
+
file: claudeBin,
|
|
244
|
+
args: ["auth", "status", "--json"],
|
|
245
|
+
env,
|
|
246
|
+
timeoutMs: 30_000,
|
|
247
|
+
});
|
|
248
|
+
const status = parseJsonObject(stdout);
|
|
249
|
+
if (status?.loggedIn === true) {
|
|
250
|
+
return {
|
|
251
|
+
choice: "anthropic-cli",
|
|
252
|
+
detail: `Claude Code is signed in (${claudeBin}) — OpenClaw will use it as its brain.`,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
params.log(`Claude Code found at ${claudeBin} but not signed in.`);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
params.log(`Claude Code at ${claudeBin} did not answer \`auth status\`: ${describeCommandFailure(error)}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const ollamaBin = findExecutable("ollama", dirs, platform);
|
|
262
|
+
if (ollamaBin) {
|
|
263
|
+
try {
|
|
264
|
+
const { stdout } = await runCommand({
|
|
265
|
+
file: ollamaBin,
|
|
266
|
+
args: ["list"],
|
|
267
|
+
env,
|
|
268
|
+
timeoutMs: 20_000,
|
|
269
|
+
});
|
|
270
|
+
const models = parseOllamaModels(stdout);
|
|
271
|
+
if (models.length > 0) {
|
|
272
|
+
return {
|
|
273
|
+
choice: "ollama",
|
|
274
|
+
detail: `Ollama has ${models.length} local model(s): ${models.slice(0, 5).join(", ")}.`,
|
|
275
|
+
warning: "Small local models are weak at tool calling (measured: a 20b model ran out of memory on 16 GB; gemma4 ignores tools). Replies may be unreliable — sign in to Claude Code for a dependable brain.",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
params.log(`Ollama found at ${ollamaBin} but it has no models.`);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
params.log(`Ollama at ${ollamaBin} did not answer \`list\`: ${describeCommandFailure(error)}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* `onboard --non-interactive …` on the private Node, then the gateway
|
|
288
|
+
* service (install is best-effort — a restart failure is guidance only,
|
|
289
|
+
* because `bridgeExisting` restarts again after wiring the plugin).
|
|
290
|
+
*/
|
|
291
|
+
export async function onboardFresh(params) {
|
|
292
|
+
const { binaryPath, authChoice, log } = params;
|
|
293
|
+
const env = buildRuntimeEnv({
|
|
294
|
+
nodeBinDir: params.nodeBinDir,
|
|
295
|
+
binaryPath,
|
|
296
|
+
home: params.home,
|
|
297
|
+
});
|
|
298
|
+
const run = (args, timeoutMs) => runOpenClaw({ binaryPath, args, env, timeoutMs, log });
|
|
299
|
+
log(`Onboarding OpenClaw with auth choice "${authChoice}"…`);
|
|
300
|
+
try {
|
|
301
|
+
await run([
|
|
302
|
+
"onboard",
|
|
303
|
+
"--non-interactive",
|
|
304
|
+
"--accept-risk",
|
|
305
|
+
"--flow",
|
|
306
|
+
"quickstart",
|
|
307
|
+
"--auth-choice",
|
|
308
|
+
authChoice,
|
|
309
|
+
"--skip-health",
|
|
310
|
+
"--json",
|
|
311
|
+
], ONBOARD_TIMEOUT_MS);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
throw new ConnectError({
|
|
315
|
+
phase: "onboard",
|
|
316
|
+
code: "install_failed",
|
|
317
|
+
message: `OpenClaw onboarding failed: ${describeCommandFailure(error)}`,
|
|
318
|
+
manualCommand: buildManualOnboardCommand({
|
|
319
|
+
binaryPath,
|
|
320
|
+
nodeBinDir: params.nodeBinDir,
|
|
321
|
+
}),
|
|
322
|
+
cause: error,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
log("Installing the OpenClaw gateway service…");
|
|
326
|
+
try {
|
|
327
|
+
await run(["gateway", "install"], GATEWAY_TIMEOUT_MS);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
log(`Gateway service install reported: ${describeCommandFailure(error)} (continuing)`);
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
await run(["gateway", "restart"], GATEWAY_TIMEOUT_MS);
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
log(`Gateway restart reported: ${describeCommandFailure(error)} (the bridge step restarts again)`);
|
|
337
|
+
}
|
|
338
|
+
}
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NDJSON progress protocol (docs/agent/EXTERNAL_RUNTIME.md §6.4).
|
|
3
|
+
*
|
|
4
|
+
* With `--json` every step is one JSON object per line on **stdout** — the
|
|
5
|
+
* desktop app reads it to draw the connect card's step timeline. Human text
|
|
6
|
+
* (subprocess output, hints) always goes to **stderr**, so stdout stays a
|
|
7
|
+
* clean machine channel; without `--json` the same events are rendered as
|
|
8
|
+
* text on stderr too.
|
|
9
|
+
*
|
|
10
|
+
* `needs_model_auth` is an honest stop, not a failure: it carries the
|
|
11
|
+
* `manualCommand` a person runs in a terminal (§6.5).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Failure carrying the phase it happened in so `main` emits exactly one
|
|
15
|
+
* error event with the contract's code. Modules throw this when phase and
|
|
16
|
+
* code are unambiguous; anything else is wrapped by the caller.
|
|
17
|
+
*/
|
|
18
|
+
export class ConnectError extends Error {
|
|
19
|
+
phase;
|
|
20
|
+
code;
|
|
21
|
+
manualCommand;
|
|
22
|
+
constructor(params) {
|
|
23
|
+
super(params.message, { cause: params.cause });
|
|
24
|
+
this.name = "ConnectError";
|
|
25
|
+
this.phase = params.phase;
|
|
26
|
+
this.code = params.code;
|
|
27
|
+
this.manualCommand = params.manualCommand;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function toConnectError(error, fallbackPhase, fallbackCode) {
|
|
31
|
+
if (error instanceof ConnectError)
|
|
32
|
+
return error;
|
|
33
|
+
return new ConnectError({
|
|
34
|
+
phase: fallbackPhase,
|
|
35
|
+
code: fallbackCode,
|
|
36
|
+
message: error instanceof Error ? error.message : String(error),
|
|
37
|
+
cause: error,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function formatProgressLine(event) {
|
|
41
|
+
const label = `[${event.phase}]`.padEnd(10);
|
|
42
|
+
if (event.status === "error") {
|
|
43
|
+
const code = event.code ? ` (${event.code})` : "";
|
|
44
|
+
const manual = event.manualCommand
|
|
45
|
+
? `\n${" ".repeat(10)}Run: ${event.manualCommand}`
|
|
46
|
+
: "";
|
|
47
|
+
return `${label}error${code}: ${event.message ?? ""}${manual}`;
|
|
48
|
+
}
|
|
49
|
+
if (event.status === "done") {
|
|
50
|
+
const account = event.account ? ` account=${event.account}` : "";
|
|
51
|
+
return `${label}done${account}${event.message ? ` — ${event.message}` : ""}`;
|
|
52
|
+
}
|
|
53
|
+
return `${label}${event.message ?? ""}`;
|
|
54
|
+
}
|
|
55
|
+
export function createProgressReporter(options) {
|
|
56
|
+
const stdout = options.stdout ?? process.stdout;
|
|
57
|
+
const stderr = options.stderr ?? process.stderr;
|
|
58
|
+
const emit = (event) => {
|
|
59
|
+
if (options.json) {
|
|
60
|
+
stdout.write(`${JSON.stringify(event)}\n`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
stderr.write(`${formatProgressLine(event)}\n`);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
json: options.json,
|
|
68
|
+
emit,
|
|
69
|
+
log: (line) => {
|
|
70
|
+
stderr.write(`${line}\n`);
|
|
71
|
+
},
|
|
72
|
+
progress: (phase, message) => emit({ phase, status: "progress", message }),
|
|
73
|
+
};
|
|
74
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version arithmetic without a semver dependency — the connector ships with
|
|
3
|
+
* zero runtime dependencies so the desktop can bundle `dist/` alone.
|
|
4
|
+
*
|
|
5
|
+
* Evaluates the server catalog's `runtime.nodeRange`
|
|
6
|
+
* (`">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0"`) and `runtime.minVersion`
|
|
7
|
+
* (docs/agent/EXTERNAL_RUNTIME.md §6.1·6.2). Ranges are `||`-joined clauses;
|
|
8
|
+
* each clause is whitespace-separated comparators (`>=X.Y.Z`, optional `<M`).
|
|
9
|
+
*/
|
|
10
|
+
const FULL_VERSION_PATTERN = /(\d+)\.(\d+)\.(\d+)/;
|
|
11
|
+
const COMPARATOR_PATTERN = /^(>=|<=|>|<|=)?v?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/;
|
|
12
|
+
/** First `X.Y.Z` inside the input (`v26.0.0`, `OpenClaw 2026.8.2 (0965053)`). */
|
|
13
|
+
export function parseVersion(input) {
|
|
14
|
+
if (!input)
|
|
15
|
+
return null;
|
|
16
|
+
const match = FULL_VERSION_PATTERN.exec(input);
|
|
17
|
+
if (!match)
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
major: Number(match[1]),
|
|
21
|
+
minor: Number(match[2]),
|
|
22
|
+
patch: Number(match[3]),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function formatVersion(version) {
|
|
26
|
+
return `${version.major}.${version.minor}.${version.patch}`;
|
|
27
|
+
}
|
|
28
|
+
function toParsed(value) {
|
|
29
|
+
if (typeof value !== "string")
|
|
30
|
+
return value;
|
|
31
|
+
const parsed = parseVersion(value);
|
|
32
|
+
if (!parsed)
|
|
33
|
+
throw new Error(`Not a version: "${value}"`);
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
36
|
+
/** -1 / 0 / 1 like `Array.prototype.sort` comparators. */
|
|
37
|
+
export function compareVersions(left, right) {
|
|
38
|
+
const a = toParsed(left);
|
|
39
|
+
const b = toParsed(right);
|
|
40
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
41
|
+
if (a[key] < b[key])
|
|
42
|
+
return -1;
|
|
43
|
+
if (a[key] > b[key])
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* OpenClaw prints `OpenClaw 2026.8.2 (0965053)`; older builds may print the
|
|
50
|
+
* bare version. Returns the `X.Y.Z` string or null when nothing parses.
|
|
51
|
+
*/
|
|
52
|
+
export function parseOpenClawVersion(output) {
|
|
53
|
+
const parsed = parseVersion(output);
|
|
54
|
+
return parsed ? formatVersion(parsed) : null;
|
|
55
|
+
}
|
|
56
|
+
function parseComparator(token, clause) {
|
|
57
|
+
const match = COMPARATOR_PATTERN.exec(token);
|
|
58
|
+
if (!match) {
|
|
59
|
+
throw new Error(`Unsupported node range clause "${clause}" (token "${token}")`);
|
|
60
|
+
}
|
|
61
|
+
const operator = (match[1] ?? "=");
|
|
62
|
+
return {
|
|
63
|
+
operator,
|
|
64
|
+
version: {
|
|
65
|
+
major: Number(match[2]),
|
|
66
|
+
minor: Number(match[3] ?? 0),
|
|
67
|
+
patch: Number(match[4] ?? 0),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function matchesComparator(version, comparator) {
|
|
72
|
+
const order = compareVersions(version, comparator.version);
|
|
73
|
+
switch (comparator.operator) {
|
|
74
|
+
case ">=":
|
|
75
|
+
return order >= 0;
|
|
76
|
+
case "<=":
|
|
77
|
+
return order <= 0;
|
|
78
|
+
case ">":
|
|
79
|
+
return order > 0;
|
|
80
|
+
case "<":
|
|
81
|
+
return order < 0;
|
|
82
|
+
case "=":
|
|
83
|
+
return order === 0;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* True when `version` satisfies any `||` clause of `range`. An empty range
|
|
88
|
+
* means "no constraint". Malformed clauses throw so a server-side typo is
|
|
89
|
+
* loud instead of silently provisioning the wrong Node.
|
|
90
|
+
*/
|
|
91
|
+
export function satisfiesNodeRange(version, range) {
|
|
92
|
+
const parsed = typeof version === "string" ? parseVersion(version) : version;
|
|
93
|
+
if (!parsed)
|
|
94
|
+
return false;
|
|
95
|
+
const clauses = range
|
|
96
|
+
.split("||")
|
|
97
|
+
.map((clause) => clause.trim())
|
|
98
|
+
.filter(Boolean);
|
|
99
|
+
if (clauses.length === 0)
|
|
100
|
+
return true;
|
|
101
|
+
return clauses.some((clause) => clause
|
|
102
|
+
.split(/\s+/)
|
|
103
|
+
.map((token) => parseComparator(token, clause))
|
|
104
|
+
.every((comparator) => matchesComparator(parsed, comparator)));
|
|
105
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pllla-connect",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Connect a self-hosted agent runtime (OpenClaw, …) to a PLLLA agent with one command",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pllla-connect": "./dist/main.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/node": "^20.14.0",
|
|
20
|
+
"typescript": "^5.5.0"
|
|
21
|
+
}
|
|
22
|
+
}
|