@ainyc/canonry 2.6.0 → 2.8.2

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
@@ -17,7 +17,7 @@ import {
17
17
  setGoogleAuthConfig,
18
18
  showFirstRunNotice,
19
19
  trackEvent
20
- } from "./chunk-LF4O276A.js";
20
+ } from "./chunk-MGBXRWLX.js";
21
21
  import {
22
22
  CcReleaseSyncStatuses,
23
23
  CliError,
@@ -40,7 +40,7 @@ import {
40
40
  saveConfig,
41
41
  saveConfigPatch,
42
42
  usageError
43
- } from "./chunk-3DUTT6H2.js";
43
+ } from "./chunk-FPZUQADO.js";
44
44
  import {
45
45
  apiKeys,
46
46
  competitors,
@@ -61,9 +61,9 @@ import { parseArgs } from "util";
61
61
  function commandId(spec) {
62
62
  return spec.path.join(".");
63
63
  }
64
- function matchesPath(args, path6) {
65
- if (args.length < path6.length) return false;
66
- return path6.every((segment, index) => args[index] === segment);
64
+ function matchesPath(args, path8) {
65
+ if (args.length < path8.length) return false;
66
+ return path8.every((segment, index) => args[index] === segment);
67
67
  }
68
68
  function withFormatOption(options) {
69
69
  if (!options) {
@@ -1569,9 +1569,9 @@ async function gaConnect(project, opts) {
1569
1569
  propertyId: opts.propertyId
1570
1570
  };
1571
1571
  if (opts.keyFile) {
1572
- const fs8 = await import("fs");
1572
+ const fs9 = await import("fs");
1573
1573
  try {
1574
- const content = fs8.readFileSync(opts.keyFile, "utf-8");
1574
+ const content = fs9.readFileSync(opts.keyFile, "utf-8");
1575
1575
  JSON.parse(content);
1576
1576
  body.keyJson = content;
1577
1577
  } catch (e) {
@@ -2187,13 +2187,15 @@ async function addCompetitors(project, domains, format) {
2187
2187
  const client = getClient5();
2188
2188
  const existing = await client.listCompetitors(project);
2189
2189
  const existingDomains = existing.map((c) => c.domain);
2190
- const addedDomains = domains.filter((domain) => !existingDomains.includes(domain));
2191
- const allDomains = [.../* @__PURE__ */ new Set([...existingDomains, ...domains])];
2192
- await client.putCompetitors(project, allDomains);
2190
+ const existingSet = new Set(existingDomains);
2191
+ const requested = new Set(uniqueStrings(domains));
2192
+ const current = await client.appendCompetitors(project, domains);
2193
+ const currentDomains = current.map((c) => c.domain);
2194
+ const addedDomains = currentDomains.filter((domain) => requested.has(domain) && !existingSet.has(domain));
2193
2195
  if (format === "json") {
2194
2196
  console.log(JSON.stringify({
2195
2197
  project,
2196
- domains: allDomains,
2198
+ domains: currentDomains,
2197
2199
  addedDomains,
2198
2200
  addedCount: addedDomains.length
2199
2201
  }, null, 2));
@@ -2205,6 +2207,35 @@ async function addCompetitors(project, domains, format) {
2205
2207
  console.log(`Added ${addedDomains.length} competitor(s) to "${project}".`);
2206
2208
  }
2207
2209
  }
2210
+ async function removeCompetitors(project, domains, format) {
2211
+ const client = getClient5();
2212
+ const existing = await client.listCompetitors(project);
2213
+ const existingDomains = existing.map((c) => c.domain);
2214
+ const requested = new Set(uniqueStrings(domains));
2215
+ const current = await client.deleteCompetitors(project, domains);
2216
+ const currentSet = new Set(current.map((c) => c.domain));
2217
+ const removedDomains = existingDomains.filter((domain) => requested.has(domain) && !currentSet.has(domain));
2218
+ if (format === "json") {
2219
+ console.log(JSON.stringify({
2220
+ project,
2221
+ domains: current.map((c) => c.domain),
2222
+ removedDomains,
2223
+ removedCount: removedDomains.length
2224
+ }, null, 2));
2225
+ return;
2226
+ }
2227
+ console.log(`Removed ${removedDomains.length} competitor(s) from "${project}".`);
2228
+ }
2229
+ function uniqueStrings(values) {
2230
+ const seen = /* @__PURE__ */ new Set();
2231
+ const result = [];
2232
+ for (const value of values) {
2233
+ if (seen.has(value)) continue;
2234
+ seen.add(value);
2235
+ result.push(value);
2236
+ }
2237
+ return result;
2238
+ }
2208
2239
  async function listCompetitors(project, format) {
2209
2240
  const client = getClient5();
2210
2241
  const comps = await client.listCompetitors(project);
@@ -2243,6 +2274,42 @@ var COMPETITOR_CLI_COMMANDS = [
2243
2274
  await addCompetitors(project, domains, input.format);
2244
2275
  }
2245
2276
  },
2277
+ {
2278
+ path: ["competitor", "remove"],
2279
+ usage: "canonry competitor remove <project> <domain...> [--format json]",
2280
+ run: async (input) => {
2281
+ const project = requireProject(input, "competitor.remove", "canonry competitor remove <project> <domain...> [--format json]");
2282
+ const domains = input.positionals.slice(1);
2283
+ if (domains.length === 0) {
2284
+ throw usageError("Error: project name and at least one domain required\nUsage: canonry competitor remove <project> <domain...> [--format json]", {
2285
+ message: "project name and at least one domain required",
2286
+ details: {
2287
+ command: "competitor.remove",
2288
+ usage: "canonry competitor remove <project> <domain...> [--format json]"
2289
+ }
2290
+ });
2291
+ }
2292
+ await removeCompetitors(project, domains, input.format);
2293
+ }
2294
+ },
2295
+ {
2296
+ path: ["competitor", "delete"],
2297
+ usage: "canonry competitor delete <project> <domain...> [--format json]",
2298
+ run: async (input) => {
2299
+ const project = requireProject(input, "competitor.delete", "canonry competitor delete <project> <domain...> [--format json]");
2300
+ const domains = input.positionals.slice(1);
2301
+ if (domains.length === 0) {
2302
+ throw usageError("Error: project name and at least one domain required\nUsage: canonry competitor delete <project> <domain...> [--format json]", {
2303
+ message: "project name and at least one domain required",
2304
+ details: {
2305
+ command: "competitor.delete",
2306
+ usage: "canonry competitor delete <project> <domain...> [--format json]"
2307
+ }
2308
+ });
2309
+ }
2310
+ await removeCompetitors(project, domains, input.format);
2311
+ }
2312
+ },
2246
2313
  {
2247
2314
  path: ["competitor", "list"],
2248
2315
  usage: "canonry competitor list <project> [--format json]",
@@ -2253,12 +2320,12 @@ var COMPETITOR_CLI_COMMANDS = [
2253
2320
  },
2254
2321
  {
2255
2322
  path: ["competitor"],
2256
- usage: "canonry competitor <add|list> <project> [args]",
2323
+ usage: "canonry competitor <add|remove|delete|list> <project> [args]",
2257
2324
  run: async (input) => {
2258
2325
  unknownSubcommand(input.positionals[0], {
2259
2326
  command: "competitor",
2260
- usage: "canonry competitor <add|list> <project> [args]",
2261
- available: ["add", "list"]
2327
+ usage: "canonry competitor <add|remove|delete|list> <project> [args]",
2328
+ available: ["add", "remove", "delete", "list"]
2262
2329
  });
2263
2330
  }
2264
2331
  }
@@ -3124,6 +3191,19 @@ async function addKeywords(project, keywords, format) {
3124
3191
  }
3125
3192
  console.log(`Added ${keywords.length} key phrase(s) to "${project}".`);
3126
3193
  }
3194
+ async function replaceKeywords(project, keywords, format) {
3195
+ const client = getClient7();
3196
+ await client.putKeywords(project, keywords);
3197
+ if (format === "json") {
3198
+ console.log(JSON.stringify({
3199
+ project,
3200
+ keywords,
3201
+ replacedCount: keywords.length
3202
+ }, null, 2));
3203
+ return;
3204
+ }
3205
+ console.log(`Set ${keywords.length} key phrase(s) for "${project}".`);
3206
+ }
3127
3207
  async function removeKeywords(project, keywords, format) {
3128
3208
  const client = getClient7();
3129
3209
  const existing = await client.listKeywords(project);
@@ -3252,6 +3332,24 @@ var KEYWORD_CLI_COMMANDS = [
3252
3332
  await addKeywords(project, keywords, input.format);
3253
3333
  }
3254
3334
  },
3335
+ {
3336
+ path: ["keyword", "replace"],
3337
+ usage: "canonry keyword replace <project> <kw...> [--format json]",
3338
+ run: async (input) => {
3339
+ const project = requireProject(input, "keyword.replace", "canonry keyword replace <project> <kw...> [--format json]");
3340
+ const keywords = input.positionals.slice(1);
3341
+ if (keywords.length === 0) {
3342
+ throw usageError("Error: project name and at least one key phrase required\nUsage: canonry keyword replace <project> <kw...> [--format json]", {
3343
+ message: "project name and at least one key phrase required",
3344
+ details: {
3345
+ command: "keyword.replace",
3346
+ usage: "canonry keyword replace <project> <kw...> [--format json]"
3347
+ }
3348
+ });
3349
+ }
3350
+ await replaceKeywords(project, keywords, input.format);
3351
+ }
3352
+ },
3255
3353
  {
3256
3354
  path: ["keyword", "remove"],
3257
3355
  usage: "canonry keyword remove <project> <kw...> [--format json]",
@@ -3341,12 +3439,324 @@ var KEYWORD_CLI_COMMANDS = [
3341
3439
  },
3342
3440
  {
3343
3441
  path: ["keyword"],
3344
- usage: "canonry keyword <add|remove|delete|list|import|generate> <project> [args]",
3442
+ usage: "canonry keyword <add|replace|remove|delete|list|import|generate> <project> [args]",
3345
3443
  run: async (input) => {
3346
3444
  unknownSubcommand(input.positionals[0], {
3347
3445
  command: "keyword",
3348
- usage: "canonry keyword <add|remove|delete|list|import|generate> <project> [args]",
3349
- available: ["add", "remove", "delete", "list", "import", "generate"]
3446
+ usage: "canonry keyword <add|replace|remove|delete|list|import|generate> <project> [args]",
3447
+ available: ["add", "replace", "remove", "delete", "list", "import", "generate"]
3448
+ });
3449
+ }
3450
+ }
3451
+ ];
3452
+
3453
+ // src/commands/mcp.ts
3454
+ import fs2 from "fs";
3455
+ import path2 from "path";
3456
+ import { createRequire } from "module";
3457
+
3458
+ // src/mcp-clients.ts
3459
+ import os from "os";
3460
+ import path from "path";
3461
+ var CLAUDE_DESKTOP_CONFIG_FILENAME = "claude_desktop_config.json";
3462
+ function homeRelative(...segments) {
3463
+ return path.join(os.homedir(), ...segments);
3464
+ }
3465
+ function claudeDesktopConfigPath() {
3466
+ switch (process.platform) {
3467
+ case "darwin":
3468
+ return homeRelative("Library", "Application Support", "Claude", CLAUDE_DESKTOP_CONFIG_FILENAME);
3469
+ case "win32": {
3470
+ const appData = process.env.APPDATA ?? homeRelative("AppData", "Roaming");
3471
+ return path.join(appData, "Claude", CLAUDE_DESKTOP_CONFIG_FILENAME);
3472
+ }
3473
+ default:
3474
+ return homeRelative(".config", "Claude", CLAUDE_DESKTOP_CONFIG_FILENAME);
3475
+ }
3476
+ }
3477
+ function cursorConfigPath() {
3478
+ return homeRelative(".cursor", "mcp.json");
3479
+ }
3480
+ function codexConfigPath() {
3481
+ return homeRelative(".codex", "config.toml");
3482
+ }
3483
+ var SUPPORTED_MCP_CLIENTS = [
3484
+ {
3485
+ id: "claude-desktop",
3486
+ label: "Claude Desktop",
3487
+ format: "json-mcp-servers",
3488
+ configPath: claudeDesktopConfigPath,
3489
+ installSupported: true
3490
+ },
3491
+ {
3492
+ id: "cursor",
3493
+ label: "Cursor",
3494
+ format: "json-mcp-servers",
3495
+ configPath: cursorConfigPath,
3496
+ installSupported: true
3497
+ },
3498
+ {
3499
+ id: "codex",
3500
+ label: "Codex CLI",
3501
+ format: "toml-mcp-servers",
3502
+ configPath: codexConfigPath,
3503
+ installSupported: false
3504
+ }
3505
+ ];
3506
+ function findMcpClient(id) {
3507
+ return SUPPORTED_MCP_CLIENTS.find((client) => client.id === id);
3508
+ }
3509
+ function listMcpClientIds() {
3510
+ return SUPPORTED_MCP_CLIENTS.map((client) => client.id);
3511
+ }
3512
+
3513
+ // src/commands/mcp.ts
3514
+ var _require = createRequire(import.meta.url);
3515
+ function resolveCanonryMcpBin() {
3516
+ const packageJsonPath = _require.resolve("../package.json");
3517
+ const packageRoot = path2.dirname(packageJsonPath);
3518
+ const pkg = _require("../package.json");
3519
+ const relativeBin = pkg.bin?.["canonry-mcp"];
3520
+ if (!relativeBin) {
3521
+ throw new CliError({
3522
+ code: "INTERNAL_ERROR",
3523
+ message: "Could not resolve canonry-mcp bin path from package.json",
3524
+ exitCode: 2
3525
+ });
3526
+ }
3527
+ return path2.resolve(packageRoot, relativeBin);
3528
+ }
3529
+ function buildEntry(opts) {
3530
+ const target = opts.binPath ?? resolveCanonryMcpBin();
3531
+ const flagArgs = opts.readOnly ? ["--read-only"] : [];
3532
+ const platform = opts.platform ?? process.platform;
3533
+ if (platform === "win32" && target.toLowerCase().endsWith(".mjs")) {
3534
+ return { command: "node", args: [target, ...flagArgs] };
3535
+ }
3536
+ return { command: target, args: flagArgs };
3537
+ }
3538
+ function entryArgs(entry) {
3539
+ return Array.isArray(entry.args) ? entry.args : [];
3540
+ }
3541
+ function entriesEqual(a, b) {
3542
+ if (a.command !== b.command) return false;
3543
+ const aArgs = entryArgs(a);
3544
+ const bArgs = entryArgs(b);
3545
+ return aArgs.length === bArgs.length && aArgs.every((arg, i) => arg === bArgs[i]);
3546
+ }
3547
+ function renderJsonSnippet(serverName, entry, format) {
3548
+ const key = format === "json-context-servers" ? "context_servers" : "mcpServers";
3549
+ return JSON.stringify({ [key]: { [serverName]: entry } }, null, 2);
3550
+ }
3551
+ function renderTomlSnippet(serverName, entry) {
3552
+ const argsLine = entry.args.length ? `args = [${entry.args.map((arg) => JSON.stringify(arg)).join(", ")}]` : "args = []";
3553
+ return [`[mcp_servers.${serverName}]`, `command = ${JSON.stringify(entry.command)}`, argsLine, ""].join("\n");
3554
+ }
3555
+ function renderClientSnippet(client, serverName, entry) {
3556
+ if (client.format === "toml-mcp-servers") return renderTomlSnippet(serverName, entry);
3557
+ return renderJsonSnippet(serverName, entry, client.format);
3558
+ }
3559
+ function readJsonConfig(configPath) {
3560
+ if (!fs2.existsSync(configPath)) return {};
3561
+ const raw = fs2.readFileSync(configPath, "utf-8").trim();
3562
+ if (!raw) return {};
3563
+ try {
3564
+ const parsed = JSON.parse(raw);
3565
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3566
+ throw new Error("Config root must be a JSON object");
3567
+ }
3568
+ return parsed;
3569
+ } catch (err) {
3570
+ throw new CliError({
3571
+ code: "VALIDATION_ERROR",
3572
+ message: `Failed to parse ${configPath}: ${err instanceof Error ? err.message : String(err)}`,
3573
+ exitCode: 1,
3574
+ details: { configPath }
3575
+ });
3576
+ }
3577
+ }
3578
+ function writeJsonConfig(configPath, value) {
3579
+ fs2.mkdirSync(path2.dirname(configPath), { recursive: true });
3580
+ fs2.writeFileSync(configPath, `${JSON.stringify(value, null, 2)}
3581
+ `, "utf-8");
3582
+ }
3583
+ function backupConfigIfPresent(configPath) {
3584
+ if (!fs2.existsSync(configPath)) return void 0;
3585
+ const backupPath = `${configPath}.canonry.bak`;
3586
+ fs2.copyFileSync(configPath, backupPath);
3587
+ return backupPath;
3588
+ }
3589
+ function findClientOrThrow(id) {
3590
+ const client = findMcpClient(id);
3591
+ if (client) return client;
3592
+ throw new CliError({
3593
+ code: "VALIDATION_ERROR",
3594
+ message: `Unknown MCP client "${id}". Supported: ${listMcpClientIds().join(", ")}`,
3595
+ exitCode: 1,
3596
+ details: { client: id, supportedClients: listMcpClientIds() }
3597
+ });
3598
+ }
3599
+ async function installMcp(opts) {
3600
+ const client = findClientOrThrow(opts.client);
3601
+ const serverName = opts.name?.trim() || "canonry";
3602
+ const configPath = opts.configPath ?? client.configPath();
3603
+ const entry = buildEntry({ binPath: opts.binPath, readOnly: opts.readOnly, platform: opts.platform });
3604
+ if (!client.installSupported) {
3605
+ const snippet = renderClientSnippet(client, serverName, entry);
3606
+ const result2 = {
3607
+ client: client.id,
3608
+ configPath,
3609
+ serverName,
3610
+ entry,
3611
+ status: "snippet-only",
3612
+ snippet,
3613
+ message: `Auto-install is not supported for ${client.label}. Add the snippet below to ${configPath}.`
3614
+ };
3615
+ emitInstallResult(result2, opts.format);
3616
+ return result2;
3617
+ }
3618
+ const containerKey = client.format === "json-context-servers" ? "context_servers" : "mcpServers";
3619
+ const existing = readJsonConfig(configPath);
3620
+ const existingContainer = existing[containerKey] ?? {};
3621
+ const existingEntry = existingContainer[serverName];
3622
+ if (existingEntry && entriesEqual(existingEntry, entry)) {
3623
+ const result2 = {
3624
+ client: client.id,
3625
+ configPath,
3626
+ serverName,
3627
+ entry,
3628
+ status: "already-installed",
3629
+ message: `${client.label} already has a "${serverName}" entry pointing to canonry-mcp.`
3630
+ };
3631
+ emitInstallResult(result2, opts.format);
3632
+ return result2;
3633
+ }
3634
+ const status = existingEntry ? "updated" : "installed";
3635
+ if (opts.dryRun) {
3636
+ const result2 = {
3637
+ client: client.id,
3638
+ configPath,
3639
+ serverName,
3640
+ entry,
3641
+ status: "dry-run",
3642
+ snippet: renderClientSnippet(client, serverName, entry),
3643
+ message: `Would ${status === "installed" ? "install" : "update"} "${serverName}" in ${configPath}.`
3644
+ };
3645
+ emitInstallResult(result2, opts.format);
3646
+ return result2;
3647
+ }
3648
+ const backupPath = backupConfigIfPresent(configPath);
3649
+ const next = {
3650
+ ...existing,
3651
+ [containerKey]: { ...existingContainer, [serverName]: entry }
3652
+ };
3653
+ writeJsonConfig(configPath, next);
3654
+ const result = {
3655
+ client: client.id,
3656
+ configPath,
3657
+ serverName,
3658
+ entry,
3659
+ status,
3660
+ backupPath,
3661
+ message: `${status === "installed" ? "Installed" : "Updated"} "${serverName}" in ${client.label} at ${configPath}. Restart ${client.label} to load it.`
3662
+ };
3663
+ emitInstallResult(result, opts.format);
3664
+ return result;
3665
+ }
3666
+ async function printMcpConfig(opts) {
3667
+ const client = findClientOrThrow(opts.client);
3668
+ const serverName = opts.name?.trim() || "canonry";
3669
+ const entry = buildEntry({ binPath: opts.binPath, readOnly: opts.readOnly, platform: opts.platform });
3670
+ const snippet = renderClientSnippet(client, serverName, entry);
3671
+ if (opts.format === "json") {
3672
+ console.log(JSON.stringify({
3673
+ client: client.id,
3674
+ configPath: client.configPath(),
3675
+ serverName,
3676
+ entry,
3677
+ snippet
3678
+ }, null, 2));
3679
+ return;
3680
+ }
3681
+ console.log(`# ${client.label} \u2014 paste into ${client.configPath()}`);
3682
+ console.log(snippet);
3683
+ }
3684
+ function emitInstallResult(result, format) {
3685
+ if (format === "json") {
3686
+ console.log(JSON.stringify(result, null, 2));
3687
+ return;
3688
+ }
3689
+ console.log(result.message);
3690
+ if (result.backupPath) console.log(`Backup: ${result.backupPath}`);
3691
+ if (result.snippet && (result.status === "snippet-only" || result.status === "dry-run")) {
3692
+ console.log();
3693
+ console.log(result.snippet);
3694
+ }
3695
+ }
3696
+
3697
+ // src/cli-commands/mcp.ts
3698
+ var CLIENT_LIST = listMcpClientIds().join("|");
3699
+ var MCP_CLI_COMMANDS = [
3700
+ {
3701
+ path: ["mcp", "install"],
3702
+ usage: `canonry mcp install --client ${CLIENT_LIST} [--name <server>] [--read-only] [--dry-run] [--config-path <path>] [--format json]`,
3703
+ options: {
3704
+ client: stringOption(),
3705
+ name: stringOption(),
3706
+ "read-only": { type: "boolean" },
3707
+ "dry-run": { type: "boolean" },
3708
+ "config-path": stringOption()
3709
+ },
3710
+ run: async (input) => {
3711
+ const usage = `canonry mcp install --client ${CLIENT_LIST} [--name <server>] [--read-only] [--dry-run] [--config-path <path>] [--format json]`;
3712
+ const client = requireStringOption(input, "client", {
3713
+ command: "mcp.install",
3714
+ usage,
3715
+ message: "--client is required",
3716
+ details: { flag: "client", supportedClients: listMcpClientIds() }
3717
+ });
3718
+ await installMcp({
3719
+ client,
3720
+ name: getString(input.values, "name"),
3721
+ readOnly: getBoolean(input.values, "read-only"),
3722
+ dryRun: getBoolean(input.values, "dry-run"),
3723
+ configPath: getString(input.values, "config-path"),
3724
+ format: input.format
3725
+ });
3726
+ }
3727
+ },
3728
+ {
3729
+ path: ["mcp", "config"],
3730
+ usage: `canonry mcp config --client ${CLIENT_LIST} [--name <server>] [--read-only] [--format json]`,
3731
+ options: {
3732
+ client: stringOption(),
3733
+ name: stringOption(),
3734
+ "read-only": { type: "boolean" }
3735
+ },
3736
+ run: async (input) => {
3737
+ const usage = `canonry mcp config --client ${CLIENT_LIST} [--name <server>] [--read-only] [--format json]`;
3738
+ const client = requireStringOption(input, "client", {
3739
+ command: "mcp.config",
3740
+ usage,
3741
+ message: "--client is required",
3742
+ details: { flag: "client", supportedClients: listMcpClientIds() }
3743
+ });
3744
+ await printMcpConfig({
3745
+ client,
3746
+ name: getString(input.values, "name"),
3747
+ readOnly: getBoolean(input.values, "read-only"),
3748
+ format: input.format
3749
+ });
3750
+ }
3751
+ },
3752
+ {
3753
+ path: ["mcp"],
3754
+ usage: "canonry mcp <install|config> [args]",
3755
+ run: async (input) => {
3756
+ unknownSubcommand(input.positionals[0], {
3757
+ command: "mcp",
3758
+ usage: "canonry mcp <install|config> [args]",
3759
+ available: ["install", "config"]
3350
3760
  });
3351
3761
  }
3352
3762
  }
@@ -3520,13 +3930,13 @@ var NOTIFY_CLI_COMMANDS = [
3520
3930
  ];
3521
3931
 
3522
3932
  // src/commands/apply.ts
3523
- import fs2 from "fs";
3933
+ import fs3 from "fs";
3524
3934
  import { parseAllDocuments } from "yaml";
3525
3935
  async function applyConfigFile(filePath) {
3526
- if (!fs2.existsSync(filePath)) {
3936
+ if (!fs3.existsSync(filePath)) {
3527
3937
  throw new Error(`File not found: ${filePath}`);
3528
3938
  }
3529
- const content = fs2.readFileSync(filePath, "utf-8");
3939
+ const content = fs3.readFileSync(filePath, "utf-8");
3530
3940
  const docs = parseAllDocuments(content);
3531
3941
  const client = createApiClient();
3532
3942
  const errors = [];
@@ -4970,12 +5380,12 @@ Usage: canonry settings provider ${name} --api-key <key> [--model <model>] [--ma
4970
5380
  ];
4971
5381
 
4972
5382
  // src/commands/snapshot.ts
4973
- import fs4 from "fs";
4974
- import path2 from "path";
5383
+ import fs5 from "fs";
5384
+ import path4 from "path";
4975
5385
 
4976
5386
  // src/snapshot-pdf.ts
4977
- import fs3 from "fs";
4978
- import path from "path";
5387
+ import fs4 from "fs";
5388
+ import path3 from "path";
4979
5389
  import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
4980
5390
  var PAGE_WIDTH = 612;
4981
5391
  var PAGE_HEIGHT = 792;
@@ -5184,9 +5594,9 @@ async function writeSnapshotPdf(report, outputPath) {
5184
5594
  renderCompetitors(pdf, report);
5185
5595
  renderQueries(pdf, report);
5186
5596
  const bytes = await doc.save();
5187
- const resolvedPath = path.resolve(outputPath);
5188
- fs3.mkdirSync(path.dirname(resolvedPath), { recursive: true });
5189
- fs3.writeFileSync(resolvedPath, bytes);
5597
+ const resolvedPath = path3.resolve(outputPath);
5598
+ fs4.mkdirSync(path3.dirname(resolvedPath), { recursive: true });
5599
+ fs4.writeFileSync(resolvedPath, bytes);
5190
5600
  return resolvedPath;
5191
5601
  }
5192
5602
  function renderCover(pdf, report) {
@@ -5344,9 +5754,9 @@ Markdown saved: ${savedMdPath}`);
5344
5754
  PDF saved: ${savedPdfPath}`);
5345
5755
  }
5346
5756
  function writeSnapshotMarkdown(report, outputPath) {
5347
- const resolvedPath = path2.resolve(outputPath);
5348
- fs4.mkdirSync(path2.dirname(resolvedPath), { recursive: true });
5349
- fs4.writeFileSync(resolvedPath, formatSnapshotMarkdown(report), "utf-8");
5757
+ const resolvedPath = path4.resolve(outputPath);
5758
+ fs5.mkdirSync(path4.dirname(resolvedPath), { recursive: true });
5759
+ fs5.writeFileSync(resolvedPath, formatSnapshotMarkdown(report), "utf-8");
5350
5760
  return resolvedPath;
5351
5761
  }
5352
5762
  function formatSnapshotMarkdown(report) {
@@ -5655,7 +6065,7 @@ var INTELLIGENCE_CLI_COMMANDS = [
5655
6065
 
5656
6066
  // src/commands/bootstrap.ts
5657
6067
  import crypto from "crypto";
5658
- import path3 from "path";
6068
+ import path5 from "path";
5659
6069
  import { eq as eq2 } from "drizzle-orm";
5660
6070
 
5661
6071
  // ../config/src/index.ts
@@ -5802,7 +6212,7 @@ async function bootstrapCommand(_opts) {
5802
6212
  );
5803
6213
  }
5804
6214
  const configDir = getConfigDir();
5805
- const databasePath = env.databasePath || path3.join(configDir, "data.db");
6215
+ const databasePath = env.databasePath || path5.join(configDir, "data.db");
5806
6216
  const existing = configExists();
5807
6217
  const existingConfig = existing ? loadConfig() : void 0;
5808
6218
  let rawApiKey;
@@ -5872,10 +6282,10 @@ async function bootstrapCommand(_opts) {
5872
6282
 
5873
6283
  // src/commands/daemon.ts
5874
6284
  import { spawn } from "child_process";
5875
- import fs5 from "fs";
5876
- import path4 from "path";
6285
+ import fs6 from "fs";
6286
+ import path6 from "path";
5877
6287
  function getPidPath() {
5878
- return path4.join(getConfigDir(), "canonry.pid");
6288
+ return path6.join(getConfigDir(), "canonry.pid");
5879
6289
  }
5880
6290
  function isProcessAlive(pid) {
5881
6291
  try {
@@ -5902,8 +6312,8 @@ async function waitForReady(host, port, maxMs = 1e4) {
5902
6312
  async function startDaemon(opts) {
5903
6313
  const pidPath = getPidPath();
5904
6314
  const format = opts.format ?? "text";
5905
- if (fs5.existsSync(pidPath)) {
5906
- const existingPid = parseInt(fs5.readFileSync(pidPath, "utf-8").trim(), 10);
6315
+ if (fs6.existsSync(pidPath)) {
6316
+ const existingPid = parseInt(fs6.readFileSync(pidPath, "utf-8").trim(), 10);
5907
6317
  if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
5908
6318
  throw new CliError({
5909
6319
  code: "DAEMON_ALREADY_RUNNING",
@@ -5914,9 +6324,9 @@ async function startDaemon(opts) {
5914
6324
  }
5915
6325
  });
5916
6326
  }
5917
- fs5.unlinkSync(pidPath);
6327
+ fs6.unlinkSync(pidPath);
5918
6328
  }
5919
- const cliPath = path4.resolve(new URL(import.meta.url).pathname);
6329
+ const cliPath = path6.resolve(new URL(import.meta.url).pathname);
5920
6330
  const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
5921
6331
  const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
5922
6332
  if (opts.port) args.push("--port", opts.port);
@@ -5935,10 +6345,10 @@ async function startDaemon(opts) {
5935
6345
  });
5936
6346
  }
5937
6347
  const configDir = getConfigDir();
5938
- if (!fs5.existsSync(configDir)) {
5939
- fs5.mkdirSync(configDir, { recursive: true });
6348
+ if (!fs6.existsSync(configDir)) {
6349
+ fs6.mkdirSync(configDir, { recursive: true });
5940
6350
  }
5941
- fs5.writeFileSync(pidPath, String(child.pid), "utf-8");
6351
+ fs6.writeFileSync(pidPath, String(child.pid), "utf-8");
5942
6352
  const port = opts.port ?? "4100";
5943
6353
  const host = opts.host ?? "127.0.0.1";
5944
6354
  if (format !== "json") {
@@ -5947,7 +6357,7 @@ async function startDaemon(opts) {
5947
6357
  const ready = await waitForReady(host, port);
5948
6358
  if (!ready) {
5949
6359
  try {
5950
- fs5.unlinkSync(pidPath);
6360
+ fs6.unlinkSync(pidPath);
5951
6361
  } catch {
5952
6362
  }
5953
6363
  throw new CliError({
@@ -5979,7 +6389,7 @@ async function startDaemon(opts) {
5979
6389
  }
5980
6390
  function stopDaemon(format = "text") {
5981
6391
  const pidPath = getPidPath();
5982
- if (!fs5.existsSync(pidPath)) {
6392
+ if (!fs6.existsSync(pidPath)) {
5983
6393
  if (format === "json") {
5984
6394
  console.log(JSON.stringify({
5985
6395
  stopped: false,
@@ -5990,7 +6400,7 @@ function stopDaemon(format = "text") {
5990
6400
  console.log("Canonry is not running (no PID file found)");
5991
6401
  return;
5992
6402
  }
5993
- const pid = parseInt(fs5.readFileSync(pidPath, "utf-8").trim(), 10);
6403
+ const pid = parseInt(fs6.readFileSync(pidPath, "utf-8").trim(), 10);
5994
6404
  if (isNaN(pid)) {
5995
6405
  if (format === "json") {
5996
6406
  console.log(JSON.stringify({
@@ -6001,7 +6411,7 @@ function stopDaemon(format = "text") {
6001
6411
  } else {
6002
6412
  console.error("Invalid PID file. Removing it.");
6003
6413
  }
6004
- fs5.unlinkSync(pidPath);
6414
+ fs6.unlinkSync(pidPath);
6005
6415
  return;
6006
6416
  }
6007
6417
  if (!isProcessAlive(pid)) {
@@ -6015,12 +6425,12 @@ function stopDaemon(format = "text") {
6015
6425
  } else {
6016
6426
  console.log(`Canonry is not running (stale PID: ${pid}). Cleaning up.`);
6017
6427
  }
6018
- fs5.unlinkSync(pidPath);
6428
+ fs6.unlinkSync(pidPath);
6019
6429
  return;
6020
6430
  }
6021
6431
  try {
6022
6432
  process.kill(pid, "SIGTERM");
6023
- fs5.unlinkSync(pidPath);
6433
+ fs6.unlinkSync(pidPath);
6024
6434
  if (format === "json") {
6025
6435
  console.log(JSON.stringify({
6026
6436
  stopped: true,
@@ -6044,9 +6454,9 @@ function stopDaemon(format = "text") {
6044
6454
 
6045
6455
  // src/commands/init.ts
6046
6456
  import crypto2 from "crypto";
6047
- import fs6 from "fs";
6457
+ import fs7 from "fs";
6048
6458
  import readline from "readline";
6049
- import path5 from "path";
6459
+ import path7 from "path";
6050
6460
  function prompt(question) {
6051
6461
  const rl = readline.createInterface({
6052
6462
  input: process.stdin,
@@ -6092,8 +6502,8 @@ async function initCommand(opts) {
6092
6502
  return void 0;
6093
6503
  }
6094
6504
  const configDir = getConfigDir();
6095
- if (!fs6.existsSync(configDir)) {
6096
- fs6.mkdirSync(configDir, { recursive: true });
6505
+ if (!fs7.existsSync(configDir)) {
6506
+ fs7.mkdirSync(configDir, { recursive: true });
6097
6507
  }
6098
6508
  const bootstrapEnv = getBootstrapEnv(process.env, {
6099
6509
  GEMINI_API_KEY: opts?.geminiKey,
@@ -6208,7 +6618,7 @@ async function initCommand(opts) {
6208
6618
  const rawApiKey = `cnry_${crypto2.randomBytes(16).toString("hex")}`;
6209
6619
  const keyHash = crypto2.createHash("sha256").update(rawApiKey).digest("hex");
6210
6620
  const keyPrefix = rawApiKey.slice(0, 9);
6211
- const databasePath = path5.join(configDir, "data.db");
6621
+ const databasePath = path7.join(configDir, "data.db");
6212
6622
  const db = createClient(databasePath);
6213
6623
  migrate(db);
6214
6624
  db.insert(apiKeys).values({
@@ -6602,7 +7012,7 @@ var SYSTEM_CLI_COMMANDS = [
6602
7012
  ];
6603
7013
 
6604
7014
  // src/cli-commands/wordpress.ts
6605
- import fs7 from "fs";
7015
+ import fs8 from "fs";
6606
7016
 
6607
7017
  // src/commands/wordpress.ts
6608
7018
  function getClient18() {
@@ -6838,12 +7248,12 @@ async function wordpressSetMeta(project, body) {
6838
7248
  printPageDetail(result);
6839
7249
  }
6840
7250
  async function wordpressBulkSetMeta(project, opts) {
6841
- const fs8 = await import("fs/promises");
6842
- const path6 = await import("path");
6843
- const filePath = path6.resolve(opts.from);
7251
+ const fs9 = await import("fs/promises");
7252
+ const path8 = await import("path");
7253
+ const filePath = path8.resolve(opts.from);
6844
7254
  let raw;
6845
7255
  try {
6846
- raw = await fs8.readFile(filePath, "utf8");
7256
+ raw = await fs9.readFile(filePath, "utf8");
6847
7257
  } catch {
6848
7258
  throw new CliError({
6849
7259
  code: "FILE_READ_ERROR",
@@ -6940,13 +7350,13 @@ async function wordpressSetSchema(project, body) {
6940
7350
  printManualAssist(`Schema update for "${body.slug}"`, result);
6941
7351
  }
6942
7352
  async function wordpressSchemaDeploy(project, opts) {
6943
- const fs8 = await import("fs/promises");
6944
- const path6 = await import("path");
7353
+ const fs9 = await import("fs/promises");
7354
+ const path8 = await import("path");
6945
7355
  const yaml = await import("yaml").catch(() => null);
6946
- const filePath = path6.resolve(opts.profile);
7356
+ const filePath = path8.resolve(opts.profile);
6947
7357
  let raw;
6948
7358
  try {
6949
- raw = await fs8.readFile(filePath, "utf8");
7359
+ raw = await fs9.readFile(filePath, "utf8");
6950
7360
  } catch {
6951
7361
  throw new CliError({
6952
7362
  code: "FILE_READ_ERROR",
@@ -7051,13 +7461,13 @@ async function wordpressOnboard(project, opts) {
7051
7461
  }
7052
7462
  let profileData;
7053
7463
  if (opts.profile) {
7054
- const fs8 = await import("fs/promises");
7055
- const path6 = await import("path");
7464
+ const fs9 = await import("fs/promises");
7465
+ const path8 = await import("path");
7056
7466
  const yaml = await import("yaml").catch(() => null);
7057
- const filePath = path6.resolve(opts.profile);
7467
+ const filePath = path8.resolve(opts.profile);
7058
7468
  let raw;
7059
7469
  try {
7060
- raw = await fs8.readFile(filePath, "utf8");
7470
+ raw = await fs9.readFile(filePath, "utf8");
7061
7471
  } catch {
7062
7472
  throw new CliError({
7063
7473
  code: "FILE_READ_ERROR",
@@ -7206,7 +7616,7 @@ function resolveContent(input, command, usage, options) {
7206
7616
  }
7207
7617
  if (contentFile) {
7208
7618
  try {
7209
- return fs7.readFileSync(contentFile, "utf-8");
7619
+ return fs8.readFileSync(contentFile, "utf-8");
7210
7620
  } catch (error) {
7211
7621
  const message = error instanceof Error ? error.message : String(error);
7212
7622
  throw usageError(`Error: could not read --content-file "${contentFile}": ${message}`, {
@@ -8142,11 +8552,12 @@ var REGISTERED_CLI_COMMANDS = [
8142
8552
  ...CDP_CLI_COMMANDS,
8143
8553
  ...GA_CLI_COMMANDS,
8144
8554
  ...INTELLIGENCE_CLI_COMMANDS,
8145
- ...AGENT_CLI_COMMANDS
8555
+ ...AGENT_CLI_COMMANDS,
8556
+ ...MCP_CLI_COMMANDS
8146
8557
  ];
8147
8558
 
8148
8559
  // src/cli.ts
8149
- import { createRequire } from "module";
8560
+ import { createRequire as createRequire2 } from "module";
8150
8561
  var USAGE = `
8151
8562
  canonry \u2014 AEO monitoring CLI
8152
8563
 
@@ -8160,8 +8571,8 @@ Setup:
8160
8571
 
8161
8572
  Projects:
8162
8573
  project Create, update, list, show, delete projects
8163
- keyword Add, remove, list, import, generate key phrases
8164
- competitor Add, list competitors
8574
+ keyword Add, replace, remove, list, import, generate key phrases
8575
+ competitor Add, remove, list competitors
8165
8576
 
8166
8577
  Monitoring:
8167
8578
  run Trigger visibility sweeps
@@ -8198,8 +8609,8 @@ Global options:
8198
8609
 
8199
8610
  Run 'canonry <command> --help' for details on a specific command.
8200
8611
  `.trim();
8201
- var _require = createRequire(import.meta.url);
8202
- var { version: VERSION } = _require("../package.json");
8612
+ var _require2 = createRequire2(import.meta.url);
8613
+ var { version: VERSION } = _require2("../package.json");
8203
8614
  function extractFormat(cmdArgs) {
8204
8615
  const idx = cmdArgs.indexOf("--format");
8205
8616
  if (idx !== -1 && cmdArgs[idx + 1] === "json") return "json";