claw-control-center 0.1.7 → 0.1.8

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/index.cjs CHANGED
@@ -5449,10 +5449,10 @@ function resolvePluginConfig(config) {
5449
5449
  function resolvePluginConfigWithHostDefaults(configPath, config) {
5450
5450
  const resolved = resolvePluginConfig(config);
5451
5451
  const hostConfig = readHostGatewayConfig(resolveHostConfigPath(configPath));
5452
- if (!resolved.gateway.baseUrl && hostConfig.baseUrl) {
5452
+ if (hostConfig.baseUrl && (!resolved.gateway.baseUrl || shouldPreferHostGateway(resolved.gateway.baseUrl, hostConfig.baseUrl))) {
5453
5453
  resolved.gateway.baseUrl = hostConfig.baseUrl;
5454
5454
  }
5455
- if (!resolved.gateway.secret && hostConfig.secret) {
5455
+ if (hostConfig.secret && (!resolved.gateway.secret || resolved.gateway.baseUrl === hostConfig.baseUrl)) {
5456
5456
  resolved.gateway.secret = hostConfig.secret;
5457
5457
  }
5458
5458
  if (!resolved.hub53ai.botId && hostConfig.hub53ai?.botId) {
@@ -5466,6 +5466,28 @@ function resolvePluginConfigWithHostDefaults(configPath, config) {
5466
5466
  }
5467
5467
  return resolved;
5468
5468
  }
5469
+ function shouldPreferHostGateway(configuredBaseUrl, hostBaseUrl) {
5470
+ if (configuredBaseUrl === hostBaseUrl) {
5471
+ return false;
5472
+ }
5473
+ const configured = parseGatewayUrl(configuredBaseUrl);
5474
+ const host = parseGatewayUrl(hostBaseUrl);
5475
+ if (!configured || !host) {
5476
+ return false;
5477
+ }
5478
+ return configured.isLoopback && host.isLoopback;
5479
+ }
5480
+ function parseGatewayUrl(value) {
5481
+ try {
5482
+ const url = new URL(value);
5483
+ const hostname = url.hostname.replace(/^\[|\]$/g, "").toLowerCase();
5484
+ return {
5485
+ isLoopback: hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"
5486
+ };
5487
+ } catch {
5488
+ return void 0;
5489
+ }
5490
+ }
5469
5491
  function sanitizePluginConfig(config) {
5470
5492
  if (!config) {
5471
5493
  return {};
@@ -8859,12 +8881,29 @@ function toRecord3(value) {
8859
8881
 
8860
8882
  // src/install-qclaw.ts
8861
8883
  var import_node_crypto4 = require("crypto");
8862
- var import_promises4 = require("fs/promises");
8884
+ var import_promises4 = require("readline/promises");
8885
+ var import_promises5 = require("fs/promises");
8863
8886
  var import_node_fs4 = require("fs");
8887
+ var import_node_os2 = require("os");
8864
8888
  var import_node_path6 = require("path");
8865
8889
  var PLUGIN_ID = "claw-control-center";
8866
8890
  var LEGACY_PLUGIN_ID = "53ai-openclaw";
8867
8891
  var COPY_ITEMS = ["dist", "openclaw.plugin.json", "package.json", "bin", "web-dist"];
8892
+ var SUPPORTED_ARGS = /* @__PURE__ */ new Set([
8893
+ "gateway",
8894
+ "bot-id",
8895
+ "secret",
8896
+ "prefer-responses-api",
8897
+ "gateway-model",
8898
+ "hub-ws-url",
8899
+ "hub-bot-id",
8900
+ "hub-secret",
8901
+ "hub-enabled",
8902
+ "extensions-dir",
8903
+ "config-path",
8904
+ "console-host",
8905
+ "console-port"
8906
+ ]);
8868
8907
  async function installIntoQClaw(input) {
8869
8908
  return installIntoHost(input, "QClaw");
8870
8909
  }
@@ -8872,19 +8911,21 @@ async function installIntoOpenClaw(input) {
8872
8911
  return installIntoHost(input, "OpenClaw");
8873
8912
  }
8874
8913
  async function installIntoHost(input, hostLabel) {
8875
- await (0, import_promises4.mkdir)(input.extensionsDir, { recursive: true });
8914
+ await (0, import_promises5.mkdir)(input.extensionsDir, { recursive: true });
8876
8915
  const destination = (0, import_node_path6.join)(input.extensionsDir, PLUGIN_ID);
8877
- await (0, import_promises4.mkdir)(destination, { recursive: true });
8916
+ await (0, import_promises5.mkdir)(destination, { recursive: true });
8878
8917
  await copyPublishablePackage(input.packageRoot, destination);
8879
- await (0, import_promises4.mkdir)((0, import_node_path6.dirname)(input.configPath), { recursive: true });
8918
+ await (0, import_promises5.mkdir)((0, import_node_path6.dirname)(input.configPath), { recursive: true });
8880
8919
  const config = await readOpenClawConfig(input.configPath);
8881
8920
  const inferredGateway = inferGatewaySettings(config);
8882
8921
  const inferredHub53AI = inferHub53AISettings(config);
8883
- const gatewayBaseUrl = input.gateway?.trim() || inferredGateway.baseUrl;
8922
+ const explicitGatewayBaseUrl = input.gateway?.trim();
8923
+ const explicitGatewaySecret = input.secret?.trim();
8924
+ const gatewayBaseUrl = explicitGatewayBaseUrl || inferredGateway.baseUrl;
8884
8925
  if (!gatewayBaseUrl) {
8885
8926
  throw new Error(`missing gateway URL and no local ${hostLabel} gateway could be inferred`);
8886
8927
  }
8887
- const secret = input.secret?.trim() || inferredGateway.secret;
8928
+ const secret = explicitGatewaySecret || inferredGateway.secret;
8888
8929
  if (!secret) {
8889
8930
  throw new Error(`missing gateway secret and no local ${hostLabel} gateway token could be inferred`);
8890
8931
  }
@@ -8919,8 +8960,16 @@ async function installIntoHost(input, hostLabel) {
8919
8960
  const previousEntry = ensureObject(entries, PLUGIN_ID);
8920
8961
  const previousConfig = ensureObject(previousEntry, "config");
8921
8962
  const previousGateway = ensureObject(previousConfig, "gateway");
8922
- previousGateway.baseUrl = gatewayBaseUrl;
8923
- previousGateway.secret = secret;
8963
+ if (explicitGatewayBaseUrl) {
8964
+ previousGateway.baseUrl = explicitGatewayBaseUrl;
8965
+ } else {
8966
+ delete previousGateway.baseUrl;
8967
+ }
8968
+ if (explicitGatewaySecret) {
8969
+ previousGateway.secret = explicitGatewaySecret;
8970
+ } else {
8971
+ delete previousGateway.secret;
8972
+ }
8924
8973
  previousGateway.preferResponsesApi = preferResponsesApi;
8925
8974
  if (botId) {
8926
8975
  previousGateway.botId = botId;
@@ -8961,7 +9010,7 @@ async function installIntoHost(input, hostLabel) {
8961
9010
  }
8962
9011
  }
8963
9012
  previousEntry.enabled = true;
8964
- await (0, import_promises4.writeFile)(input.configPath, `${JSON.stringify(config, null, 2)}
9013
+ await (0, import_promises5.writeFile)(input.configPath, `${JSON.stringify(config, null, 2)}
8965
9014
  `);
8966
9015
  return {
8967
9016
  configPath: input.configPath,
@@ -8978,7 +9027,11 @@ async function runInstallCommand(input) {
8978
9027
  throw new Error("expected subcommand: install");
8979
9028
  }
8980
9029
  const args = parseArgs(argv.slice(1));
8981
- const destination = resolveInstallDestination(args);
9030
+ const destination = await resolveInstallDestination(args, {
9031
+ hostDefinitions: input.hostDefinitions,
9032
+ selectHost: input.selectHost,
9033
+ ttyPath: input.ttyPath
9034
+ });
8982
9035
  const result = await installIntoHost(
8983
9036
  {
8984
9037
  packageRoot: input.packageRoot,
@@ -8996,34 +9049,141 @@ async function runInstallCommand(input) {
8996
9049
  consoleHost: args["console-host"],
8997
9050
  consolePort: args["console-port"] ? Number(args["console-port"]) : void 0
8998
9051
  },
8999
- "Claw"
9052
+ destination.label
9000
9053
  );
9001
9054
  process.stdout.write(
9002
9055
  [
9003
- `Installed ${PLUGIN_ID} into Claw.`,
9056
+ `Installed ${PLUGIN_ID} into ${destination.label}.`,
9004
9057
  `Extensions: ${result.extensionsDir}`,
9005
9058
  `Config: ${result.configPath}`,
9006
9059
  `Gateway: ${result.gatewayBaseUrl}`,
9007
9060
  `53AIHub: ${result.hub53aiConfigured ? "configured" : "not configured"}`,
9008
9061
  `Plugin build: ${result.pluginBuild}`,
9009
- "Restart your Claw host to load the plugin."
9062
+ `Restart ${destination.label} to load the plugin.`
9010
9063
  ].join("\n") + "\n"
9011
9064
  );
9012
9065
  }
9013
- function resolveInstallDestination(args) {
9014
- if (args.target !== void 0) {
9015
- throw new Error("--target is no longer supported; pass --config-path and --extensions-dir from your Claw host");
9016
- }
9066
+ async function resolveInstallDestination(args, options = {}) {
9017
9067
  const explicitConfigPath = args["config-path"] ? (0, import_node_path6.resolve)(args["config-path"]) : void 0;
9018
9068
  const explicitExtensionsDir = args["extensions-dir"] ? (0, import_node_path6.resolve)(args["extensions-dir"]) : void 0;
9019
- if (!explicitConfigPath || !explicitExtensionsDir) {
9020
- throw new Error("missing --config-path and --extensions-dir; provide the paths generated by your Claw host");
9069
+ if (explicitConfigPath && explicitExtensionsDir) {
9070
+ return {
9071
+ configPath: explicitConfigPath,
9072
+ extensionsDir: explicitExtensionsDir,
9073
+ label: "Claw"
9074
+ };
9075
+ }
9076
+ if (explicitConfigPath || explicitExtensionsDir) {
9077
+ throw new Error("pass --config-path and --extensions-dir together, or omit both to auto-detect Claw");
9078
+ }
9079
+ const detected = detectInstallHosts(options.hostDefinitions ?? getDefaultHostDefinitions());
9080
+ if (detected.length === 1) {
9081
+ return toInstallDestination(detected[0]);
9082
+ }
9083
+ if (detected.length > 1) {
9084
+ const selected = options.selectHost ? await options.selectHost(detected) : await promptForInstallHost(detected, options.ttyPath ?? "/dev/tty");
9085
+ return toInstallDestination(selected);
9086
+ }
9087
+ throw new Error(
9088
+ [
9089
+ "could not auto-detect an installed Claw host.",
9090
+ "Pass --config-path and --extensions-dir to install into a specific Claw.",
9091
+ "",
9092
+ "Supported default locations:",
9093
+ ...formatHostList(options.hostDefinitions ?? getDefaultHostDefinitions())
9094
+ ].join("\n")
9095
+ );
9096
+ }
9097
+ function getDefaultHostDefinitions() {
9098
+ const home = (0, import_node_os2.homedir)();
9099
+ const qclawHome = (0, import_node_path6.resolve)(home, ".qclaw");
9100
+ const openClawHome = (0, import_node_path6.resolve)(home, ".openclaw");
9101
+ return [
9102
+ {
9103
+ id: "qclaw",
9104
+ label: "QClaw",
9105
+ configPath: (0, import_node_path6.join)(qclawHome, "openclaw.json"),
9106
+ extensionsDir: (0, import_node_path6.resolve)(home, "Library/Application Support/QClaw/openclaw/config/extensions")
9107
+ },
9108
+ {
9109
+ id: "openclaw",
9110
+ label: "OpenClaw",
9111
+ configPath: (0, import_node_path6.join)(openClawHome, "openclaw.json"),
9112
+ extensionsDir: (0, import_node_path6.resolve)(openClawHome, "extensions")
9113
+ }
9114
+ ];
9115
+ }
9116
+ function detectInstallHosts(hosts) {
9117
+ const seen = /* @__PURE__ */ new Set();
9118
+ return hosts.filter((host) => {
9119
+ const key = `${(0, import_node_path6.resolve)(host.configPath)}\0${(0, import_node_path6.resolve)(host.extensionsDir)}`;
9120
+ if (seen.has(key) || !(0, import_node_fs4.existsSync)(host.configPath)) {
9121
+ return false;
9122
+ }
9123
+ seen.add(key);
9124
+ return true;
9125
+ });
9126
+ }
9127
+ async function promptForInstallHost(hosts, ttyPath) {
9128
+ let handle;
9129
+ try {
9130
+ handle = await (0, import_promises5.open)(ttyPath, "r+");
9131
+ const input = handle.createReadStream();
9132
+ const output = handle.createWriteStream();
9133
+ const readline = (0, import_promises4.createInterface)({ input, output });
9134
+ try {
9135
+ output.write("Multiple Claw installations were detected.\n");
9136
+ output.write("Choose where to install claw-control-center:\n");
9137
+ hosts.forEach((host, index) => {
9138
+ output.write(`${index + 1}. ${host.label}
9139
+ `);
9140
+ output.write(` Config: ${host.configPath}
9141
+ `);
9142
+ output.write(` Extensions: ${host.extensionsDir}
9143
+ `);
9144
+ });
9145
+ const answer = await readline.question(`Install location [1-${hosts.length}]: `);
9146
+ const selectedIndex = Number(answer.trim());
9147
+ if (!Number.isInteger(selectedIndex) || selectedIndex < 1 || selectedIndex > hosts.length) {
9148
+ throw new Error(`invalid install location: ${answer}`);
9149
+ }
9150
+ return hosts[selectedIndex - 1];
9151
+ } finally {
9152
+ readline.close();
9153
+ input.destroy();
9154
+ output.end();
9155
+ }
9156
+ } catch (error) {
9157
+ if (error instanceof Error && error.message.startsWith("invalid install location:")) {
9158
+ throw error;
9159
+ }
9160
+ throw new Error(
9161
+ [
9162
+ "multiple Claw installations were detected, but no interactive terminal was available.",
9163
+ "Run the installer again with --config-path and --extensions-dir.",
9164
+ "",
9165
+ "Detected locations:",
9166
+ ...formatHostList(hosts)
9167
+ ].join("\n")
9168
+ );
9169
+ } finally {
9170
+ await handle?.close().catch(() => void 0);
9021
9171
  }
9172
+ }
9173
+ function toInstallDestination(host) {
9022
9174
  return {
9023
- configPath: explicitConfigPath,
9024
- extensionsDir: explicitExtensionsDir
9175
+ configPath: (0, import_node_path6.resolve)(host.configPath),
9176
+ extensionsDir: (0, import_node_path6.resolve)(host.extensionsDir),
9177
+ label: host.label
9025
9178
  };
9026
9179
  }
9180
+ function formatHostList(hosts) {
9181
+ return hosts.flatMap((host) => [
9182
+ `- ${host.label}`,
9183
+ ` Config: ${host.configPath}`,
9184
+ ` Extensions: ${host.extensionsDir}`
9185
+ ]);
9186
+ }
9027
9187
  function parseArgs(argv) {
9028
9188
  const parsed = {};
9029
9189
  for (let index = 0; index < argv.length; index += 1) {
@@ -9031,12 +9191,19 @@ function parseArgs(argv) {
9031
9191
  if (!entry.startsWith("--")) {
9032
9192
  continue;
9033
9193
  }
9194
+ const key = entry.slice(2);
9195
+ if (key === "target") {
9196
+ throw new Error("--target has been removed; omit it to auto-detect Claw or pass --config-path and --extensions-dir");
9197
+ }
9198
+ if (!SUPPORTED_ARGS.has(key)) {
9199
+ throw new Error(`unknown option: --${key}`);
9200
+ }
9034
9201
  const next = argv[index + 1];
9035
9202
  if (next && !next.startsWith("--")) {
9036
- parsed[entry.slice(2)] = next;
9203
+ parsed[key] = next;
9037
9204
  index += 1;
9038
9205
  } else {
9039
- parsed[entry.slice(2)] = "true";
9206
+ parsed[key] = "true";
9040
9207
  }
9041
9208
  }
9042
9209
  return parsed;
@@ -9051,8 +9218,8 @@ async function copyPublishablePackage(packageRoot, destination) {
9051
9218
  continue;
9052
9219
  }
9053
9220
  const target = (0, import_node_path6.join)(destination, relativePath);
9054
- await (0, import_promises4.rm)(target, { recursive: true, force: true });
9055
- await (0, import_promises4.cp)(source, target, { recursive: true, force: true });
9221
+ await (0, import_promises5.rm)(target, { recursive: true, force: true });
9222
+ await (0, import_promises5.cp)(source, target, { recursive: true, force: true });
9056
9223
  }
9057
9224
  }
9058
9225
  async function readPluginBuildInfo(destination) {
@@ -9060,7 +9227,7 @@ async function readPluginBuildInfo(destination) {
9060
9227
  const entryPath = (0, import_node_path6.join)(destination, "dist", "index.cjs");
9061
9228
  let version = "unknown";
9062
9229
  try {
9063
- const packageJson = JSON.parse(await (0, import_promises4.readFile)(packagePath, "utf8"));
9230
+ const packageJson = JSON.parse(await (0, import_promises5.readFile)(packagePath, "utf8"));
9064
9231
  if (typeof packageJson.version === "string" && packageJson.version.trim()) {
9065
9232
  version = packageJson.version.trim();
9066
9233
  }
@@ -9068,7 +9235,7 @@ async function readPluginBuildInfo(destination) {
9068
9235
  version = "unknown";
9069
9236
  }
9070
9237
  try {
9071
- const entry = await (0, import_promises4.readFile)(entryPath);
9238
+ const entry = await (0, import_promises5.readFile)(entryPath);
9072
9239
  const digest = (0, import_node_crypto4.createHash)("sha256").update(entry).digest("hex").slice(0, 12);
9073
9240
  return `${PLUGIN_ID}@${version} sha256:${digest}`;
9074
9241
  } catch {
@@ -9079,7 +9246,7 @@ async function readOpenClawConfig(configPath) {
9079
9246
  if (!(0, import_node_fs4.existsSync)(configPath)) {
9080
9247
  return {};
9081
9248
  }
9082
- return JSON.parse(await (0, import_promises4.readFile)(configPath, "utf8"));
9249
+ return JSON.parse(await (0, import_promises5.readFile)(configPath, "utf8"));
9083
9250
  }
9084
9251
  function inferGatewaySettings(config) {
9085
9252
  const gateway = config.gateway ?? {};
@@ -26,12 +26,29 @@ __export(install_qclaw_exports, {
26
26
  });
27
27
  module.exports = __toCommonJS(install_qclaw_exports);
28
28
  var import_node_crypto = require("crypto");
29
- var import_promises = require("fs/promises");
29
+ var import_promises = require("readline/promises");
30
+ var import_promises2 = require("fs/promises");
30
31
  var import_node_fs = require("fs");
32
+ var import_node_os = require("os");
31
33
  var import_node_path = require("path");
32
34
  var PLUGIN_ID = "claw-control-center";
33
35
  var LEGACY_PLUGIN_ID = "53ai-openclaw";
34
36
  var COPY_ITEMS = ["dist", "openclaw.plugin.json", "package.json", "bin", "web-dist"];
37
+ var SUPPORTED_ARGS = /* @__PURE__ */ new Set([
38
+ "gateway",
39
+ "bot-id",
40
+ "secret",
41
+ "prefer-responses-api",
42
+ "gateway-model",
43
+ "hub-ws-url",
44
+ "hub-bot-id",
45
+ "hub-secret",
46
+ "hub-enabled",
47
+ "extensions-dir",
48
+ "config-path",
49
+ "console-host",
50
+ "console-port"
51
+ ]);
35
52
  async function installIntoQClaw(input) {
36
53
  return installIntoHost(input, "QClaw");
37
54
  }
@@ -39,19 +56,21 @@ async function installIntoOpenClaw(input) {
39
56
  return installIntoHost(input, "OpenClaw");
40
57
  }
41
58
  async function installIntoHost(input, hostLabel) {
42
- await (0, import_promises.mkdir)(input.extensionsDir, { recursive: true });
59
+ await (0, import_promises2.mkdir)(input.extensionsDir, { recursive: true });
43
60
  const destination = (0, import_node_path.join)(input.extensionsDir, PLUGIN_ID);
44
- await (0, import_promises.mkdir)(destination, { recursive: true });
61
+ await (0, import_promises2.mkdir)(destination, { recursive: true });
45
62
  await copyPublishablePackage(input.packageRoot, destination);
46
- await (0, import_promises.mkdir)((0, import_node_path.dirname)(input.configPath), { recursive: true });
63
+ await (0, import_promises2.mkdir)((0, import_node_path.dirname)(input.configPath), { recursive: true });
47
64
  const config = await readOpenClawConfig(input.configPath);
48
65
  const inferredGateway = inferGatewaySettings(config);
49
66
  const inferredHub53AI = inferHub53AISettings(config);
50
- const gatewayBaseUrl = input.gateway?.trim() || inferredGateway.baseUrl;
67
+ const explicitGatewayBaseUrl = input.gateway?.trim();
68
+ const explicitGatewaySecret = input.secret?.trim();
69
+ const gatewayBaseUrl = explicitGatewayBaseUrl || inferredGateway.baseUrl;
51
70
  if (!gatewayBaseUrl) {
52
71
  throw new Error(`missing gateway URL and no local ${hostLabel} gateway could be inferred`);
53
72
  }
54
- const secret = input.secret?.trim() || inferredGateway.secret;
73
+ const secret = explicitGatewaySecret || inferredGateway.secret;
55
74
  if (!secret) {
56
75
  throw new Error(`missing gateway secret and no local ${hostLabel} gateway token could be inferred`);
57
76
  }
@@ -86,8 +105,16 @@ async function installIntoHost(input, hostLabel) {
86
105
  const previousEntry = ensureObject(entries, PLUGIN_ID);
87
106
  const previousConfig = ensureObject(previousEntry, "config");
88
107
  const previousGateway = ensureObject(previousConfig, "gateway");
89
- previousGateway.baseUrl = gatewayBaseUrl;
90
- previousGateway.secret = secret;
108
+ if (explicitGatewayBaseUrl) {
109
+ previousGateway.baseUrl = explicitGatewayBaseUrl;
110
+ } else {
111
+ delete previousGateway.baseUrl;
112
+ }
113
+ if (explicitGatewaySecret) {
114
+ previousGateway.secret = explicitGatewaySecret;
115
+ } else {
116
+ delete previousGateway.secret;
117
+ }
91
118
  previousGateway.preferResponsesApi = preferResponsesApi;
92
119
  if (botId) {
93
120
  previousGateway.botId = botId;
@@ -128,7 +155,7 @@ async function installIntoHost(input, hostLabel) {
128
155
  }
129
156
  }
130
157
  previousEntry.enabled = true;
131
- await (0, import_promises.writeFile)(input.configPath, `${JSON.stringify(config, null, 2)}
158
+ await (0, import_promises2.writeFile)(input.configPath, `${JSON.stringify(config, null, 2)}
132
159
  `);
133
160
  return {
134
161
  configPath: input.configPath,
@@ -145,7 +172,11 @@ async function runInstallCommand(input) {
145
172
  throw new Error("expected subcommand: install");
146
173
  }
147
174
  const args = parseArgs(argv.slice(1));
148
- const destination = resolveInstallDestination(args);
175
+ const destination = await resolveInstallDestination(args, {
176
+ hostDefinitions: input.hostDefinitions,
177
+ selectHost: input.selectHost,
178
+ ttyPath: input.ttyPath
179
+ });
149
180
  const result = await installIntoHost(
150
181
  {
151
182
  packageRoot: input.packageRoot,
@@ -163,34 +194,141 @@ async function runInstallCommand(input) {
163
194
  consoleHost: args["console-host"],
164
195
  consolePort: args["console-port"] ? Number(args["console-port"]) : void 0
165
196
  },
166
- "Claw"
197
+ destination.label
167
198
  );
168
199
  process.stdout.write(
169
200
  [
170
- `Installed ${PLUGIN_ID} into Claw.`,
201
+ `Installed ${PLUGIN_ID} into ${destination.label}.`,
171
202
  `Extensions: ${result.extensionsDir}`,
172
203
  `Config: ${result.configPath}`,
173
204
  `Gateway: ${result.gatewayBaseUrl}`,
174
205
  `53AIHub: ${result.hub53aiConfigured ? "configured" : "not configured"}`,
175
206
  `Plugin build: ${result.pluginBuild}`,
176
- "Restart your Claw host to load the plugin."
207
+ `Restart ${destination.label} to load the plugin.`
177
208
  ].join("\n") + "\n"
178
209
  );
179
210
  }
180
- function resolveInstallDestination(args) {
181
- if (args.target !== void 0) {
182
- throw new Error("--target is no longer supported; pass --config-path and --extensions-dir from your Claw host");
183
- }
211
+ async function resolveInstallDestination(args, options = {}) {
184
212
  const explicitConfigPath = args["config-path"] ? (0, import_node_path.resolve)(args["config-path"]) : void 0;
185
213
  const explicitExtensionsDir = args["extensions-dir"] ? (0, import_node_path.resolve)(args["extensions-dir"]) : void 0;
186
- if (!explicitConfigPath || !explicitExtensionsDir) {
187
- throw new Error("missing --config-path and --extensions-dir; provide the paths generated by your Claw host");
214
+ if (explicitConfigPath && explicitExtensionsDir) {
215
+ return {
216
+ configPath: explicitConfigPath,
217
+ extensionsDir: explicitExtensionsDir,
218
+ label: "Claw"
219
+ };
220
+ }
221
+ if (explicitConfigPath || explicitExtensionsDir) {
222
+ throw new Error("pass --config-path and --extensions-dir together, or omit both to auto-detect Claw");
223
+ }
224
+ const detected = detectInstallHosts(options.hostDefinitions ?? getDefaultHostDefinitions());
225
+ if (detected.length === 1) {
226
+ return toInstallDestination(detected[0]);
227
+ }
228
+ if (detected.length > 1) {
229
+ const selected = options.selectHost ? await options.selectHost(detected) : await promptForInstallHost(detected, options.ttyPath ?? "/dev/tty");
230
+ return toInstallDestination(selected);
188
231
  }
232
+ throw new Error(
233
+ [
234
+ "could not auto-detect an installed Claw host.",
235
+ "Pass --config-path and --extensions-dir to install into a specific Claw.",
236
+ "",
237
+ "Supported default locations:",
238
+ ...formatHostList(options.hostDefinitions ?? getDefaultHostDefinitions())
239
+ ].join("\n")
240
+ );
241
+ }
242
+ function getDefaultHostDefinitions() {
243
+ const home = (0, import_node_os.homedir)();
244
+ const qclawHome = (0, import_node_path.resolve)(home, ".qclaw");
245
+ const openClawHome = (0, import_node_path.resolve)(home, ".openclaw");
246
+ return [
247
+ {
248
+ id: "qclaw",
249
+ label: "QClaw",
250
+ configPath: (0, import_node_path.join)(qclawHome, "openclaw.json"),
251
+ extensionsDir: (0, import_node_path.resolve)(home, "Library/Application Support/QClaw/openclaw/config/extensions")
252
+ },
253
+ {
254
+ id: "openclaw",
255
+ label: "OpenClaw",
256
+ configPath: (0, import_node_path.join)(openClawHome, "openclaw.json"),
257
+ extensionsDir: (0, import_node_path.resolve)(openClawHome, "extensions")
258
+ }
259
+ ];
260
+ }
261
+ function detectInstallHosts(hosts) {
262
+ const seen = /* @__PURE__ */ new Set();
263
+ return hosts.filter((host) => {
264
+ const key = `${(0, import_node_path.resolve)(host.configPath)}\0${(0, import_node_path.resolve)(host.extensionsDir)}`;
265
+ if (seen.has(key) || !(0, import_node_fs.existsSync)(host.configPath)) {
266
+ return false;
267
+ }
268
+ seen.add(key);
269
+ return true;
270
+ });
271
+ }
272
+ async function promptForInstallHost(hosts, ttyPath) {
273
+ let handle;
274
+ try {
275
+ handle = await (0, import_promises2.open)(ttyPath, "r+");
276
+ const input = handle.createReadStream();
277
+ const output = handle.createWriteStream();
278
+ const readline = (0, import_promises.createInterface)({ input, output });
279
+ try {
280
+ output.write("Multiple Claw installations were detected.\n");
281
+ output.write("Choose where to install claw-control-center:\n");
282
+ hosts.forEach((host, index) => {
283
+ output.write(`${index + 1}. ${host.label}
284
+ `);
285
+ output.write(` Config: ${host.configPath}
286
+ `);
287
+ output.write(` Extensions: ${host.extensionsDir}
288
+ `);
289
+ });
290
+ const answer = await readline.question(`Install location [1-${hosts.length}]: `);
291
+ const selectedIndex = Number(answer.trim());
292
+ if (!Number.isInteger(selectedIndex) || selectedIndex < 1 || selectedIndex > hosts.length) {
293
+ throw new Error(`invalid install location: ${answer}`);
294
+ }
295
+ return hosts[selectedIndex - 1];
296
+ } finally {
297
+ readline.close();
298
+ input.destroy();
299
+ output.end();
300
+ }
301
+ } catch (error) {
302
+ if (error instanceof Error && error.message.startsWith("invalid install location:")) {
303
+ throw error;
304
+ }
305
+ throw new Error(
306
+ [
307
+ "multiple Claw installations were detected, but no interactive terminal was available.",
308
+ "Run the installer again with --config-path and --extensions-dir.",
309
+ "",
310
+ "Detected locations:",
311
+ ...formatHostList(hosts)
312
+ ].join("\n")
313
+ );
314
+ } finally {
315
+ await handle?.close().catch(() => void 0);
316
+ }
317
+ }
318
+ function toInstallDestination(host) {
189
319
  return {
190
- configPath: explicitConfigPath,
191
- extensionsDir: explicitExtensionsDir
320
+ configPath: (0, import_node_path.resolve)(host.configPath),
321
+ extensionsDir: (0, import_node_path.resolve)(host.extensionsDir),
322
+ label: host.label
192
323
  };
193
324
  }
325
+ function formatHostList(hosts) {
326
+ return hosts.flatMap((host) => [
327
+ `- ${host.label}`,
328
+ ` Config: ${host.configPath}`,
329
+ ` Extensions: ${host.extensionsDir}`
330
+ ]);
331
+ }
194
332
  function parseArgs(argv) {
195
333
  const parsed = {};
196
334
  for (let index = 0; index < argv.length; index += 1) {
@@ -198,12 +336,19 @@ function parseArgs(argv) {
198
336
  if (!entry.startsWith("--")) {
199
337
  continue;
200
338
  }
339
+ const key = entry.slice(2);
340
+ if (key === "target") {
341
+ throw new Error("--target has been removed; omit it to auto-detect Claw or pass --config-path and --extensions-dir");
342
+ }
343
+ if (!SUPPORTED_ARGS.has(key)) {
344
+ throw new Error(`unknown option: --${key}`);
345
+ }
201
346
  const next = argv[index + 1];
202
347
  if (next && !next.startsWith("--")) {
203
- parsed[entry.slice(2)] = next;
348
+ parsed[key] = next;
204
349
  index += 1;
205
350
  } else {
206
- parsed[entry.slice(2)] = "true";
351
+ parsed[key] = "true";
207
352
  }
208
353
  }
209
354
  return parsed;
@@ -218,8 +363,8 @@ async function copyPublishablePackage(packageRoot, destination) {
218
363
  continue;
219
364
  }
220
365
  const target = (0, import_node_path.join)(destination, relativePath);
221
- await (0, import_promises.rm)(target, { recursive: true, force: true });
222
- await (0, import_promises.cp)(source, target, { recursive: true, force: true });
366
+ await (0, import_promises2.rm)(target, { recursive: true, force: true });
367
+ await (0, import_promises2.cp)(source, target, { recursive: true, force: true });
223
368
  }
224
369
  }
225
370
  async function readPluginBuildInfo(destination) {
@@ -227,7 +372,7 @@ async function readPluginBuildInfo(destination) {
227
372
  const entryPath = (0, import_node_path.join)(destination, "dist", "index.cjs");
228
373
  let version = "unknown";
229
374
  try {
230
- const packageJson = JSON.parse(await (0, import_promises.readFile)(packagePath, "utf8"));
375
+ const packageJson = JSON.parse(await (0, import_promises2.readFile)(packagePath, "utf8"));
231
376
  if (typeof packageJson.version === "string" && packageJson.version.trim()) {
232
377
  version = packageJson.version.trim();
233
378
  }
@@ -235,7 +380,7 @@ async function readPluginBuildInfo(destination) {
235
380
  version = "unknown";
236
381
  }
237
382
  try {
238
- const entry = await (0, import_promises.readFile)(entryPath);
383
+ const entry = await (0, import_promises2.readFile)(entryPath);
239
384
  const digest = (0, import_node_crypto.createHash)("sha256").update(entry).digest("hex").slice(0, 12);
240
385
  return `${PLUGIN_ID}@${version} sha256:${digest}`;
241
386
  } catch {
@@ -246,7 +391,7 @@ async function readOpenClawConfig(configPath) {
246
391
  if (!(0, import_node_fs.existsSync)(configPath)) {
247
392
  return {};
248
393
  }
249
- return JSON.parse(await (0, import_promises.readFile)(configPath, "utf8"));
394
+ return JSON.parse(await (0, import_promises2.readFile)(configPath, "utf8"));
250
395
  }
251
396
  function inferGatewaySettings(config) {
252
397
  const gateway = config.gateway ?? {};
@@ -14,6 +14,13 @@ type InstallInput = {
14
14
  consoleHost?: string;
15
15
  consolePort?: number;
16
16
  };
17
+ type InstallLabel = "Claw" | "QClaw" | "OpenClaw";
18
+ type HostDefinition = {
19
+ id: string;
20
+ label: InstallLabel;
21
+ configPath: string;
22
+ extensionsDir: string;
23
+ };
17
24
  declare function installIntoQClaw(input: InstallInput): Promise<{
18
25
  configPath: string;
19
26
  extensionsDir: string;
@@ -33,6 +40,9 @@ declare function installIntoOpenClaw(input: InstallInput): Promise<{
33
40
  declare function runInstallCommand(input: {
34
41
  argv?: string[];
35
42
  packageRoot: string;
43
+ hostDefinitions?: HostDefinition[];
44
+ selectHost?: (hosts: HostDefinition[]) => Promise<HostDefinition>;
45
+ ttyPath?: string;
36
46
  }): Promise<void>;
37
47
 
38
- export { installIntoOpenClaw, installIntoQClaw, runInstallCommand };
48
+ export { type HostDefinition, installIntoOpenClaw, installIntoQClaw, runInstallCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claw-control-center",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "types": "dist/index.d.ts",