@shadowob/connector 1.1.3-dev.271 → 1.1.4

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 CHANGED
@@ -1,11 +1,469 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { spawnSync } from "child_process";
5
- import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
+ import { spawnSync as spawnSync2 } from "child_process";
5
+ import { cpSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
6
+ import { homedir as homedir2 } from "os";
7
+ import { dirname as dirname2, resolve as resolve2 } from "path";
8
+ import { fileURLToPath } from "url";
9
+
10
+ // src/cc-connect-installer.ts
11
+ import { execFileSync, spawnSync } from "child_process";
12
+ import { createHash } from "crypto";
13
+ import {
14
+ chmodSync,
15
+ existsSync,
16
+ mkdirSync,
17
+ readdirSync,
18
+ renameSync,
19
+ rmSync,
20
+ writeFileSync
21
+ } from "fs";
22
+ import { get as httpGet } from "http";
23
+ import { get as httpsGet } from "https";
6
24
  import { homedir } from "os";
7
25
  import { dirname, resolve } from "path";
8
- import { fileURLToPath } from "url";
26
+
27
+ // src/cc-connect-fork.ts
28
+ var CC_CONNECT_FORK_REPO = "buggyblues/cc-connect";
29
+ var CC_CONNECT_FORK_REF = "63b5d59127b3004bc7002f2d51892b1f2a91ea83";
30
+ var CC_CONNECT_FORK_SHORT_REF = CC_CONNECT_FORK_REF.slice(0, 7);
31
+ var CC_CONNECT_FORK_PACKAGE_VERSION = "1.3.3-beta.5";
32
+ var CC_CONNECT_FORK_DOCS_URL = `https://github.com/${CC_CONNECT_FORK_REPO}/blob/main/docs/shadowob.md`;
33
+
34
+ // src/cc-connect-installer.ts
35
+ var NAME = "cc-connect";
36
+ var RELEASE_ARCHIVE_SHA256 = {
37
+ "cc-connect-v1.3.3-beta.5-darwin-amd64.tar.gz": "71677cd565f6ea79e186ffc1b842e4548faa3baeb2e3dd2ced25ab2e95c416a3",
38
+ "cc-connect-v1.3.3-beta.5-darwin-arm64.tar.gz": "c1fc5a3d4cfe6db97a5d864ea844dbfd86345b89cea3d89871330568e6eb43cf",
39
+ "cc-connect-v1.3.3-beta.5-linux-amd64.tar.gz": "812484d19733044c8c5d67d997a921e351e2ae4e9a200ce5ae258ab338400bc1",
40
+ "cc-connect-v1.3.3-beta.5-linux-arm64.tar.gz": "290110a60e905f5e25f6133f52c1b3988ab6aeff1b1199318398cfef06f81e9a",
41
+ "cc-connect-v1.3.3-beta.5-windows-amd64.zip": "a90f8a669a48412fc5896613173b5e9b3dc5fdebdee731b6f6b9d4625a200952",
42
+ "cc-connect-v1.3.3-beta.5-windows-arm64.zip": "3c5ff24769d95c42b95a717949dc6369169c029e1110ab4bdd76d12e0043d283"
43
+ };
44
+ var PLATFORM_MAP = {
45
+ darwin: "darwin",
46
+ linux: "linux",
47
+ win32: "windows"
48
+ };
49
+ var ARCH_MAP = {
50
+ x64: "amd64",
51
+ arm64: "arm64"
52
+ };
53
+ function log(options, message) {
54
+ options.log?.(message);
55
+ }
56
+ function quoteArg(value) {
57
+ return /^[A-Za-z0-9_./:@=-]+$/.test(value) ? value : JSON.stringify(value);
58
+ }
59
+ function expandHome(value) {
60
+ return value.startsWith("~/") ? resolve(homedir(), value.slice(2)) : resolve(value);
61
+ }
62
+ function installRoot() {
63
+ const override = process.env.SHADOW_CC_CONNECT_HOME?.trim();
64
+ return override ? expandHome(override) : resolve(homedir(), ".shadowob/connector/cc-connect");
65
+ }
66
+ function binaryName() {
67
+ return process.platform === "win32" ? `${NAME}.exe` : NAME;
68
+ }
69
+ function cachedBinaryPath() {
70
+ return resolve(installRoot(), CC_CONNECT_FORK_SHORT_REF, "bin", binaryName());
71
+ }
72
+ function runCommand(command, args, options) {
73
+ const rendered = [command, ...args].map(quoteArg).join(" ");
74
+ if (options.dryRun) {
75
+ console.log(`[dry-run] ${rendered}`);
76
+ return;
77
+ }
78
+ const result = spawnSync(command, args, {
79
+ cwd: options.cwd,
80
+ env: options.env,
81
+ stdio: "inherit"
82
+ });
83
+ if (result.status !== 0) {
84
+ throw new Error(`Command failed with exit code ${result.status ?? "unknown"}: ${rendered}`);
85
+ }
86
+ }
87
+ function platformInfo() {
88
+ const platform = PLATFORM_MAP[process.platform];
89
+ const arch = ARCH_MAP[process.arch];
90
+ if (!platform || !arch) {
91
+ throw new Error(
92
+ `Unsupported cc-connect platform: ${process.platform}/${process.arch}. Supported: linux/darwin/windows x64/arm64`
93
+ );
94
+ }
95
+ return { platform, arch, ext: platform === "windows" ? ".zip" : ".tar.gz" };
96
+ }
97
+ function fetchBuffer(url, redirects = 5) {
98
+ return new Promise((resolvePromise, reject) => {
99
+ if (redirects <= 0) {
100
+ reject(new Error(`Too many redirects for ${url}`));
101
+ return;
102
+ }
103
+ const client = url.startsWith("https:") ? httpsGet : httpGet;
104
+ const request = client(url, { headers: { "User-Agent": "shadowob-connector" } }, (response) => {
105
+ const location = response.headers.location;
106
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && location) {
107
+ response.resume();
108
+ const next = new URL(location, url).toString();
109
+ resolvePromise(fetchBuffer(next, redirects - 1));
110
+ return;
111
+ }
112
+ if (response.statusCode !== 200) {
113
+ response.resume();
114
+ reject(new Error(`HTTP ${response.statusCode ?? "unknown"} for ${url}`));
115
+ return;
116
+ }
117
+ const chunks = [];
118
+ response.on("data", (chunk) => chunks.push(chunk));
119
+ response.on("end", () => resolvePromise(Buffer.concat(chunks)));
120
+ response.on("error", reject);
121
+ });
122
+ request.on("error", reject);
123
+ });
124
+ }
125
+ function findExtractedBinary(dir) {
126
+ const expected = binaryName();
127
+ const entries = readdirSync(dir);
128
+ if (entries.includes(expected)) return resolve(dir, expected);
129
+ return entries.filter((entry) => entry.startsWith(NAME)).map((entry) => resolve(dir, entry)).find(
130
+ (entry) => process.platform === "win32" ? entry.endsWith(".exe") : !entry.endsWith(".gz")
131
+ );
132
+ }
133
+ function extractReleaseArchive(archivePath, binDir, ext) {
134
+ if (ext === ".tar.gz") {
135
+ runCommand("tar", ["xzf", archivePath, "-C", binDir], { dryRun: false });
136
+ } else {
137
+ const unzip = spawnSync("unzip", ["-o", archivePath, "-d", binDir], { stdio: "inherit" });
138
+ if (unzip.status !== 0) {
139
+ runCommand("powershell", ["-Command", `Expand-Archive -Force '${archivePath}' '${binDir}'`], {
140
+ dryRun: false
141
+ });
142
+ }
143
+ }
144
+ const extracted = findExtractedBinary(binDir);
145
+ const target = resolve(binDir, binaryName());
146
+ if (!extracted) throw new Error("cc-connect release archive did not contain a binary");
147
+ if (extracted !== target) renameSync(extracted, target);
148
+ }
149
+ function verifyReleaseChecksum(filename, data) {
150
+ const expected = RELEASE_ARCHIVE_SHA256[filename];
151
+ if (!expected) throw new Error(`No pinned SHA-256 for ${filename}`);
152
+ const actual = createHash("sha256").update(data).digest("hex");
153
+ if (actual !== expected) {
154
+ throw new Error(`SHA-256 mismatch for ${filename}: expected ${expected}, got ${actual}`);
155
+ }
156
+ }
157
+ async function installFromRelease(binaryPath, options) {
158
+ const { platform, arch, ext } = platformInfo();
159
+ const version = `v${CC_CONNECT_FORK_PACKAGE_VERSION}`;
160
+ const filename = `${NAME}-${version}-${platform}-${arch}${ext}`;
161
+ const url = `https://github.com/${CC_CONNECT_FORK_REPO}/releases/download/${version}/${filename}`;
162
+ const binDir = dirname(binaryPath);
163
+ const archivePath = resolve(binDir, `_release${ext}`);
164
+ log(options, `[cc-connect] Trying fork release asset ${url}`);
165
+ try {
166
+ const data = await fetchBuffer(url);
167
+ verifyReleaseChecksum(filename, data);
168
+ mkdirSync(binDir, { recursive: true });
169
+ writeFileSync(archivePath, data);
170
+ extractReleaseArchive(archivePath, binDir, ext);
171
+ rmSync(archivePath, { force: true });
172
+ if (process.platform !== "win32") chmodSync(binaryPath, 493);
173
+ return true;
174
+ } catch (error) {
175
+ rmSync(archivePath, { force: true });
176
+ log(
177
+ options,
178
+ `[cc-connect] Fork release asset unavailable (${error instanceof Error ? error.message : String(error)}); building from source`
179
+ );
180
+ return false;
181
+ }
182
+ }
183
+ async function ensureSourceArchive(options) {
184
+ const sourceDir = resolve(installRoot(), CC_CONNECT_FORK_SHORT_REF, "source");
185
+ if (existsSync(resolve(sourceDir, "go.mod"))) return sourceDir;
186
+ if (existsSync(sourceDir)) rmSync(sourceDir, { recursive: true, force: true });
187
+ const parent = dirname(sourceDir);
188
+ const extractDir = resolve(parent, `_source-${process.pid}-${Date.now()}`);
189
+ const archivePath = resolve(parent, `${CC_CONNECT_FORK_SHORT_REF}.tar.gz`);
190
+ const sourceUrl = `https://github.com/${CC_CONNECT_FORK_REPO}/archive/${CC_CONNECT_FORK_REF}.tar.gz`;
191
+ log(options, `[cc-connect] Pulling fork source ${sourceUrl}`);
192
+ mkdirSync(parent, { recursive: true });
193
+ const data = await fetchBuffer(sourceUrl);
194
+ writeFileSync(archivePath, data);
195
+ mkdirSync(extractDir, { recursive: true });
196
+ try {
197
+ runCommand("tar", ["xzf", archivePath, "-C", extractDir], { dryRun: false });
198
+ const extracted = readdirSync(extractDir, { withFileTypes: true }).find(
199
+ (entry) => entry.isDirectory()
200
+ );
201
+ if (!extracted) throw new Error("cc-connect source archive did not contain a directory");
202
+ mkdirSync(dirname(sourceDir), { recursive: true });
203
+ renameSync(resolve(extractDir, extracted.name), sourceDir);
204
+ } finally {
205
+ rmSync(archivePath, { force: true });
206
+ rmSync(extractDir, { recursive: true, force: true });
207
+ }
208
+ return sourceDir;
209
+ }
210
+ async function buildFromSource(binaryPath, options) {
211
+ const sourceDir = await ensureSourceArchive(options);
212
+ const buildTime = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
213
+ const ldflags = [
214
+ "-s",
215
+ "-w",
216
+ "-X",
217
+ "main.version=dev",
218
+ "-X",
219
+ `main.commit=${CC_CONNECT_FORK_SHORT_REF}`,
220
+ "-X",
221
+ `main.buildTime=${buildTime}`
222
+ ].join(" ");
223
+ mkdirSync(dirname(binaryPath), { recursive: true });
224
+ runCommand(
225
+ "go",
226
+ ["build", "-tags", "no_web", "-ldflags", ldflags, "-o", binaryPath, "./cmd/cc-connect"],
227
+ {
228
+ cwd: sourceDir,
229
+ dryRun: false,
230
+ env: goBuildEnv()
231
+ }
232
+ );
233
+ if (process.platform !== "win32") chmodSync(binaryPath, 493);
234
+ }
235
+ function goBuildEnv() {
236
+ return {
237
+ ...process.env,
238
+ CGO_ENABLED: "0",
239
+ GOPROXY: process.env.SHADOW_CC_CONNECT_GOPROXY?.trim() || "https://proxy.golang.org,direct",
240
+ GOSUMDB: process.env.SHADOW_CC_CONNECT_GOSUMDB?.trim() || process.env.GOSUMDB || "sum.golang.org"
241
+ };
242
+ }
243
+ function binaryLooksUsable(path) {
244
+ if (!existsSync(path)) return false;
245
+ try {
246
+ const out = execFileSync(path, ["--version"], { encoding: "utf8", timeout: 5e3 });
247
+ return out.includes(CC_CONNECT_FORK_SHORT_REF) || out.includes(CC_CONNECT_FORK_PACKAGE_VERSION);
248
+ } catch {
249
+ return false;
250
+ }
251
+ }
252
+ async function ensureCcConnectFork(options) {
253
+ const override = process.env.SHADOW_CC_CONNECT_BIN?.trim();
254
+ if (override) {
255
+ const binaryPath2 = expandHome(override);
256
+ if (!existsSync(binaryPath2))
257
+ throw new Error(`SHADOW_CC_CONNECT_BIN does not exist: ${binaryPath2}`);
258
+ return { binaryPath: binaryPath2, source: "env" };
259
+ }
260
+ const binaryPath = cachedBinaryPath();
261
+ if (binaryLooksUsable(binaryPath)) {
262
+ return { binaryPath, source: "cache" };
263
+ }
264
+ if (options.dryRun) {
265
+ console.log(
266
+ `[dry-run] install ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF} -> ${binaryPath}`
267
+ );
268
+ return { binaryPath, source: "source" };
269
+ }
270
+ mkdirSync(dirname(binaryPath), { recursive: true });
271
+ const fromRelease = await installFromRelease(binaryPath, options);
272
+ if (!fromRelease) await buildFromSource(binaryPath, options);
273
+ if (!binaryLooksUsable(binaryPath)) {
274
+ throw new Error(`Installed cc-connect binary did not pass version verification: ${binaryPath}`);
275
+ }
276
+ return { binaryPath, source: fromRelease ? "release" : "source" };
277
+ }
278
+
279
+ // src/config-writers.ts
280
+ import { parse as parseDotenv } from "dotenv";
281
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
282
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
283
+ var SHADOW_ENV_VALUES = {
284
+ SHADOW_ALLOW_ALL_USERS: "true",
285
+ SHADOW_HEARTBEAT_INTERVAL_SECONDS: "30",
286
+ SHADOW_SLASH_COMMANDS_JSON: "[]"
287
+ };
288
+ function isRecord(value) {
289
+ return !!value && typeof value === "object" && !Array.isArray(value);
290
+ }
291
+ function asRecord(value) {
292
+ return isRecord(value) ? { ...value } : {};
293
+ }
294
+ function uniqueStrings(values, required) {
295
+ return [
296
+ .../* @__PURE__ */ new Set([...values.filter((value) => typeof value === "string"), required])
297
+ ];
298
+ }
299
+ function ensureTrailingNewline(value) {
300
+ return value.endsWith("\n") ? value : `${value}
301
+ `;
302
+ }
303
+ function quoteEnv(value) {
304
+ return /^[A-Za-z0-9_./:@-]+$/.test(value) ? value : JSON.stringify(value);
305
+ }
306
+ function normalizeJsonRoot(existing, label) {
307
+ if (!existing.trim()) return {};
308
+ const parsed = JSON.parse(existing);
309
+ if (!isRecord(parsed)) {
310
+ throw new Error(`${label} config must be a JSON object`);
311
+ }
312
+ return { ...parsed };
313
+ }
314
+ function parseYamlRoot(existing, label) {
315
+ if (!existing.trim()) return {};
316
+ const parsed = parseYaml(existing);
317
+ if (parsed == null) return {};
318
+ if (!isRecord(parsed)) {
319
+ throw new Error(`${label} config must be a YAML object`);
320
+ }
321
+ return { ...parsed };
322
+ }
323
+ function parseTomlRoot(existing, label) {
324
+ if (!existing.trim()) return {};
325
+ const parsed = parseToml(existing);
326
+ if (!isRecord(parsed)) {
327
+ throw new Error(`${label} config must be a TOML table`);
328
+ }
329
+ return parsed;
330
+ }
331
+ function mergeEnvContent(existing, values) {
332
+ parseDotenv(existing);
333
+ const updates = {
334
+ SHADOW_BASE_URL: values.serverUrl,
335
+ SHADOW_TOKEN: values.token,
336
+ ...SHADOW_ENV_VALUES
337
+ };
338
+ const seen = /* @__PURE__ */ new Set();
339
+ const lines = existing.length > 0 ? existing.split(/\r?\n/) : [];
340
+ const next = [];
341
+ for (const line of lines) {
342
+ const match = line.match(/^(\s*(?:export\s+)?)((?:[A-Za-z_][A-Za-z0-9_]*))\s*=/);
343
+ const key = match?.[2];
344
+ if (!key || !(key in updates)) {
345
+ next.push(line);
346
+ continue;
347
+ }
348
+ if (seen.has(key)) continue;
349
+ seen.add(key);
350
+ next.push(`${match[1] ?? ""}${key}=${quoteEnv(updates[key] ?? "")}`);
351
+ }
352
+ for (const [key, value] of Object.entries(updates)) {
353
+ if (!seen.has(key)) next.push(`${key}=${quoteEnv(value)}`);
354
+ }
355
+ while (next.length > 0 && next[next.length - 1] === "") next.pop();
356
+ return ensureTrailingNewline(next.join("\n"));
357
+ }
358
+ function mergeOpenClawConfigContent(existing, values) {
359
+ const root = normalizeJsonRoot(existing, "OpenClaw");
360
+ const channels = asRecord(root.channels);
361
+ const legacyShadow = asRecord(channels["openclaw-shadowob"]);
362
+ const shadow = asRecord(channels.shadowob);
363
+ channels.shadowob = {
364
+ ...legacyShadow,
365
+ ...shadow,
366
+ token: values.token,
367
+ serverUrl: values.serverUrl
368
+ };
369
+ delete channels["openclaw-shadowob"];
370
+ root.channels = channels;
371
+ const plugins = asRecord(root.plugins);
372
+ plugins.enabled = plugins.enabled ?? true;
373
+ plugins.allow = uniqueStrings(
374
+ Array.isArray(plugins.allow) ? plugins.allow : [],
375
+ "openclaw-shadowob"
376
+ );
377
+ const entries = asRecord(plugins.entries);
378
+ entries["openclaw-shadowob"] = {
379
+ ...asRecord(entries["openclaw-shadowob"]),
380
+ enabled: true
381
+ };
382
+ plugins.entries = entries;
383
+ root.plugins = plugins;
384
+ return ensureTrailingNewline(JSON.stringify(root, null, 2));
385
+ }
386
+ function mergeHermesConfigContent(existing, values) {
387
+ const root = parseYamlRoot(existing, "Hermes");
388
+ const plugins = asRecord(root.plugins);
389
+ plugins.enabled = uniqueStrings(Array.isArray(plugins.enabled) ? plugins.enabled : [], "shadowob");
390
+ root.plugins = plugins;
391
+ const platforms = asRecord(root.platforms);
392
+ const shadowob = asRecord(platforms.shadowob);
393
+ const extra = asRecord(shadowob.extra);
394
+ platforms.shadowob = {
395
+ ...shadowob,
396
+ enabled: true,
397
+ token: values.token,
398
+ extra: {
399
+ mention_only: false,
400
+ rest_only: false,
401
+ catchup_minutes: 0,
402
+ download_media: true,
403
+ slash_commands: [],
404
+ ...extra,
405
+ base_url: values.serverUrl
406
+ }
407
+ };
408
+ root.platforms = platforms;
409
+ return ensureTrailingNewline(stringifyYaml(root));
410
+ }
411
+ function asTomlTable(value) {
412
+ return isRecord(value) && !Array.isArray(value) ? { ...value } : {};
413
+ }
414
+ function tomlArray(value) {
415
+ if (!Array.isArray(value)) return [];
416
+ const tables = [];
417
+ for (const item of value) {
418
+ if (isRecord(item)) tables.push({ ...item });
419
+ }
420
+ return tables;
421
+ }
422
+ function ccConnectProjectWorkDir(project) {
423
+ const agent = asTomlTable(project.agent);
424
+ const options = asTomlTable(agent.options);
425
+ return typeof options.work_dir === "string" ? options.work_dir : typeof project.work_dir === "string" ? project.work_dir : void 0;
426
+ }
427
+ function mergeCcConnectConfigContent(existing, values) {
428
+ const root = parseTomlRoot(existing, "cc-connect");
429
+ const projects = tomlArray(root.projects);
430
+ let project = projects.find((item) => item.name === values.projectName) ?? projects.find((item) => ccConnectProjectWorkDir(item) === values.workDir);
431
+ if (!project) {
432
+ project = {};
433
+ projects.push(project);
434
+ }
435
+ project.name = values.projectName;
436
+ delete project.work_dir;
437
+ delete project.agent_type;
438
+ const agent = asTomlTable(project.agent);
439
+ const agentOptions = asTomlTable(agent.options);
440
+ agent.type = values.agentType;
441
+ agent.options = {
442
+ ...agentOptions,
443
+ work_dir: values.workDir
444
+ };
445
+ project.agent = agent;
446
+ const platforms = tomlArray(project.platforms);
447
+ let shadowPlatform = platforms.find((item) => item.type === "shadowob");
448
+ if (!shadowPlatform) {
449
+ shadowPlatform = {};
450
+ platforms.push(shadowPlatform);
451
+ }
452
+ const options = asTomlTable(shadowPlatform.options);
453
+ shadowPlatform.type = "shadowob";
454
+ shadowPlatform.options = {
455
+ allow_from: "*",
456
+ listen_dms: true,
457
+ share_session_in_channel: false,
458
+ progress_style: "compact",
459
+ ...options,
460
+ token: values.token,
461
+ server_url: values.serverUrl
462
+ };
463
+ project.platforms = platforms;
464
+ root.projects = projects;
465
+ return ensureTrailingNewline(stringifyToml(root));
466
+ }
9
467
 
10
468
  // src/index.ts
11
469
  var DEFAULT_SERVER_URL = "https://shadowob.com";
@@ -94,7 +552,14 @@ function buildOpenClawPlan(input) {
94
552
  "typing",
95
553
  "activityStatus",
96
554
  "reactions",
97
- "editDelete"
555
+ "editDelete",
556
+ "statusChecks",
557
+ "usageCosts",
558
+ "multiAgentBinding",
559
+ "shadowCliLogin",
560
+ "notifications",
561
+ "officialSkills",
562
+ "cronTasks"
98
563
  ]
99
564
  };
100
565
  }
@@ -180,7 +645,12 @@ function buildHermesPlan(input) {
180
645
  "onlineStatus",
181
646
  "typing",
182
647
  "activityStatus",
183
- "cronDelivery"
648
+ "cronDelivery",
649
+ "statusChecks",
650
+ "usageCosts",
651
+ "shadowCliLogin",
652
+ "notifications",
653
+ "officialSkills"
184
654
  ]
185
655
  };
186
656
  }
@@ -195,8 +665,12 @@ function buildCcConnectPlan(input) {
195
665
  "",
196
666
  "[[projects]]",
197
667
  `name = "${projectName}"`,
668
+ "",
669
+ "[projects.agent]",
670
+ `type = "${agentType}"`,
671
+ "",
672
+ "[projects.agent.options]",
198
673
  `work_dir = "${workDir}"`,
199
- `agent_type = "${agentType}"`,
200
674
  "",
201
675
  "[[projects.platforms]]",
202
676
  'type = "shadowob"',
@@ -209,15 +683,6 @@ function buildCcConnectPlan(input) {
209
683
  "share_session_in_channel = false",
210
684
  'progress_style = "compact"'
211
685
  ].join("\n");
212
- const commands = [
213
- { label: "Install cc-connect", command: "npm install -g cc-connect" },
214
- { label: "Create config directory", command: "mkdir -p ~/.cc-connect" },
215
- {
216
- label: "Edit config",
217
- command: "$EDITOR ~/.cc-connect/config.toml"
218
- },
219
- { label: "Start cc-connect", command: "cc-connect" }
220
- ];
221
686
  const connectCommand = [
222
687
  "npx @shadowob/connector@latest connect",
223
688
  "--target cc-connect",
@@ -227,12 +692,26 @@ function buildCcConnectPlan(input) {
227
692
  `--project-name ${shellQuote(projectName)}`,
228
693
  `--agent-type ${shellQuote(agentType)}`
229
694
  ].join(" ");
695
+ const installCommand = `${connectCommand} --install`;
696
+ const startCommand = `${connectCommand} --install --start`;
697
+ const commands = [
698
+ {
699
+ label: "Install ShadowOB cc-connect fork",
700
+ command: installCommand
701
+ },
702
+ { label: "Create config directory", command: "mkdir -p ~/.cc-connect" },
703
+ {
704
+ label: "Edit config",
705
+ command: "$EDITOR ~/.cc-connect/config.toml"
706
+ },
707
+ { label: "Start ShadowOB cc-connect fork", command: startCommand }
708
+ ];
230
709
  return {
231
710
  target: "cc-connect",
232
711
  title: "cc-connect",
233
- summary: "Use cc-connect ShadowOB Socket.IO platform support with this Buddy token.",
234
- connectCommand,
235
- quickCommand: commands.map((item) => item.command).join(" && "),
712
+ summary: `Use ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF} with ShadowOB Socket.IO platform support for this Buddy token.`,
713
+ connectCommand: startCommand,
714
+ quickCommand: startCommand,
236
715
  commands,
237
716
  configBlocks: [{ label: "~/.cc-connect/config.toml", language: "toml", content: tomlConfig }],
238
717
  aiPrompt: [
@@ -243,9 +722,9 @@ function buildCcConnectPlan(input) {
243
722
  `Project work_dir: ${workDir}`,
244
723
  `Agent type: ${agentType}`,
245
724
  "",
246
- "Install cc-connect, add the TOML platform block, and start cc-connect."
725
+ `Install ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF}, add the TOML platform block, and start cc-connect.`
247
726
  ].join("\n"),
248
- docsUrl: "https://github.com/buggyblues/cc-connect/blob/main/docs/shadowob.md",
727
+ docsUrl: CC_CONNECT_FORK_DOCS_URL,
249
728
  capabilities: [
250
729
  "channelMessages",
251
730
  "dms",
@@ -255,7 +734,12 @@ function buildCcConnectPlan(input) {
255
734
  "slashCommands",
256
735
  "typing",
257
736
  "streamingPreviews",
258
- "forms"
737
+ "forms",
738
+ "statusChecks",
739
+ "usageCosts",
740
+ "multiAgentBinding",
741
+ "shadowCliLogin",
742
+ "notifications"
259
743
  ]
260
744
  };
261
745
  }
@@ -286,13 +770,14 @@ function usage() {
286
770
  " shadowob-connector connect --target <openclaw|hermes|cc-connect> --server-url <url> --token <token>",
287
771
  "",
288
772
  "Options:",
773
+ " --openclaw-config <path> OpenClaw JSON config, default $OPENCLAW_CONFIG or ~/.shadowob/openclaw.json",
289
774
  " --hermes-home <path> Hermes config directory, default $HERMES_HOME or ~/.hermes",
290
775
  " --work-dir <path> cc-connect project work directory",
291
776
  " --project-name <name> cc-connect project name",
292
777
  " --agent-type <type> cc-connect agent type, default codex",
293
778
  " --json Print the full plan as JSON",
294
779
  " --force Overwrite target config files when needed",
295
- " --install Install cc-connect when target is cc-connect",
780
+ " --install Install the ShadowOB cc-connect fork when target is cc-connect",
296
781
  " --no-install Skip Hermes dependency install and plugin enablement",
297
782
  " --start Start Hermes gateway or cc-connect after setup",
298
783
  " --dry-run Show what would be applied without changing files"
@@ -316,6 +801,7 @@ function parseArgs(args) {
316
801
  target,
317
802
  serverUrl: readOption(optionArgs, "--server-url") ?? "https://shadowob.com",
318
803
  token: readOption(optionArgs, "--token") ?? "",
804
+ openclawConfig: readOption(optionArgs, "--openclaw-config"),
319
805
  hermesHome: readOption(optionArgs, "--hermes-home"),
320
806
  workDir: readOption(optionArgs, "--work-dir"),
321
807
  projectName: readOption(optionArgs, "--project-name"),
@@ -349,81 +835,99 @@ function runShell(command, dryRun) {
349
835
  console.log(`[dry-run] ${command}`);
350
836
  return;
351
837
  }
352
- const result = spawnSync(command, { shell: true, stdio: "inherit" });
838
+ const result = spawnSync2(command, { shell: true, stdio: "inherit" });
353
839
  if (result.status !== 0) {
354
840
  throw new Error(`Command failed with exit code ${result.status ?? "unknown"}: ${command}`);
355
841
  }
356
842
  }
843
+ function runBinary(binaryPath, args, dryRun) {
844
+ const rendered = [binaryPath, ...args].map((arg) => /^[A-Za-z0-9_./:@=-]+$/.test(arg) ? arg : JSON.stringify(arg)).join(" ");
845
+ if (dryRun) {
846
+ console.log(`[dry-run] ${rendered}`);
847
+ return;
848
+ }
849
+ const result = spawnSync2(binaryPath, args, { stdio: "inherit" });
850
+ if (result.status !== 0) {
851
+ throw new Error(`Command failed with exit code ${result.status ?? "unknown"}: ${rendered}`);
852
+ }
853
+ }
357
854
  function writeFile(path, content, dryRun) {
358
855
  if (dryRun) {
359
856
  console.log(`[dry-run] write ${path}`);
360
857
  return;
361
858
  }
362
- mkdirSync(dirname(path), { recursive: true });
363
- writeFileSync(path, content.endsWith("\n") ? content : `${content}
859
+ mkdirSync2(dirname2(path), { recursive: true });
860
+ writeFileSync2(path, content.endsWith("\n") ? content : `${content}
364
861
  `);
365
862
  }
366
- function upsertManagedBlock(path, name, content, dryRun) {
367
- const begin = `# BEGIN ShadowOB ${name}`;
368
- const end = `# END ShadowOB ${name}`;
369
- const block = `${begin}
370
- ${content}
371
- ${end}`;
372
- const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
373
- const pattern = new RegExp(
374
- `${begin.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${end.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`
375
- );
376
- const next = existing.match(pattern) ? existing.replace(pattern, block) : [existing.trimEnd(), block].filter(Boolean).join("\n\n");
377
- writeFile(path, next, dryRun);
378
- }
379
863
  function packageRoot() {
380
- return resolve(dirname(fileURLToPath(import.meta.url)), "..");
864
+ return resolve2(dirname2(fileURLToPath(import.meta.url)), "..");
381
865
  }
382
- function expandHome(value) {
383
- return value.startsWith("~/") ? resolve(homedir(), value.slice(2)) : resolve(value);
866
+ function expandHome2(value) {
867
+ return value.startsWith("~/") ? resolve2(homedir2(), value.slice(2)) : resolve2(value);
868
+ }
869
+ function readExisting(path) {
870
+ return existsSync2(path) ? readFileSync(path, "utf8") : "";
871
+ }
872
+ function normalizeServerUrl2(value) {
873
+ const trimmed = value.trim() || "https://shadowob.com";
874
+ return trimmed.endsWith("/api") ? trimmed.slice(0, -4) : trimmed.replace(/\/$/, "");
384
875
  }
385
876
  function hermesPluginSource() {
386
877
  const candidates = [
387
- resolve(packageRoot(), "hermes-shadowob-plugin"),
388
- resolve(process.cwd(), "packages/connector/hermes-shadowob-plugin")
878
+ resolve2(packageRoot(), "hermes-shadowob-plugin"),
879
+ resolve2(process.cwd(), "packages/connector/hermes-shadowob-plugin")
389
880
  ];
390
- const found = candidates.find((candidate) => existsSync(candidate));
881
+ const found = candidates.find((candidate) => existsSync2(candidate));
391
882
  if (!found) throw new Error("Cannot find bundled hermes-shadowob-plugin directory");
392
883
  return found;
393
884
  }
394
885
  function applyOpenClaw(options) {
395
886
  const plan = createConnectorPlan(options);
396
- for (const step of plan.commands) {
397
- console.log(`Applying: ${step.label}`);
398
- runShell(step.command, options.dryRun);
887
+ const configPath = expandHome2(
888
+ options.openclawConfig ?? process.env.OPENCLAW_CONFIG ?? "~/.shadowob/openclaw.json"
889
+ );
890
+ console.log("Applying: Install plugin");
891
+ runShell("openclaw plugins install @shadowob/openclaw-shadowob", options.dryRun);
892
+ console.log(`Applying: Merge OpenClaw config ${configPath}`);
893
+ const next = mergeOpenClawConfigContent(readExisting(configPath), {
894
+ token: options.token,
895
+ serverUrl: normalizeServerUrl2(options.serverUrl)
896
+ });
897
+ writeFile(configPath, next, options.dryRun);
898
+ const restart = plan.commands.find((step) => step.label === "Restart gateway");
899
+ if (restart) {
900
+ console.log(`Applying: ${restart.label}`);
901
+ runShell(restart.command, options.dryRun);
399
902
  }
400
903
  }
401
904
  function applyHermes(options) {
402
905
  const plan = createConnectorPlan(options);
403
- const hermesDir = expandHome(options.hermesHome ?? process.env.HERMES_HOME ?? "~/.hermes");
404
- const pluginTarget = resolve(hermesDir, "plugins/shadowob");
405
- const envPath = resolve(hermesDir, ".env");
406
- const configPath = resolve(hermesDir, "config.yaml");
407
- const generatedConfigPath = resolve(hermesDir, "config.shadowob.yaml");
906
+ const hermesDir = expandHome2(options.hermesHome ?? process.env.HERMES_HOME ?? "~/.hermes");
907
+ const pluginTarget = resolve2(hermesDir, "plugins/shadowob");
908
+ const envPath = resolve2(hermesDir, ".env");
909
+ const configPath = resolve2(hermesDir, "config.yaml");
408
910
  const envBlock = plan.configBlocks.find((block) => block.label === "~/.hermes/.env");
409
- const yamlBlock = plan.configBlocks.find((block) => block.label === "~/.hermes/config.yaml");
410
- if (!envBlock || !yamlBlock) throw new Error("Hermes plan is missing config blocks");
911
+ if (!envBlock) throw new Error("Hermes plan is missing config blocks");
411
912
  if (options.dryRun) {
412
913
  console.log(`[dry-run] copy ${hermesPluginSource()} -> ${pluginTarget}`);
413
914
  } else {
414
- mkdirSync(resolve(hermesDir, "plugins"), { recursive: true });
915
+ mkdirSync2(resolve2(hermesDir, "plugins"), { recursive: true });
415
916
  cpSync(hermesPluginSource(), pluginTarget, { recursive: true, force: true });
416
917
  }
417
- upsertManagedBlock(envPath, "Hermes ShadowOB", envBlock.content, options.dryRun);
418
- if (!existsSync(configPath) || options.force) {
419
- writeFile(configPath, yamlBlock.content, options.dryRun);
420
- } else {
421
- writeFile(generatedConfigPath, yamlBlock.content, options.dryRun);
422
- console.log(`Existing Hermes config kept. Generated ShadowOB config: ${generatedConfigPath}`);
423
- }
918
+ const nextEnv = options.force ? envBlock.content : mergeEnvContent(readExisting(envPath), {
919
+ token: options.token,
920
+ serverUrl: normalizeServerUrl2(options.serverUrl)
921
+ });
922
+ writeFile(envPath, nextEnv, options.dryRun);
923
+ const nextConfig = mergeHermesConfigContent(options.force ? "" : readExisting(configPath), {
924
+ token: options.token,
925
+ serverUrl: normalizeServerUrl2(options.serverUrl)
926
+ });
927
+ writeFile(configPath, nextConfig, options.dryRun);
424
928
  if (options.install) {
425
929
  runShell(
426
- `python -m pip install -r "${resolve(pluginTarget, "requirements.txt")}"`,
930
+ `python -m pip install -r "${resolve2(pluginTarget, "requirements.txt")}"`,
427
931
  options.dryRun
428
932
  );
429
933
  runShell("hermes plugins enable shadowob", options.dryRun);
@@ -432,26 +936,33 @@ function applyHermes(options) {
432
936
  runShell("hermes gateway", options.dryRun);
433
937
  }
434
938
  }
435
- function applyCcConnect(options) {
939
+ async function applyCcConnect(options) {
436
940
  const plan = createConnectorPlan(options);
437
941
  const configBlock = plan.configBlocks.find((block) => block.label === "~/.cc-connect/config.toml");
438
942
  if (!configBlock) throw new Error("cc-connect plan is missing config block");
439
- const configPath = resolve(homedir(), ".cc-connect/config.toml");
440
- const generatedPath = resolve(homedir(), ".cc-connect/config.shadowob.toml");
441
- if (!existsSync(configPath) || options.force) {
442
- writeFile(configPath, configBlock.content, options.dryRun);
443
- } else {
444
- writeFile(generatedPath, configBlock.content, options.dryRun);
445
- console.log(`Existing cc-connect config kept. Generated ShadowOB config: ${generatedPath}`);
446
- }
447
- if (options.install) {
448
- runShell("npm install -g cc-connect", options.dryRun);
943
+ const configPath = resolve2(homedir2(), ".cc-connect/config.toml");
944
+ const nextConfig = options.force ? configBlock.content : mergeCcConnectConfigContent(readExisting(configPath), {
945
+ token: options.token,
946
+ serverUrl: normalizeServerUrl2(options.serverUrl),
947
+ projectName: options.projectName?.trim() || "shadow-buddy",
948
+ workDir: options.workDir?.trim() || ".",
949
+ agentType: options.agentType?.trim() || "codex"
950
+ });
951
+ writeFile(configPath, nextConfig, options.dryRun);
952
+ let binaryPath;
953
+ if (options.install || options.start) {
954
+ const installed = await ensureCcConnectFork({
955
+ dryRun: options.dryRun,
956
+ log: (message) => console.log(message)
957
+ });
958
+ binaryPath = installed.binaryPath;
959
+ console.log(`cc-connect binary: ${binaryPath}`);
449
960
  }
450
961
  if (options.start) {
451
- runShell("cc-connect", options.dryRun);
962
+ runBinary(binaryPath ?? "cc-connect", [], options.dryRun);
452
963
  }
453
964
  }
454
- function connect(options) {
965
+ async function connect(options) {
455
966
  if (options.target === "openclaw") {
456
967
  applyOpenClaw(options);
457
968
  return;
@@ -460,18 +971,19 @@ function connect(options) {
460
971
  applyHermes(options);
461
972
  return;
462
973
  }
463
- applyCcConnect(options);
974
+ await applyCcConnect(options);
464
975
  }
465
- try {
976
+ async function main() {
466
977
  const options = parseArgs(process.argv.slice(2));
467
978
  if (options.command === "connect") {
468
- connect(options);
979
+ await connect(options);
469
980
  } else {
470
981
  printPlan(options);
471
982
  }
472
- } catch (error) {
983
+ }
984
+ main().catch((error) => {
473
985
  console.error(error instanceof Error ? error.message : String(error));
474
986
  console.error("");
475
987
  console.error(usage());
476
988
  process.exit(1);
477
- }
989
+ });