@ainyc/canonry 1.46.1 → 1.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,39 +1,57 @@
1
1
  #!/usr/bin/env node --import tsx
2
2
  import {
3
+ AGENT_WEBHOOK_EVENTS,
4
+ AgentManager,
5
+ CliError,
6
+ EXIT_SYSTEM_ERROR,
7
+ EXIT_USER_ERROR,
3
8
  ProviderNames,
4
9
  RunKinds,
10
+ attachAgentWebhookDirect,
11
+ buildAgentWebhookUrl,
5
12
  computeCompetitorOverlap,
6
13
  configExists,
14
+ configureOpenClawGateway,
7
15
  createServer,
16
+ detectOpenClaw,
8
17
  determineAnswerMentioned,
9
18
  determineCitationState,
10
19
  effectiveDomains,
11
20
  extractRecommendedCompetitors,
12
21
  formatAuditFactorScore,
22
+ getAeroStateDir,
13
23
  getConfigDir,
14
24
  getConfigPath,
15
25
  getOrCreateAnonymousId,
26
+ initializeOpenClawProfile,
27
+ installOpenClaw,
16
28
  isFirstRun,
17
29
  isTelemetryEnabled,
18
30
  loadConfig,
19
31
  notificationEventSchema,
32
+ printCliError,
33
+ providerEnvVar,
20
34
  providerQuotaPolicySchema,
21
35
  reparseStoredResult,
22
36
  reparseStoredResult2,
23
37
  reparseStoredResult3,
24
38
  reparseStoredResult4,
39
+ resolveAgentCredentials,
25
40
  resolveProviderInput,
26
41
  saveConfig,
27
42
  saveConfigPatch,
43
+ seedWorkspace,
28
44
  setGoogleAuthConfig,
45
+ setOpenClawModel,
29
46
  showFirstRunNotice,
30
- trackEvent
31
- } from "./chunk-RMLIF47M.js";
47
+ trackEvent,
48
+ usageError,
49
+ writeAgentEnv
50
+ } from "./chunk-25QLMK4F.js";
32
51
  import {
33
52
  apiKeys,
34
53
  competitors,
35
54
  createClient,
36
- createLogger,
37
55
  migrate,
38
56
  parseJsonColumn,
39
57
  projects,
@@ -44,84 +62,14 @@ import {
44
62
  // src/cli.ts
45
63
  import { pathToFileURL } from "url";
46
64
 
47
- // src/cli-error.ts
48
- var EXIT_USER_ERROR = 1;
49
- var EXIT_SYSTEM_ERROR = 2;
50
- var CliError = class extends Error {
51
- code;
52
- displayMessage;
53
- details;
54
- exitCode;
55
- constructor(options) {
56
- super(options.message);
57
- this.name = "CliError";
58
- this.code = options.code;
59
- this.displayMessage = options.displayMessage;
60
- this.details = options.details;
61
- this.exitCode = options.exitCode ?? EXIT_USER_ERROR;
62
- }
63
- };
64
- function usageError(displayMessage, options) {
65
- const firstLine = displayMessage.split("\n", 1)[0] ?? "Error: invalid command usage";
66
- return new CliError({
67
- code: "CLI_USAGE_ERROR",
68
- message: options?.message ?? firstLine.replace(/^Error:\s*/, ""),
69
- displayMessage,
70
- details: options?.details
71
- });
72
- }
73
- function printCliError(err, format) {
74
- if (format === "json") {
75
- if (err instanceof CliError) {
76
- console.error(
77
- JSON.stringify(
78
- {
79
- error: {
80
- code: err.code,
81
- message: err.message,
82
- ...err.details ? { details: err.details } : {}
83
- }
84
- },
85
- null,
86
- 2
87
- )
88
- );
89
- return;
90
- }
91
- const message = err instanceof Error ? err.message : "An unexpected error occurred";
92
- console.error(
93
- JSON.stringify(
94
- {
95
- error: {
96
- code: "CLI_ERROR",
97
- message
98
- }
99
- },
100
- null,
101
- 2
102
- )
103
- );
104
- return;
105
- }
106
- if (err instanceof CliError && err.displayMessage) {
107
- console.error(err.displayMessage);
108
- return;
109
- }
110
- if (err instanceof Error) {
111
- console.error(`Error: ${err.message}`);
112
- return;
113
- }
114
- console.error("An unexpected error occurred");
115
- }
116
-
117
65
  // src/cli-dispatch.ts
118
66
  import { parseArgs } from "util";
119
67
  function commandId(spec) {
120
68
  return spec.path.join(".");
121
69
  }
122
- function matchesPath(args, path9) {
123
- if (args.length < path9.length) return false;
124
- return path9.every((segment, index) => args[index] === segment);
70
+ function matchesPath(args, path7) {
71
+ if (args.length < path7.length) return false;
72
+ return path7.every((segment, index) => args[index] === segment);
125
73
  }
126
74
  function withFormatOption(options) {
127
75
  if (!options) {
@@ -614,9 +562,9 @@ var ApiClient = class {
614
562
  }
615
563
  return this.probePromise;
616
564
  }
617
- async request(method, path9, body) {
565
+ async request(method, path7, body) {
618
566
  await this.probeBasePath();
619
- const url = `${this.baseUrl}${path9}`;
567
+ const url = `${this.baseUrl}${path7}`;
620
568
  const serializedBody = body != null ? JSON.stringify(body) : void 0;
621
569
  const headers = {
622
570
  "Authorization": `Bearer ${this.apiKey}`,
@@ -1695,9 +1643,9 @@ async function gaConnect(project, opts) {
1695
1643
  propertyId: opts.propertyId
1696
1644
  };
1697
1645
  if (opts.keyFile) {
1698
- const fs10 = await import("fs");
1646
+ const fs8 = await import("fs");
1699
1647
  try {
1700
- const content = fs10.readFileSync(opts.keyFile, "utf-8");
1648
+ const content = fs8.readFileSync(opts.keyFile, "utf-8");
1701
1649
  JSON.parse(content);
1702
1650
  body.keyJson = content;
1703
1651
  } catch (e) {
@@ -2455,10 +2403,10 @@ Open this URL in your browser to authorize Google ${opts.type.toUpperCase()} acc
2455
2403
  console.log("(Ensure this URI is listed in your Google Cloud Console OAuth client's authorized redirect URIs)\n");
2456
2404
  }
2457
2405
  try {
2458
- const { spawn: spawn3 } = await import("child_process");
2406
+ const { spawn: spawn2 } = await import("child_process");
2459
2407
  const platform = process.platform;
2460
2408
  const [cmd, ...extraArgs] = platform === "darwin" ? ["open", authUrl] : platform === "win32" ? ["cmd", "/c", "start", "", authUrl] : ["xdg-open", authUrl];
2461
- spawn3(cmd, [...extraArgs], { detached: true, stdio: "ignore" }).unref();
2409
+ spawn2(cmd, [...extraArgs], { detached: true, stdio: "ignore" }).unref();
2462
2410
  console.log("(Browser opened automatically)");
2463
2411
  } catch {
2464
2412
  console.log("(Could not open browser automatically \u2014 please copy the URL above)");
@@ -3524,7 +3472,9 @@ var EVENT_DESCRIPTIONS = {
3524
3472
  "citation.lost": "A keyword lost its citation status",
3525
3473
  "citation.gained": "A keyword gained citation status",
3526
3474
  "run.completed": "A visibility run completed successfully",
3527
- "run.failed": "A visibility run failed"
3475
+ "run.failed": "A visibility run failed",
3476
+ "insight.critical": "A critical-severity insight was generated",
3477
+ "insight.high": "A high-severity insight was generated"
3528
3478
  };
3529
3479
  function listEvents(format) {
3530
3480
  const events = notificationEventSchema.options;
@@ -6917,12 +6867,12 @@ async function wordpressSetMeta(project, body) {
6917
6867
  printPageDetail(result);
6918
6868
  }
6919
6869
  async function wordpressBulkSetMeta(project, opts) {
6920
- const fs10 = await import("fs/promises");
6921
- const path9 = await import("path");
6922
- const filePath = path9.resolve(opts.from);
6870
+ const fs8 = await import("fs/promises");
6871
+ const path7 = await import("path");
6872
+ const filePath = path7.resolve(opts.from);
6923
6873
  let raw;
6924
6874
  try {
6925
- raw = await fs10.readFile(filePath, "utf8");
6875
+ raw = await fs8.readFile(filePath, "utf8");
6926
6876
  } catch {
6927
6877
  throw new CliError({
6928
6878
  code: "FILE_READ_ERROR",
@@ -7019,13 +6969,13 @@ async function wordpressSetSchema(project, body) {
7019
6969
  printManualAssist(`Schema update for "${body.slug}"`, result);
7020
6970
  }
7021
6971
  async function wordpressSchemaDeploy(project, opts) {
7022
- const fs10 = await import("fs/promises");
7023
- const path9 = await import("path");
6972
+ const fs8 = await import("fs/promises");
6973
+ const path7 = await import("path");
7024
6974
  const yaml = await import("yaml").catch(() => null);
7025
- const filePath = path9.resolve(opts.profile);
6975
+ const filePath = path7.resolve(opts.profile);
7026
6976
  let raw;
7027
6977
  try {
7028
- raw = await fs10.readFile(filePath, "utf8");
6978
+ raw = await fs8.readFile(filePath, "utf8");
7029
6979
  } catch {
7030
6980
  throw new CliError({
7031
6981
  code: "FILE_READ_ERROR",
@@ -7130,13 +7080,13 @@ async function wordpressOnboard(project, opts) {
7130
7080
  }
7131
7081
  let profileData;
7132
7082
  if (opts.profile) {
7133
- const fs10 = await import("fs/promises");
7134
- const path9 = await import("path");
7083
+ const fs8 = await import("fs/promises");
7084
+ const path7 = await import("path");
7135
7085
  const yaml = await import("yaml").catch(() => null);
7136
- const filePath = path9.resolve(opts.profile);
7086
+ const filePath = path7.resolve(opts.profile);
7137
7087
  let raw;
7138
7088
  try {
7139
- raw = await fs10.readFile(filePath, "utf8");
7089
+ raw = await fs8.readFile(filePath, "utf8");
7140
7090
  } catch {
7141
7091
  throw new CliError({
7142
7092
  code: "FILE_READ_ERROR",
@@ -7718,445 +7668,7 @@ var WORDPRESS_CLI_COMMANDS = [
7718
7668
  ];
7719
7669
 
7720
7670
  // src/commands/agent.ts
7721
- import path8 from "path";
7722
-
7723
- // src/agent-manager.ts
7724
- import { execFileSync, spawn as spawn2 } from "child_process";
7725
- import fs8 from "fs";
7726
7671
  import path6 from "path";
7727
- var log = createLogger("AgentManager");
7728
- var PROCESS_MARKER = "canonry-openclaw-gateway";
7729
- var AgentManager = class {
7730
- constructor(config, stateDir) {
7731
- this.config = config;
7732
- this.stateDir = stateDir;
7733
- this.processJsonPath = path6.join(stateDir, "process.json");
7734
- }
7735
- processJsonPath;
7736
- /**
7737
- * Check if the gateway process is running.
7738
- * Cleans up stale process.json if the process is dead or belongs to a
7739
- * different process (PID reuse).
7740
- */
7741
- status() {
7742
- const info = this.readProcessInfo();
7743
- if (!info) {
7744
- return { state: "stopped" };
7745
- }
7746
- if (info.marker !== PROCESS_MARKER) {
7747
- this.removeProcessJson();
7748
- return { state: "stopped" };
7749
- }
7750
- if (isProcessAlive2(info.pid) && this.verifyProcessIdentity(info.pid)) {
7751
- return {
7752
- state: "running",
7753
- pid: info.pid,
7754
- port: info.gatewayPort,
7755
- startedAt: info.startedAt
7756
- };
7757
- }
7758
- this.removeProcessJson();
7759
- return { state: "stopped" };
7760
- }
7761
- /**
7762
- * Start the OpenClaw gateway as a detached background process.
7763
- * Idempotent — no-op if already running.
7764
- * Waits briefly for the process to confirm it hasn't crashed on startup.
7765
- */
7766
- async start() {
7767
- const currentStatus = this.status();
7768
- if (currentStatus.state === "running") {
7769
- log.info("already.running", { pid: currentStatus.pid });
7770
- return;
7771
- }
7772
- const binary = this.config.binary ?? "openclaw";
7773
- const profile = this.config.profile ?? "aero";
7774
- const port = this.config.gatewayPort ?? 3579;
7775
- if (!fs8.existsSync(this.stateDir)) {
7776
- fs8.mkdirSync(this.stateDir, { recursive: true });
7777
- }
7778
- const logFile = path6.join(this.stateDir, "gateway.log");
7779
- const logFd = fs8.openSync(logFile, "a");
7780
- const dotEnv = this.loadDotEnv();
7781
- const child = spawn2(binary, ["--profile", profile, "gateway"], {
7782
- detached: true,
7783
- stdio: ["ignore", logFd, logFd],
7784
- env: {
7785
- ...process.env,
7786
- ...dotEnv,
7787
- OPENCLAW_PROFILE: profile,
7788
- OPENCLAW_GATEWAY_PORT: String(port),
7789
- OPENCLAW_STATE_DIR: this.stateDir
7790
- }
7791
- });
7792
- const startupResult = await new Promise((resolve) => {
7793
- let settled = false;
7794
- const settle = (r) => {
7795
- if (settled) return;
7796
- settled = true;
7797
- resolve(r);
7798
- };
7799
- child.on("error", (err) => settle({ error: err }));
7800
- child.on("exit", (code) => settle({ exitCode: code }));
7801
- setTimeout(() => settle({}), 500);
7802
- });
7803
- child.unref();
7804
- fs8.closeSync(logFd);
7805
- if (startupResult.error) {
7806
- throw new Error(`Failed to start OpenClaw gateway: ${startupResult.error.message}`);
7807
- }
7808
- if (startupResult.exitCode != null) {
7809
- throw new Error(`OpenClaw gateway exited immediately (code ${startupResult.exitCode}). Check ${path6.join(this.stateDir, "gateway.log")} for details.`);
7810
- }
7811
- if (child.pid == null) {
7812
- throw new Error("Failed to start OpenClaw gateway: no PID returned by spawn");
7813
- }
7814
- if (!isProcessAlive2(child.pid)) {
7815
- throw new Error(`OpenClaw gateway exited immediately after spawn. Check ${path6.join(this.stateDir, "gateway.log")} for details.`);
7816
- }
7817
- const processInfo = {
7818
- pid: child.pid,
7819
- gatewayPort: port,
7820
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
7821
- marker: PROCESS_MARKER
7822
- };
7823
- fs8.writeFileSync(this.processJsonPath, JSON.stringify(processInfo, null, 2), "utf-8");
7824
- log.info("started", { pid: child.pid, port });
7825
- }
7826
- /**
7827
- * Stop the gateway process.
7828
- * Uses DenchClaw escalation: SIGTERM → 800ms poll → SIGKILL.
7829
- * Idempotent — no-op if already stopped.
7830
- */
7831
- async stop() {
7832
- const info = this.readProcessInfo();
7833
- if (!info) return;
7834
- if (isProcessAlive2(info.pid) && info.marker === PROCESS_MARKER && this.verifyProcessIdentity(info.pid)) {
7835
- await terminateWithEscalation(info.pid);
7836
- }
7837
- this.removeProcessJson();
7838
- log.info("stopped", { pid: info.pid });
7839
- }
7840
- /**
7841
- * Stop the gateway, wipe the workspace directory, and prepare for re-seeding.
7842
- */
7843
- async reset() {
7844
- await this.stop();
7845
- const workspaceDir = path6.join(this.stateDir, "workspace");
7846
- if (fs8.existsSync(workspaceDir)) {
7847
- fs8.rmSync(workspaceDir, { recursive: true, force: true });
7848
- log.info("workspace.wiped", { dir: workspaceDir });
7849
- }
7850
- }
7851
- /**
7852
- * Verify that the PID actually belongs to an openclaw process by checking
7853
- * the full command line. Requires "openclaw" in the args to avoid matching
7854
- * unrelated Node processes after PID reuse.
7855
- */
7856
- verifyProcessIdentity(pid) {
7857
- try {
7858
- if (process.platform === "darwin") {
7859
- const out = execFileSync("ps", ["-p", String(pid), "-o", "args="], {
7860
- encoding: "utf-8",
7861
- timeout: 2e3
7862
- }).trim();
7863
- return out.includes("openclaw");
7864
- }
7865
- if (process.platform === "linux") {
7866
- const cmdline = fs8.readFileSync(`/proc/${pid}/cmdline`, "utf-8");
7867
- return cmdline.includes("openclaw");
7868
- }
7869
- return true;
7870
- } catch {
7871
- return false;
7872
- }
7873
- }
7874
- readProcessInfo() {
7875
- if (!fs8.existsSync(this.processJsonPath)) return null;
7876
- try {
7877
- return JSON.parse(fs8.readFileSync(this.processJsonPath, "utf-8"));
7878
- } catch {
7879
- return null;
7880
- }
7881
- }
7882
- removeProcessJson() {
7883
- try {
7884
- fs8.unlinkSync(this.processJsonPath);
7885
- } catch {
7886
- }
7887
- }
7888
- /** Parse a simple KEY=value dotenv file from the state dir. */
7889
- loadDotEnv() {
7890
- const envFile = path6.join(this.stateDir, ".env");
7891
- if (!fs8.existsSync(envFile)) return {};
7892
- const result = {};
7893
- for (const line of fs8.readFileSync(envFile, "utf-8").split("\n")) {
7894
- const trimmed = line.trim();
7895
- if (!trimmed || trimmed.startsWith("#")) continue;
7896
- const eq3 = trimmed.indexOf("=");
7897
- if (eq3 < 1) continue;
7898
- result[trimmed.slice(0, eq3)] = trimmed.slice(eq3 + 1);
7899
- }
7900
- return result;
7901
- }
7902
- };
7903
- function isProcessAlive2(pid) {
7904
- try {
7905
- process.kill(pid, 0);
7906
- return true;
7907
- } catch {
7908
- return false;
7909
- }
7910
- }
7911
- async function terminateWithEscalation(pid) {
7912
- try {
7913
- process.kill(pid, "SIGTERM");
7914
- } catch {
7915
- return;
7916
- }
7917
- const deadline = Date.now() + 800;
7918
- while (Date.now() < deadline) {
7919
- if (!isProcessAlive2(pid)) return;
7920
- await new Promise((resolve) => setTimeout(resolve, 100));
7921
- }
7922
- try {
7923
- process.kill(pid, "SIGKILL");
7924
- } catch {
7925
- }
7926
- }
7927
-
7928
- // src/agent-bootstrap.ts
7929
- import { execFileSync as execFileSync2, execSync } from "child_process";
7930
- import fs9 from "fs";
7931
- import os from "os";
7932
- import path7 from "path";
7933
- import { fileURLToPath } from "url";
7934
- var CACHE_TTL_MS = 5 * 60 * 1e3;
7935
- var cachedResult = null;
7936
- var cachedAt = 0;
7937
- function getAeroStateDir(profile = "aero") {
7938
- return path7.join(os.homedir(), `.openclaw-${profile}`);
7939
- }
7940
- async function detectOpenClaw(config) {
7941
- if (cachedResult && Date.now() - cachedAt < CACHE_TTL_MS) {
7942
- return cachedResult;
7943
- }
7944
- let result;
7945
- if (config?.binary) {
7946
- const version = probeVersion(config.binary);
7947
- if (version) {
7948
- result = { found: true, path: config.binary, version };
7949
- cachedResult = result;
7950
- cachedAt = Date.now();
7951
- return result;
7952
- }
7953
- }
7954
- const binaryPath = findInPath();
7955
- if (binaryPath) {
7956
- const version = probeVersion(binaryPath);
7957
- if (version) {
7958
- result = { found: true, path: binaryPath, version };
7959
- cachedResult = result;
7960
- cachedAt = Date.now();
7961
- return result;
7962
- }
7963
- }
7964
- result = { found: false };
7965
- cachedResult = result;
7966
- cachedAt = Date.now();
7967
- return result;
7968
- }
7969
- detectOpenClaw.resetCache = () => {
7970
- cachedResult = null;
7971
- cachedAt = 0;
7972
- };
7973
- function probeVersion(binaryPath) {
7974
- try {
7975
- const output = execFileSync2(binaryPath, ["--version"], {
7976
- timeout: 5e3,
7977
- encoding: "utf-8"
7978
- });
7979
- const match = output.toString().trim().match(/(\d+\.\d+\.\d+)/);
7980
- return match ? match[1] : output.toString().trim();
7981
- } catch {
7982
- return null;
7983
- }
7984
- }
7985
- function findInPath() {
7986
- const cmd = process.platform === "win32" ? "where" : "which";
7987
- try {
7988
- const output = execFileSync2(cmd, ["openclaw"], {
7989
- timeout: 5e3,
7990
- encoding: "utf-8"
7991
- });
7992
- return output.toString().trim().split("\n")[0] || null;
7993
- } catch {
7994
- return null;
7995
- }
7996
- }
7997
- async function installOpenClaw(opts) {
7998
- try {
7999
- execSync("npm install -g openclaw", {
8000
- timeout: 12e4,
8001
- stdio: opts?.silent ? "pipe" : "inherit"
8002
- });
8003
- } catch (err) {
8004
- return {
8005
- success: false,
8006
- error: err instanceof Error ? err.message : String(err)
8007
- };
8008
- }
8009
- detectOpenClaw.resetCache();
8010
- const detection = await detectOpenClaw();
8011
- if (!detection.found) {
8012
- return {
8013
- success: false,
8014
- error: "npm install succeeded but openclaw binary was not found in PATH"
8015
- };
8016
- }
8017
- return { success: true, detection };
8018
- }
8019
- function seedWorkspace(stateDir) {
8020
- const workspaceDir = path7.join(stateDir, "workspace");
8021
- fs9.mkdirSync(workspaceDir, { recursive: true });
8022
- const __dirname = path7.dirname(fileURLToPath(import.meta.url));
8023
- const assetsDir = path7.join(__dirname, "..", "assets", "agent-workspace");
8024
- if (!fs9.existsSync(assetsDir)) {
8025
- return;
8026
- }
8027
- copyDirRecursive(assetsDir, workspaceDir);
8028
- }
8029
- function initializeOpenClawProfile(binary, profile, workspaceDir) {
8030
- try {
8031
- execFileSync2(binary, [
8032
- "--profile",
8033
- profile,
8034
- "onboard",
8035
- "--non-interactive",
8036
- "--accept-risk",
8037
- "--mode",
8038
- "local",
8039
- "--workspace",
8040
- workspaceDir,
8041
- "--skip-channels",
8042
- "--skip-skills",
8043
- "--skip-health",
8044
- "--no-install-daemon"
8045
- ], { timeout: 3e4, stdio: "pipe" });
8046
- } catch (err) {
8047
- const stderr = err instanceof Error && "stderr" in err ? String(err.stderr) : "";
8048
- if (stderr.toLowerCase().includes("already")) return;
8049
- throw new CliError({
8050
- code: "AGENT_PROFILE_INIT_FAILED",
8051
- message: `Failed to initialize OpenClaw profile: ${stderr || (err instanceof Error ? err.message : String(err))}`,
8052
- displayMessage: `Failed to initialize OpenClaw profile "${profile}".`
8053
- });
8054
- }
8055
- }
8056
- function configureOpenClawGateway(binary, profile, gatewayPort) {
8057
- const entries = [
8058
- ["gateway.mode", "local", false],
8059
- ["gateway.port", String(gatewayPort), true]
8060
- ];
8061
- for (const [key, value, strict] of entries) {
8062
- try {
8063
- const args = ["--profile", profile, "config", "set", key, value];
8064
- if (strict) args.push("--strict-json");
8065
- execFileSync2(binary, args, { timeout: 1e4, stdio: "pipe" });
8066
- } catch (err) {
8067
- throw new CliError({
8068
- code: "AGENT_GATEWAY_CONFIG_FAILED",
8069
- message: `Failed to set ${key}=${value}: ${err instanceof Error ? err.message : String(err)}`,
8070
- displayMessage: `Failed to configure OpenClaw gateway (${key}).`
8071
- });
8072
- }
8073
- }
8074
- }
8075
- function setOpenClawModel(binary, profile, model) {
8076
- try {
8077
- execFileSync2(binary, [
8078
- "--profile",
8079
- profile,
8080
- "models",
8081
- "set",
8082
- model
8083
- ], { timeout: 1e4, stdio: "pipe" });
8084
- } catch (err) {
8085
- throw new CliError({
8086
- code: "AGENT_MODEL_SET_FAILED",
8087
- message: `Failed to set agent model to ${model}: ${err instanceof Error ? err.message : String(err)}`,
8088
- displayMessage: `Failed to set agent model to "${model}".`
8089
- });
8090
- }
8091
- }
8092
- function providerEnvVar(provider) {
8093
- const map = {
8094
- anthropic: "ANTHROPIC_API_KEY",
8095
- openai: "OPENAI_API_KEY",
8096
- google: "GOOGLE_API_KEY",
8097
- "google-vertex": "GOOGLE_API_KEY",
8098
- groq: "GROQ_API_KEY",
8099
- mistral: "MISTRAL_API_KEY",
8100
- xai: "XAI_API_KEY",
8101
- openrouter: "OPENROUTER_API_KEY",
8102
- cerebras: "CEREBRAS_API_KEY"
8103
- };
8104
- return map[provider] ?? `${provider.toUpperCase().replace(/-/g, "_")}_API_KEY`;
8105
- }
8106
- function writeAgentEnv(stateDir, key, value) {
8107
- const envFile = path7.join(stateDir, ".env");
8108
- let lines = [];
8109
- if (fs9.existsSync(envFile)) {
8110
- lines = fs9.readFileSync(envFile, "utf-8").split("\n");
8111
- }
8112
- const prefix = `${key}=`;
8113
- const idx = lines.findIndex((l) => l.startsWith(prefix));
8114
- const entry = `${key}=${value}`;
8115
- if (idx >= 0) {
8116
- lines[idx] = entry;
8117
- } else {
8118
- lines.push(entry);
8119
- }
8120
- while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
8121
- fs9.writeFileSync(envFile, lines.join("\n") + "\n", "utf-8");
8122
- }
8123
- function resolveAgentCredentials(opts) {
8124
- const provider = opts.agentProvider ?? "anthropic";
8125
- if (opts.agentKey) {
8126
- return { provider, key: opts.agentKey, model: opts.agentModel };
8127
- }
8128
- const envVar = providerEnvVar(provider);
8129
- const envKey = process.env[envVar];
8130
- if (envKey) {
8131
- return { provider, key: envKey, model: opts.agentModel };
8132
- }
8133
- const genericKey = process.env.CANONRY_AGENT_KEY;
8134
- if (genericKey) {
8135
- return { provider, key: genericKey, model: opts.agentModel };
8136
- }
8137
- const envFile = path7.join(opts.stateDir, ".env");
8138
- if (fs9.existsSync(envFile)) {
8139
- const hasKey = fs9.readFileSync(envFile, "utf-8").split("\n").some((l) => l.includes("_API_KEY="));
8140
- if (hasKey) {
8141
- return { provider, key: void 0, model: opts.agentModel };
8142
- }
8143
- }
8144
- return { provider, key: void 0, model: opts.agentModel };
8145
- }
8146
- function copyDirRecursive(src, dest) {
8147
- fs9.mkdirSync(dest, { recursive: true });
8148
- for (const entry of fs9.readdirSync(src, { withFileTypes: true })) {
8149
- const srcPath = path7.join(src, entry.name);
8150
- const destPath = path7.join(dest, entry.name);
8151
- if (entry.isDirectory()) {
8152
- copyDirRecursive(srcPath, destPath);
8153
- } else {
8154
- fs9.copyFileSync(srcPath, destPath);
8155
- }
8156
- }
8157
- }
8158
-
8159
- // src/commands/agent.ts
8160
7672
  function resolveStateDir(opts) {
8161
7673
  if (opts?.stateDir) return opts.stateDir;
8162
7674
  try {
@@ -8266,7 +7778,7 @@ async function agentSetup(opts) {
8266
7778
  autoStart: existingConfig.autoStart
8267
7779
  }
8268
7780
  });
8269
- initializeOpenClawProfile(detection.path, profile, path8.join(stateDir, "workspace"));
7781
+ initializeOpenClawProfile(detection.path, profile, path6.join(stateDir, "workspace"));
8270
7782
  configureOpenClawGateway(detection.path, profile, gatewayPort);
8271
7783
  const creds = agentLLM ?? resolveAgentCredentials({
8272
7784
  agentProvider: opts?.agentProvider,
@@ -8287,6 +7799,7 @@ async function agentSetup(opts) {
8287
7799
  }
8288
7800
  }
8289
7801
  seedWorkspace(stateDir);
7802
+ const attachSummary = await attachAgentWebhookToAllProjects(gatewayPort);
8290
7803
  if (opts?.format === "json") {
8291
7804
  console.log(JSON.stringify({
8292
7805
  state: "configured",
@@ -8294,16 +7807,117 @@ async function agentSetup(opts) {
8294
7807
  version: detection.version,
8295
7808
  profile,
8296
7809
  gatewayPort,
8297
- stateDir
7810
+ stateDir,
7811
+ attached: attachSummary
8298
7812
  }, null, 2));
8299
7813
  } else {
8300
7814
  console.log(`OpenClaw: ${detection.path} (${detection.version})`);
8301
7815
  console.log(`Profile: ${profile}`);
8302
7816
  console.log(`Gateway port: ${gatewayPort}`);
8303
7817
  console.log(`State dir: ${stateDir}`);
7818
+ if (attachSummary.attached > 0 || attachSummary.alreadyAttached > 0) {
7819
+ console.log(
7820
+ `Agent webhook: ${attachSummary.attached} attached, ${attachSummary.alreadyAttached} already present (via ${attachSummary.path})`
7821
+ );
7822
+ }
8304
7823
  console.log("Agent setup complete.");
8305
7824
  }
8306
7825
  }
7826
+ async function attachAgentWebhookToAllProjects(gatewayPort) {
7827
+ let config;
7828
+ try {
7829
+ config = loadConfig();
7830
+ } catch {
7831
+ return { path: "skipped", attached: 0, alreadyAttached: 0 };
7832
+ }
7833
+ try {
7834
+ const client = createApiClient();
7835
+ const projectList = await client.listProjects();
7836
+ const agentUrl = buildAgentWebhookUrl(gatewayPort);
7837
+ let attached2 = 0;
7838
+ let alreadyAttached2 = 0;
7839
+ for (const project of projectList) {
7840
+ const existing = await client.listNotifications(project.name);
7841
+ if (existing.some((n) => n.source === "agent")) {
7842
+ alreadyAttached2++;
7843
+ continue;
7844
+ }
7845
+ await client.createNotification(project.name, {
7846
+ channel: "webhook",
7847
+ url: agentUrl,
7848
+ events: [...AGENT_WEBHOOK_EVENTS],
7849
+ source: "agent"
7850
+ });
7851
+ attached2++;
7852
+ }
7853
+ return { path: "api", attached: attached2, alreadyAttached: alreadyAttached2 };
7854
+ } catch (err) {
7855
+ if (!isConnectionError(err)) throw err;
7856
+ }
7857
+ const db = createClient(config.database);
7858
+ migrate(db);
7859
+ const rows = db.select({ id: projects.id }).from(projects).all();
7860
+ let attached = 0;
7861
+ let alreadyAttached = 0;
7862
+ for (const row of rows) {
7863
+ const result = attachAgentWebhookDirect(db, row.id, gatewayPort);
7864
+ if (result === "attached") attached++;
7865
+ else alreadyAttached++;
7866
+ }
7867
+ return { path: "db", attached, alreadyAttached };
7868
+ }
7869
+ function isConnectionError(err) {
7870
+ if (!(err instanceof Error)) return false;
7871
+ const msg = err.message.toLowerCase();
7872
+ const code = err.code ?? "";
7873
+ return code === "CONNECTION_ERROR" || code === "ECONNREFUSED" || code === "ENOTFOUND" || msg.includes("could not connect") || msg.includes("econnrefused") || msg.includes("fetch failed") || msg.includes("connection refused");
7874
+ }
7875
+ async function agentAttach(opts) {
7876
+ const config = loadConfig();
7877
+ const gatewayPort = config.agent?.gatewayPort ?? 3579;
7878
+ const agentUrl = buildAgentWebhookUrl(gatewayPort);
7879
+ const client = createApiClient();
7880
+ const existing = await client.listNotifications(opts.project);
7881
+ const hasAgent = existing.some((n) => n.source === "agent");
7882
+ if (hasAgent) {
7883
+ if (opts.format === "json") {
7884
+ console.log(JSON.stringify({ status: "already-attached", project: opts.project }));
7885
+ } else {
7886
+ console.log(`Agent webhook already attached to "${opts.project}"`);
7887
+ }
7888
+ return;
7889
+ }
7890
+ const result = await client.createNotification(opts.project, {
7891
+ channel: "webhook",
7892
+ url: agentUrl,
7893
+ events: [...AGENT_WEBHOOK_EVENTS],
7894
+ source: "agent"
7895
+ });
7896
+ if (opts.format === "json") {
7897
+ console.log(JSON.stringify({ status: "attached", project: opts.project, notificationId: result.id }));
7898
+ } else {
7899
+ console.log(`Agent webhook attached to "${opts.project}"`);
7900
+ }
7901
+ }
7902
+ async function agentDetach(opts) {
7903
+ const client = createApiClient();
7904
+ const existing = await client.listNotifications(opts.project);
7905
+ const agentNotif = existing.find((n) => n.source === "agent");
7906
+ if (!agentNotif) {
7907
+ if (opts.format === "json") {
7908
+ console.log(JSON.stringify({ status: "not-attached", project: opts.project }));
7909
+ } else {
7910
+ console.log(`No agent webhook found on "${opts.project}"`);
7911
+ }
7912
+ return;
7913
+ }
7914
+ await client.deleteNotification(opts.project, agentNotif.id);
7915
+ if (opts.format === "json") {
7916
+ console.log(JSON.stringify({ status: "detached", project: opts.project }));
7917
+ } else {
7918
+ console.log(`Agent webhook detached from "${opts.project}"`);
7919
+ }
7920
+ }
8307
7921
  async function autoInstallOrFail(format) {
8308
7922
  if (format !== "json") {
8309
7923
  console.log("OpenClaw not found, installing via npm...");
@@ -8369,6 +7983,34 @@ var AGENT_CLI_COMMANDS = [
8369
7983
  await agentReset({ format: input.format });
8370
7984
  }
8371
7985
  },
7986
+ {
7987
+ path: ["agent", "attach"],
7988
+ usage: "canonry agent attach <project> [--format json]",
7989
+ options: {},
7990
+ run: async (input) => {
7991
+ const project = input.positionals[0];
7992
+ if (!project) {
7993
+ console.error("Usage: canonry agent attach <project>");
7994
+ process.exitCode = 1;
7995
+ return;
7996
+ }
7997
+ await agentAttach({ project, format: input.format });
7998
+ }
7999
+ },
8000
+ {
8001
+ path: ["agent", "detach"],
8002
+ usage: "canonry agent detach <project> [--format json]",
8003
+ options: {},
8004
+ run: async (input) => {
8005
+ const project = input.positionals[0];
8006
+ if (!project) {
8007
+ console.error("Usage: canonry agent detach <project>");
8008
+ process.exitCode = 1;
8009
+ return;
8010
+ }
8011
+ await agentDetach({ project, format: input.format });
8012
+ }
8013
+ },
8372
8014
  {
8373
8015
  path: ["agent", "setup"],
8374
8016
  usage: "canonry agent setup [--agent-provider <id>] [--agent-key <key>] [--agent-model <model>] [--gateway-port <port>] [--gemini-key <key>] [--openai-key <key>] [--claude-key <key>] [--perplexity-key <key>] [--format json]",