@ainyc/canonry 2.5.1 → 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
@@ -1,4 +1,23 @@
1
1
  #!/usr/bin/env node --import tsx
2
+ import {
3
+ coerceAgentProvider,
4
+ computeCompetitorOverlap,
5
+ createServer,
6
+ determineCitationState,
7
+ extractRecommendedCompetitors,
8
+ formatAuditFactorScore,
9
+ getOrCreateAnonymousId,
10
+ isFirstRun,
11
+ isTelemetryEnabled,
12
+ listAgentProviders,
13
+ reparseStoredResult,
14
+ reparseStoredResult2,
15
+ reparseStoredResult3,
16
+ reparseStoredResult4,
17
+ setGoogleAuthConfig,
18
+ showFirstRunNotice,
19
+ trackEvent
20
+ } from "./chunk-MGBXRWLX.js";
2
21
  import {
3
22
  CcReleaseSyncStatuses,
4
23
  CliError,
@@ -6,39 +25,22 @@ import {
6
25
  ProviderNames,
7
26
  RunKinds,
8
27
  RunStatuses,
9
- coerceAgentProvider,
10
- computeCompetitorOverlap,
11
28
  configExists,
12
29
  createApiClient,
13
- createServer,
14
30
  determineAnswerMentioned,
15
- determineCitationState,
16
31
  effectiveDomains,
17
- extractRecommendedCompetitors,
18
- formatAuditFactorScore,
19
32
  getConfigDir,
20
33
  getConfigPath,
21
- getOrCreateAnonymousId,
22
34
  isEndpointMissing,
23
- isFirstRun,
24
- isTelemetryEnabled,
25
- listAgentProviders,
26
35
  loadConfig,
27
36
  notificationEventSchema,
28
37
  printCliError,
29
38
  providerQuotaPolicySchema,
30
- reparseStoredResult,
31
- reparseStoredResult2,
32
- reparseStoredResult3,
33
- reparseStoredResult4,
34
39
  resolveProviderInput,
35
40
  saveConfig,
36
41
  saveConfigPatch,
37
- setGoogleAuthConfig,
38
- showFirstRunNotice,
39
- trackEvent,
40
42
  usageError
41
- } from "./chunk-CFS35BKX.js";
43
+ } from "./chunk-FPZUQADO.js";
42
44
  import {
43
45
  apiKeys,
44
46
  competitors,
@@ -48,7 +50,8 @@ import {
48
50
  projects,
49
51
  querySnapshots,
50
52
  runs
51
- } from "./chunk-32YTAZBL.js";
53
+ } from "./chunk-PYHANJ3B.js";
54
+ import "./chunk-MLKGABMK.js";
52
55
 
53
56
  // src/cli.ts
54
57
  import { pathToFileURL } from "url";
@@ -58,9 +61,9 @@ import { parseArgs } from "util";
58
61
  function commandId(spec) {
59
62
  return spec.path.join(".");
60
63
  }
61
- function matchesPath(args, path6) {
62
- if (args.length < path6.length) return false;
63
- 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);
64
67
  }
65
68
  function withFormatOption(options) {
66
69
  if (!options) {
@@ -295,7 +298,7 @@ async function backfillAnswerVisibilityCommand(opts) {
295
298
  console.log(` Errors: ${providerErrors}`);
296
299
  }
297
300
  async function backfillInsightsCommand(project, opts) {
298
- const { IntelligenceService } = await import("./intelligence-service-U7YQ4NXV.js");
301
+ const { IntelligenceService } = await import("./intelligence-service-2ZABHNR4.js");
299
302
  const config = loadConfig();
300
303
  const db = createClient(config.database);
301
304
  migrate(db);
@@ -1566,9 +1569,9 @@ async function gaConnect(project, opts) {
1566
1569
  propertyId: opts.propertyId
1567
1570
  };
1568
1571
  if (opts.keyFile) {
1569
- const fs8 = await import("fs");
1572
+ const fs9 = await import("fs");
1570
1573
  try {
1571
- const content = fs8.readFileSync(opts.keyFile, "utf-8");
1574
+ const content = fs9.readFileSync(opts.keyFile, "utf-8");
1572
1575
  JSON.parse(content);
1573
1576
  body.keyJson = content;
1574
1577
  } catch (e) {
@@ -2184,13 +2187,15 @@ async function addCompetitors(project, domains, format) {
2184
2187
  const client = getClient5();
2185
2188
  const existing = await client.listCompetitors(project);
2186
2189
  const existingDomains = existing.map((c) => c.domain);
2187
- const addedDomains = domains.filter((domain) => !existingDomains.includes(domain));
2188
- const allDomains = [.../* @__PURE__ */ new Set([...existingDomains, ...domains])];
2189
- 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));
2190
2195
  if (format === "json") {
2191
2196
  console.log(JSON.stringify({
2192
2197
  project,
2193
- domains: allDomains,
2198
+ domains: currentDomains,
2194
2199
  addedDomains,
2195
2200
  addedCount: addedDomains.length
2196
2201
  }, null, 2));
@@ -2202,6 +2207,35 @@ async function addCompetitors(project, domains, format) {
2202
2207
  console.log(`Added ${addedDomains.length} competitor(s) to "${project}".`);
2203
2208
  }
2204
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
+ }
2205
2239
  async function listCompetitors(project, format) {
2206
2240
  const client = getClient5();
2207
2241
  const comps = await client.listCompetitors(project);
@@ -2240,6 +2274,42 @@ var COMPETITOR_CLI_COMMANDS = [
2240
2274
  await addCompetitors(project, domains, input.format);
2241
2275
  }
2242
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
+ },
2243
2313
  {
2244
2314
  path: ["competitor", "list"],
2245
2315
  usage: "canonry competitor list <project> [--format json]",
@@ -2250,12 +2320,12 @@ var COMPETITOR_CLI_COMMANDS = [
2250
2320
  },
2251
2321
  {
2252
2322
  path: ["competitor"],
2253
- usage: "canonry competitor <add|list> <project> [args]",
2323
+ usage: "canonry competitor <add|remove|delete|list> <project> [args]",
2254
2324
  run: async (input) => {
2255
2325
  unknownSubcommand(input.positionals[0], {
2256
2326
  command: "competitor",
2257
- usage: "canonry competitor <add|list> <project> [args]",
2258
- available: ["add", "list"]
2327
+ usage: "canonry competitor <add|remove|delete|list> <project> [args]",
2328
+ available: ["add", "remove", "delete", "list"]
2259
2329
  });
2260
2330
  }
2261
2331
  }
@@ -3121,6 +3191,19 @@ async function addKeywords(project, keywords, format) {
3121
3191
  }
3122
3192
  console.log(`Added ${keywords.length} key phrase(s) to "${project}".`);
3123
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
+ }
3124
3207
  async function removeKeywords(project, keywords, format) {
3125
3208
  const client = getClient7();
3126
3209
  const existing = await client.listKeywords(project);
@@ -3249,6 +3332,24 @@ var KEYWORD_CLI_COMMANDS = [
3249
3332
  await addKeywords(project, keywords, input.format);
3250
3333
  }
3251
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
+ },
3252
3353
  {
3253
3354
  path: ["keyword", "remove"],
3254
3355
  usage: "canonry keyword remove <project> <kw...> [--format json]",
@@ -3338,12 +3439,324 @@ var KEYWORD_CLI_COMMANDS = [
3338
3439
  },
3339
3440
  {
3340
3441
  path: ["keyword"],
3341
- usage: "canonry keyword <add|remove|delete|list|import|generate> <project> [args]",
3442
+ usage: "canonry keyword <add|replace|remove|delete|list|import|generate> <project> [args]",
3342
3443
  run: async (input) => {
3343
3444
  unknownSubcommand(input.positionals[0], {
3344
3445
  command: "keyword",
3345
- usage: "canonry keyword <add|remove|delete|list|import|generate> <project> [args]",
3346
- 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"]
3347
3760
  });
3348
3761
  }
3349
3762
  }
@@ -3517,13 +3930,13 @@ var NOTIFY_CLI_COMMANDS = [
3517
3930
  ];
3518
3931
 
3519
3932
  // src/commands/apply.ts
3520
- import fs2 from "fs";
3933
+ import fs3 from "fs";
3521
3934
  import { parseAllDocuments } from "yaml";
3522
3935
  async function applyConfigFile(filePath) {
3523
- if (!fs2.existsSync(filePath)) {
3936
+ if (!fs3.existsSync(filePath)) {
3524
3937
  throw new Error(`File not found: ${filePath}`);
3525
3938
  }
3526
- const content = fs2.readFileSync(filePath, "utf-8");
3939
+ const content = fs3.readFileSync(filePath, "utf-8");
3527
3940
  const docs = parseAllDocuments(content);
3528
3941
  const client = createApiClient();
3529
3942
  const errors = [];
@@ -4967,12 +5380,12 @@ Usage: canonry settings provider ${name} --api-key <key> [--model <model>] [--ma
4967
5380
  ];
4968
5381
 
4969
5382
  // src/commands/snapshot.ts
4970
- import fs4 from "fs";
4971
- import path2 from "path";
5383
+ import fs5 from "fs";
5384
+ import path4 from "path";
4972
5385
 
4973
5386
  // src/snapshot-pdf.ts
4974
- import fs3 from "fs";
4975
- import path from "path";
5387
+ import fs4 from "fs";
5388
+ import path3 from "path";
4976
5389
  import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
4977
5390
  var PAGE_WIDTH = 612;
4978
5391
  var PAGE_HEIGHT = 792;
@@ -5181,9 +5594,9 @@ async function writeSnapshotPdf(report, outputPath) {
5181
5594
  renderCompetitors(pdf, report);
5182
5595
  renderQueries(pdf, report);
5183
5596
  const bytes = await doc.save();
5184
- const resolvedPath = path.resolve(outputPath);
5185
- fs3.mkdirSync(path.dirname(resolvedPath), { recursive: true });
5186
- fs3.writeFileSync(resolvedPath, bytes);
5597
+ const resolvedPath = path3.resolve(outputPath);
5598
+ fs4.mkdirSync(path3.dirname(resolvedPath), { recursive: true });
5599
+ fs4.writeFileSync(resolvedPath, bytes);
5187
5600
  return resolvedPath;
5188
5601
  }
5189
5602
  function renderCover(pdf, report) {
@@ -5341,9 +5754,9 @@ Markdown saved: ${savedMdPath}`);
5341
5754
  PDF saved: ${savedPdfPath}`);
5342
5755
  }
5343
5756
  function writeSnapshotMarkdown(report, outputPath) {
5344
- const resolvedPath = path2.resolve(outputPath);
5345
- fs4.mkdirSync(path2.dirname(resolvedPath), { recursive: true });
5346
- 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");
5347
5760
  return resolvedPath;
5348
5761
  }
5349
5762
  function formatSnapshotMarkdown(report) {
@@ -5652,7 +6065,7 @@ var INTELLIGENCE_CLI_COMMANDS = [
5652
6065
 
5653
6066
  // src/commands/bootstrap.ts
5654
6067
  import crypto from "crypto";
5655
- import path3 from "path";
6068
+ import path5 from "path";
5656
6069
  import { eq as eq2 } from "drizzle-orm";
5657
6070
 
5658
6071
  // ../config/src/index.ts
@@ -5799,7 +6212,7 @@ async function bootstrapCommand(_opts) {
5799
6212
  );
5800
6213
  }
5801
6214
  const configDir = getConfigDir();
5802
- const databasePath = env.databasePath || path3.join(configDir, "data.db");
6215
+ const databasePath = env.databasePath || path5.join(configDir, "data.db");
5803
6216
  const existing = configExists();
5804
6217
  const existingConfig = existing ? loadConfig() : void 0;
5805
6218
  let rawApiKey;
@@ -5869,10 +6282,10 @@ async function bootstrapCommand(_opts) {
5869
6282
 
5870
6283
  // src/commands/daemon.ts
5871
6284
  import { spawn } from "child_process";
5872
- import fs5 from "fs";
5873
- import path4 from "path";
6285
+ import fs6 from "fs";
6286
+ import path6 from "path";
5874
6287
  function getPidPath() {
5875
- return path4.join(getConfigDir(), "canonry.pid");
6288
+ return path6.join(getConfigDir(), "canonry.pid");
5876
6289
  }
5877
6290
  function isProcessAlive(pid) {
5878
6291
  try {
@@ -5899,8 +6312,8 @@ async function waitForReady(host, port, maxMs = 1e4) {
5899
6312
  async function startDaemon(opts) {
5900
6313
  const pidPath = getPidPath();
5901
6314
  const format = opts.format ?? "text";
5902
- if (fs5.existsSync(pidPath)) {
5903
- 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);
5904
6317
  if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
5905
6318
  throw new CliError({
5906
6319
  code: "DAEMON_ALREADY_RUNNING",
@@ -5911,9 +6324,9 @@ async function startDaemon(opts) {
5911
6324
  }
5912
6325
  });
5913
6326
  }
5914
- fs5.unlinkSync(pidPath);
6327
+ fs6.unlinkSync(pidPath);
5915
6328
  }
5916
- const cliPath = path4.resolve(new URL(import.meta.url).pathname);
6329
+ const cliPath = path6.resolve(new URL(import.meta.url).pathname);
5917
6330
  const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
5918
6331
  const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
5919
6332
  if (opts.port) args.push("--port", opts.port);
@@ -5932,10 +6345,10 @@ async function startDaemon(opts) {
5932
6345
  });
5933
6346
  }
5934
6347
  const configDir = getConfigDir();
5935
- if (!fs5.existsSync(configDir)) {
5936
- fs5.mkdirSync(configDir, { recursive: true });
6348
+ if (!fs6.existsSync(configDir)) {
6349
+ fs6.mkdirSync(configDir, { recursive: true });
5937
6350
  }
5938
- fs5.writeFileSync(pidPath, String(child.pid), "utf-8");
6351
+ fs6.writeFileSync(pidPath, String(child.pid), "utf-8");
5939
6352
  const port = opts.port ?? "4100";
5940
6353
  const host = opts.host ?? "127.0.0.1";
5941
6354
  if (format !== "json") {
@@ -5944,7 +6357,7 @@ async function startDaemon(opts) {
5944
6357
  const ready = await waitForReady(host, port);
5945
6358
  if (!ready) {
5946
6359
  try {
5947
- fs5.unlinkSync(pidPath);
6360
+ fs6.unlinkSync(pidPath);
5948
6361
  } catch {
5949
6362
  }
5950
6363
  throw new CliError({
@@ -5976,7 +6389,7 @@ async function startDaemon(opts) {
5976
6389
  }
5977
6390
  function stopDaemon(format = "text") {
5978
6391
  const pidPath = getPidPath();
5979
- if (!fs5.existsSync(pidPath)) {
6392
+ if (!fs6.existsSync(pidPath)) {
5980
6393
  if (format === "json") {
5981
6394
  console.log(JSON.stringify({
5982
6395
  stopped: false,
@@ -5987,7 +6400,7 @@ function stopDaemon(format = "text") {
5987
6400
  console.log("Canonry is not running (no PID file found)");
5988
6401
  return;
5989
6402
  }
5990
- const pid = parseInt(fs5.readFileSync(pidPath, "utf-8").trim(), 10);
6403
+ const pid = parseInt(fs6.readFileSync(pidPath, "utf-8").trim(), 10);
5991
6404
  if (isNaN(pid)) {
5992
6405
  if (format === "json") {
5993
6406
  console.log(JSON.stringify({
@@ -5998,7 +6411,7 @@ function stopDaemon(format = "text") {
5998
6411
  } else {
5999
6412
  console.error("Invalid PID file. Removing it.");
6000
6413
  }
6001
- fs5.unlinkSync(pidPath);
6414
+ fs6.unlinkSync(pidPath);
6002
6415
  return;
6003
6416
  }
6004
6417
  if (!isProcessAlive(pid)) {
@@ -6012,12 +6425,12 @@ function stopDaemon(format = "text") {
6012
6425
  } else {
6013
6426
  console.log(`Canonry is not running (stale PID: ${pid}). Cleaning up.`);
6014
6427
  }
6015
- fs5.unlinkSync(pidPath);
6428
+ fs6.unlinkSync(pidPath);
6016
6429
  return;
6017
6430
  }
6018
6431
  try {
6019
6432
  process.kill(pid, "SIGTERM");
6020
- fs5.unlinkSync(pidPath);
6433
+ fs6.unlinkSync(pidPath);
6021
6434
  if (format === "json") {
6022
6435
  console.log(JSON.stringify({
6023
6436
  stopped: true,
@@ -6041,9 +6454,9 @@ function stopDaemon(format = "text") {
6041
6454
 
6042
6455
  // src/commands/init.ts
6043
6456
  import crypto2 from "crypto";
6044
- import fs6 from "fs";
6457
+ import fs7 from "fs";
6045
6458
  import readline from "readline";
6046
- import path5 from "path";
6459
+ import path7 from "path";
6047
6460
  function prompt(question) {
6048
6461
  const rl = readline.createInterface({
6049
6462
  input: process.stdin,
@@ -6089,8 +6502,8 @@ async function initCommand(opts) {
6089
6502
  return void 0;
6090
6503
  }
6091
6504
  const configDir = getConfigDir();
6092
- if (!fs6.existsSync(configDir)) {
6093
- fs6.mkdirSync(configDir, { recursive: true });
6505
+ if (!fs7.existsSync(configDir)) {
6506
+ fs7.mkdirSync(configDir, { recursive: true });
6094
6507
  }
6095
6508
  const bootstrapEnv = getBootstrapEnv(process.env, {
6096
6509
  GEMINI_API_KEY: opts?.geminiKey,
@@ -6205,7 +6618,7 @@ async function initCommand(opts) {
6205
6618
  const rawApiKey = `cnry_${crypto2.randomBytes(16).toString("hex")}`;
6206
6619
  const keyHash = crypto2.createHash("sha256").update(rawApiKey).digest("hex");
6207
6620
  const keyPrefix = rawApiKey.slice(0, 9);
6208
- const databasePath = path5.join(configDir, "data.db");
6621
+ const databasePath = path7.join(configDir, "data.db");
6209
6622
  const db = createClient(databasePath);
6210
6623
  migrate(db);
6211
6624
  db.insert(apiKeys).values({
@@ -6599,7 +7012,7 @@ var SYSTEM_CLI_COMMANDS = [
6599
7012
  ];
6600
7013
 
6601
7014
  // src/cli-commands/wordpress.ts
6602
- import fs7 from "fs";
7015
+ import fs8 from "fs";
6603
7016
 
6604
7017
  // src/commands/wordpress.ts
6605
7018
  function getClient18() {
@@ -6835,12 +7248,12 @@ async function wordpressSetMeta(project, body) {
6835
7248
  printPageDetail(result);
6836
7249
  }
6837
7250
  async function wordpressBulkSetMeta(project, opts) {
6838
- const fs8 = await import("fs/promises");
6839
- const path6 = await import("path");
6840
- 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);
6841
7254
  let raw;
6842
7255
  try {
6843
- raw = await fs8.readFile(filePath, "utf8");
7256
+ raw = await fs9.readFile(filePath, "utf8");
6844
7257
  } catch {
6845
7258
  throw new CliError({
6846
7259
  code: "FILE_READ_ERROR",
@@ -6937,13 +7350,13 @@ async function wordpressSetSchema(project, body) {
6937
7350
  printManualAssist(`Schema update for "${body.slug}"`, result);
6938
7351
  }
6939
7352
  async function wordpressSchemaDeploy(project, opts) {
6940
- const fs8 = await import("fs/promises");
6941
- const path6 = await import("path");
7353
+ const fs9 = await import("fs/promises");
7354
+ const path8 = await import("path");
6942
7355
  const yaml = await import("yaml").catch(() => null);
6943
- const filePath = path6.resolve(opts.profile);
7356
+ const filePath = path8.resolve(opts.profile);
6944
7357
  let raw;
6945
7358
  try {
6946
- raw = await fs8.readFile(filePath, "utf8");
7359
+ raw = await fs9.readFile(filePath, "utf8");
6947
7360
  } catch {
6948
7361
  throw new CliError({
6949
7362
  code: "FILE_READ_ERROR",
@@ -7048,13 +7461,13 @@ async function wordpressOnboard(project, opts) {
7048
7461
  }
7049
7462
  let profileData;
7050
7463
  if (opts.profile) {
7051
- const fs8 = await import("fs/promises");
7052
- const path6 = await import("path");
7464
+ const fs9 = await import("fs/promises");
7465
+ const path8 = await import("path");
7053
7466
  const yaml = await import("yaml").catch(() => null);
7054
- const filePath = path6.resolve(opts.profile);
7467
+ const filePath = path8.resolve(opts.profile);
7055
7468
  let raw;
7056
7469
  try {
7057
- raw = await fs8.readFile(filePath, "utf8");
7470
+ raw = await fs9.readFile(filePath, "utf8");
7058
7471
  } catch {
7059
7472
  throw new CliError({
7060
7473
  code: "FILE_READ_ERROR",
@@ -7203,7 +7616,7 @@ function resolveContent(input, command, usage, options) {
7203
7616
  }
7204
7617
  if (contentFile) {
7205
7618
  try {
7206
- return fs7.readFileSync(contentFile, "utf-8");
7619
+ return fs8.readFileSync(contentFile, "utf-8");
7207
7620
  } catch (error) {
7208
7621
  const message = error instanceof Error ? error.message : String(error);
7209
7622
  throw usageError(`Error: could not read --content-file "${contentFile}": ${message}`, {
@@ -8139,11 +8552,12 @@ var REGISTERED_CLI_COMMANDS = [
8139
8552
  ...CDP_CLI_COMMANDS,
8140
8553
  ...GA_CLI_COMMANDS,
8141
8554
  ...INTELLIGENCE_CLI_COMMANDS,
8142
- ...AGENT_CLI_COMMANDS
8555
+ ...AGENT_CLI_COMMANDS,
8556
+ ...MCP_CLI_COMMANDS
8143
8557
  ];
8144
8558
 
8145
8559
  // src/cli.ts
8146
- import { createRequire } from "module";
8560
+ import { createRequire as createRequire2 } from "module";
8147
8561
  var USAGE = `
8148
8562
  canonry \u2014 AEO monitoring CLI
8149
8563
 
@@ -8157,8 +8571,8 @@ Setup:
8157
8571
 
8158
8572
  Projects:
8159
8573
  project Create, update, list, show, delete projects
8160
- keyword Add, remove, list, import, generate key phrases
8161
- competitor Add, list competitors
8574
+ keyword Add, replace, remove, list, import, generate key phrases
8575
+ competitor Add, remove, list competitors
8162
8576
 
8163
8577
  Monitoring:
8164
8578
  run Trigger visibility sweeps
@@ -8195,8 +8609,8 @@ Global options:
8195
8609
 
8196
8610
  Run 'canonry <command> --help' for details on a specific command.
8197
8611
  `.trim();
8198
- var _require = createRequire(import.meta.url);
8199
- var { version: VERSION } = _require("../package.json");
8612
+ var _require2 = createRequire2(import.meta.url);
8613
+ var { version: VERSION } = _require2("../package.json");
8200
8614
  function extractFormat(cmdArgs) {
8201
8615
  const idx = cmdArgs.indexOf("--format");
8202
8616
  if (idx !== -1 && cmdArgs[idx + 1] === "json") return "json";