@coolclaw/clawtopia-connector 0.1.0 → 0.2.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/cli.js DELETED
@@ -1,750 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- ChannelClient,
4
- JsonFileStateStore,
5
- RuntimeConnector,
6
- WorkerClient,
7
- activeFlavor,
8
- assertEnvironmentMatches,
9
- bundledWorkerPath,
10
- defaultConfig,
11
- loadConfig,
12
- normalizeProfileId,
13
- profileConfigPath,
14
- profileEnvironment,
15
- saveConfig
16
- } from "./chunk-5OV44BPP.js";
17
-
18
- // src/cli.ts
19
- import { chmod, copyFile, mkdir as mkdir3, open, readFile as readFile2, rename, rm as rm2, stat, writeFile as writeFile3 } from "fs/promises";
20
- import { existsSync as existsSync4, realpathSync } from "fs";
21
- import { createHash, randomUUID as randomUUID2 } from "crypto";
22
- import { execFile as execFile2, spawn as spawn2 } from "child_process";
23
- import { promisify as promisify2 } from "util";
24
-
25
- // src/openclaw-executor.ts
26
- import { spawn } from "child_process";
27
- import { randomUUID } from "crypto";
28
- var OpenClawExecutor = class {
29
- constructor(executable = "openclaw", cwd, env) {
30
- this.executable = executable;
31
- this.cwd = cwd;
32
- this.env = env;
33
- }
34
- executable;
35
- cwd;
36
- env;
37
- handler;
38
- sessions = /* @__PURE__ */ new Set();
39
- setEventHandler(handler) {
40
- this.handler = handler;
41
- }
42
- async initialize(runtime) {
43
- if (runtime !== "openclaw") throw new Error(`OpenClaw executor cannot initialize ${runtime}`);
44
- return { workerVersion: "openclaw-adapter.v1", protocolVersion: 1, runtime, agents: ["openclaw"], capabilities: { sessions: true, attachments: false } };
45
- }
46
- async startSession(input) {
47
- const id = input.sessionId ?? `openclaw-${randomUUID()}`;
48
- this.sessions.add(id);
49
- return { sessionId: id, agentSessionId: input.agentSessionId ?? id, runtime: "openclaw", alive: true };
50
- }
51
- async resumeSession(input) {
52
- this.sessions.add(input.sessionId);
53
- return { sessionId: input.sessionId, agentSessionId: input.agentSessionId ?? input.sessionId, runtime: "openclaw", alive: true, resumed: true };
54
- }
55
- async send(input) {
56
- if (!this.sessions.has(input.sessionId)) throw new Error(`unknown OpenClaw session ${input.sessionId}`);
57
- const args = ["agent", "--json", "--session-id", input.sessionId, "--message", input.prompt];
58
- const child = spawn(this.executable, args, { cwd: this.cwd, env: { ...process.env, ...this.env }, stdio: ["ignore", "pipe", "pipe"] });
59
- let stdout = "";
60
- let stderr = "";
61
- child.stdout.setEncoding("utf8");
62
- child.stdout.on("data", (chunk) => {
63
- stdout += chunk;
64
- });
65
- child.stderr.setEncoding("utf8");
66
- child.stderr.on("data", (chunk) => {
67
- stderr += chunk;
68
- });
69
- await new Promise((resolve2, reject) => {
70
- child.once("error", reject);
71
- child.once("close", (code) => code === 0 ? resolve2() : reject(new Error(stderr.trim() || `openclaw exited with code ${code}`)));
72
- });
73
- let parsed;
74
- try {
75
- parsed = JSON.parse(stdout);
76
- } catch {
77
- parsed = void 0;
78
- }
79
- const text = typeof parsed?.result === "string" ? parsed.result : typeof parsed?.reply === "string" ? parsed.reply : typeof parsed?.text === "string" ? parsed.text : stdout.trim();
80
- if (text) await this.handler?.({ event: "text", sessionId: input.sessionId, content: text });
81
- await this.handler?.({ event: "result", sessionId: input.sessionId, done: true });
82
- return { accepted: true };
83
- }
84
- async respondPermission(_input) {
85
- return { accepted: false };
86
- }
87
- async cancel(_sessionId) {
88
- return { accepted: false };
89
- }
90
- async closeSession(sessionId) {
91
- this.sessions.delete(sessionId);
92
- return { accepted: true };
93
- }
94
- async health() {
95
- return { ok: true, sessions: this.sessions.size, runtime: "openclaw", version: "openclaw-adapter.v1" };
96
- }
97
- async shutdown() {
98
- this.sessions.clear();
99
- }
100
- };
101
-
102
- // src/openclaw-profile.ts
103
- import { existsSync } from "fs";
104
- import { mkdir, readFile, writeFile } from "fs/promises";
105
- import { homedir } from "os";
106
- import { join } from "path";
107
- var COPIED_KEYS = ["models", "auth"];
108
- var EXCLUDED_KEY_PATTERN = /channel|plugin|extension|coolclaw|clawtopia|binding|token/iu;
109
- function defaultOpenClawHome(env = process.env) {
110
- if (env.OPENCLAW_HOME) return env.OPENCLAW_HOME;
111
- if (process.platform === "win32") return join(env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "openclaw");
112
- return join(homedir(), ".openclaw");
113
- }
114
- function filterOpenClawConfig(source) {
115
- const result = {};
116
- for (const key of COPIED_KEYS) {
117
- if (EXCLUDED_KEY_PATTERN.test(key)) continue;
118
- if (source[key] !== void 0) result[key] = source[key];
119
- }
120
- const agents = source.agents;
121
- if (agents && typeof agents === "object" && !Array.isArray(agents)) {
122
- const defaults = agents.defaults;
123
- if (defaults !== void 0) result.agents = { defaults };
124
- }
125
- return result;
126
- }
127
- async function prepareOpenClawProfile(workDir, sourceHome = defaultOpenClawHome()) {
128
- const home = join(workDir, "openclaw");
129
- await mkdir(join(home, "workspace"), { recursive: true, mode: 448 });
130
- const configPath = join(home, "openclaw.json");
131
- const sourceConfigPath = join(sourceHome, "openclaw.json");
132
- let inherited = {};
133
- if (sourceHome !== home && existsSync(sourceConfigPath)) {
134
- try {
135
- inherited = filterOpenClawConfig(JSON.parse(await readFile(sourceConfigPath, "utf8")));
136
- } catch {
137
- inherited = {};
138
- }
139
- }
140
- let current = {};
141
- if (existsSync(configPath)) {
142
- try {
143
- current = JSON.parse(await readFile(configPath, "utf8"));
144
- } catch {
145
- current = {};
146
- }
147
- }
148
- const merged = { ...inherited, ...current };
149
- await writeFile(configPath, `${JSON.stringify(merged, null, 2)}
150
- `, { mode: 384 });
151
- return { home, configPath, inheritedKeys: Object.keys(inherited) };
152
- }
153
-
154
- // src/cli.ts
155
- import { dirname, join as join4, resolve } from "path";
156
- import { fileURLToPath } from "url";
157
-
158
- // src/diagnostics.ts
159
- import { appendFileSync, chmodSync, existsSync as existsSync2, renameSync, statSync } from "fs";
160
- import { join as join2 } from "path";
161
- function redactDiagnostic(text, secrets = []) {
162
- let safe = text;
163
- for (const secret of secrets.filter(Boolean)) safe = safe.split(secret).join("[REDACTED]");
164
- return safe.replace(/(Bearer\s+)[^\s"',]+/giu, "$1[REDACTED]").replace(/((?:[\w-]*(?:token|secret|password|api[_-]?key|pairing[_-]?code))\s*["']?\s*(?:[=:]|\s)\s*["']?)[^\s"',}&]+/giu, "$1[REDACTED]").replace(/\b[a-f0-9]{32,64}\b/giu, "[REDACTED]").slice(0, 4e3);
165
- }
166
- function createDiagnosticLogger(workDir, identity, secrets = []) {
167
- const path = join2(workDir, "connector.log");
168
- return (entry) => {
169
- const record = { time: (/* @__PURE__ */ new Date()).toISOString(), ...identity, ...entry };
170
- for (const [key, value] of Object.entries(record)) {
171
- if (typeof value === "string") Object.assign(record, { [key]: redactDiagnostic(value, secrets) });
172
- }
173
- const line = `${JSON.stringify(record)}
174
- `;
175
- try {
176
- if (existsSync2(path) && statSync(path).size > 5 * 1024 * 1024) renameSync(path, `${path}.1`);
177
- appendFileSync(path, line, { mode: 384 });
178
- chmodSync(path, 384);
179
- } catch {
180
- process.stderr.write("connector diagnostic file unavailable\n");
181
- }
182
- process.stderr.write(line);
183
- };
184
- }
185
-
186
- // src/service-manager.ts
187
- import { execFile } from "child_process";
188
- import { existsSync as existsSync3 } from "fs";
189
- import { mkdir as mkdir2, rm, writeFile as writeFile2 } from "fs/promises";
190
- import { homedir as homedir2 } from "os";
191
- import { join as join3 } from "path";
192
- import { promisify } from "util";
193
- var execFileAsync = promisify(execFile);
194
- function serviceSupported(platform = process.platform) {
195
- return platform === "darwin" || platform === "linux";
196
- }
197
- function serviceDefinition(profileId, executable = process.argv[1]) {
198
- if (process.platform === "darwin") {
199
- const label = `com.clawtopia.connector.${profileId}`;
200
- const logPath = join3(homedir2(), "Library", "Logs", `${label}.log`);
201
- const content = `<?xml version="1.0" encoding="UTF-8"?><plist version="1.0"><dict><key>Label</key><string>${label}</string><key>ProgramArguments</key><array><string>${process.execPath}</string><string>${executable}</string><string>_start</string><string>--profile</string><string>${profileId}</string></array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ProcessType</key><string>Background</string><key>StandardOutPath</key><string>${logPath}</string><key>StandardErrorPath</key><string>${logPath}</string></dict></plist>
202
- `;
203
- return { path: join3(homedir2(), "Library", "LaunchAgents", `${label}.plist`), content, label, logPath, logHint: `tail -f ${logPath}` };
204
- }
205
- if (process.platform === "linux") {
206
- const label = `clawtopia-connector-${profileId}.service`;
207
- const content = `[Unit]
208
- Description=Clawtopia Agent Connector (${profileId})
209
- After=network-online.target
210
-
211
- [Service]
212
- ExecStart=${process.execPath} ${executable} _start --profile ${profileId}
213
- Restart=always
214
- RestartSec=5
215
-
216
- [Install]
217
- WantedBy=default.target
218
- `;
219
- const path = join3(process.env.XDG_CONFIG_HOME ?? join3(homedir2(), ".config"), "systemd", "user", label);
220
- return { path, content, label, logPath: "journald", logHint: `journalctl --user -u ${label} -f` };
221
- }
222
- throw new Error(`automatic startup is unsupported on ${process.platform}; use clawtopia start --foreground`);
223
- }
224
- function serviceInstalled(profileId) {
225
- if (!serviceSupported()) return false;
226
- try {
227
- return existsSync3(serviceDefinition(profileId).path);
228
- } catch {
229
- return false;
230
- }
231
- }
232
- async function run(command, args, { optional = false } = {}) {
233
- try {
234
- await execFileAsync(command, args);
235
- } catch (error) {
236
- if (optional) return;
237
- const detail = error instanceof Error ? error.message : String(error);
238
- throw new Error(`${command} ${args.join(" ")} failed: ${detail}`);
239
- }
240
- }
241
- function domainTarget() {
242
- return `gui/${process.getuid?.() ?? 0}`;
243
- }
244
- async function installService(profileId) {
245
- const definition = serviceDefinition(profileId);
246
- await mkdir2(join3(definition.path, ".."), { recursive: true, mode: 448 });
247
- await writeFile2(definition.path, definition.content, { mode: 384 });
248
- if (process.platform === "darwin") {
249
- await mkdir2(join3(definition.logPath, ".."), { recursive: true });
250
- await run("launchctl", ["bootout", `${domainTarget()}/${definition.label}`], { optional: true });
251
- await run("launchctl", ["enable", `${domainTarget()}/${definition.label}`], { optional: true });
252
- await run("launchctl", ["bootstrap", domainTarget(), definition.path]);
253
- return definition.path;
254
- }
255
- await run("systemctl", ["--user", "daemon-reload"]);
256
- await run("systemctl", ["--user", "enable", "--now", definition.label]);
257
- return definition.path;
258
- }
259
- async function stopService(profileId) {
260
- const definition = serviceDefinition(profileId);
261
- if (process.platform === "darwin") {
262
- await run("launchctl", ["bootout", `${domainTarget()}/${definition.label}`], { optional: true });
263
- return;
264
- }
265
- await run("systemctl", ["--user", "stop", definition.label], { optional: true });
266
- }
267
- async function startService(profileId) {
268
- const definition = serviceDefinition(profileId);
269
- if (process.platform === "darwin") {
270
- await run("launchctl", ["enable", `${domainTarget()}/${definition.label}`], { optional: true });
271
- await run("launchctl", ["bootstrap", domainTarget(), definition.path]);
272
- return;
273
- }
274
- await run("systemctl", ["--user", "start", definition.label]);
275
- }
276
- async function restartService(profileId) {
277
- const definition = serviceDefinition(profileId);
278
- if (process.platform === "darwin") {
279
- await run("launchctl", ["kickstart", "-k", `${domainTarget()}/${definition.label}`]);
280
- return;
281
- }
282
- await run("systemctl", ["--user", "restart", definition.label]);
283
- }
284
- async function disableService(profileId) {
285
- const definition = serviceDefinition(profileId);
286
- if (process.platform === "darwin") {
287
- await run("launchctl", ["bootout", `${domainTarget()}/${definition.label}`], { optional: true });
288
- await run("launchctl", ["disable", `${domainTarget()}/${definition.label}`]);
289
- return;
290
- }
291
- await run("systemctl", ["--user", "disable", "--now", definition.label], { optional: true });
292
- }
293
- async function uninstallService(profileId) {
294
- const definition = serviceDefinition(profileId);
295
- if (process.platform === "darwin") {
296
- await run("launchctl", ["bootout", `${domainTarget()}/${definition.label}`], { optional: true });
297
- await run("launchctl", ["disable", `${domainTarget()}/${definition.label}`], { optional: true });
298
- } else if (process.platform === "linux") {
299
- await run("systemctl", ["--user", "disable", "--now", definition.label], { optional: true });
300
- }
301
- await rm(definition.path, { force: true });
302
- if (process.platform === "linux") await run("systemctl", ["--user", "daemon-reload"], { optional: true });
303
- return definition.path;
304
- }
305
- function serviceLogHint(profileId) {
306
- return serviceDefinition(profileId).logHint;
307
- }
308
-
309
- // src/cli.ts
310
- var execFileAsync2 = promisify2(execFile2);
311
- async function main(argv = process.argv.slice(2)) {
312
- const command = argv[0] ?? "status";
313
- const flags = parseFlags(argv.slice(1));
314
- if (command === "help" || command === "--help" || flags.help === "true") {
315
- printHelp();
316
- return 0;
317
- }
318
- const profile = flags.profile ? normalizeProfileId(flags.profile) : void 0;
319
- const path = profileConfigPath(profile);
320
- if (command === "connect") {
321
- const pairingCode = flags.pairing_code ?? flags.code ?? process.env.CLAWTOPIA_PAIRING_CODE;
322
- if (!pairingCode || pairingCode === "true") throw new Error("connect requires --pairing-code <code>");
323
- const base = defaultConfig(profile);
324
- const overrides = configOverrides(flags);
325
- const requestedRuntime = overrides.runtime ?? base.runtime;
326
- const gatewayUrl = overrides.gatewayUrl ?? base.gatewayUrl;
327
- const endpoint = flags.pairing_endpoint ?? process.env.CLAWTOPIA_PAIRING_ENDPOINT ?? base.pairingEndpoint ?? "/api/agent/pairing/exchange";
328
- const exchangeId = process.env.CLAWTOPIA_EXCHANGE_ID ?? randomUUID2();
329
- const response = await fetch(`${gatewayUrl.replace(/\/+$/u, "")}${endpoint.startsWith("/") ? endpoint : `/${endpoint}`}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ pairingCode, code: pairingCode, runtime: requestedRuntime, profileId: profile, exchangeId }) });
330
- const responseBody = await response.json().catch(() => ({}));
331
- const data = isRecord(responseBody.data) ? responseBody.data : responseBody;
332
- if (!response.ok || typeof data.agentId !== "string" && typeof data.agentId !== "number" || typeof data.token !== "string" || !data.token) {
333
- const message = typeof responseBody.message === "string" ? responseBody.message : `pairing exchange failed: HTTP ${response.status}`;
334
- throw new Error(message);
335
- }
336
- const agentId = String(data.agentId);
337
- assertEnvironmentMatches(activeFlavor(), data.environment);
338
- if (typeof data.runtime === "string" && normalizeRuntime(data.runtime) !== requestedRuntime) throw new Error(`pairing runtime mismatch: requested ${requestedRuntime}, server returned ${data.runtime}`);
339
- if (profile && typeof data.profileId === "string" && data.profileId !== profile) throw new Error(`pairing profile mismatch: requested ${profile}, server returned ${data.profileId}`);
340
- const id = normalizeProfileId(profile ?? (typeof data.profileId === "string" ? data.profileId : `agent-${agentId}-${createHash("sha256").update(gatewayUrl).digest("hex").slice(0, 8)}`));
341
- let config = { ...defaultConfig(id), ...overrides, profileId: id, gatewayUrl: typeof data.gatewayUrl === "string" ? data.gatewayUrl : gatewayUrl, environment: typeof data.environment === "string" ? data.environment : base.environment, agentId, token: data.token, runtime: normalizeRuntime(typeof data.runtime === "string" ? data.runtime : overrides.runtime ?? base.runtime) ?? base.runtime, pairingEndpoint: endpoint };
342
- const target = profileConfigPath(id);
343
- const previous = await loadConfig(target).catch(() => void 0);
344
- if (previous?.agentId && previous.agentId !== agentId) throw new Error(`profile ${id} already belongs to Agent ${previous.agentId}; select a different --profile`);
345
- if (previous?.agentId && previous.gatewayUrl.replace(/\/+$/u, "") !== config.gatewayUrl.replace(/\/+$/u, "")) throw new Error(`profile ${id} already belongs to a different platform environment; select a different --profile`);
346
- await mkdir3(config.workDir, { recursive: true, mode: 448 });
347
- config = await materializeBundledWorker(config);
348
- await saveConfig(config, target);
349
- let servicePath;
350
- if (flags.foreground !== "true" && flags.daemon !== "false" && serviceSupported()) {
351
- try {
352
- servicePath = await installService(id);
353
- } catch (error) {
354
- console.error(`user service setup failed, falling back to a detached process: ${error instanceof Error ? error.message : String(error)}`);
355
- }
356
- }
357
- console.log(JSON.stringify({ profileId: id, agentId, config: target, paired: true, service: servicePath ?? null }, null, 2));
358
- if (servicePath) {
359
- console.log(`connector service started for profile ${id}; logs: ${serviceLogHint(id)}`);
360
- return 0;
361
- }
362
- if (flags.foreground !== "true" && flags.daemon !== "false") {
363
- const child = spawn2(process.execPath, [process.argv[1], "_start", "--profile", id], {
364
- cwd: process.cwd(),
365
- env: process.env,
366
- detached: true,
367
- stdio: "ignore"
368
- });
369
- child.unref();
370
- console.log(`connector started in background for profile ${id}`);
371
- return 0;
372
- }
373
- return runStart(config, target);
374
- }
375
- if (["status", "doctor"].includes(command)) {
376
- let config;
377
- try {
378
- config = await loadConfig(path);
379
- } catch {
380
- console.log(`not configured: run clawtopia connect --pairing-code <code> (${path})`);
381
- return command === "doctor" ? 1 : 0;
382
- }
383
- if (command === "doctor") {
384
- const migrated = await materializeBundledWorker(config);
385
- if (migrated.workerPath !== config.workerPath) {
386
- config = migrated;
387
- await saveConfig(config, path);
388
- }
389
- }
390
- if (command === "status") {
391
- console.log(JSON.stringify({ configured: Boolean(config.agentId && config.token), gatewayUrl: config.gatewayUrl, runtime: config.runtime, workerPath: config.workerPath, workerSha256: config.workerSha256 ?? null }, null, 2));
392
- return 0;
393
- }
394
- const report = await diagnose(config);
395
- console.log(JSON.stringify({ config: path, ...report }, null, 2));
396
- return report.ok ? 0 : 1;
397
- }
398
- if (command === "install" || command === "enable") {
399
- const id = profile ?? process.env.CLAWTOPIA_AGENT_PROFILE ?? "default";
400
- const servicePath = await installService(id);
401
- console.log(`enabled user service ${servicePath}; logs: ${serviceLogHint(id)}`);
402
- return 0;
403
- }
404
- if (command === "disable") {
405
- const id = profile ?? process.env.CLAWTOPIA_AGENT_PROFILE ?? "default";
406
- if (!serviceInstalled(id)) {
407
- console.log("no user service installed");
408
- return 0;
409
- }
410
- await disableService(id);
411
- console.log(`disabled user service for profile ${id}; it will not be restarted until clawtopia enable`);
412
- return 0;
413
- }
414
- if (command === "restart") {
415
- const id = profile ?? process.env.CLAWTOPIA_AGENT_PROFILE ?? "default";
416
- if (!serviceInstalled(id)) throw new Error(`no user service for profile ${id}; run clawtopia install first`);
417
- await restartService(id);
418
- console.log(`restarted user service for profile ${id}`);
419
- return 0;
420
- }
421
- if (command === "logs") {
422
- const id = profile ?? process.env.CLAWTOPIA_AGENT_PROFILE ?? "default";
423
- const config = await loadConfig(path).catch(() => void 0);
424
- console.log(JSON.stringify({ service: serviceInstalled(id) ? serviceLogHint(id) : null, diagnostics: config ? join4(config.workDir, "diagnostics.log") : null }, null, 2));
425
- return 0;
426
- }
427
- if (command === "start") {
428
- let config = await loadConfig(path);
429
- config = await materializeBundledWorker(config);
430
- await saveConfig(config, path);
431
- if (!config.agentId || !config.token) throw new Error("agentId and token are required; run connect --pairing-code <code> first");
432
- const report = await diagnose(config);
433
- if (!report.ok) throw new Error(`doctor failed; run clawtopia doctor for details`);
434
- if (flags.foreground !== "true" && serviceInstalled(config.profileId)) {
435
- await startService(config.profileId);
436
- console.log(`started user service for profile ${config.profileId}; logs: ${serviceLogHint(config.profileId)}`);
437
- return 0;
438
- }
439
- return runStart(config, path);
440
- }
441
- if (command === "_start") {
442
- const config = await loadConfig(path);
443
- return runStart(config, path);
444
- }
445
- if (command === "start-foreground") {
446
- const config = await loadConfig(path);
447
- return runStart(config, path);
448
- }
449
- async function runStart(config, configPath) {
450
- const migrated = await materializeBundledWorker(config);
451
- if (migrated.workerPath !== config.workerPath) {
452
- config = migrated;
453
- await saveConfig(config, configPath);
454
- }
455
- if (!config.agentId || !config.token) throw new Error("agentId and token are required; run connect --pairing-code <code> first");
456
- const report = await diagnose(config);
457
- if (!report.ok) throw new Error(`doctor failed; run clawtopia doctor for details`);
458
- await mkdir3(config.workDir, { recursive: true, mode: 448 });
459
- const log = createDiagnosticLogger(config.workDir, { profileId: config.profileId, agentId: config.agentId, runtime: config.runtime }, [config.token]);
460
- const runtimeEnv = profileEnvironment(config, configPath);
461
- if (config.runtime === "openclaw") {
462
- const openclaw = await prepareOpenClawProfile(config.workDir);
463
- runtimeEnv.OPENCLAW_HOME = openclaw.home;
464
- log({ event: "openclaw.profile", home: openclaw.home, inherited: openclaw.inheritedKeys.join(",") || "none" });
465
- }
466
- const worker = config.runtime === "openclaw" ? new OpenClawExecutor(config.runtimePath || "openclaw", config.workDir, runtimeEnv) : new WorkerClient({
467
- executable: config.workerPath,
468
- args: [],
469
- cwd: config.workDir,
470
- env: runtimeEnv,
471
- requestTimeoutMs: 6e4,
472
- onStderr: (error) => log({ event: "worker.stderr", error }),
473
- onEvent: (event) => {
474
- if (event.event === "error") log({ event: "worker.error", sessionId: event.sessionId, error: event.error ?? event.content ?? "worker error (no detail)" });
475
- }
476
- });
477
- const channel = new ChannelClient({
478
- gatewayUrl: config.gatewayUrl,
479
- agentId: config.agentId,
480
- token: config.token,
481
- connectorVersion: config.connectorVersion,
482
- onError: (error) => log({ event: "channel.error", error: error.message }),
483
- onStateChange: (phase) => log({ event: "channel.state", phase })
484
- });
485
- const connector = new RuntimeConnector({ channel, worker, runtime: config.runtime, runtimePath: config.runtimePath, backend: config.backend, appServerUrl: config.appServerUrl, connectorVersion: config.connectorVersion, workDir: config.workDir, stateStore: new JsonFileStateStore(join4(config.workDir, "connector-state.json")), onPermission: () => config.permissionPolicy ?? "deny", onDiagnostic: log });
486
- log({ event: "connector.start" });
487
- try {
488
- await connector.start();
489
- } catch (error) {
490
- log({ event: "connector.start_failed", error: error instanceof Error ? error.message : String(error) });
491
- throw error;
492
- }
493
- await writeFile3(join4(config.workDir, "connector.pid"), `${process.pid}
494
- `, { mode: 384 });
495
- console.log(`clawtopia connected as ${config.agentId}; press Ctrl-C to stop`);
496
- await new Promise((resolve2) => {
497
- process.once("SIGINT", () => resolve2());
498
- process.once("SIGTERM", () => resolve2());
499
- });
500
- await connector.stop();
501
- await rm2(join4(config.workDir, "connector.pid"), { force: true });
502
- return 0;
503
- }
504
- async function materializeBundledWorker(config) {
505
- if (config.runtime === "openclaw") return config;
506
- const target = join4(config.workDir, "worker", "agent-worker");
507
- if (config.workerPath === target && existsSync4(target)) return config;
508
- const bundled = bundledWorkerPath();
509
- const looksLikeBundledWorker = config.workerPath === "agent-worker" || /[\\/]worker[\\/](?:darwin|linux)-(?:x64|arm64)[\\/]agent-worker$/u.test(config.workerPath);
510
- if (!looksLikeBundledWorker || bundled === "agent-worker") return config;
511
- const source = existsSync4(config.workerPath) ? config.workerPath : bundled;
512
- await mkdir3(dirname(target), { recursive: true, mode: 448 });
513
- await copyFile(source, target);
514
- await chmod(target, 448);
515
- return { ...config, workerPath: target, workerSha256: await sha256File(target) };
516
- }
517
- if (command === "stop") {
518
- const config = await loadConfig(path);
519
- if (serviceInstalled(config.profileId)) {
520
- await stopService(config.profileId);
521
- console.log(`stopped user service for profile ${config.profileId}`);
522
- }
523
- const pidPath = join4(config.workDir, "connector.pid");
524
- try {
525
- const pid = Number((await readFile2(pidPath, "utf8")).trim());
526
- if (!Number.isInteger(pid) || pid <= 1) throw new Error("invalid pid file");
527
- process.kill(pid, "SIGTERM");
528
- await rm2(pidPath, { force: true });
529
- console.log(`stopped ${pid}`);
530
- } catch (error) {
531
- if (error.code === "ENOENT" || error.code === "ESRCH") {
532
- await rm2(pidPath, { force: true });
533
- console.log("connector process is not running");
534
- } else throw error;
535
- }
536
- return 0;
537
- }
538
- if (command === "upgrade") {
539
- const config = await loadConfig(path);
540
- const source = flags.source;
541
- const expected = (flags.sha256 ?? "").toLowerCase();
542
- if (!source) throw new Error("upgrade requires --source <worker-binary> and --sha256 <64-hex-digest>");
543
- if (!/^[a-f0-9]{64}$/u.test(expected)) throw new Error("upgrade requires a 64-character lowercase SHA-256 via --sha256");
544
- const digest = await sha256File(source);
545
- if (digest !== expected) throw new Error(`worker checksum mismatch: expected ${expected}, got ${digest}`);
546
- const sourceStat = await stat(source);
547
- if (!sourceStat.isFile()) throw new Error(`worker source is not a regular file: ${source}`);
548
- await mkdir3(dirname(config.workerPath), { recursive: true, mode: 448 });
549
- const temporary = `${config.workerPath}.tmp-${process.pid}-${Date.now()}`;
550
- try {
551
- await copyFile(source, temporary);
552
- await chmod(temporary, sourceStat.mode & 511 | 73);
553
- const handle = await open(temporary, "r");
554
- try {
555
- await handle.sync();
556
- } finally {
557
- await handle.close();
558
- }
559
- await rename(temporary, config.workerPath);
560
- } catch (error) {
561
- await rm2(temporary, { force: true });
562
- throw error;
563
- }
564
- await saveConfig({ ...config, workerSha256: digest }, path);
565
- console.log(`upgraded worker ${config.workerPath} (${digest})`);
566
- return 0;
567
- }
568
- if (command === "uninstall") {
569
- if (process.platform === "darwin" || process.platform === "linux") {
570
- const servicePath = await uninstallService(profile ?? process.env.CLAWTOPIA_AGENT_PROFILE ?? "default");
571
- console.log(`removed user service ${servicePath}`);
572
- }
573
- const config = await loadConfig(path).catch((error) => {
574
- if (error.code === "ENOENT") return void 0;
575
- throw error;
576
- });
577
- if (!config) {
578
- console.log(`connector is not configured: ${path}`);
579
- return 0;
580
- }
581
- const pidPath = join4(config.workDir, "connector.pid");
582
- try {
583
- const pid = Number((await readFile2(pidPath, "utf8")).trim());
584
- if (Number.isInteger(pid) && pid > 1) {
585
- try {
586
- process.kill(pid, "SIGTERM");
587
- } catch (error) {
588
- if (error.code !== "ESRCH") throw error;
589
- }
590
- }
591
- } catch (error) {
592
- if (error.code !== "ENOENT") throw error;
593
- }
594
- await rm2(path, { force: true });
595
- await rm2(join4(config.workDir, "connector-state.json"), { force: true });
596
- await rm2(join4(config.workDir, "connector.pid"), { force: true });
597
- console.log(`removed connector configuration ${path}`);
598
- return 0;
599
- }
600
- console.error(`unknown command: ${command}`);
601
- return 2;
602
- }
603
- function printHelp() {
604
- console.log(`clawtopia - connect a local Agent runtime to Clawtopia
605
-
606
- \u63A5\u5165\uFF1A
607
- connect --pairing-code <code> [--profile <name>] \u914D\u5BF9\u5E76\u542F\u52A8\u8FDE\u63A5\u5668
608
-
609
- \u8FD0\u884C\u7BA1\u7406\uFF1A
610
- install | enable [--profile <name>] \u5B89\u88C5\u5E76\u542F\u7528\u767B\u5F55\u540E\u81EA\u52A8\u542F\u52A8
611
- disable [--profile <name>] \u505C\u6B62\u5E76\u5173\u95ED\u81EA\u52A8\u542F\u52A8\uFF0C\u4E0D\u4F1A\u88AB\u91CD\u65B0\u62C9\u8D77
612
- uninstall [--profile <name>] \u505C\u6B62\u3001\u79FB\u9664\u670D\u52A1\u5B9A\u4E49\u5E76\u6E05\u7406\u672C\u5730\u51ED\u636E
613
- start [--foreground] | stop | restart \u542F\u52A8\u3001\u505C\u6B62\u6216\u91CD\u542F\u5F53\u524D profile
614
- status | doctor | logs \u67E5\u770B\u72B6\u6001\u3001\u8FD0\u884C\u8BCA\u65AD\u6216\u67E5\u770B\u65E5\u5FD7\u4F4D\u7F6E
615
- upgrade --source <file> --sha256 <digest> \u66F4\u65B0 worker
616
-
617
- `);
618
- }
619
- function parseFlags(args) {
620
- const result = {};
621
- for (let i = 0; i < args.length; i += 1) {
622
- const raw = args[i];
623
- if (!raw.startsWith("--")) continue;
624
- const key = raw.slice(2).replaceAll("-", "_");
625
- const next = args[i + 1];
626
- if (next && !next.startsWith("--")) {
627
- result[key] = next;
628
- i += 1;
629
- } else result[key] = "true";
630
- }
631
- return result;
632
- }
633
- function configOverrides(flags) {
634
- const env = process.env;
635
- const values = {
636
- gatewayUrl: flags.gateway_url ?? env.CLAWTOPIA_GATEWAY_URL,
637
- agentId: flags.agent_id ?? env.CLAWTOPIA_AGENT_ID,
638
- token: flags.token ?? env.CLAWTOPIA_AGENT_TOKEN,
639
- runtime: normalizeRuntime(flags.runtime ?? env.CLAWTOPIA_AGENT_RUNTIME),
640
- runtimePath: flags.runtime_path ?? env.CLAWTOPIA_AGENT_RUNTIME_PATH,
641
- workerPath: flags.worker ?? env.CLAWTOPIA_AGENT_WORKER,
642
- workDir: flags.work_dir ?? env.CLAWTOPIA_AGENT_WORK_DIR,
643
- backend: flags.backend ?? env.CLAWTOPIA_CODEX_BACKEND,
644
- appServerUrl: flags.app_server_url ?? env.CLAWTOPIA_CODEX_APP_SERVER_URL,
645
- permissionPolicy: normalizePermissionPolicy(flags.permission ?? env.CLAWTOPIA_AGENT_PERMISSION)
646
- };
647
- return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== void 0));
648
- }
649
- function normalizeRuntime(value) {
650
- if (!value) return void 0;
651
- if (value === "claude") return "claudecode";
652
- if (value === "open-claw") return "openclaw";
653
- if (value === "claudecode" || value === "codex" || value === "openclaw") return value;
654
- throw new Error(`unsupported runtime: ${value}; expected claudecode, codex or openclaw`);
655
- }
656
- function normalizePermissionPolicy(value) {
657
- if (!value) return void 0;
658
- if (value === "allow" || value === "deny") return value;
659
- throw new Error(`unsupported permission policy: ${value}; expected allow or deny`);
660
- }
661
- async function diagnose(config) {
662
- const platform = process.platform;
663
- const architecture = process.arch;
664
- const worker = { path: config.workerPath, executable: config.runtime === "openclaw" };
665
- if (config.runtime === "openclaw") worker.error = "worker not used by OpenClaw runtime";
666
- try {
667
- if (config.runtime === "openclaw") throw new Error("worker not used by OpenClaw runtime");
668
- const resolvedWorker = await resolveExecutablePath(config.workerPath);
669
- worker.path = resolvedWorker;
670
- const metadata = await stat(resolvedWorker);
671
- worker.executable = metadata.isFile() && (metadata.mode & 73) !== 0;
672
- if (!worker.executable) worker.error = "worker is not an executable regular file";
673
- if (worker.executable) {
674
- worker.sha256 = await sha256File(resolvedWorker);
675
- if (config.workerSha256) worker.checksumMatches = worker.sha256 === config.workerSha256.toLowerCase();
676
- const detected = await detectBinaryArchitecture(resolvedWorker);
677
- if (detected) {
678
- worker.architecture = detected;
679
- worker.architectureCompatible = binaryMatchesHost(detected, architecture);
680
- }
681
- }
682
- } catch (error) {
683
- worker.error = error.message;
684
- }
685
- const runtimeCommand = config.runtimePath || (config.runtime === "codex" ? "codex" : config.runtime === "openclaw" ? "openclaw" : "claude");
686
- const runtime = { command: runtimeCommand, executable: false };
687
- try {
688
- const result = await execFileAsync2(runtimeCommand, ["--version"], { timeout: 5e3, maxBuffer: 16 * 1024 });
689
- runtime.executable = true;
690
- runtime.version = `${result.stdout}${result.stderr}`.trim().split(/\r?\n/u)[0]?.slice(0, 200);
691
- } catch (error) {
692
- runtime.error = error.message;
693
- }
694
- const workerOk = config.runtime === "openclaw" || worker.executable && worker.checksumMatches !== false && worker.architectureCompatible !== false;
695
- const ok = workerOk && runtime.executable && Boolean(config.agentId && config.token);
696
- return { ok, platform, architecture, worker, runtime, tokenConfigured: Boolean(config.token), agentConfigured: Boolean(config.agentId) };
697
- }
698
- async function resolveExecutablePath(command) {
699
- if (command.includes("/") || command.includes("\\")) return command;
700
- const lookup = process.platform === "win32" ? "where" : "which";
701
- const result = await execFileAsync2(lookup, [command], { timeout: 2e3, maxBuffer: 8 * 1024 });
702
- const resolved = result.stdout.trim().split(/\r?\n/u)[0];
703
- if (!resolved) throw new Error(`executable not found on PATH: ${command}`);
704
- return resolved;
705
- }
706
- async function sha256File(path) {
707
- const hash = createHash("sha256");
708
- const handle = await open(path, "r");
709
- try {
710
- const buffer = Buffer.allocUnsafe(1024 * 1024);
711
- let bytes;
712
- do {
713
- bytes = (await handle.read(buffer, 0, buffer.length, null)).bytesRead;
714
- if (bytes) hash.update(buffer.subarray(0, bytes));
715
- } while (bytes);
716
- } finally {
717
- await handle.close();
718
- }
719
- return hash.digest("hex");
720
- }
721
- async function detectBinaryArchitecture(path) {
722
- try {
723
- const result = await execFileAsync2("file", ["-b", path], { timeout: 2e3, maxBuffer: 8 * 1024 });
724
- return result.stdout.trim().split(/,\s*/u).find((part) => /(?:x86-64|x86_64|aarch64|arm64|arm|i386|80386|universal)/iu.test(part));
725
- } catch {
726
- return void 0;
727
- }
728
- }
729
- function binaryMatchesHost(description, architecture) {
730
- if (/universal|fat binary/iu.test(description)) return true;
731
- if (architecture === "arm64") return /aarch64|arm64/iu.test(description);
732
- if (architecture === "x64") return /x86-64|x86_64/iu.test(description);
733
- if (architecture === "arm") return /\barm\b/iu.test(description);
734
- if (architecture === "ia32") return /i386|80386/iu.test(description);
735
- return true;
736
- }
737
- function isRecord(value) {
738
- return typeof value === "object" && value !== null;
739
- }
740
- if (process.argv[1] && fileURLToPath(import.meta.url) === realpathSync(resolve(process.argv[1]))) {
741
- main().then((code) => {
742
- process.exitCode = code;
743
- }).catch((error) => {
744
- console.error(error instanceof Error ? error.message : error);
745
- process.exitCode = 1;
746
- });
747
- }
748
- export {
749
- main
750
- };