@shadowob/connector 1.1.6 → 1.1.7

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
@@ -2,10 +2,12 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { spawnSync as spawnSync2 } from "child_process";
5
- import { cpSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
5
+ import { chmodSync as chmodSync2, cpSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
6
6
  import { homedir as homedir2 } from "os";
7
7
  import { dirname as dirname2, resolve as resolve2 } from "path";
8
8
  import { fileURLToPath } from "url";
9
+ import { parse as parseToml2 } from "smol-toml";
10
+ import { parse as parseYaml2 } from "yaml";
9
11
 
10
12
  // src/cc-connect-installer.ts
11
13
  import { execFileSync, spawnSync } from "child_process";
@@ -249,6 +251,15 @@ function binaryLooksUsable(path) {
249
251
  return false;
250
252
  }
251
253
  }
254
+ function getCcConnectBinaryStatus() {
255
+ const override = process.env.SHADOW_CC_CONNECT_BIN?.trim();
256
+ if (override) {
257
+ const binaryPath2 = expandHome(override);
258
+ return { binaryPath: binaryPath2, usable: binaryLooksUsable(binaryPath2), source: "env" };
259
+ }
260
+ const binaryPath = cachedBinaryPath();
261
+ return { binaryPath, usable: binaryLooksUsable(binaryPath), source: "cache" };
262
+ }
252
263
  async function ensureCcConnectFork(options) {
253
264
  const override = process.env.SHADOW_CC_CONNECT_BIN?.trim();
254
265
  if (override) {
@@ -522,7 +533,7 @@ function buildOpenClawPlan(input) {
522
533
  return {
523
534
  target: "openclaw",
524
535
  title: "OpenClaw",
525
- summary: "Install the Shadow channel plugin and bind this Buddy token to OpenClaw.",
536
+ summary: "Install the Shadow channel plugin, Shadow CLI bin/skills, and a Buddy CLI profile for OpenClaw.",
526
537
  connectCommand,
527
538
  quickCommand,
528
539
  commands,
@@ -533,6 +544,9 @@ function buildOpenClawPlan(input) {
533
544
  `Shadow server URL: ${serverUrl}`,
534
545
  `Buddy token: ${token}`,
535
546
  "",
547
+ `Preferred one-line setup: ${connectCommand}`,
548
+ "The connector installs/configures the Shadow CLI, official Shadow skill files, and the Buddy profile before applying the OpenClaw channel config.",
549
+ "",
536
550
  "Run these steps in order:",
537
551
  ...commands.map((item, index) => `${index + 1}. ${item.command}`),
538
552
  "",
@@ -617,7 +631,7 @@ function buildHermesPlan(input) {
617
631
  return {
618
632
  target: "hermes",
619
633
  title: "Hermes Agent",
620
- summary: "Install the ShadowOB Hermes platform plugin and run Hermes gateway for this Buddy.",
634
+ summary: "Install the ShadowOB Hermes platform plugin, Shadow CLI bin/skills, and a Buddy CLI profile.",
621
635
  connectCommand,
622
636
  quickCommand: commands.map((item) => item.command).join(" && "),
623
637
  commands,
@@ -631,7 +645,8 @@ function buildHermesPlan(input) {
631
645
  `Shadow server URL: ${serverUrl}`,
632
646
  `Buddy token: ${token}`,
633
647
  "",
634
- "Install the bundled ShadowOB platform plugin, write the environment values above, enable the plugin, then run hermes gateway. The plugin resolves the Buddy agent id and channel policy from Shadow at runtime."
648
+ `Preferred one-line setup: ${connectCommand}`,
649
+ "The connector installs/configures the Shadow CLI, official Shadow skill files, and the Buddy profile before writing Hermes config. The plugin resolves the Buddy agent id and channel policy from Shadow at runtime."
635
650
  ].join("\n"),
636
651
  docsUrl: "https://hermes-agent.nousresearch.com/docs/user-guide/messaging",
637
652
  capabilities: [
@@ -709,7 +724,7 @@ function buildCcConnectPlan(input) {
709
724
  return {
710
725
  target: "cc-connect",
711
726
  title: "cc-connect",
712
- summary: `Use ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF} with ShadowOB Socket.IO platform support for this Buddy token.`,
727
+ summary: `Use ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF} with ShadowOB Socket.IO support, Shadow CLI bin/skills, and a Buddy CLI profile.`,
713
728
  connectCommand: startCommand,
714
729
  quickCommand: startCommand,
715
730
  commands,
@@ -722,7 +737,8 @@ function buildCcConnectPlan(input) {
722
737
  `Project work_dir: ${workDir}`,
723
738
  `Agent type: ${agentType}`,
724
739
  "",
725
- `Install ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF}, add the TOML platform block, and start cc-connect.`
740
+ `Preferred one-line setup: ${startCommand}`,
741
+ `Install ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF}, install/configure the Shadow CLI and official Shadow skill files, add the TOML platform block, and start cc-connect.`
726
742
  ].join("\n"),
727
743
  docsUrl: CC_CONNECT_FORK_DOCS_URL,
728
744
  capabilities: [
@@ -752,6 +768,10 @@ function createConnectorPlan(input) {
752
768
 
753
769
  // src/cli.ts
754
770
  var TARGETS = /* @__PURE__ */ new Set(["openclaw", "hermes", "cc-connect"]);
771
+ var COMMANDS = /* @__PURE__ */ new Set(["plan", "connect", "update", "doctor", "fix", "status", "scan"]);
772
+ var ALL_TARGETS = ["openclaw", "hermes", "cc-connect"];
773
+ var SHADOW_CLI_PACKAGE = "@shadowob/cli@latest";
774
+ var SHADOW_CONNECTOR_PACKAGE = "@shadowob/connector@latest";
755
775
  function readOption(args, name) {
756
776
  const prefix = `${name}=`;
757
777
  const inline = args.find((arg) => arg.startsWith(prefix));
@@ -768,8 +788,14 @@ function usage() {
768
788
  "Usage:",
769
789
  " shadowob-connector plan --target <openclaw|hermes|cc-connect> --server-url <url> --token <token>",
770
790
  " shadowob-connector connect --target <openclaw|hermes|cc-connect> --server-url <url> --token <token>",
791
+ " shadowob-connector update --target <openclaw|hermes|cc-connect> --server-url <url> --token <token>",
792
+ " shadowob-connector fix --target <openclaw|hermes|cc-connect> --server-url <url> --token <token>",
793
+ " shadowob-connector scan [--target <openclaw|hermes|cc-connect>] [--server-url <url>] [--token <token>]",
794
+ " shadowob-connector doctor [--target <openclaw|hermes|cc-connect>]",
795
+ " shadowob-connector status [--target <openclaw|hermes|cc-connect>]",
771
796
  "",
772
797
  "Options:",
798
+ " --server-url <url> Shadow server URL, default https://shadowob.com",
773
799
  " --openclaw-config <path> OpenClaw JSON config, default $OPENCLAW_CONFIG or ~/.shadowob/openclaw.json",
774
800
  " --hermes-home <path> Hermes config directory, default $HERMES_HOME or ~/.hermes",
775
801
  " --work-dir <path> cc-connect project work directory",
@@ -777,25 +803,33 @@ function usage() {
777
803
  " --agent-type <type> cc-connect agent type, default codex",
778
804
  " --json Print the full plan as JSON",
779
805
  " --force Overwrite target config files when needed",
780
- " --install Install the ShadowOB cc-connect fork when target is cc-connect",
781
- " --no-install Skip Hermes dependency install and plugin enablement",
806
+ " --install Install connector runtime dependencies",
807
+ " --no-install Skip connector runtime dependency installation",
782
808
  " --start Start Hermes gateway or cc-connect after setup",
783
809
  " --dry-run Show what would be applied without changing files"
784
810
  ].join("\n");
785
811
  }
812
+ function requireTarget(options) {
813
+ if (!options.target) throw new Error("Missing or invalid --target");
814
+ return options.target;
815
+ }
786
816
  function parseArgs(args) {
787
817
  if (hasFlag(args, "--help") || hasFlag(args, "-h")) {
788
818
  console.log(usage());
789
819
  process.exit(0);
790
820
  }
791
821
  const commandArg = args[0];
792
- const command = commandArg === "connect" || commandArg === "plan" ? commandArg : "plan";
793
- const optionArgs = command === "plan" ? args.filter((arg) => arg !== "plan") : args.slice(1);
822
+ const hasCommand = commandArg ? COMMANDS.has(commandArg) : false;
823
+ const command = hasCommand ? commandArg : "plan";
824
+ const optionArgs = hasCommand ? args.slice(1) : args;
794
825
  const target = readOption(optionArgs, "--target");
795
- if (!target || !TARGETS.has(target)) {
826
+ if (target && !TARGETS.has(target)) {
827
+ throw new Error("Missing or invalid --target");
828
+ }
829
+ if (!target && command !== "doctor" && command !== "status" && command !== "scan") {
796
830
  throw new Error("Missing or invalid --target");
797
831
  }
798
- const install = target === "cc-connect" ? hasFlag(optionArgs, "--install") : !hasFlag(optionArgs, "--no-install");
832
+ const install = command === "fix" || command === "update" ? !hasFlag(optionArgs, "--no-install") : target === "cc-connect" ? hasFlag(optionArgs, "--install") : !hasFlag(optionArgs, "--no-install");
799
833
  return {
800
834
  command,
801
835
  target,
@@ -814,7 +848,8 @@ function parseArgs(args) {
814
848
  };
815
849
  }
816
850
  function printPlan(options) {
817
- const plan = createConnectorPlan(options);
851
+ const target = requireTarget(options);
852
+ const plan = createConnectorPlan({ ...options, target });
818
853
  if (options.json) {
819
854
  console.log(JSON.stringify(plan, null, 2));
820
855
  return;
@@ -873,6 +908,570 @@ function normalizeServerUrl2(value) {
873
908
  const trimmed = value.trim() || "https://shadowob.com";
874
909
  return trimmed.endsWith("/api") ? trimmed.slice(0, -4) : trimmed.replace(/\/$/, "");
875
910
  }
911
+ function shellQuote2(value) {
912
+ if (!value) return "''";
913
+ if (/^[A-Za-z0-9_./:@=-]+$/.test(value)) return value;
914
+ return `'${value.replace(/'/g, `'\\''`)}'`;
915
+ }
916
+ function tokenForCommand(options) {
917
+ return options.token.trim() || "<BUDDY_TOKEN>";
918
+ }
919
+ function connectorCommand(command, target, options, extras = []) {
920
+ const parts = ["shadowob-connector", command, "--target", target];
921
+ if (command !== "doctor" && command !== "status") {
922
+ parts.push(
923
+ "--server-url",
924
+ normalizeServerUrl2(options.serverUrl),
925
+ "--token",
926
+ tokenForCommand(options)
927
+ );
928
+ }
929
+ parts.push(...extras);
930
+ return parts.map(shellQuote2).join(" ");
931
+ }
932
+ function commandExists(command) {
933
+ const result = spawnSync2(command, ["--version"], { stdio: "ignore" });
934
+ return result.status === 0;
935
+ }
936
+ function writeExecutable(path, content, dryRun) {
937
+ writeFile(path, content, dryRun);
938
+ if (dryRun) return;
939
+ chmodSync2(path, 493);
940
+ }
941
+ function ensureNpxShim(options) {
942
+ if (commandExists(options.command)) return;
943
+ const localBin = resolve2(homedir2(), ".local/bin");
944
+ const target = resolve2(localBin, options.command);
945
+ const content = [
946
+ "#!/usr/bin/env sh",
947
+ `exec npx -y ${options.packageSpec} ${options.binaryName === options.command ? "" : options.binaryName} "$@"`,
948
+ ""
949
+ ].join("\n").replace(' "$@"', ' "$@"');
950
+ console.log(`Applying: Install ${options.command} shim ${target}`);
951
+ writeExecutable(target, content, options.dryRun);
952
+ const pathEntries = (process.env.PATH ?? "").split(":");
953
+ if (!pathEntries.includes(localBin)) {
954
+ console.log(`Note: add ${localBin} to PATH so agents can run ${options.command}`);
955
+ }
956
+ }
957
+ function shadowCliProfileName(options) {
958
+ return options.projectName?.trim() || "shadow-buddy";
959
+ }
960
+ function writeShadowCliProfile(options) {
961
+ const configPath = resolve2(homedir2(), ".shadowob/shadowob.config.json");
962
+ const current = (() => {
963
+ try {
964
+ return JSON.parse(readExisting(configPath));
965
+ } catch {
966
+ return {};
967
+ }
968
+ })();
969
+ const profileName = shadowCliProfileName(options);
970
+ const next = {
971
+ ...current,
972
+ profiles: {
973
+ ...current.profiles ?? {},
974
+ [profileName]: {
975
+ serverUrl: normalizeServerUrl2(options.serverUrl),
976
+ token: options.token
977
+ }
978
+ },
979
+ currentProfile: profileName
980
+ };
981
+ console.log(`Applying: Configure Shadow CLI profile ${profileName}`);
982
+ writeFile(configPath, JSON.stringify(next, null, 2), options.dryRun);
983
+ }
984
+ function shadowobSkillMarkdown() {
985
+ const candidates = [
986
+ resolve2(packageRoot(), "skills/shadowob/SKILL.md"),
987
+ resolve2(process.cwd(), "skills/shadowob-cli/SKILL.md"),
988
+ resolve2(process.cwd(), "packages/openclaw-shadowob/skills/shadowob/SKILL.md")
989
+ ];
990
+ let currentDir = packageRoot();
991
+ while (true) {
992
+ candidates.push(resolve2(currentDir, "skills/shadowob-cli/SKILL.md"));
993
+ const parentDir = dirname2(currentDir);
994
+ if (parentDir === currentDir) break;
995
+ currentDir = parentDir;
996
+ }
997
+ const found = candidates.find((candidate) => existsSync2(candidate));
998
+ if (!found) throw new Error("Cannot find bundled Shadow CLI skill");
999
+ return readFileSync(found, "utf8");
1000
+ }
1001
+ function shadowobSkillTargets(options) {
1002
+ const hermesDir = expandHome2(options.hermesHome ?? process.env.HERMES_HOME ?? "~/.hermes");
1003
+ return Array.from(
1004
+ /* @__PURE__ */ new Set([
1005
+ resolve2(homedir2(), ".shadowob/skills/shadowob/SKILL.md"),
1006
+ resolve2(homedir2(), ".agents/skills/shadowob/SKILL.md"),
1007
+ resolve2(homedir2(), ".codex/skills/shadowob/SKILL.md"),
1008
+ resolve2(homedir2(), ".claude/skills/shadowob/SKILL.md"),
1009
+ resolve2(homedir2(), ".gemini/skills/shadowob/SKILL.md"),
1010
+ resolve2(homedir2(), ".opencode/skills/shadowob/SKILL.md"),
1011
+ resolve2(homedir2(), ".openclaw/skills/shadowob/SKILL.md"),
1012
+ resolve2(hermesDir, "skills/shadowob/SKILL.md")
1013
+ ])
1014
+ );
1015
+ }
1016
+ function installShadowCliAndSkills(options) {
1017
+ ensureNpxShim({
1018
+ command: "shadowob",
1019
+ packageSpec: SHADOW_CLI_PACKAGE,
1020
+ binaryName: "shadowob",
1021
+ dryRun: options.dryRun
1022
+ });
1023
+ ensureNpxShim({
1024
+ command: "shadowob-connector",
1025
+ packageSpec: SHADOW_CONNECTOR_PACKAGE,
1026
+ binaryName: "shadowob-connector",
1027
+ dryRun: options.dryRun
1028
+ });
1029
+ const skill = shadowobSkillMarkdown();
1030
+ for (const target of shadowobSkillTargets(options)) {
1031
+ console.log(`Applying: Install Shadow skill ${target}`);
1032
+ writeFile(target, skill, options.dryRun);
1033
+ }
1034
+ writeShadowCliProfile(options);
1035
+ }
1036
+ function check(target, status, label, detail, fix) {
1037
+ return { target, status, label, detail, fix };
1038
+ }
1039
+ function selectedTargets(options) {
1040
+ return options.target ? [options.target] : [...ALL_TARGETS];
1041
+ }
1042
+ function parseJsonFile(path, label) {
1043
+ try {
1044
+ const content = readExisting(path);
1045
+ if (!content.trim()) return { error: `${label} config is empty` };
1046
+ const parsed = JSON.parse(content);
1047
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1048
+ return { error: `${label} config must be an object` };
1049
+ }
1050
+ return { value: parsed };
1051
+ } catch (error) {
1052
+ return { error: error instanceof Error ? error.message : String(error) };
1053
+ }
1054
+ }
1055
+ function asObject(value) {
1056
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
1057
+ }
1058
+ function diagnoseCommon(options) {
1059
+ const checks = [
1060
+ check(
1061
+ "common",
1062
+ commandExists("shadowob") ? "ok" : "warn",
1063
+ "Shadow CLI command",
1064
+ commandExists("shadowob") ? "shadowob is on PATH" : "shadowob is not on PATH",
1065
+ "Run fix/update to install the ~/.local/bin/shadowob shim."
1066
+ ),
1067
+ check(
1068
+ "common",
1069
+ commandExists("shadowob-connector") ? "ok" : "warn",
1070
+ "Connector command",
1071
+ commandExists("shadowob-connector") ? "shadowob-connector is on PATH" : "shadowob-connector is not on PATH",
1072
+ "Run fix/update to install the ~/.local/bin/shadowob-connector shim."
1073
+ )
1074
+ ];
1075
+ const profilePath = resolve2(homedir2(), ".shadowob/shadowob.config.json");
1076
+ if (!existsSync2(profilePath)) {
1077
+ checks.push(
1078
+ check(
1079
+ "common",
1080
+ "warn",
1081
+ "Shadow CLI profile",
1082
+ `${profilePath} does not exist`,
1083
+ "Run fix/update with --token to write the Buddy profile."
1084
+ )
1085
+ );
1086
+ } else {
1087
+ const parsed = parseJsonFile(profilePath, "Shadow CLI");
1088
+ const profiles = asObject(parsed.value?.profiles);
1089
+ const profileName = shadowCliProfileName(options);
1090
+ checks.push(
1091
+ check(
1092
+ "common",
1093
+ parsed.error ? "fail" : profiles[profileName] ? "ok" : "warn",
1094
+ "Shadow CLI profile",
1095
+ parsed.error ?? (profiles[profileName] ? `profile ${profileName} exists` : `profile ${profileName} is missing`),
1096
+ "Run fix/update with --token to write the Buddy profile."
1097
+ )
1098
+ );
1099
+ }
1100
+ const skillTargets = shadowobSkillTargets(options);
1101
+ const installed = skillTargets.filter((target) => existsSync2(target)).length;
1102
+ checks.push(
1103
+ check(
1104
+ "common",
1105
+ installed > 0 ? "ok" : "warn",
1106
+ "Shadow skill files",
1107
+ `${installed}/${skillTargets.length} common skill locations contain shadowob/SKILL.md`,
1108
+ "Run fix/update to install the official Shadow skill files."
1109
+ )
1110
+ );
1111
+ return checks;
1112
+ }
1113
+ function diagnoseOpenClaw(options) {
1114
+ const configPath = expandHome2(
1115
+ options.openclawConfig ?? process.env.OPENCLAW_CONFIG ?? "~/.shadowob/openclaw.json"
1116
+ );
1117
+ const checks = [
1118
+ check(
1119
+ "openclaw",
1120
+ commandExists("openclaw") ? "ok" : "warn",
1121
+ "OpenClaw command",
1122
+ commandExists("openclaw") ? "openclaw is on PATH" : "openclaw is not on PATH",
1123
+ "Install OpenClaw before starting the gateway."
1124
+ )
1125
+ ];
1126
+ if (!existsSync2(configPath)) {
1127
+ checks.push(
1128
+ check(
1129
+ "openclaw",
1130
+ "fail",
1131
+ "OpenClaw config",
1132
+ `${configPath} does not exist`,
1133
+ "Run fix/update."
1134
+ )
1135
+ );
1136
+ return checks;
1137
+ }
1138
+ const parsed = parseJsonFile(configPath, "OpenClaw");
1139
+ if (parsed.error) {
1140
+ checks.push(
1141
+ check(
1142
+ "openclaw",
1143
+ "fail",
1144
+ "OpenClaw config",
1145
+ parsed.error,
1146
+ "Fix the JSON or run fix/update with --force."
1147
+ )
1148
+ );
1149
+ return checks;
1150
+ }
1151
+ const root = parsed.value ?? {};
1152
+ const channels = asObject(root.channels);
1153
+ const shadow = asObject(channels.shadowob);
1154
+ const plugins = asObject(root.plugins);
1155
+ const pluginEntries = asObject(plugins.entries);
1156
+ checks.push(
1157
+ check(
1158
+ "openclaw",
1159
+ typeof shadow.token === "string" && shadow.token.length > 0 ? "ok" : "fail",
1160
+ "OpenClaw Shadow token",
1161
+ typeof shadow.token === "string" && shadow.token.length > 0 ? "channels.shadowob.token is set" : "channels.shadowob.token is missing",
1162
+ "Run fix/update with --token."
1163
+ ),
1164
+ check(
1165
+ "openclaw",
1166
+ typeof shadow.serverUrl === "string" && shadow.serverUrl.length > 0 ? "ok" : "fail",
1167
+ "OpenClaw Shadow server URL",
1168
+ typeof shadow.serverUrl === "string" && shadow.serverUrl.length > 0 ? `channels.shadowob.serverUrl=${shadow.serverUrl}` : "channels.shadowob.serverUrl is missing",
1169
+ "Run fix/update with --server-url."
1170
+ ),
1171
+ check(
1172
+ "openclaw",
1173
+ asObject(pluginEntries["openclaw-shadowob"]).enabled === true ? "ok" : "warn",
1174
+ "OpenClaw Shadow plugin entry",
1175
+ asObject(pluginEntries["openclaw-shadowob"]).enabled === true ? "openclaw-shadowob plugin entry is enabled" : "openclaw-shadowob plugin entry is missing or disabled",
1176
+ "Run fix/update."
1177
+ )
1178
+ );
1179
+ return checks;
1180
+ }
1181
+ function diagnoseHermes(options) {
1182
+ const hermesDir = expandHome2(options.hermesHome ?? process.env.HERMES_HOME ?? "~/.hermes");
1183
+ const pluginTarget = resolve2(hermesDir, "plugins/shadowob");
1184
+ const envPath = resolve2(hermesDir, ".env");
1185
+ const configPath = resolve2(hermesDir, "config.yaml");
1186
+ const checks = [
1187
+ check(
1188
+ "hermes",
1189
+ commandExists("hermes") ? "ok" : "warn",
1190
+ "Hermes command",
1191
+ commandExists("hermes") ? "hermes is on PATH" : "hermes is not on PATH",
1192
+ "Install Hermes before starting the gateway."
1193
+ ),
1194
+ check(
1195
+ "hermes",
1196
+ existsSync2(pluginTarget) ? "ok" : "fail",
1197
+ "Hermes Shadow plugin",
1198
+ existsSync2(pluginTarget) ? `${pluginTarget} exists` : `${pluginTarget} is missing`,
1199
+ "Run fix/update."
1200
+ )
1201
+ ];
1202
+ const env = readExisting(envPath);
1203
+ checks.push(
1204
+ check(
1205
+ "hermes",
1206
+ env.includes("SHADOW_TOKEN=") && env.includes("SHADOW_BASE_URL=") ? "ok" : "fail",
1207
+ "Hermes environment",
1208
+ existsSync2(envPath) ? "SHADOW_TOKEN and SHADOW_BASE_URL are present" : `${envPath} does not exist`,
1209
+ "Run fix/update with --token and --server-url."
1210
+ )
1211
+ );
1212
+ if (!existsSync2(configPath)) {
1213
+ checks.push(
1214
+ check("hermes", "fail", "Hermes config", `${configPath} does not exist`, "Run fix/update.")
1215
+ );
1216
+ return checks;
1217
+ }
1218
+ try {
1219
+ const parsed = parseYaml2(readExisting(configPath));
1220
+ const root = asObject(parsed);
1221
+ const shadow = asObject(asObject(root.platforms).shadowob);
1222
+ checks.push(
1223
+ check(
1224
+ "hermes",
1225
+ shadow.enabled === true && typeof shadow.token === "string" ? "ok" : "fail",
1226
+ "Hermes Shadow platform",
1227
+ shadow.enabled === true && typeof shadow.token === "string" ? "platforms.shadowob is enabled" : "platforms.shadowob is missing token or enabled=true",
1228
+ "Run fix/update."
1229
+ )
1230
+ );
1231
+ } catch (error) {
1232
+ checks.push(
1233
+ check(
1234
+ "hermes",
1235
+ "fail",
1236
+ "Hermes config",
1237
+ error instanceof Error ? error.message : String(error),
1238
+ "Fix the YAML or run fix/update with --force."
1239
+ )
1240
+ );
1241
+ }
1242
+ return checks;
1243
+ }
1244
+ function diagnoseCcConnect(options) {
1245
+ const configPath = resolve2(homedir2(), ".cc-connect/config.toml");
1246
+ const binary = getCcConnectBinaryStatus();
1247
+ const checks = [
1248
+ check(
1249
+ "cc-connect",
1250
+ binary.usable ? "ok" : "warn",
1251
+ "cc-connect Shadow fork",
1252
+ binary.usable ? `${binary.binaryPath} passes version check` : `${binary.binaryPath} is missing or does not match the pinned Shadow fork`,
1253
+ "Run fix/update with --install."
1254
+ )
1255
+ ];
1256
+ if (!existsSync2(configPath)) {
1257
+ checks.push(
1258
+ check(
1259
+ "cc-connect",
1260
+ "fail",
1261
+ "cc-connect config",
1262
+ `${configPath} does not exist`,
1263
+ "Run fix/update."
1264
+ )
1265
+ );
1266
+ return checks;
1267
+ }
1268
+ try {
1269
+ const root = parseToml2(readExisting(configPath));
1270
+ const projects = Array.isArray(root.projects) ? root.projects : [];
1271
+ const projectName = options.projectName?.trim() || "shadow-buddy";
1272
+ const workDir = options.workDir?.trim() || ".";
1273
+ const project = projects.find((item) => asObject(item).name === projectName) ?? projects.find((item) => asObject(asObject(asObject(item).agent).options).work_dir === workDir);
1274
+ const projectPlatforms = asObject(project).platforms;
1275
+ const platforms = Array.isArray(projectPlatforms) ? projectPlatforms : [];
1276
+ const shadow = platforms.find((item) => asObject(item).type === "shadowob");
1277
+ const shadowOptions = asObject(asObject(shadow).options);
1278
+ checks.push(
1279
+ check(
1280
+ "cc-connect",
1281
+ project ? "ok" : "fail",
1282
+ "cc-connect project",
1283
+ project ? `project ${projectName} is configured` : `project ${projectName} is missing`,
1284
+ "Run fix/update with --project-name and --work-dir."
1285
+ ),
1286
+ check(
1287
+ "cc-connect",
1288
+ typeof shadowOptions.token === "string" && typeof shadowOptions.server_url === "string" ? "ok" : "fail",
1289
+ "cc-connect Shadow platform",
1290
+ typeof shadowOptions.token === "string" && typeof shadowOptions.server_url === "string" ? "shadowob platform has token and server_url" : "shadowob platform is missing token or server_url",
1291
+ "Run fix/update with --token and --server-url."
1292
+ )
1293
+ );
1294
+ } catch (error) {
1295
+ checks.push(
1296
+ check(
1297
+ "cc-connect",
1298
+ "fail",
1299
+ "cc-connect config",
1300
+ error instanceof Error ? error.message : String(error),
1301
+ "Fix the TOML or run fix/update with --force."
1302
+ )
1303
+ );
1304
+ }
1305
+ return checks;
1306
+ }
1307
+ function diagnostics(options) {
1308
+ const checks = diagnoseCommon(options);
1309
+ for (const target of selectedTargets(options)) {
1310
+ if (target === "openclaw") checks.push(...diagnoseOpenClaw(options));
1311
+ if (target === "hermes") checks.push(...diagnoseHermes(options));
1312
+ if (target === "cc-connect") checks.push(...diagnoseCcConnect(options));
1313
+ }
1314
+ return checks;
1315
+ }
1316
+ function printDiagnostics(options, mode) {
1317
+ const checks = diagnostics(options);
1318
+ if (options.json) {
1319
+ console.log(
1320
+ JSON.stringify({ ok: !checks.some((item) => item.status === "fail"), checks }, null, 2)
1321
+ );
1322
+ return !checks.some((item) => item.status === "fail");
1323
+ }
1324
+ console.log(`# Connector ${mode}`);
1325
+ for (const item of checks) {
1326
+ const marker = item.status === "ok" ? "OK" : item.status === "warn" ? "WARN" : "FAIL";
1327
+ console.log(
1328
+ `[${marker}] ${item.target}: ${item.label}${item.detail ? ` - ${item.detail}` : ""}`
1329
+ );
1330
+ if (mode === "doctor" && item.status !== "ok" && item.fix) {
1331
+ console.log(` fix: ${item.fix}`);
1332
+ }
1333
+ }
1334
+ return !checks.some((item) => item.status === "fail");
1335
+ }
1336
+ function firstExistingPath(paths) {
1337
+ return paths.find((path) => existsSync2(path));
1338
+ }
1339
+ function openClawConfigCandidates(options) {
1340
+ return Array.from(
1341
+ new Set(
1342
+ [
1343
+ options.openclawConfig,
1344
+ process.env.OPENCLAW_CONFIG,
1345
+ process.env.OPENCLAW_CONFIG_PATH,
1346
+ "~/.shadowob/openclaw.json",
1347
+ "~/.openclaw/openclaw.json"
1348
+ ].filter((value) => !!value?.trim()).map(expandHome2)
1349
+ )
1350
+ );
1351
+ }
1352
+ function ccConnectScanExtras(options) {
1353
+ const configPath = resolve2(homedir2(), ".cc-connect/config.toml");
1354
+ const fallback = [
1355
+ "--work-dir",
1356
+ options.workDir?.trim() || ".",
1357
+ "--project-name",
1358
+ options.projectName?.trim() || "shadow-buddy",
1359
+ "--agent-type",
1360
+ options.agentType?.trim() || "codex"
1361
+ ];
1362
+ if (!existsSync2(configPath)) return fallback;
1363
+ try {
1364
+ const root = parseToml2(readExisting(configPath));
1365
+ const projects = Array.isArray(root.projects) ? root.projects : [];
1366
+ const configuredProject = projects.find((project2) => {
1367
+ const platformsValue = asObject(project2).platforms;
1368
+ const platforms = Array.isArray(platformsValue) ? platformsValue : [];
1369
+ return platforms.some((platform) => asObject(platform).type === "shadowob");
1370
+ }) ?? projects[0];
1371
+ const project = asObject(configuredProject);
1372
+ const agent = asObject(project.agent);
1373
+ const agentOptions = asObject(agent.options);
1374
+ return [
1375
+ "--work-dir",
1376
+ options.workDir?.trim() || (typeof agentOptions.work_dir === "string" ? agentOptions.work_dir : "."),
1377
+ "--project-name",
1378
+ options.projectName?.trim() || (typeof project.name === "string" ? project.name : "shadow-buddy"),
1379
+ "--agent-type",
1380
+ options.agentType?.trim() || (typeof agent.type === "string" ? agent.type : "codex")
1381
+ ];
1382
+ } catch {
1383
+ return fallback;
1384
+ }
1385
+ }
1386
+ function scanOpenClaw(options) {
1387
+ const configPath = firstExistingPath(openClawConfigCandidates(options));
1388
+ const evidence = [];
1389
+ if (commandExists("openclaw")) evidence.push("openclaw command is on PATH");
1390
+ if (configPath) evidence.push(`config found at ${configPath}`);
1391
+ const detected = evidence.length > 0;
1392
+ return {
1393
+ target: "openclaw",
1394
+ detected,
1395
+ evidence,
1396
+ configPath,
1397
+ connectCommand: connectorCommand("connect", "openclaw", options),
1398
+ updateCommand: connectorCommand("update", "openclaw", options),
1399
+ doctorCommand: connectorCommand("doctor", "openclaw", options),
1400
+ statusCommand: connectorCommand("status", "openclaw", options)
1401
+ };
1402
+ }
1403
+ function scanHermes(options) {
1404
+ const hermesDir = expandHome2(options.hermesHome ?? process.env.HERMES_HOME ?? "~/.hermes");
1405
+ const configPath = resolve2(hermesDir, "config.yaml");
1406
+ const evidence = [];
1407
+ if (commandExists("hermes")) evidence.push("hermes command is on PATH");
1408
+ if (existsSync2(configPath)) evidence.push(`config found at ${configPath}`);
1409
+ if (existsSync2(resolve2(hermesDir, "plugins/shadowob"))) {
1410
+ evidence.push(`shadowob plugin found under ${resolve2(hermesDir, "plugins/shadowob")}`);
1411
+ }
1412
+ return {
1413
+ target: "hermes",
1414
+ detected: evidence.length > 0,
1415
+ evidence,
1416
+ configPath: existsSync2(configPath) ? configPath : void 0,
1417
+ connectCommand: connectorCommand("connect", "hermes", options),
1418
+ updateCommand: connectorCommand("update", "hermes", options),
1419
+ doctorCommand: connectorCommand("doctor", "hermes", options),
1420
+ statusCommand: connectorCommand("status", "hermes", options)
1421
+ };
1422
+ }
1423
+ function scanCcConnect(options) {
1424
+ const configPath = resolve2(homedir2(), ".cc-connect/config.toml");
1425
+ const binary = getCcConnectBinaryStatus();
1426
+ const evidence = [];
1427
+ if (commandExists("cc-connect")) evidence.push("cc-connect command is on PATH");
1428
+ if (binary.usable) evidence.push(`Shadow fork binary found at ${binary.binaryPath}`);
1429
+ if (existsSync2(configPath)) evidence.push(`config found at ${configPath}`);
1430
+ const extras = ccConnectScanExtras(options);
1431
+ return {
1432
+ target: "cc-connect",
1433
+ detected: evidence.length > 0,
1434
+ evidence,
1435
+ configPath: existsSync2(configPath) ? configPath : void 0,
1436
+ connectCommand: connectorCommand("connect", "cc-connect", options, extras),
1437
+ updateCommand: connectorCommand("update", "cc-connect", options, extras),
1438
+ doctorCommand: connectorCommand("doctor", "cc-connect", options),
1439
+ statusCommand: connectorCommand("status", "cc-connect", options)
1440
+ };
1441
+ }
1442
+ function scanConnectors(options) {
1443
+ return selectedTargets(options).map((target) => {
1444
+ if (target === "openclaw") return scanOpenClaw(options);
1445
+ if (target === "hermes") return scanHermes(options);
1446
+ return scanCcConnect(options);
1447
+ });
1448
+ }
1449
+ function printScan(options) {
1450
+ const results = scanConnectors(options);
1451
+ if (options.json) {
1452
+ console.log(
1453
+ JSON.stringify({ serverUrl: normalizeServerUrl2(options.serverUrl), results }, null, 2)
1454
+ );
1455
+ return;
1456
+ }
1457
+ console.log("# Connector scan");
1458
+ console.log(`Shadow server URL: ${normalizeServerUrl2(options.serverUrl)}`);
1459
+ console.log(`Buddy token: ${options.token.trim() ? "provided" : "<BUDDY_TOKEN>"}`);
1460
+ for (const result of results) {
1461
+ console.log("");
1462
+ console.log(`## ${result.target}`);
1463
+ console.log(`Detected: ${result.detected ? "yes" : "no"}`);
1464
+ if (result.evidence.length > 0) {
1465
+ console.log("Evidence:");
1466
+ for (const item of result.evidence) console.log(`- ${item}`);
1467
+ }
1468
+ console.log("Connection instructions:");
1469
+ console.log(`- connect: ${result.connectCommand}`);
1470
+ console.log(`- update: ${result.updateCommand}`);
1471
+ console.log(`- doctor: ${result.doctorCommand}`);
1472
+ console.log(`- status: ${result.statusCommand}`);
1473
+ }
1474
+ }
876
1475
  function hermesPluginSource() {
877
1476
  const candidates = [
878
1477
  resolve2(packageRoot(), "hermes-shadowob-plugin"),
@@ -882,33 +1481,39 @@ function hermesPluginSource() {
882
1481
  if (!found) throw new Error("Cannot find bundled hermes-shadowob-plugin directory");
883
1482
  return found;
884
1483
  }
885
- function applyOpenClaw(options) {
886
- const plan = createConnectorPlan(options);
1484
+ function applyOpenClaw(options, behavior = { restart: true }) {
1485
+ const target = requireTarget(options);
1486
+ const plan = createConnectorPlan({ ...options, target });
887
1487
  const configPath = expandHome2(
888
1488
  options.openclawConfig ?? process.env.OPENCLAW_CONFIG ?? "~/.shadowob/openclaw.json"
889
1489
  );
890
- console.log("Applying: Install plugin");
891
- runShell("openclaw plugins install @shadowob/openclaw-shadowob", options.dryRun);
1490
+ installShadowCliAndSkills(options);
892
1491
  console.log(`Applying: Merge OpenClaw config ${configPath}`);
893
1492
  const next = mergeOpenClawConfigContent(readExisting(configPath), {
894
1493
  token: options.token,
895
1494
  serverUrl: normalizeServerUrl2(options.serverUrl)
896
1495
  });
897
1496
  writeFile(configPath, next, options.dryRun);
1497
+ if (options.install) {
1498
+ console.log("Applying: Install plugin");
1499
+ runShell("openclaw plugins install @shadowob/openclaw-shadowob", options.dryRun);
1500
+ }
898
1501
  const restart = plan.commands.find((step) => step.label === "Restart gateway");
899
- if (restart) {
1502
+ if (restart && behavior.restart) {
900
1503
  console.log(`Applying: ${restart.label}`);
901
1504
  runShell(restart.command, options.dryRun);
902
1505
  }
903
1506
  }
904
1507
  function applyHermes(options) {
905
- const plan = createConnectorPlan(options);
1508
+ const target = requireTarget(options);
1509
+ const plan = createConnectorPlan({ ...options, target });
906
1510
  const hermesDir = expandHome2(options.hermesHome ?? process.env.HERMES_HOME ?? "~/.hermes");
907
1511
  const pluginTarget = resolve2(hermesDir, "plugins/shadowob");
908
1512
  const envPath = resolve2(hermesDir, ".env");
909
1513
  const configPath = resolve2(hermesDir, "config.yaml");
910
1514
  const envBlock = plan.configBlocks.find((block) => block.label === "~/.hermes/.env");
911
1515
  if (!envBlock) throw new Error("Hermes plan is missing config blocks");
1516
+ installShadowCliAndSkills(options);
912
1517
  if (options.dryRun) {
913
1518
  console.log(`[dry-run] copy ${hermesPluginSource()} -> ${pluginTarget}`);
914
1519
  } else {
@@ -937,9 +1542,11 @@ function applyHermes(options) {
937
1542
  }
938
1543
  }
939
1544
  async function applyCcConnect(options) {
940
- const plan = createConnectorPlan(options);
1545
+ const target = requireTarget(options);
1546
+ const plan = createConnectorPlan({ ...options, target });
941
1547
  const configBlock = plan.configBlocks.find((block) => block.label === "~/.cc-connect/config.toml");
942
1548
  if (!configBlock) throw new Error("cc-connect plan is missing config block");
1549
+ installShadowCliAndSkills(options);
943
1550
  const configPath = resolve2(homedir2(), ".cc-connect/config.toml");
944
1551
  const nextConfig = options.force ? configBlock.content : mergeCcConnectConfigContent(readExisting(configPath), {
945
1552
  token: options.token,
@@ -963,20 +1570,41 @@ async function applyCcConnect(options) {
963
1570
  }
964
1571
  }
965
1572
  async function connect(options) {
966
- if (options.target === "openclaw") {
1573
+ const target = requireTarget(options);
1574
+ if (target === "openclaw") {
967
1575
  applyOpenClaw(options);
968
1576
  return;
969
1577
  }
970
- if (options.target === "hermes") {
1578
+ if (target === "hermes") {
971
1579
  applyHermes(options);
972
1580
  return;
973
1581
  }
974
1582
  await applyCcConnect(options);
975
1583
  }
1584
+ async function repair(options, mode) {
1585
+ const target = requireTarget(options);
1586
+ console.log(`Applying: ${mode} ${target} connector`);
1587
+ if (target === "openclaw") {
1588
+ applyOpenClaw(options, { restart: options.start });
1589
+ return;
1590
+ }
1591
+ if (target === "hermes") {
1592
+ applyHermes({ ...options, start: options.start });
1593
+ return;
1594
+ }
1595
+ await applyCcConnect({ ...options, start: options.start });
1596
+ }
976
1597
  async function main() {
977
1598
  const options = parseArgs(process.argv.slice(2));
978
1599
  if (options.command === "connect") {
979
1600
  await connect(options);
1601
+ } else if (options.command === "fix" || options.command === "update") {
1602
+ await repair(options, options.command);
1603
+ } else if (options.command === "doctor" || options.command === "status") {
1604
+ const ok = printDiagnostics(options, options.command);
1605
+ if (options.command === "doctor" && !ok) process.exitCode = 1;
1606
+ } else if (options.command === "scan") {
1607
+ printScan(options);
980
1608
  } else {
981
1609
  printPlan(options);
982
1610
  }