@rethinkingstudio/clawpilot 2.0.0-beta.0 → 2.0.0-beta.1

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.
@@ -1,354 +0,0 @@
1
- import {
2
- execFileSync,
3
- execSync,
4
- type ExecSyncOptionsWithStringEncoding,
5
- } from "child_process";
6
- import { existsSync } from "fs";
7
- import { delimiter, dirname, join } from "path";
8
- import { homedir } from "os";
9
-
10
- const HOME_DIR = homedir();
11
- const NODE_BIN_DIR = dirname(process.execPath);
12
- const IS_WINDOWS = process.platform === "win32";
13
- const OPENCLAW_CANDIDATE_NAMES = IS_WINDOWS
14
- ? ["openclaw.exe", "openclaw.cmd", "openclaw"]
15
- : ["openclaw"];
16
- const KNOWN_BIN_DIRS = [
17
- NODE_BIN_DIR,
18
- join(HOME_DIR, ".npm-global", "bin"),
19
- join(HOME_DIR, "AppData", "Roaming", "npm"),
20
- join(HOME_DIR, ".pnpm"),
21
- join(HOME_DIR, "Library", "pnpm"),
22
- join(HOME_DIR, ".config", "yarn", "global", "node_modules", ".bin"),
23
- join(HOME_DIR, ".local", "bin"),
24
- join(HOME_DIR, ".bun", "bin"),
25
- join(HOME_DIR, ".volta", "bin"),
26
- join(HOME_DIR, ".asdf", "shims"),
27
- join(HOME_DIR, ".nvm", "current", "bin"),
28
- "/opt/homebrew/bin",
29
- "/opt/homebrew/sbin",
30
- "/usr/local/bin",
31
- "/usr/local/sbin",
32
- ];
33
-
34
- function unique(values: string[]): string[] {
35
- const seen = new Set<string>();
36
- const out: string[] = [];
37
- for (const value of values) {
38
- if (!value || seen.has(value)) continue;
39
- seen.add(value);
40
- out.push(value);
41
- }
42
- return out;
43
- }
44
-
45
- export const SUBPROCESS_ENV: NodeJS.ProcessEnv = {
46
- ...process.env,
47
- HOME: HOME_DIR,
48
- PATH: unique([
49
- ...KNOWN_BIN_DIRS,
50
- ...(process.env.PATH ?? "").split(delimiter),
51
- ]).join(delimiter),
52
- };
53
-
54
- let cachedOpenclawBin: string | null = null;
55
- let cachedOpenclawConfigPath: string | null = null;
56
-
57
- function resolveWindowsExecutable(pathValue: string): string | null {
58
- if (!IS_WINDOWS) {
59
- return existsSync(pathValue) ? pathValue : null;
60
- }
61
-
62
- const trimmed = pathValue.trim().replace(/^['"]|['"]$/g, "");
63
- if (!trimmed) return null;
64
- if (existsSync(trimmed)) return trimmed;
65
-
66
- for (const ext of [".cmd", ".exe", ".bat"]) {
67
- const candidate = `${trimmed}${ext}`;
68
- if (existsSync(candidate)) {
69
- return candidate;
70
- }
71
- }
72
-
73
- return null;
74
- }
75
-
76
- function normalizeResolvedOpenclawBin(pathValue: string | null): string | null {
77
- if (!pathValue) return null;
78
- return resolveWindowsExecutable(pathValue) ?? (existsSync(pathValue) ? pathValue : null);
79
- }
80
-
81
- function findOpenclawInKnownPaths(): string | null {
82
- for (const dir of KNOWN_BIN_DIRS) {
83
- for (const name of OPENCLAW_CANDIDATE_NAMES) {
84
- const candidate = join(dir, name);
85
- if (existsSync(candidate)) {
86
- return candidate;
87
- }
88
- }
89
- }
90
- return null;
91
- }
92
-
93
- function resolveViaShell(): string | null {
94
- const options: ExecSyncOptionsWithStringEncoding = {
95
- stdio: "pipe",
96
- env: SUBPROCESS_ENV,
97
- timeout: 3000,
98
- encoding: "utf8",
99
- };
100
-
101
- const commands = IS_WINDOWS
102
- ? ["where.exe openclaw"]
103
- : ["command -v openclaw", "which openclaw"];
104
-
105
- for (const command of commands) {
106
- try {
107
- const resolved = execSync(command, options)
108
- .trim()
109
- .split(/\r?\n/)
110
- .map((line) => line.trim())
111
- .find(Boolean);
112
- const normalized = normalizeResolvedOpenclawBin(resolved ?? null);
113
- if (normalized) {
114
- return normalized;
115
- }
116
- } catch {
117
- // try next strategy
118
- }
119
- }
120
- return null;
121
- }
122
-
123
- function readCommandOutput(command: string): string | null {
124
- const options: ExecSyncOptionsWithStringEncoding = {
125
- stdio: "pipe",
126
- env: SUBPROCESS_ENV,
127
- timeout: 3000,
128
- encoding: "utf8",
129
- };
130
-
131
- try {
132
- const output = execSync(command, options).trim();
133
- return output || null;
134
- } catch {
135
- return null;
136
- }
137
- }
138
-
139
- function findOpenclawViaPackageManagers(): string | null {
140
- const candidateDirs: string[] = [];
141
-
142
- const npmPrefix = readCommandOutput("npm config get prefix");
143
- if (npmPrefix && !/^undefined|null$/i.test(npmPrefix)) {
144
- candidateDirs.push(IS_WINDOWS ? npmPrefix : join(npmPrefix, "bin"));
145
- }
146
-
147
- const pnpmGlobalBin = readCommandOutput("pnpm config get global-bin-dir");
148
- if (pnpmGlobalBin && !/^undefined|null$/i.test(pnpmGlobalBin)) {
149
- candidateDirs.push(pnpmGlobalBin);
150
- }
151
-
152
- const npmExec = readCommandOutput("npm exec --yes -- which openclaw");
153
- {
154
- const normalized = normalizeResolvedOpenclawBin(npmExec);
155
- if (normalized) {
156
- return normalized;
157
- }
158
- }
159
-
160
- for (const dir of unique(candidateDirs)) {
161
- for (const name of OPENCLAW_CANDIDATE_NAMES) {
162
- const candidate = join(dir, name);
163
- if (existsSync(candidate)) {
164
- return candidate;
165
- }
166
- }
167
- }
168
-
169
- return null;
170
- }
171
-
172
- export function resolveOpenclawBin(forceRefresh = false): string {
173
- if (!forceRefresh && cachedOpenclawBin && existsSync(cachedOpenclawBin)) {
174
- return cachedOpenclawBin;
175
- }
176
-
177
- const envOverride = process.env.OPENCLAW_BIN;
178
- if (envOverride && existsSync(envOverride)) {
179
- cachedOpenclawBin = normalizeResolvedOpenclawBin(envOverride) ?? envOverride;
180
- console.log(`[clawpilot] openclaw resolved from OPENCLAW_BIN: ${envOverride}`);
181
- return cachedOpenclawBin;
182
- }
183
-
184
- const resolved = resolveViaShell() ?? findOpenclawViaPackageManagers() ?? findOpenclawInKnownPaths();
185
- if (resolved) {
186
- cachedOpenclawBin = resolved;
187
- console.log(`[clawpilot] openclaw resolved: ${resolved}`);
188
- return resolved;
189
- }
190
-
191
- cachedOpenclawBin = IS_WINDOWS ? "openclaw.cmd" : "openclaw";
192
- console.warn("[clawpilot] could not resolve openclaw path, using bare name");
193
- return cachedOpenclawBin;
194
- }
195
-
196
- function splitShellArgs(input: string): string[] {
197
- const args: string[] = [];
198
- let current = "";
199
- let quote: "'" | '"' | null = null;
200
- let escaping = false;
201
-
202
- for (const ch of input) {
203
- if (escaping) {
204
- current += ch;
205
- escaping = false;
206
- continue;
207
- }
208
-
209
- if (ch === "\\") {
210
- escaping = true;
211
- continue;
212
- }
213
-
214
- if (quote) {
215
- if (ch === quote) {
216
- quote = null;
217
- } else {
218
- current += ch;
219
- }
220
- continue;
221
- }
222
-
223
- if (ch === "'" || ch === '"') {
224
- quote = ch;
225
- continue;
226
- }
227
-
228
- if (/\s/.test(ch)) {
229
- if (current) {
230
- args.push(current);
231
- current = "";
232
- }
233
- continue;
234
- }
235
-
236
- current += ch;
237
- }
238
-
239
- if (escaping) current += "\\";
240
- if (current) args.push(current);
241
- return args;
242
- }
243
-
244
- function quoteWindowsArg(value: string): string {
245
- if (value.length === 0) return '""';
246
- if (!/[\s"]/u.test(value)) return value;
247
- return `"${value.replace(/"/g, '\\"')}"`;
248
- }
249
-
250
- function execErrorOutput(err: unknown): string {
251
- const e = err as { stdout?: Buffer | string; stderr?: Buffer | string };
252
- const out = e.stdout?.toString() ?? "";
253
- const errStr = e.stderr?.toString() ?? "";
254
- return [out, errStr].filter(Boolean).join("\n").trim();
255
- }
256
-
257
- export function execOpenclaw(args: string): Buffer {
258
- let bin = resolveOpenclawBin();
259
- const argv = splitShellArgs(args);
260
- try {
261
- if (IS_WINDOWS) {
262
- bin = normalizeResolvedOpenclawBin(bin) ?? bin;
263
- }
264
- if (IS_WINDOWS && !/\.exe$/i.test(bin)) {
265
- const command = [quoteWindowsArg(bin), ...argv.map(quoteWindowsArg)].join(" ");
266
- return execFileSync(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], { stdio: "pipe", env: SUBPROCESS_ENV });
267
- }
268
- return execFileSync(bin, argv, { stdio: "pipe", env: SUBPROCESS_ENV });
269
- } catch (error) {
270
- const output = execErrorOutput(error);
271
- const shouldRetry =
272
- bin === "openclaw" ||
273
- /command not found|ENOENT|not recognized/i.test(output);
274
-
275
- if (!shouldRetry) {
276
- throw error;
277
- }
278
-
279
- bin = resolveOpenclawBin(true);
280
- if (IS_WINDOWS) {
281
- bin = normalizeResolvedOpenclawBin(bin) ?? bin;
282
- }
283
- if (IS_WINDOWS && !/\.exe$/i.test(bin)) {
284
- const command = [quoteWindowsArg(bin), ...argv.map(quoteWindowsArg)].join(" ");
285
- return execFileSync(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], { stdio: "pipe", env: SUBPROCESS_ENV });
286
- }
287
- return execFileSync(bin, argv, { stdio: "pipe", env: SUBPROCESS_ENV });
288
- }
289
- }
290
-
291
- const DEFAULT_OPENCLAW_CONFIG_PATH = join(HOME_DIR, ".openclaw", "openclaw.json");
292
-
293
- function resolveEnvPlaceholder(name: string): string | null {
294
- if (name === "OPENCLAW_HOME") {
295
- return process.env.OPENCLAW_HOME ?? HOME_DIR;
296
- }
297
-
298
- if (name === "HOME") {
299
- return process.env.HOME ?? HOME_DIR;
300
- }
301
-
302
- if (name === "USERPROFILE") {
303
- return process.env.USERPROFILE ?? HOME_DIR;
304
- }
305
-
306
- return process.env[name] ?? null;
307
- }
308
-
309
- function expandEnvVars(value: string): string {
310
- return value
311
- .replace(/\$\{([^}]+)\}/g, (match, name) => resolveEnvPlaceholder(name) ?? match)
312
- .replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (match, name) => resolveEnvPlaceholder(name) ?? match)
313
- .replace(/%([^%]+)%/g, (match, name) => resolveEnvPlaceholder(name) ?? match);
314
- }
315
-
316
- function normalizeOpenclawPath(value: string): string {
317
- const trimmed = expandEnvVars(value.trim().replace(/^['"]|['"]$/g, ""));
318
- if (trimmed === "~") return HOME_DIR;
319
- if (trimmed.startsWith("~/")) {
320
- return join(HOME_DIR, trimmed.slice(2));
321
- }
322
- return trimmed;
323
- }
324
-
325
- export function resolveOpenclawConfigPath(forceRefresh = false): string {
326
- if (!forceRefresh && cachedOpenclawConfigPath && existsSync(cachedOpenclawConfigPath)) {
327
- return cachedOpenclawConfigPath;
328
- }
329
-
330
- try {
331
- const output = execOpenclaw("config file").toString("utf8").trim();
332
- const resolved = output
333
- .split("\n")
334
- .map(line => line.trim())
335
- .find(line => line.endsWith(".json"));
336
-
337
- if (resolved) {
338
- const normalized = normalizeOpenclawPath(resolved);
339
- if (!/[$%]/.test(normalized)) {
340
- cachedOpenclawConfigPath = normalized;
341
- return normalized;
342
- }
343
- }
344
- } catch {
345
- // Fall back to the legacy path search below.
346
- }
347
-
348
- cachedOpenclawConfigPath = DEFAULT_OPENCLAW_CONFIG_PATH;
349
- return DEFAULT_OPENCLAW_CONFIG_PATH;
350
- }
351
-
352
- export function resolveOpenclawDir(forceRefresh = false): string {
353
- return dirname(resolveOpenclawConfigPath(forceRefresh));
354
- }
@@ -1,128 +0,0 @@
1
- import { hostname } from "os";
2
- import { execSync } from "child_process";
3
- import { getManagedServiceInstallHint, getServicePlatform, supportsManagedServiceInstall } from "../platform/service-manager.js";
4
-
5
- function sanitizeDisplayName(name: string): string {
6
- // Replace smart quotes and other problematic characters with regular ones
7
- return name
8
- .replace(/[\u2018\u2019\u201C\u201D]/g, "'") // Smart quotes → regular quotes
9
- .replace(/[\u2013\u2014]/g, "-") // En/em dashes → regular dash
10
- .replace(/[\u00A0]/g, " ") // Non-breaking space → regular space
11
- .replace(/[^\x20-\x7E]/g, ""); // Remove any other non-ASCII characters
12
- }
13
-
14
- function getDisplayName(): string {
15
- if (getServicePlatform() !== "macos") {
16
- return hostname();
17
- }
18
- try {
19
- const raw = execSync("scutil --get ComputerName", { encoding: "utf8" }).trim();
20
- return sanitizeDisplayName(raw);
21
- } catch {
22
- return hostname();
23
- }
24
- }
25
- import { configExists, readConfig, writeConfig } from "../config/config.js";
26
- import { installCommand } from "./install.js";
27
- import qrcodeTerminal from "qrcode-terminal";
28
- import { t } from "../i18n/index.js";
29
- import { DEFAULT_RELAY_SERVER } from "../generated/build-config.js";
30
-
31
- interface PairOptions {
32
- server?: string;
33
- name?: string;
34
- codeOnly?: boolean;
35
- }
36
-
37
- export async function pairCommand(opts: PairOptions): Promise<void> {
38
- const relayServerUrl = opts.server ?? DEFAULT_RELAY_SERVER;
39
- const httpBase = relayServerUrl.replace(/^wss?/, "http");
40
-
41
- let gatewayId: string;
42
- let relaySecret: string;
43
- let accessCode: string;
44
- let displayName: string;
45
-
46
- if (configExists()) {
47
- const config = readConfig();
48
- gatewayId = config.gatewayId;
49
- relaySecret = config.relaySecret;
50
- displayName = opts.name ? sanitizeDisplayName(opts.name) : config.displayName;
51
-
52
- console.log(t("pair.alreadyRegistered", gatewayId));
53
-
54
- const res = await fetch(`${httpBase}/api/relay/accesscode`, {
55
- method: "POST",
56
- headers: { "Content-Type": "application/json" },
57
- body: JSON.stringify({ gatewayId, relaySecret }),
58
- });
59
-
60
- if (!res.ok) {
61
- const body = await res.text();
62
- if (res.status === 401) {
63
- throw new Error(t("pair.invalidCredentials"));
64
- }
65
- throw new Error(t("pair.refreshFailed", String(res.status), body));
66
- }
67
-
68
- const data = (await res.json()) as { accessCode: string };
69
- accessCode = data.accessCode;
70
-
71
- writeConfig({ ...config, relayServerUrl, displayName });
72
- } else {
73
- displayName = opts.name ? sanitizeDisplayName(opts.name) : getDisplayName();
74
- console.log(t("pair.registering"));
75
-
76
- const res = await fetch(`${httpBase}/api/relay/register`, {
77
- method: "POST",
78
- headers: { "Content-Type": "application/json" },
79
- body: JSON.stringify({ displayName }),
80
- });
81
-
82
- if (!res.ok) {
83
- const body = await res.text();
84
- throw new Error(t("pair.registrationFailed", String(res.status), body));
85
- }
86
-
87
- const data = (await res.json()) as {
88
- gatewayId: string;
89
- relaySecret: string;
90
- accessCode: string;
91
- };
92
-
93
- gatewayId = data.gatewayId;
94
- relaySecret = data.relaySecret;
95
- accessCode = data.accessCode;
96
-
97
- writeConfig({ relayServerUrl, gatewayId, relaySecret, displayName });
98
-
99
- console.log(t("pair.registered", gatewayId));
100
- }
101
-
102
- const qrPayload = JSON.stringify({
103
- version: 1,
104
- server: httpBase,
105
- gatewayId,
106
- accessCode,
107
- displayName,
108
- });
109
-
110
- if (opts.codeOnly) {
111
- console.log(accessCode);
112
- } else {
113
- console.log(t("pair.scanQR"));
114
- qrcodeTerminal.generate(qrPayload, { small: true });
115
- console.log(t("pair.accessCode", accessCode));
116
- }
117
-
118
- if ((getServicePlatform() === "linux" || getServicePlatform() === "windows") && !supportsManagedServiceInstall()) {
119
- console.log(t("pair.installGlobalHint"));
120
- const hint = getManagedServiceInstallHint();
121
- if (hint) {
122
- console.log(t("install.startManually", hint));
123
- }
124
- }
125
-
126
- console.log(t("pair.installingService"));
127
- installCommand();
128
- }