@canonry/canonry 4.167.1 → 4.168.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: aero
3
- description: AEO analyst orchestration coordinates canonry sweeps and aeo-audit analysis with persistent memory and proactive regression response.
3
+ description: "Diagnose AEO regressions and report on them: why mention or citation coverage moved, which queries and answer engines changed, and what to do about it. Use when a coverage number moved and needs explaining, when preparing a client report or month-over-month comparison, or when a `cnry` sweep completed and needs analysis. Coordinates canonry sweeps with aeo-audit site analysis and keeps durable project memory. Use the canonry skill for setup and operations instead."
4
4
  metadata:
5
5
  homepage: https://canonry.ai
6
6
  repository: https://github.com/AINYC/aero
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: canonry
3
- description: "Set up and operate Canonry AEO projects. Inspect mention and citation coverage, diagnose regressions, and run technical audits. Connect Cloudflare direct-push or queue-pull traffic. Act through the Canonry CLI or MCP tools. Examples: inspect project acme coverage (run + report), diagnose query drift via attribution (report.html + visibility-attribution), or submit GSC sitemaps (gsc-sitemap-submission batched)."
3
+ description: "Operate Canonry (the `cnry` / `canonry` CLI) for AEO. Load this BEFORE any canonry operator task: creating or configuring a project, connecting GSC, GA4, Bing, Google Business Profile or a Cloudflare traffic source, running or scheduling a sweep, reading mention and citation coverage, running a technical audit, submitting sitemaps, or diagnosing why a number moved. Covers anything touching cnry, canonry doctor, ~/.canonry, @canonry/canonry, the canonry_* MCP tools, mention share, or direct-push / queue-pull traffic. Load it before acting, not after something fails."
4
4
  compatibility: Requires Node.js 22.14+ and globally installed @canonry/canonry; canonry-mcp must be on PATH.
5
5
  metadata:
6
6
  agent: >-
@@ -0,0 +1,123 @@
1
+ import {
2
+ BUNDLED_SKILL_NAMES,
3
+ PACKAGE_VERSION,
4
+ configExists,
5
+ installSkills,
6
+ loadConfigRaw,
7
+ saveConfigPatch
8
+ } from "./chunk-C4UWZLQX.js";
9
+ import {
10
+ SKILL_MANIFEST_FILENAME,
11
+ SkillsClients
12
+ } from "./chunk-TP4KXU3E.js";
13
+
14
+ // src/skills-autosync.ts
15
+ import fs from "fs";
16
+ import os from "os";
17
+ import path from "path";
18
+ var DEFAULT_SYNC_INTERVAL_SECONDS = 24 * 60 * 60;
19
+ function peekVersionChange() {
20
+ if (!configExists()) return { state: "unknown" };
21
+ const raw = loadConfigRaw();
22
+ if (raw === null) return { state: "unknown" };
23
+ const lastSeen = raw.lastSkillsSyncedVersion;
24
+ if (lastSeen === PACKAGE_VERSION) return { state: "unchanged" };
25
+ return { state: "changed", lastSeen };
26
+ }
27
+ function syncIntervalSeconds(env = process.env) {
28
+ const raw = env.CANONRY_SKILLS_SYNC_SECS;
29
+ if (!raw) return DEFAULT_SYNC_INTERVAL_SECONDS;
30
+ const parsed = Number(raw);
31
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_SYNC_INTERVAL_SECONDS;
32
+ }
33
+ function isDisabled(env = process.env) {
34
+ const raw = env.CANONRY_NO_AUTO_SKILLS_SYNC;
35
+ return raw === "1" || raw === "true";
36
+ }
37
+ function ownedInstallTargets(cwd = process.cwd(), home = os.homedir()) {
38
+ const seen = /* @__PURE__ */ new Set();
39
+ const candidates = [
40
+ { dir: path.resolve(home), user: true },
41
+ { dir: path.resolve(cwd), user: false }
42
+ ];
43
+ const targets = [];
44
+ for (const { dir, user } of candidates) {
45
+ if (seen.has(dir)) continue;
46
+ seen.add(dir);
47
+ const skills = BUNDLED_SKILL_NAMES.filter(
48
+ (name) => fs.existsSync(path.join(dir, ".claude", "skills", name, SKILL_MANIFEST_FILENAME))
49
+ );
50
+ if (skills.length === 0) continue;
51
+ const hasCodex = skills.some((name) => {
52
+ try {
53
+ return fs.lstatSync(path.join(dir, ".codex", "skills", name)).isSymbolicLink();
54
+ } catch {
55
+ return false;
56
+ }
57
+ });
58
+ targets.push({ dir, user, skills, client: hasCodex ? SkillsClients.all : SkillsClients.claude });
59
+ }
60
+ return targets;
61
+ }
62
+ async function autoSyncSkills(env = process.env) {
63
+ const skipped = { ran: false, reason: "skipped", updated: [], conflicts: [], targets: [] };
64
+ if (isDisabled(env)) return skipped;
65
+ const probe = peekVersionChange();
66
+ if (probe.state === "unknown") return skipped;
67
+ let reason;
68
+ if (probe.state === "changed") {
69
+ reason = "version-changed";
70
+ } else {
71
+ const intervalSeconds = syncIntervalSeconds(env);
72
+ if (intervalSeconds === 0) return skipped;
73
+ const raw = loadConfigRaw();
74
+ if (raw === null) return skipped;
75
+ const last = raw.lastSkillsVerifiedAt ? Date.parse(raw.lastSkillsVerifiedAt) : 0;
76
+ const elapsed = (Date.now() - (Number.isFinite(last) ? last : 0)) / 1e3;
77
+ if (elapsed < intervalSeconds) return skipped;
78
+ reason = "interval-elapsed";
79
+ }
80
+ const targets = ownedInstallTargets();
81
+ if (targets.length === 0) {
82
+ return skipped;
83
+ }
84
+ const updated = [];
85
+ const conflicts = [];
86
+ for (const target of targets) {
87
+ for (const skill of target.skills) {
88
+ try {
89
+ const summary = await installSkills({
90
+ ...target.user ? { user: true } : { dir: target.dir },
91
+ skills: [skill],
92
+ client: target.client
93
+ });
94
+ for (const result of summary.results) {
95
+ for (const p of result.updated ?? []) updated.push(p);
96
+ for (const p of result.conflicts ?? []) conflicts.push(p);
97
+ }
98
+ } catch {
99
+ }
100
+ }
101
+ }
102
+ recordVerified();
103
+ return { ran: true, reason, updated, conflicts, targets: targets.map((t) => t.dir) };
104
+ }
105
+ function recordVerified() {
106
+ try {
107
+ saveConfigPatch({
108
+ lastSkillsSyncedVersion: PACKAGE_VERSION,
109
+ lastSkillsVerifiedAt: (/* @__PURE__ */ new Date()).toISOString()
110
+ });
111
+ } catch {
112
+ }
113
+ }
114
+ function formatAutoSyncNotice(result) {
115
+ if (!result.ran || result.conflicts.length === 0) return null;
116
+ const count = result.conflicts.length;
117
+ return `canonry: ${count} locally edited skill file${count === 1 ? "" : "s"} kept as-is (engine is now v${PACKAGE_VERSION}). Run "canonry skills install --force" to take the new version.`;
118
+ }
119
+
120
+ export {
121
+ autoSyncSkills,
122
+ formatAutoSyncNotice
123
+ };
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  AGENT_MEMORY_KEY_MAX_LENGTH,
3
3
  AGENT_MEMORY_VALUE_MAX_BYTES,
4
+ CodingAgents,
4
5
  DISCOVERY_MAX_PROBES_CAP,
5
6
  DISCOVERY_PROBE_CONCURRENCY_CAP,
7
+ SKILL_MANIFEST_FILENAME,
8
+ SkillsClients,
6
9
  adsActivateTreeRequestSchema,
7
10
  adsAdCreateRequestSchema,
8
11
  adsAdGroupCreateRequestSchema,
@@ -16,6 +19,8 @@ import {
16
19
  adsPauseRequestSchema,
17
20
  adsUnresolvedOperationListQuerySchema,
18
21
  backlinkSourceSchema,
22
+ classifySkillFile,
23
+ coerceSkillManifest,
19
24
  competitorBatchRequestSchema,
20
25
  describeError,
21
26
  discoveryBucketSchema,
@@ -77,6 +82,7 @@ import {
77
82
  runTriggerRequestSchema,
78
83
  schedulableRunKindSchema,
79
84
  scheduleUpsertRequestSchema,
85
+ skillsClientSchema,
80
86
  trafficConnectCloudRunRequestSchema,
81
87
  trafficConnectVercelRequestSchema,
82
88
  trafficConnectWordpressRequestSchema,
@@ -626,7 +632,7 @@ var serializeObjectParam = ({
626
632
 
627
633
  // ../api-client-generated/src/generated/core/utils.gen.ts
628
634
  var PATH_PARAM_RE = /\{[^{}]+\}/g;
629
- var defaultPathSerializer = ({ path: path2, url: _url }) => {
635
+ var defaultPathSerializer = ({ path: path3, url: _url }) => {
630
636
  let url = _url;
631
637
  const matches = _url.match(PATH_PARAM_RE);
632
638
  if (matches) {
@@ -645,7 +651,7 @@ var defaultPathSerializer = ({ path: path2, url: _url }) => {
645
651
  name = name.substring(1);
646
652
  style = "matrix";
647
653
  }
648
- const value = path2[name];
654
+ const value = path3[name];
649
655
  if (value === void 0 || value === null) {
650
656
  continue;
651
657
  }
@@ -689,15 +695,15 @@ var defaultPathSerializer = ({ path: path2, url: _url }) => {
689
695
  };
690
696
  var getUrl = ({
691
697
  baseUrl,
692
- path: path2,
698
+ path: path3,
693
699
  query,
694
700
  querySerializer,
695
701
  url: _url
696
702
  }) => {
697
703
  const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
698
704
  let url = (baseUrl ?? "") + pathUrl;
699
- if (path2) {
700
- url = defaultPathSerializer({ path: path2, url });
705
+ if (path3) {
706
+ url = defaultPathSerializer({ path: path3, url });
701
707
  }
702
708
  let search = query ? querySerializer(query) : "";
703
709
  if (search.startsWith("?")) {
@@ -5376,9 +5382,9 @@ var ApiClient = class {
5376
5382
  * structured-error behavior of `request()`; the caller reads `res.body`
5377
5383
  * and releases the response when done.
5378
5384
  */
5379
- async streamPost(path2, body, signal) {
5385
+ async streamPost(path3, body, signal) {
5380
5386
  await this.probeBasePath();
5381
- const url = `${this.originUrl}/api/v1${path2}`;
5387
+ const url = `${this.originUrl}/api/v1${path3}`;
5382
5388
  const headers = {
5383
5389
  Authorization: `Bearer ${this.apiKey}`,
5384
5390
  "Content-Type": "application/json",
@@ -7809,6 +7815,384 @@ import { createRequire } from "module";
7809
7815
  var _require = createRequire(import.meta.url);
7810
7816
  var PACKAGE_VERSION = _require("../package.json").version;
7811
7817
 
7818
+ // src/commands/skills.ts
7819
+ import crypto from "crypto";
7820
+ import fs2 from "fs";
7821
+ import os2 from "os";
7822
+ import path2 from "path";
7823
+ import { fileURLToPath } from "url";
7824
+ var BUNDLED_SKILL_NAMES = ["canonry", "aero"];
7825
+ function resolveBundledSkillsRoot(pkgDir) {
7826
+ const here = pkgDir ?? path2.dirname(fileURLToPath(import.meta.url));
7827
+ const candidates = [
7828
+ path2.join(here, "../assets/agent-workspace/skills"),
7829
+ path2.join(here, "../../assets/agent-workspace/skills"),
7830
+ path2.join(here, "../../../../skills")
7831
+ ];
7832
+ for (const candidate of candidates) {
7833
+ if (BUNDLED_SKILL_NAMES.every((name) => fs2.existsSync(path2.join(candidate, name, "SKILL.md")))) {
7834
+ return candidate;
7835
+ }
7836
+ }
7837
+ throw new CliError({
7838
+ code: "INTERNAL_ERROR",
7839
+ message: `Bundled skills not found. Searched:
7840
+ ${candidates.join("\n ")}`,
7841
+ exitCode: 2
7842
+ });
7843
+ }
7844
+ function parseDescription(content) {
7845
+ const fmMatch = /^---\n([\s\S]*?)\n---/.exec(content);
7846
+ if (!fmMatch) return "";
7847
+ const descMatch = /^description:\s*(\S.*)$/m.exec(fmMatch[1]);
7848
+ if (!descMatch) return "";
7849
+ return descMatch[1].replace(/^["']|["']$/g, "").trim();
7850
+ }
7851
+ function getBundledSkills(pkgDir) {
7852
+ const root = resolveBundledSkillsRoot(pkgDir);
7853
+ return BUNDLED_SKILL_NAMES.map((name) => {
7854
+ const skillDir = path2.join(root, name);
7855
+ const skillFile = path2.join(skillDir, "SKILL.md");
7856
+ const content = fs2.readFileSync(skillFile, "utf-8");
7857
+ return { name, description: parseDescription(content), bundledPath: skillDir };
7858
+ });
7859
+ }
7860
+ function walkRelative(dir, prefix = "") {
7861
+ const out = [];
7862
+ for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
7863
+ const rel = prefix ? path2.join(prefix, entry.name) : entry.name;
7864
+ const full = path2.join(dir, entry.name);
7865
+ if (entry.isDirectory()) {
7866
+ out.push(...walkRelative(full, rel));
7867
+ } else if (entry.isFile()) {
7868
+ out.push(rel);
7869
+ }
7870
+ }
7871
+ return out.sort();
7872
+ }
7873
+ function sha256File(filePath) {
7874
+ return crypto.createHash("sha256").update(fs2.readFileSync(filePath)).digest("hex");
7875
+ }
7876
+ function readSkillManifest(skillDir) {
7877
+ try {
7878
+ return coerceSkillManifest(JSON.parse(fs2.readFileSync(path2.join(skillDir, SKILL_MANIFEST_FILENAME), "utf-8")));
7879
+ } catch {
7880
+ return null;
7881
+ }
7882
+ }
7883
+ function writeSkillManifest(skillDir, manifest) {
7884
+ fs2.writeFileSync(path2.join(skillDir, SKILL_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}
7885
+ `, "utf-8");
7886
+ }
7887
+ function reconcileSkillTree(srcDir, destDir, manifest, force) {
7888
+ const result = { added: [], updated: [], unchanged: [], conflicts: [], bundledHashes: /* @__PURE__ */ new Map() };
7889
+ for (const rel of walkRelative(srcDir)) {
7890
+ const srcPath = path2.join(srcDir, rel);
7891
+ const destPath = path2.join(destDir, rel);
7892
+ const bundledHash = sha256File(srcPath);
7893
+ result.bundledHashes.set(rel, bundledHash);
7894
+ const installedHash = fs2.existsSync(destPath) ? sha256File(destPath) : void 0;
7895
+ const state = classifySkillFile({ bundledHash, installedHash, manifestHash: manifest?.files[rel] });
7896
+ switch (state) {
7897
+ case "missing":
7898
+ fs2.mkdirSync(path2.dirname(destPath), { recursive: true });
7899
+ fs2.copyFileSync(srcPath, destPath);
7900
+ result.added.push(rel);
7901
+ break;
7902
+ case "unchanged":
7903
+ result.unchanged.push(rel);
7904
+ break;
7905
+ case "stale":
7906
+ fs2.copyFileSync(srcPath, destPath);
7907
+ result.updated.push(rel);
7908
+ break;
7909
+ case "edited":
7910
+ if (force) {
7911
+ fs2.copyFileSync(srcPath, destPath);
7912
+ result.updated.push(rel);
7913
+ } else {
7914
+ result.conflicts.push(rel);
7915
+ }
7916
+ break;
7917
+ }
7918
+ }
7919
+ return result;
7920
+ }
7921
+ function buildManifest(skillName, recon, prior) {
7922
+ const conflicts = new Set(recon.conflicts);
7923
+ const files = {};
7924
+ for (const [rel, bundledHash] of recon.bundledHashes) {
7925
+ files[rel] = conflicts.has(rel) ? prior?.files[rel] ?? bundledHash : bundledHash;
7926
+ }
7927
+ return { skill: skillName, version: PACKAGE_VERSION, files };
7928
+ }
7929
+ function describeChanges(recon) {
7930
+ const parts = [];
7931
+ if (recon.added.length > 0) parts.push(`${recon.added.length} added`);
7932
+ if (recon.updated.length > 0) parts.push(`${recon.updated.length} refreshed`);
7933
+ return parts.length > 0 ? parts.join(", ") : "no changes";
7934
+ }
7935
+ function buildClaudeMessage(name, status, recon) {
7936
+ const rel = `.claude/skills/${name}`;
7937
+ let message = status === "installed" ? `Installed ${rel}` : status === "updated" ? `Updated ${rel} (${describeChanges(recon)})` : `Already installed: ${rel}`;
7938
+ if (recon.conflicts.length > 0) {
7939
+ message += ` \u2014 ${recon.conflicts.length} file(s) differ from the bundle (local edits kept; pass --force to overwrite)`;
7940
+ }
7941
+ return message;
7942
+ }
7943
+ function installClaudeSkill(skill, targetDir, force) {
7944
+ const targetPath = path2.join(targetDir, ".claude", "skills", skill.name);
7945
+ const existedBefore = fs2.existsSync(path2.join(targetPath, "SKILL.md"));
7946
+ const priorManifest = readSkillManifest(targetPath);
7947
+ fs2.mkdirSync(targetPath, { recursive: true });
7948
+ const recon = reconcileSkillTree(skill.bundledPath, targetPath, priorManifest, force);
7949
+ writeSkillManifest(targetPath, buildManifest(skill.name, recon, priorManifest));
7950
+ const changed = recon.added.length + recon.updated.length;
7951
+ const status = !existedBefore ? "installed" : changed > 0 ? "updated" : "already-installed";
7952
+ return {
7953
+ skill: skill.name,
7954
+ client: CodingAgents.claude,
7955
+ targetPath,
7956
+ status,
7957
+ message: buildClaudeMessage(skill.name, status, recon),
7958
+ added: recon.added,
7959
+ updated: recon.updated,
7960
+ unchanged: recon.unchanged,
7961
+ conflicts: recon.conflicts
7962
+ };
7963
+ }
7964
+ function installCodexSymlink(skill, targetDir, force) {
7965
+ const codexPath = path2.join(targetDir, ".codex", "skills", skill.name);
7966
+ const claudePath = path2.join(targetDir, ".claude", "skills", skill.name);
7967
+ const linkTarget = path2.relative(path2.dirname(codexPath), claudePath);
7968
+ fs2.mkdirSync(path2.dirname(codexPath), { recursive: true });
7969
+ let stat;
7970
+ try {
7971
+ stat = fs2.lstatSync(codexPath);
7972
+ } catch {
7973
+ stat = void 0;
7974
+ }
7975
+ if (stat?.isSymbolicLink()) {
7976
+ const existing = fs2.readlinkSync(codexPath);
7977
+ if (existing === linkTarget) {
7978
+ return {
7979
+ skill: skill.name,
7980
+ client: CodingAgents.codex,
7981
+ targetPath: codexPath,
7982
+ status: "already-linked",
7983
+ message: `Already linked: .codex/skills/${skill.name}`
7984
+ };
7985
+ }
7986
+ if (!force) {
7987
+ throw new CliError({
7988
+ code: "VALIDATION_ERROR",
7989
+ message: `.codex/skills/${skill.name} is a symlink pointing elsewhere (${existing}). Pass --force to relink.`,
7990
+ details: { skill: skill.name, targetPath: codexPath, existingTarget: existing },
7991
+ exitCode: 1
7992
+ });
7993
+ }
7994
+ fs2.unlinkSync(codexPath);
7995
+ fs2.symlinkSync(linkTarget, codexPath);
7996
+ return {
7997
+ skill: skill.name,
7998
+ client: CodingAgents.codex,
7999
+ targetPath: codexPath,
8000
+ status: "relinked",
8001
+ message: `Relinked .codex/skills/${skill.name} \u2192 ${linkTarget}`
8002
+ };
8003
+ }
8004
+ if (stat) {
8005
+ if (!force) {
8006
+ throw new CliError({
8007
+ code: "VALIDATION_ERROR",
8008
+ message: `.codex/skills/${skill.name} exists but is not a symlink. Pass --force to replace.`,
8009
+ details: { skill: skill.name, targetPath: codexPath },
8010
+ exitCode: 1
8011
+ });
8012
+ }
8013
+ fs2.rmSync(codexPath, { recursive: true, force: true });
8014
+ }
8015
+ fs2.symlinkSync(linkTarget, codexPath);
8016
+ return {
8017
+ skill: skill.name,
8018
+ client: CodingAgents.codex,
8019
+ targetPath: codexPath,
8020
+ status: stat ? "relinked" : "linked",
8021
+ message: stat ? `Replaced and linked .codex/skills/${skill.name} \u2192 ${linkTarget}` : `Linked .codex/skills/${skill.name} \u2192 ${linkTarget}`
8022
+ };
8023
+ }
8024
+ function buildSummaryMessage(results) {
8025
+ const counts = {};
8026
+ for (const r of results) counts[r.status] = (counts[r.status] ?? 0) + 1;
8027
+ const parts = Object.entries(counts).map(([status, n]) => `${n} ${status}`);
8028
+ let message = `Skills install summary: ${parts.join(", ")}.`;
8029
+ const totalConflicts = results.reduce((sum, r) => sum + (r.conflicts?.length ?? 0), 0);
8030
+ if (totalConflicts > 0) {
8031
+ message += ` ${totalConflicts} file(s) differ from the bundle and were kept \u2014 pass --force to overwrite local edits.`;
8032
+ }
8033
+ return message;
8034
+ }
8035
+ function getBundledSkillSnapshots(pkgDir) {
8036
+ return getBundledSkills(pkgDir).map((skill) => {
8037
+ const files = {};
8038
+ for (const rel of walkRelative(skill.bundledPath)) {
8039
+ files[rel] = sha256File(path2.join(skill.bundledPath, rel));
8040
+ }
8041
+ return {
8042
+ name: skill.name,
8043
+ version: PACKAGE_VERSION,
8044
+ files,
8045
+ description: readBundledSkillDescription(skill.bundledPath)
8046
+ };
8047
+ });
8048
+ }
8049
+ function readBundledSkillDescription(skillDir) {
8050
+ let raw;
8051
+ try {
8052
+ raw = fs2.readFileSync(path2.join(skillDir, "SKILL.md"), "utf-8");
8053
+ } catch {
8054
+ return void 0;
8055
+ }
8056
+ const parts = raw.split("---");
8057
+ if (parts.length < 3) return void 0;
8058
+ const lines = (parts[1] ?? "").split("\n");
8059
+ const collected = [];
8060
+ let inside = false;
8061
+ for (const line of lines) {
8062
+ const isTopLevelKey = /^[a-z][\w-]*:/i.test(line);
8063
+ if (inside && isTopLevelKey) break;
8064
+ if (isTopLevelKey && line.startsWith("description:")) {
8065
+ inside = true;
8066
+ collected.push(line.slice("description:".length).trim());
8067
+ continue;
8068
+ }
8069
+ if (inside) collected.push(line.trim());
8070
+ }
8071
+ if (!inside) return void 0;
8072
+ const value = collected.join(" ").trim();
8073
+ const unquoted = value.length >= 2 && value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value;
8074
+ return unquoted.length > 0 ? unquoted : void 0;
8075
+ }
8076
+ async function installSkills(opts = {}) {
8077
+ const targetDir = opts.user ? os2.homedir() : path2.resolve(opts.dir ?? process.cwd());
8078
+ const client2 = opts.client ?? SkillsClients.all;
8079
+ const force = opts.force ?? false;
8080
+ const allSkills = getBundledSkills();
8081
+ const requestedNames = opts.skills && opts.skills.length > 0 ? opts.skills : allSkills.map((s) => s.name);
8082
+ const knownNames = new Set(allSkills.map((s) => s.name));
8083
+ const unknown = requestedNames.filter((n) => !knownNames.has(n));
8084
+ if (unknown.length > 0) {
8085
+ throw new CliError({
8086
+ code: "VALIDATION_ERROR",
8087
+ message: `Unknown skill(s): ${unknown.join(", ")}. Available: ${[...knownNames].join(", ")}`,
8088
+ details: { unknownSkills: unknown, availableSkills: [...knownNames] },
8089
+ exitCode: 1
8090
+ });
8091
+ }
8092
+ const skillsToInstall = allSkills.filter((s) => requestedNames.includes(s.name));
8093
+ fs2.mkdirSync(targetDir, { recursive: true });
8094
+ const results = [];
8095
+ for (const skill of skillsToInstall) {
8096
+ results.push(installClaudeSkill(skill, targetDir, force));
8097
+ if (client2 !== SkillsClients.claude) {
8098
+ results.push(installCodexSymlink(skill, targetDir, force));
8099
+ }
8100
+ }
8101
+ return {
8102
+ targetDir,
8103
+ results,
8104
+ message: buildSummaryMessage(results)
8105
+ };
8106
+ }
8107
+ async function listSkills(opts = {}) {
8108
+ const skills = getBundledSkills();
8109
+ if (isMachineFormat(opts.format)) {
8110
+ console.log(JSON.stringify({
8111
+ skills: skills.map((s) => ({
8112
+ name: s.name,
8113
+ description: s.description,
8114
+ claudePath: `.claude/skills/${s.name}`,
8115
+ codexPath: `.codex/skills/${s.name}`
8116
+ }))
8117
+ }, null, 2));
8118
+ return;
8119
+ }
8120
+ console.log("Bundled canonry skills:\n");
8121
+ for (const skill of skills) {
8122
+ console.log(` ${skill.name}`);
8123
+ if (skill.description) console.log(` ${skill.description}`);
8124
+ console.log(` Claude: .claude/skills/${skill.name}/`);
8125
+ console.log(` Codex: .codex/skills/${skill.name} (symlink \u2192 ../../.claude/skills/${skill.name})`);
8126
+ console.log();
8127
+ }
8128
+ }
8129
+ function emitInstallSummary(summary, format) {
8130
+ if (isMachineFormat(format)) {
8131
+ console.log(JSON.stringify(summary, null, 2));
8132
+ return;
8133
+ }
8134
+ for (const r of summary.results) console.log(r.message);
8135
+ console.log(`
8136
+ Target: ${summary.targetDir}`);
8137
+ console.log(summary.message);
8138
+ }
8139
+ function getMissingUserSkillsNudge(home, agentPlugin) {
8140
+ if (!home) return null;
8141
+ const skillsBase = path2.join(home, ".claude", "skills");
8142
+ const installed = [];
8143
+ const missing = [];
8144
+ for (const name of BUNDLED_SKILL_NAMES) {
8145
+ const skillFile = path2.join(skillsBase, name, "SKILL.md");
8146
+ if (existsSafe(skillFile)) installed.push(name);
8147
+ else missing.push(name);
8148
+ }
8149
+ if (agentPlugin && agentPlugin.configuredClients.length > 0) {
8150
+ const unverifiedClients = agentPlugin.configuredClients.filter((client2) => !agentPlugin.verifiedClients.includes(client2));
8151
+ const mismatchedClients = agentPlugin.verifiedClients.filter((client2) => agentPlugin.verifiedClientVersions?.[client2] !== PACKAGE_VERSION);
8152
+ if (unverifiedClients.length === 0 && mismatchedClients.length === 0) return null;
8153
+ const displayClients = (clients) => clients.map((client2) => client2 === "claude-code" ? "Claude Code" : "Codex").join(" + ");
8154
+ const problems = [];
8155
+ if (unverifiedClients.length > 0) {
8156
+ problems.push(`The Canonry plugin is enabled for ${displayClients(unverifiedClients)}, but its cached manifest and skill assets could not be verified.`);
8157
+ }
8158
+ if (mismatchedClients.length > 0) {
8159
+ const versions = mismatchedClients.map((client2) => `${client2 === "claude-code" ? "Claude Code" : "Codex"} v${agentPlugin.verifiedClientVersions?.[client2] ?? "unknown"}`).join(", ");
8160
+ problems.push(`${versions} ${mismatchedClients.length === 1 ? "does" : "do"} not match the running Canonry v${PACKAGE_VERSION}.`);
8161
+ }
8162
+ return {
8163
+ message: `Tip: ${problems.join(" ")} Update or reinstall \`canonry@canonry\` with the affected client plugin manager.`,
8164
+ missing,
8165
+ installed
8166
+ };
8167
+ }
8168
+ if (missing.length === 0) return null;
8169
+ const fix = missing.length === BUNDLED_SKILL_NAMES.length ? "canonry skills install --user" : `canonry skills install ${missing.join(" ")} --user`;
8170
+ return {
8171
+ message: `Tip: ${missing.join(" + ")} skill${missing.length === 1 ? "" : "s"} not installed in ~/.claude/skills/. Run \`${fix}\` so Claude/Codex sessions on this host auto-load the canonry reference docs.`,
8172
+ missing,
8173
+ installed
8174
+ };
8175
+ }
8176
+ function existsSafe(p) {
8177
+ try {
8178
+ return fs2.existsSync(p);
8179
+ } catch {
8180
+ return false;
8181
+ }
8182
+ }
8183
+ function parseSkillsClient(value) {
8184
+ if (!value) return SkillsClients.all;
8185
+ const parsed = skillsClientSchema.safeParse(value);
8186
+ if (parsed.success) return parsed.data;
8187
+ const allowed = skillsClientSchema.options;
8188
+ throw new CliError({
8189
+ code: "VALIDATION_ERROR",
8190
+ message: `Invalid --client value "${value}". Must be one of: ${allowed.join(", ")}`,
8191
+ details: { flag: "client", value, allowed },
8192
+ exitCode: 1
8193
+ });
8194
+ }
8195
+
7812
8196
  // src/mcp/tool-registry.ts
7813
8197
  import { z as z3 } from "zod";
7814
8198
 
@@ -11064,9 +11448,16 @@ export {
11064
11448
  isEndpointMissing,
11065
11449
  systemError,
11066
11450
  printCliError,
11451
+ PACKAGE_VERSION,
11452
+ BUNDLED_SKILL_NAMES,
11453
+ getBundledSkillSnapshots,
11454
+ installSkills,
11455
+ listSkills,
11456
+ emitInstallSummary,
11457
+ getMissingUserSkillsNudge,
11458
+ parseSkillsClient,
11067
11459
  createApiClient,
11068
11460
  ApiClient,
11069
- PACKAGE_VERSION,
11070
11461
  measurementDraftOperationSchema,
11071
11462
  runMeasurementDraftAction,
11072
11463
  canonryMcpTools,