@ainyc/canonry 3.2.5 → 3.3.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,17 +17,19 @@ import {
17
17
  setGoogleAuthConfig,
18
18
  showFirstRunNotice,
19
19
  trackEvent
20
- } from "./chunk-AQ3AS25Y.js";
20
+ } from "./chunk-HQ47AA6H.js";
21
21
  import {
22
22
  CcReleaseSyncStatuses,
23
23
  CheckScopes,
24
24
  CheckStatuses,
25
25
  CliError,
26
+ CodingAgents,
26
27
  EXIT_SYSTEM_ERROR,
27
28
  EXIT_USER_ERROR,
28
29
  ProviderNames,
29
30
  RunKinds,
30
31
  RunStatuses,
32
+ SkillsClients,
31
33
  configExists,
32
34
  createApiClient,
33
35
  determineAnswerMentioned,
@@ -44,8 +46,9 @@ import {
44
46
  resolveProviderInput,
45
47
  saveConfig,
46
48
  saveConfigPatch,
49
+ skillsClientSchema,
47
50
  usageError
48
- } from "./chunk-PS7JRDL3.js";
51
+ } from "./chunk-ALMP3NBQ.js";
49
52
  import {
50
53
  apiKeys,
51
54
  competitors,
@@ -68,9 +71,9 @@ import { parseArgs } from "util";
68
71
  function commandId(spec) {
69
72
  return spec.path.join(".");
70
73
  }
71
- function matchesPath(args, path8) {
72
- if (args.length < path8.length) return false;
73
- return path8.every((segment, index) => args[index] === segment);
74
+ function matchesPath(args, path9) {
75
+ if (args.length < path9.length) return false;
76
+ return path9.every((segment, index) => args[index] === segment);
74
77
  }
75
78
  function withFormatOption(options) {
76
79
  if (!options) {
@@ -448,6 +451,131 @@ async function backfillAiReferralPathsCommand(opts) {
448
451
  console.log(` Updated: ${updated}`);
449
452
  console.log(` Unchanged: ${unchanged}`);
450
453
  }
454
+ async function backfillAnswerMentionsCommand(opts) {
455
+ const config = loadConfig();
456
+ const db = createClient(config.database);
457
+ migrate(db);
458
+ const projectFilter = opts?.project?.trim();
459
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq(projects.name, projectFilter)).all() : db.select().from(projects).all();
460
+ let examined = 0;
461
+ let updated = 0;
462
+ let mentioned = 0;
463
+ if (scopedProjects.length > 0) {
464
+ const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and(
465
+ eq(runs.kind, RunKinds["answer-visibility"]),
466
+ inArray(runs.projectId, scopedProjects.map((project) => project.id))
467
+ )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq(runs.kind, RunKinds["answer-visibility"])).all();
468
+ const runIdsByProject = /* @__PURE__ */ new Map();
469
+ for (const run of runRows) {
470
+ const existing = runIdsByProject.get(run.projectId);
471
+ if (existing) existing.push(run.id);
472
+ else runIdsByProject.set(run.projectId, [run.id]);
473
+ }
474
+ for (const project of scopedProjects) {
475
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq(competitors.projectId, project.id)).all().map((row) => row.domain);
476
+ const runIds = runIdsByProject.get(project.id) ?? [];
477
+ if (runIds.length === 0) continue;
478
+ const projectDomains = effectiveDomains({
479
+ canonicalDomain: project.canonicalDomain,
480
+ ownedDomains: parseJsonColumn(project.ownedDomains, [])
481
+ });
482
+ for (let offset = 0; offset < runIds.length; offset += SNAPSHOT_BATCH_SIZE) {
483
+ const batchRunIds = runIds.slice(offset, offset + SNAPSHOT_BATCH_SIZE);
484
+ const snapshotRows = db.select({
485
+ id: querySnapshots.id,
486
+ provider: querySnapshots.provider,
487
+ answerMentioned: querySnapshots.answerMentioned,
488
+ answerText: querySnapshots.answerText,
489
+ citedDomains: querySnapshots.citedDomains,
490
+ competitorOverlap: querySnapshots.competitorOverlap,
491
+ recommendedCompetitors: querySnapshots.recommendedCompetitors,
492
+ rawResponse: querySnapshots.rawResponse
493
+ }).from(querySnapshots).where(inArray(querySnapshots.runId, batchRunIds)).all();
494
+ const pendingUpdates = [];
495
+ for (const snapshot of snapshotRows) {
496
+ examined++;
497
+ const answerText = snapshot.answerText ?? "";
498
+ const nextAnswerMentioned = determineAnswerMentioned(answerText, project.displayName, projectDomains);
499
+ if (nextAnswerMentioned) mentioned++;
500
+ const citedDomains = parseJsonColumn(snapshot.citedDomains, []);
501
+ const groundingSources = readStoredGroundingSources(snapshot.rawResponse);
502
+ const normalized = {
503
+ provider: snapshot.provider,
504
+ answerText,
505
+ citedDomains,
506
+ groundingSources,
507
+ searchQueries: []
508
+ };
509
+ const nextCompetitorOverlap = JSON.stringify(
510
+ computeCompetitorOverlap(normalized, competitorDomains)
511
+ );
512
+ const nextRecommendedCompetitors = JSON.stringify(
513
+ extractRecommendedCompetitors(
514
+ answerText,
515
+ projectDomains,
516
+ citedDomains,
517
+ competitorDomains
518
+ )
519
+ );
520
+ const nextPatch = {};
521
+ if (snapshot.answerMentioned !== nextAnswerMentioned) {
522
+ nextPatch.answerMentioned = nextAnswerMentioned;
523
+ }
524
+ if (snapshot.competitorOverlap !== nextCompetitorOverlap) {
525
+ nextPatch.competitorOverlap = nextCompetitorOverlap;
526
+ }
527
+ if (snapshot.recommendedCompetitors !== nextRecommendedCompetitors) {
528
+ nextPatch.recommendedCompetitors = nextRecommendedCompetitors;
529
+ }
530
+ if (Object.keys(nextPatch).length > 0) {
531
+ pendingUpdates.push({ id: snapshot.id, patch: nextPatch });
532
+ }
533
+ }
534
+ if (pendingUpdates.length > 0) {
535
+ db.transaction((tx) => {
536
+ for (const update of pendingUpdates) {
537
+ tx.update(querySnapshots).set(update.patch).where(eq(querySnapshots.id, update.id)).run();
538
+ }
539
+ });
540
+ updated += pendingUpdates.length;
541
+ }
542
+ }
543
+ }
544
+ }
545
+ const result = {
546
+ project: projectFilter ?? null,
547
+ projects: scopedProjects.length,
548
+ examined,
549
+ updated,
550
+ mentioned
551
+ };
552
+ if (opts?.format === "json") {
553
+ console.log(JSON.stringify(result, null, 2));
554
+ return;
555
+ }
556
+ console.log("Answer mentions backfill complete.\n");
557
+ if (projectFilter) console.log(` Project: ${projectFilter}`);
558
+ console.log(` Projects: ${scopedProjects.length}`);
559
+ console.log(` Examined: ${examined}`);
560
+ console.log(` Updated: ${updated}`);
561
+ console.log(` Mentioned: ${mentioned}`);
562
+ }
563
+ function readStoredGroundingSources(rawResponse) {
564
+ const envelope = parseJsonColumn(rawResponse, {});
565
+ const sources = envelope.groundingSources;
566
+ if (!Array.isArray(sources)) return [];
567
+ const result = [];
568
+ for (const source of sources) {
569
+ if (source && typeof source === "object") {
570
+ const uri = source.uri;
571
+ const title = source.title;
572
+ if (typeof uri === "string") {
573
+ result.push({ uri, title: typeof title === "string" ? title : "" });
574
+ }
575
+ }
576
+ }
577
+ return result;
578
+ }
451
579
  async function backfillInsightsCommand(project, opts) {
452
580
  const { IntelligenceService } = await import("./intelligence-service-FNJTFSI3.js");
453
581
  const config = loadConfig();
@@ -628,6 +756,20 @@ var BACKFILL_CLI_COMMANDS = [
628
756
  });
629
757
  }
630
758
  },
759
+ {
760
+ path: ["backfill", "answer-mentions"],
761
+ usage: "canonry backfill answer-mentions [--project <name>] [--format json]",
762
+ options: {
763
+ project: stringOption()
764
+ },
765
+ allowPositionals: false,
766
+ run: async (input) => {
767
+ await backfillAnswerMentionsCommand({
768
+ project: getString(input.values, "project"),
769
+ format: input.format
770
+ });
771
+ }
772
+ },
631
773
  {
632
774
  path: ["backfill", "insights"],
633
775
  usage: "canonry backfill insights <project> [--from-run <id>] [--to-run <id>] [--format json]",
@@ -675,12 +817,12 @@ var BACKFILL_CLI_COMMANDS = [
675
817
  },
676
818
  {
677
819
  path: ["backfill"],
678
- usage: "canonry backfill <answer-visibility|insights|normalized-paths|ai-referral-paths> [options]",
820
+ usage: "canonry backfill <answer-visibility|answer-mentions|insights|normalized-paths|ai-referral-paths> [options]",
679
821
  run: async (input) => {
680
822
  unknownSubcommand(input.positionals[0], {
681
823
  command: "backfill",
682
- usage: "canonry backfill <answer-visibility|insights|normalized-paths|ai-referral-paths> [options]",
683
- available: ["answer-visibility", "insights", "normalized-paths", "ai-referral-paths"]
824
+ usage: "canonry backfill <answer-visibility|answer-mentions|insights|normalized-paths|ai-referral-paths> [options]",
825
+ available: ["answer-visibility", "answer-mentions", "insights", "normalized-paths", "ai-referral-paths"]
684
826
  });
685
827
  }
686
828
  }
@@ -1873,9 +2015,9 @@ async function gaConnect(project, opts) {
1873
2015
  propertyId: opts.propertyId
1874
2016
  };
1875
2017
  if (opts.keyFile) {
1876
- const fs9 = await import("fs");
2018
+ const fs10 = await import("fs");
1877
2019
  try {
1878
- const content = fs9.readFileSync(opts.keyFile, "utf-8");
2020
+ const content = fs10.readFileSync(opts.keyFile, "utf-8");
1879
2021
  JSON.parse(content);
1880
2022
  body.keyJson = content;
1881
2023
  } catch (e) {
@@ -5739,13 +5881,307 @@ Usage: canonry settings provider ${name} --api-key <key> [--model <model>] [--ma
5739
5881
  }
5740
5882
  ];
5741
5883
 
5884
+ // src/commands/skills.ts
5885
+ import fs4 from "fs";
5886
+ import path3 from "path";
5887
+ import { fileURLToPath } from "url";
5888
+ var BUNDLED_SKILL_NAMES = ["canonry-setup", "aero"];
5889
+ function resolveBundledSkillsRoot(pkgDir) {
5890
+ const here = pkgDir ?? path3.dirname(fileURLToPath(import.meta.url));
5891
+ const candidates = [
5892
+ path3.join(here, "../assets/agent-workspace/skills"),
5893
+ path3.join(here, "../../assets/agent-workspace/skills"),
5894
+ path3.join(here, "../../../../skills")
5895
+ ];
5896
+ for (const candidate of candidates) {
5897
+ if (BUNDLED_SKILL_NAMES.every((name) => fs4.existsSync(path3.join(candidate, name, "SKILL.md")))) {
5898
+ return candidate;
5899
+ }
5900
+ }
5901
+ throw new CliError({
5902
+ code: "INTERNAL_ERROR",
5903
+ message: `Bundled skills not found. Searched:
5904
+ ${candidates.join("\n ")}`,
5905
+ exitCode: 2
5906
+ });
5907
+ }
5908
+ function parseDescription(content) {
5909
+ const fmMatch = /^---\n([\s\S]*?)\n---/.exec(content);
5910
+ if (!fmMatch) return "";
5911
+ const descMatch = /^description:\s*(.+?)$/m.exec(fmMatch[1]);
5912
+ if (!descMatch) return "";
5913
+ return descMatch[1].replace(/^["']|["']$/g, "").trim();
5914
+ }
5915
+ function getBundledSkills(pkgDir) {
5916
+ const root = resolveBundledSkillsRoot(pkgDir);
5917
+ return BUNDLED_SKILL_NAMES.map((name) => {
5918
+ const skillDir = path3.join(root, name);
5919
+ const skillFile = path3.join(skillDir, "SKILL.md");
5920
+ const content = fs4.readFileSync(skillFile, "utf-8");
5921
+ return { name, description: parseDescription(content), bundledPath: skillDir };
5922
+ });
5923
+ }
5924
+ function walkRelative(dir, prefix = "") {
5925
+ const out = [];
5926
+ for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
5927
+ const rel = prefix ? path3.join(prefix, entry.name) : entry.name;
5928
+ const full = path3.join(dir, entry.name);
5929
+ if (entry.isDirectory()) {
5930
+ out.push(...walkRelative(full, rel));
5931
+ } else if (entry.isFile()) {
5932
+ out.push(rel);
5933
+ }
5934
+ }
5935
+ return out.sort();
5936
+ }
5937
+ function compareDirContent(srcDir, destDir) {
5938
+ if (!fs4.existsSync(destDir)) return "missing";
5939
+ if (!fs4.statSync(destDir).isDirectory()) return "different";
5940
+ const srcFiles = walkRelative(srcDir);
5941
+ const destFiles = walkRelative(destDir);
5942
+ if (srcFiles.length !== destFiles.length) return "different";
5943
+ for (let i = 0; i < srcFiles.length; i++) {
5944
+ if (srcFiles[i] !== destFiles[i]) return "different";
5945
+ const srcBytes = fs4.readFileSync(path3.join(srcDir, srcFiles[i]));
5946
+ const destBytes = fs4.readFileSync(path3.join(destDir, destFiles[i]));
5947
+ if (!srcBytes.equals(destBytes)) return "different";
5948
+ }
5949
+ return "match";
5950
+ }
5951
+ function copyDirRecursive(src, dest) {
5952
+ fs4.mkdirSync(dest, { recursive: true });
5953
+ for (const entry of fs4.readdirSync(src, { withFileTypes: true })) {
5954
+ const srcPath = path3.join(src, entry.name);
5955
+ const destPath = path3.join(dest, entry.name);
5956
+ if (entry.isDirectory()) {
5957
+ copyDirRecursive(srcPath, destPath);
5958
+ } else if (entry.isFile()) {
5959
+ fs4.copyFileSync(srcPath, destPath);
5960
+ }
5961
+ }
5962
+ }
5963
+ function installClaudeSkill(skill, targetDir, force) {
5964
+ const targetPath = path3.join(targetDir, ".claude", "skills", skill.name);
5965
+ const compare = compareDirContent(skill.bundledPath, targetPath);
5966
+ if (compare === "match") {
5967
+ return {
5968
+ skill: skill.name,
5969
+ client: CodingAgents.claude,
5970
+ targetPath,
5971
+ status: "already-installed",
5972
+ message: `Already installed: .claude/skills/${skill.name}`
5973
+ };
5974
+ }
5975
+ if (compare === "different" && !force) {
5976
+ throw new CliError({
5977
+ code: "VALIDATION_ERROR",
5978
+ message: `.claude/skills/${skill.name}/ already exists and differs from the bundled skill. Pass --force to overwrite.`,
5979
+ details: { skill: skill.name, targetPath },
5980
+ exitCode: 1
5981
+ });
5982
+ }
5983
+ if (compare === "different") {
5984
+ fs4.rmSync(targetPath, { recursive: true, force: true });
5985
+ }
5986
+ copyDirRecursive(skill.bundledPath, targetPath);
5987
+ return {
5988
+ skill: skill.name,
5989
+ client: CodingAgents.claude,
5990
+ targetPath,
5991
+ status: compare === "missing" ? "installed" : "updated",
5992
+ message: compare === "missing" ? `Installed .claude/skills/${skill.name}` : `Updated .claude/skills/${skill.name}`
5993
+ };
5994
+ }
5995
+ function installCodexSymlink(skill, targetDir, force) {
5996
+ const codexPath = path3.join(targetDir, ".codex", "skills", skill.name);
5997
+ const claudePath = path3.join(targetDir, ".claude", "skills", skill.name);
5998
+ const linkTarget = path3.relative(path3.dirname(codexPath), claudePath);
5999
+ fs4.mkdirSync(path3.dirname(codexPath), { recursive: true });
6000
+ let stat;
6001
+ try {
6002
+ stat = fs4.lstatSync(codexPath);
6003
+ } catch {
6004
+ stat = void 0;
6005
+ }
6006
+ if (stat?.isSymbolicLink()) {
6007
+ const existing = fs4.readlinkSync(codexPath);
6008
+ if (existing === linkTarget) {
6009
+ return {
6010
+ skill: skill.name,
6011
+ client: CodingAgents.codex,
6012
+ targetPath: codexPath,
6013
+ status: "already-linked",
6014
+ message: `Already linked: .codex/skills/${skill.name}`
6015
+ };
6016
+ }
6017
+ if (!force) {
6018
+ throw new CliError({
6019
+ code: "VALIDATION_ERROR",
6020
+ message: `.codex/skills/${skill.name} is a symlink pointing elsewhere (${existing}). Pass --force to relink.`,
6021
+ details: { skill: skill.name, targetPath: codexPath, existingTarget: existing },
6022
+ exitCode: 1
6023
+ });
6024
+ }
6025
+ fs4.unlinkSync(codexPath);
6026
+ fs4.symlinkSync(linkTarget, codexPath);
6027
+ return {
6028
+ skill: skill.name,
6029
+ client: CodingAgents.codex,
6030
+ targetPath: codexPath,
6031
+ status: "relinked",
6032
+ message: `Relinked .codex/skills/${skill.name} \u2192 ${linkTarget}`
6033
+ };
6034
+ }
6035
+ if (stat) {
6036
+ if (!force) {
6037
+ throw new CliError({
6038
+ code: "VALIDATION_ERROR",
6039
+ message: `.codex/skills/${skill.name} exists but is not a symlink. Pass --force to replace.`,
6040
+ details: { skill: skill.name, targetPath: codexPath },
6041
+ exitCode: 1
6042
+ });
6043
+ }
6044
+ fs4.rmSync(codexPath, { recursive: true, force: true });
6045
+ }
6046
+ fs4.symlinkSync(linkTarget, codexPath);
6047
+ return {
6048
+ skill: skill.name,
6049
+ client: CodingAgents.codex,
6050
+ targetPath: codexPath,
6051
+ status: stat ? "relinked" : "linked",
6052
+ message: stat ? `Replaced and linked .codex/skills/${skill.name} \u2192 ${linkTarget}` : `Linked .codex/skills/${skill.name} \u2192 ${linkTarget}`
6053
+ };
6054
+ }
6055
+ function buildSummaryMessage(results) {
6056
+ const counts = {};
6057
+ for (const r of results) counts[r.status] = (counts[r.status] ?? 0) + 1;
6058
+ const parts = Object.entries(counts).map(([status, n]) => `${n} ${status}`);
6059
+ return `Skills install summary: ${parts.join(", ")}.`;
6060
+ }
6061
+ async function installSkills(opts = {}) {
6062
+ const targetDir = path3.resolve(opts.dir ?? process.cwd());
6063
+ const client = opts.client ?? SkillsClients.all;
6064
+ const force = opts.force ?? false;
6065
+ const allSkills = getBundledSkills();
6066
+ const requestedNames = opts.skills && opts.skills.length > 0 ? opts.skills : allSkills.map((s) => s.name);
6067
+ const knownNames = new Set(allSkills.map((s) => s.name));
6068
+ const unknown = requestedNames.filter((n) => !knownNames.has(n));
6069
+ if (unknown.length > 0) {
6070
+ throw new CliError({
6071
+ code: "VALIDATION_ERROR",
6072
+ message: `Unknown skill(s): ${unknown.join(", ")}. Available: ${[...knownNames].join(", ")}`,
6073
+ details: { unknownSkills: unknown, availableSkills: [...knownNames] },
6074
+ exitCode: 1
6075
+ });
6076
+ }
6077
+ const skillsToInstall = allSkills.filter((s) => requestedNames.includes(s.name));
6078
+ fs4.mkdirSync(targetDir, { recursive: true });
6079
+ const results = [];
6080
+ for (const skill of skillsToInstall) {
6081
+ results.push(installClaudeSkill(skill, targetDir, force));
6082
+ if (client !== SkillsClients.claude) {
6083
+ results.push(installCodexSymlink(skill, targetDir, force));
6084
+ }
6085
+ }
6086
+ return {
6087
+ targetDir,
6088
+ results,
6089
+ message: buildSummaryMessage(results)
6090
+ };
6091
+ }
6092
+ async function listSkills(opts = {}) {
6093
+ const skills = getBundledSkills();
6094
+ if (opts.format === "json") {
6095
+ console.log(JSON.stringify({
6096
+ skills: skills.map((s) => ({
6097
+ name: s.name,
6098
+ description: s.description,
6099
+ claudePath: `.claude/skills/${s.name}`,
6100
+ codexPath: `.codex/skills/${s.name}`
6101
+ }))
6102
+ }, null, 2));
6103
+ return;
6104
+ }
6105
+ console.log("Bundled canonry skills:\n");
6106
+ for (const skill of skills) {
6107
+ console.log(` ${skill.name}`);
6108
+ if (skill.description) console.log(` ${skill.description}`);
6109
+ console.log(` Claude: .claude/skills/${skill.name}/`);
6110
+ console.log(` Codex: .codex/skills/${skill.name} (symlink \u2192 ../../.claude/skills/${skill.name})`);
6111
+ console.log();
6112
+ }
6113
+ }
6114
+ function emitInstallSummary(summary, format) {
6115
+ if (format === "json") {
6116
+ console.log(JSON.stringify(summary, null, 2));
6117
+ return;
6118
+ }
6119
+ for (const r of summary.results) console.log(r.message);
6120
+ console.log(`
6121
+ Target: ${summary.targetDir}`);
6122
+ console.log(summary.message);
6123
+ }
6124
+ function parseSkillsClient(value) {
6125
+ if (!value) return SkillsClients.all;
6126
+ const parsed = skillsClientSchema.safeParse(value);
6127
+ if (parsed.success) return parsed.data;
6128
+ const allowed = skillsClientSchema.options;
6129
+ throw new CliError({
6130
+ code: "VALIDATION_ERROR",
6131
+ message: `Invalid --client value "${value}". Must be one of: ${allowed.join(", ")}`,
6132
+ details: { flag: "client", value, allowed },
6133
+ exitCode: 1
6134
+ });
6135
+ }
6136
+
6137
+ // src/cli-commands/skills.ts
6138
+ var SKILLS_CLI_COMMANDS = [
6139
+ {
6140
+ path: ["skills", "list"],
6141
+ usage: "canonry skills list [--format json]",
6142
+ run: async (input) => {
6143
+ await listSkills({ format: input.format });
6144
+ }
6145
+ },
6146
+ {
6147
+ path: ["skills", "install"],
6148
+ usage: "canonry skills install [skill...] [--dir <path>] [--client claude|codex|all] [--force] [--format json]",
6149
+ options: {
6150
+ dir: stringOption(),
6151
+ client: stringOption(),
6152
+ force: { type: "boolean" }
6153
+ },
6154
+ allowPositionals: true,
6155
+ run: async (input) => {
6156
+ const summary = await installSkills({
6157
+ dir: getString(input.values, "dir"),
6158
+ skills: input.positionals.length > 0 ? input.positionals : void 0,
6159
+ client: parseSkillsClient(getString(input.values, "client")),
6160
+ force: getBoolean(input.values, "force")
6161
+ });
6162
+ emitInstallSummary(summary, input.format);
6163
+ }
6164
+ },
6165
+ {
6166
+ path: ["skills"],
6167
+ usage: "canonry skills <list|install> [args]",
6168
+ run: async (input) => {
6169
+ unknownSubcommand(input.positionals[0], {
6170
+ command: "skills",
6171
+ usage: "canonry skills <list|install> [args]",
6172
+ available: ["list", "install"]
6173
+ });
6174
+ }
6175
+ }
6176
+ ];
6177
+
5742
6178
  // src/commands/snapshot.ts
5743
- import fs5 from "fs";
5744
- import path4 from "path";
6179
+ import fs6 from "fs";
6180
+ import path5 from "path";
5745
6181
 
5746
6182
  // src/snapshot-pdf.ts
5747
- import fs4 from "fs";
5748
- import path3 from "path";
6183
+ import fs5 from "fs";
6184
+ import path4 from "path";
5749
6185
  import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
5750
6186
  var PAGE_WIDTH = 612;
5751
6187
  var PAGE_HEIGHT = 792;
@@ -5954,9 +6390,9 @@ async function writeSnapshotPdf(report, outputPath) {
5954
6390
  renderCompetitors(pdf, report);
5955
6391
  renderQueries(pdf, report);
5956
6392
  const bytes = await doc.save();
5957
- const resolvedPath = path3.resolve(outputPath);
5958
- fs4.mkdirSync(path3.dirname(resolvedPath), { recursive: true });
5959
- fs4.writeFileSync(resolvedPath, bytes);
6393
+ const resolvedPath = path4.resolve(outputPath);
6394
+ fs5.mkdirSync(path4.dirname(resolvedPath), { recursive: true });
6395
+ fs5.writeFileSync(resolvedPath, bytes);
5960
6396
  return resolvedPath;
5961
6397
  }
5962
6398
  function renderCover(pdf, report) {
@@ -6114,9 +6550,9 @@ Markdown saved: ${savedMdPath}`);
6114
6550
  PDF saved: ${savedPdfPath}`);
6115
6551
  }
6116
6552
  function writeSnapshotMarkdown(report, outputPath) {
6117
- const resolvedPath = path4.resolve(outputPath);
6118
- fs5.mkdirSync(path4.dirname(resolvedPath), { recursive: true });
6119
- fs5.writeFileSync(resolvedPath, formatSnapshotMarkdown(report), "utf-8");
6553
+ const resolvedPath = path5.resolve(outputPath);
6554
+ fs6.mkdirSync(path5.dirname(resolvedPath), { recursive: true });
6555
+ fs6.writeFileSync(resolvedPath, formatSnapshotMarkdown(report), "utf-8");
6120
6556
  return resolvedPath;
6121
6557
  }
6122
6558
  function formatSnapshotMarkdown(report) {
@@ -6771,7 +7207,7 @@ var CONTENT_CLI_COMMANDS = [
6771
7207
 
6772
7208
  // src/commands/bootstrap.ts
6773
7209
  import crypto from "crypto";
6774
- import path5 from "path";
7210
+ import path6 from "path";
6775
7211
  import { eq as eq2 } from "drizzle-orm";
6776
7212
 
6777
7213
  // ../config/src/index.ts
@@ -6918,7 +7354,7 @@ async function bootstrapCommand(_opts) {
6918
7354
  );
6919
7355
  }
6920
7356
  const configDir = getConfigDir();
6921
- const databasePath = env.databasePath || path5.join(configDir, "data.db");
7357
+ const databasePath = env.databasePath || path6.join(configDir, "data.db");
6922
7358
  const existing = configExists();
6923
7359
  const existingConfig = existing ? loadConfig() : void 0;
6924
7360
  let rawApiKey;
@@ -6988,10 +7424,10 @@ async function bootstrapCommand(_opts) {
6988
7424
 
6989
7425
  // src/commands/daemon.ts
6990
7426
  import { spawn } from "child_process";
6991
- import fs6 from "fs";
6992
- import path6 from "path";
7427
+ import fs7 from "fs";
7428
+ import path7 from "path";
6993
7429
  function getPidPath() {
6994
- return path6.join(getConfigDir(), "canonry.pid");
7430
+ return path7.join(getConfigDir(), "canonry.pid");
6995
7431
  }
6996
7432
  function isProcessAlive(pid) {
6997
7433
  try {
@@ -7018,8 +7454,8 @@ async function waitForReady(host, port, maxMs = 1e4) {
7018
7454
  async function startDaemon(opts) {
7019
7455
  const pidPath = getPidPath();
7020
7456
  const format = opts.format ?? "text";
7021
- if (fs6.existsSync(pidPath)) {
7022
- const existingPid = parseInt(fs6.readFileSync(pidPath, "utf-8").trim(), 10);
7457
+ if (fs7.existsSync(pidPath)) {
7458
+ const existingPid = parseInt(fs7.readFileSync(pidPath, "utf-8").trim(), 10);
7023
7459
  if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
7024
7460
  throw new CliError({
7025
7461
  code: "DAEMON_ALREADY_RUNNING",
@@ -7030,9 +7466,9 @@ async function startDaemon(opts) {
7030
7466
  }
7031
7467
  });
7032
7468
  }
7033
- fs6.unlinkSync(pidPath);
7469
+ fs7.unlinkSync(pidPath);
7034
7470
  }
7035
- const cliPath = path6.resolve(new URL(import.meta.url).pathname);
7471
+ const cliPath = path7.resolve(new URL(import.meta.url).pathname);
7036
7472
  const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
7037
7473
  const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
7038
7474
  if (opts.port) args.push("--port", opts.port);
@@ -7051,10 +7487,10 @@ async function startDaemon(opts) {
7051
7487
  });
7052
7488
  }
7053
7489
  const configDir = getConfigDir();
7054
- if (!fs6.existsSync(configDir)) {
7055
- fs6.mkdirSync(configDir, { recursive: true });
7490
+ if (!fs7.existsSync(configDir)) {
7491
+ fs7.mkdirSync(configDir, { recursive: true });
7056
7492
  }
7057
- fs6.writeFileSync(pidPath, String(child.pid), "utf-8");
7493
+ fs7.writeFileSync(pidPath, String(child.pid), "utf-8");
7058
7494
  const port = opts.port ?? "4100";
7059
7495
  const host = opts.host ?? "127.0.0.1";
7060
7496
  if (format !== "json") {
@@ -7063,7 +7499,7 @@ async function startDaemon(opts) {
7063
7499
  const ready = await waitForReady(host, port);
7064
7500
  if (!ready) {
7065
7501
  try {
7066
- fs6.unlinkSync(pidPath);
7502
+ fs7.unlinkSync(pidPath);
7067
7503
  } catch {
7068
7504
  }
7069
7505
  throw new CliError({
@@ -7095,7 +7531,7 @@ async function startDaemon(opts) {
7095
7531
  }
7096
7532
  function stopDaemon(format = "text") {
7097
7533
  const pidPath = getPidPath();
7098
- if (!fs6.existsSync(pidPath)) {
7534
+ if (!fs7.existsSync(pidPath)) {
7099
7535
  if (format === "json") {
7100
7536
  console.log(JSON.stringify({
7101
7537
  stopped: false,
@@ -7106,7 +7542,7 @@ function stopDaemon(format = "text") {
7106
7542
  console.log("Canonry is not running (no PID file found)");
7107
7543
  return;
7108
7544
  }
7109
- const pid = parseInt(fs6.readFileSync(pidPath, "utf-8").trim(), 10);
7545
+ const pid = parseInt(fs7.readFileSync(pidPath, "utf-8").trim(), 10);
7110
7546
  if (isNaN(pid)) {
7111
7547
  if (format === "json") {
7112
7548
  console.log(JSON.stringify({
@@ -7117,7 +7553,7 @@ function stopDaemon(format = "text") {
7117
7553
  } else {
7118
7554
  console.error("Invalid PID file. Removing it.");
7119
7555
  }
7120
- fs6.unlinkSync(pidPath);
7556
+ fs7.unlinkSync(pidPath);
7121
7557
  return;
7122
7558
  }
7123
7559
  if (!isProcessAlive(pid)) {
@@ -7131,12 +7567,12 @@ function stopDaemon(format = "text") {
7131
7567
  } else {
7132
7568
  console.log(`Canonry is not running (stale PID: ${pid}). Cleaning up.`);
7133
7569
  }
7134
- fs6.unlinkSync(pidPath);
7570
+ fs7.unlinkSync(pidPath);
7135
7571
  return;
7136
7572
  }
7137
7573
  try {
7138
7574
  process.kill(pid, "SIGTERM");
7139
- fs6.unlinkSync(pidPath);
7575
+ fs7.unlinkSync(pidPath);
7140
7576
  if (format === "json") {
7141
7577
  console.log(JSON.stringify({
7142
7578
  stopped: true,
@@ -7160,9 +7596,9 @@ function stopDaemon(format = "text") {
7160
7596
 
7161
7597
  // src/commands/init.ts
7162
7598
  import crypto2 from "crypto";
7163
- import fs7 from "fs";
7599
+ import fs8 from "fs";
7164
7600
  import readline from "readline";
7165
- import path7 from "path";
7601
+ import path8 from "path";
7166
7602
  function prompt(question) {
7167
7603
  const rl = readline.createInterface({
7168
7604
  input: process.stdin,
@@ -7180,6 +7616,12 @@ var DEFAULT_QUOTA = {
7180
7616
  maxRequestsPerMinute: 10,
7181
7617
  maxRequestsPerDay: 500
7182
7618
  };
7619
+ var PROJECT_MARKERS = [".git", "canonry.yaml", "canonry.yml", "package.json"];
7620
+ function cwdLooksLikeProject(dir) {
7621
+ const home = process.env.HOME ?? "";
7622
+ if (home && path8.resolve(dir) === path8.resolve(home)) return false;
7623
+ return PROJECT_MARKERS.some((marker) => fs8.existsSync(path8.join(dir, marker)));
7624
+ }
7183
7625
  var DEFAULT_AGENT_MODELS = {
7184
7626
  anthropic: "anthropic/claude-sonnet-4-6",
7185
7627
  openai: "openai/gpt-4o",
@@ -7208,8 +7650,8 @@ async function initCommand(opts) {
7208
7650
  return void 0;
7209
7651
  }
7210
7652
  const configDir = getConfigDir();
7211
- if (!fs7.existsSync(configDir)) {
7212
- fs7.mkdirSync(configDir, { recursive: true });
7653
+ if (!fs8.existsSync(configDir)) {
7654
+ fs8.mkdirSync(configDir, { recursive: true });
7213
7655
  }
7214
7656
  const bootstrapEnv = getBootstrapEnv(process.env, {
7215
7657
  GEMINI_API_KEY: opts?.geminiKey,
@@ -7324,7 +7766,7 @@ async function initCommand(opts) {
7324
7766
  const rawApiKey = `cnry_${crypto2.randomBytes(16).toString("hex")}`;
7325
7767
  const keyHash = crypto2.createHash("sha256").update(rawApiKey).digest("hex");
7326
7768
  const keyPrefix = rawApiKey.slice(0, 9);
7327
- const databasePath = path7.join(configDir, "data.db");
7769
+ const databasePath = path8.join(configDir, "data.db");
7328
7770
  const db = createClient(databasePath);
7329
7771
  migrate(db);
7330
7772
  db.insert(apiKeys).values({
@@ -7343,6 +7785,20 @@ async function initCommand(opts) {
7343
7785
  google
7344
7786
  });
7345
7787
  const providerNames = Object.keys(providers);
7788
+ let skillsSummary;
7789
+ let skillsTip;
7790
+ if (!opts?.skipSkills) {
7791
+ const skillsTarget = opts?.skillsDir ?? process.cwd();
7792
+ if (cwdLooksLikeProject(skillsTarget)) {
7793
+ try {
7794
+ skillsSummary = await installSkills({ dir: skillsTarget });
7795
+ } catch (err) {
7796
+ skillsTip = `Skills auto-install failed: ${err instanceof Error ? err.message : String(err)}. Run "canonry skills install" manually.`;
7797
+ }
7798
+ } else {
7799
+ skillsTip = 'Run "canonry skills install" in a project directory to add the canonry + Aero playbook to .claude/skills/ and .codex/skills/.';
7800
+ }
7801
+ }
7346
7802
  if (format === "json") {
7347
7803
  console.log(JSON.stringify({
7348
7804
  initialized: true,
@@ -7351,7 +7807,9 @@ async function initCommand(opts) {
7351
7807
  apiUrl: `http://localhost:${process.env.CANONRY_PORT || "4100"}`,
7352
7808
  apiKey: rawApiKey,
7353
7809
  providers: providerNames,
7354
- googleConfigured: !!google
7810
+ googleConfigured: !!google,
7811
+ skills: skillsSummary,
7812
+ skillsTip
7355
7813
  }, null, 2));
7356
7814
  } else {
7357
7815
  console.log(`
@@ -7359,6 +7817,13 @@ Config saved to ${getConfigPath()}`);
7359
7817
  console.log(`Database created at ${databasePath}`);
7360
7818
  console.log(`API key: ${rawApiKey}`);
7361
7819
  console.log(`Providers: ${providerNames.join(", ")}`);
7820
+ if (skillsSummary) {
7821
+ console.log(`
7822
+ ${skillsSummary.message}`);
7823
+ console.log(`Skills target: ${skillsSummary.targetDir}`);
7824
+ }
7825
+ if (skillsTip) console.log(`
7826
+ ${skillsTip}`);
7362
7827
  }
7363
7828
  let agentLLM;
7364
7829
  const agentProvider = opts?.agentProvider;
@@ -7623,7 +8088,7 @@ function applyServerEnv(values) {
7623
8088
  var SYSTEM_CLI_COMMANDS = [
7624
8089
  {
7625
8090
  path: ["init"],
7626
- usage: "canonry init [--force] [--gemini-key <key>] [--openai-key <key>] [--claude-key <key>] [--perplexity-key <key>] [--local-url <url>] [--local-model <name>] [--local-key <key>] [--google-client-id <id>] [--google-client-secret <key>] [--format json]",
8091
+ usage: "canonry init [--force] [--gemini-key <key>] [--openai-key <key>] [--claude-key <key>] [--perplexity-key <key>] [--local-url <url>] [--local-model <name>] [--local-key <key>] [--google-client-id <id>] [--google-client-secret <key>] [--skip-skills] [--skills-dir <path>] [--format json]",
7627
8092
  options: {
7628
8093
  force: { type: "boolean", short: "f", default: false },
7629
8094
  "gemini-key": stringOption(),
@@ -7634,7 +8099,9 @@ var SYSTEM_CLI_COMMANDS = [
7634
8099
  "local-model": stringOption(),
7635
8100
  "local-key": stringOption(),
7636
8101
  "google-client-id": stringOption(),
7637
- "google-client-secret": stringOption()
8102
+ "google-client-secret": stringOption(),
8103
+ "skip-skills": { type: "boolean" },
8104
+ "skills-dir": stringOption()
7638
8105
  },
7639
8106
  allowPositionals: false,
7640
8107
  run: async (input) => {
@@ -7649,6 +8116,8 @@ var SYSTEM_CLI_COMMANDS = [
7649
8116
  localKey: getString(input.values, "local-key"),
7650
8117
  googleClientId: getString(input.values, "google-client-id"),
7651
8118
  googleClientSecret: getString(input.values, "google-client-secret"),
8119
+ skipSkills: getBoolean(input.values, "skip-skills"),
8120
+ skillsDir: getString(input.values, "skills-dir"),
7652
8121
  format: input.format
7653
8122
  });
7654
8123
  }
@@ -7745,7 +8214,7 @@ var SYSTEM_CLI_COMMANDS = [
7745
8214
  ];
7746
8215
 
7747
8216
  // src/cli-commands/wordpress.ts
7748
- import fs8 from "fs";
8217
+ import fs9 from "fs";
7749
8218
 
7750
8219
  // src/commands/wordpress.ts
7751
8220
  function getClient18() {
@@ -7981,12 +8450,12 @@ async function wordpressSetMeta(project, body) {
7981
8450
  printPageDetail(result);
7982
8451
  }
7983
8452
  async function wordpressBulkSetMeta(project, opts) {
7984
- const fs9 = await import("fs/promises");
7985
- const path8 = await import("path");
7986
- const filePath = path8.resolve(opts.from);
8453
+ const fs10 = await import("fs/promises");
8454
+ const path9 = await import("path");
8455
+ const filePath = path9.resolve(opts.from);
7987
8456
  let raw;
7988
8457
  try {
7989
- raw = await fs9.readFile(filePath, "utf8");
8458
+ raw = await fs10.readFile(filePath, "utf8");
7990
8459
  } catch {
7991
8460
  throw new CliError({
7992
8461
  code: "FILE_READ_ERROR",
@@ -8083,13 +8552,13 @@ async function wordpressSetSchema(project, body) {
8083
8552
  printManualAssist(`Schema update for "${body.slug}"`, result);
8084
8553
  }
8085
8554
  async function wordpressSchemaDeploy(project, opts) {
8086
- const fs9 = await import("fs/promises");
8087
- const path8 = await import("path");
8555
+ const fs10 = await import("fs/promises");
8556
+ const path9 = await import("path");
8088
8557
  const yaml = await import("yaml").catch(() => null);
8089
- const filePath = path8.resolve(opts.profile);
8558
+ const filePath = path9.resolve(opts.profile);
8090
8559
  let raw;
8091
8560
  try {
8092
- raw = await fs9.readFile(filePath, "utf8");
8561
+ raw = await fs10.readFile(filePath, "utf8");
8093
8562
  } catch {
8094
8563
  throw new CliError({
8095
8564
  code: "FILE_READ_ERROR",
@@ -8194,13 +8663,13 @@ async function wordpressOnboard(project, opts) {
8194
8663
  }
8195
8664
  let profileData;
8196
8665
  if (opts.profile) {
8197
- const fs9 = await import("fs/promises");
8198
- const path8 = await import("path");
8666
+ const fs10 = await import("fs/promises");
8667
+ const path9 = await import("path");
8199
8668
  const yaml = await import("yaml").catch(() => null);
8200
- const filePath = path8.resolve(opts.profile);
8669
+ const filePath = path9.resolve(opts.profile);
8201
8670
  let raw;
8202
8671
  try {
8203
- raw = await fs9.readFile(filePath, "utf8");
8672
+ raw = await fs10.readFile(filePath, "utf8");
8204
8673
  } catch {
8205
8674
  throw new CliError({
8206
8675
  code: "FILE_READ_ERROR",
@@ -8349,7 +8818,7 @@ function resolveContent(input, command, usage, options) {
8349
8818
  }
8350
8819
  if (contentFile) {
8351
8820
  try {
8352
- return fs8.readFileSync(contentFile, "utf-8");
8821
+ return fs9.readFileSync(contentFile, "utf-8");
8353
8822
  } catch (error) {
8354
8823
  const message = error instanceof Error ? error.message : String(error);
8355
8824
  throw usageError(`Error: could not read --content-file "${contentFile}": ${message}`, {
@@ -9283,6 +9752,7 @@ var REGISTERED_CLI_COMMANDS = [
9283
9752
  ...KEYWORD_CLI_COMMANDS,
9284
9753
  ...COMPETITOR_CLI_COMMANDS,
9285
9754
  ...SETTINGS_CLI_COMMANDS,
9755
+ ...SKILLS_CLI_COMMANDS,
9286
9756
  ...SNAPSHOT_CLI_COMMANDS,
9287
9757
  ...RUN_CLI_COMMANDS,
9288
9758
  ...OPERATOR_CLI_COMMANDS,
@@ -9312,6 +9782,7 @@ Setup:
9312
9782
  bootstrap Bootstrap config/database from env vars
9313
9783
  serve Start the local server (foreground)
9314
9784
  start / stop Start/stop as a background daemon
9785
+ skills List or install bundled agent skills (claude/codex)
9315
9786
 
9316
9787
  Projects:
9317
9788
  project Create, update, list, show, delete projects