@canonry/canonry 4.167.0 → 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.
- package/README.md +31 -19
- package/assets/agent-workspace/skills/aero/SKILL.md +1 -1
- package/assets/agent-workspace/skills/canonry/SKILL.md +1 -1
- package/dist/chunk-76JMZAFZ.js +123 -0
- package/dist/{chunk-P5LR3NDQ.js → chunk-C4UWZLQX.js} +399 -8
- package/dist/{chunk-7M3QVRJD.js → chunk-CDACTPTY.js} +79 -1
- package/dist/{chunk-SXENNCJF.js → chunk-R2SZWJDV.js} +96 -452
- package/dist/cli.js +16 -8
- package/dist/index.d.ts +19 -0
- package/dist/index.js +3 -3
- package/dist/{intelligence-service-ULDOVV5Y.js → intelligence-service-KBSDKHED.js} +1 -1
- package/dist/mcp.js +23 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -41,33 +41,45 @@ The dashboard, CLI, and agent tools share the same project API.
|
|
|
41
41
|
|
|
42
42
|
You need Node.js `>=22.14` and `<26`, plus a public, crawlable site. Site Health does not need an answer-provider key.
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
npm install -g @canonry/canonry
|
|
46
|
-
cnry bootstrap
|
|
47
|
-
cnry serve
|
|
48
|
-
```
|
|
44
|
+
1. Install Canonry.
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
```bash
|
|
47
|
+
npm install -g @canonry/canonry
|
|
48
|
+
```
|
|
51
49
|
|
|
52
|
-
|
|
50
|
+
2. Create the local configuration, SQLite database, and full-instance API key.
|
|
53
51
|
|
|
54
|
-
|
|
52
|
+
```bash
|
|
53
|
+
cnry bootstrap
|
|
54
|
+
```
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
cnry project create my-site --domain example.com --country US --language en
|
|
58
|
-
cnry technical-aeo run my-site --max-pages 100 --wait --format json
|
|
59
|
-
```
|
|
56
|
+
Keep the output private. Provider credentials are optional. Bootstrap imports supported variables that are already in your environment.
|
|
60
57
|
|
|
61
|
-
|
|
58
|
+
3. Start Canonry.
|
|
62
59
|
|
|
63
|
-
```bash
|
|
64
|
-
cnry
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
```bash
|
|
61
|
+
cnry serve
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
4. Open [http://127.0.0.1:4100/setup](http://127.0.0.1:4100/setup). If prompted, create a dashboard password.
|
|
65
|
+
|
|
66
|
+
5. Enter your domain and approve the public-site crawl. This crawl creates a persisted Page Health baseline. AI Visibility is optional.
|
|
67
|
+
|
|
68
|
+
6. To use the terminal instead, keep `cnry serve` running. Open a second terminal and run:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
cnry project create my-site --domain example.com --country US --language en
|
|
72
|
+
cnry technical-aeo run my-site --max-pages 100 --wait --format json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
7. Read the run ID and status from the output. If the status is `completed` or `partial`, read evidence from that run:
|
|
67
76
|
|
|
68
|
-
|
|
77
|
+
```bash
|
|
78
|
+
cnry technical-aeo score my-site --run-id <run-id> --format json
|
|
79
|
+
cnry technical-aeo pages my-site --run-id <run-id> --sort score-asc --limit 10 --format jsonl
|
|
80
|
+
```
|
|
69
81
|
|
|
70
|
-
|
|
82
|
+
`--wait` polls for up to 15 minutes. If the scan remains active, use the progress command below. If it fails or is cancelled, inspect it with `cnry run show <run-id> --format json`.
|
|
71
83
|
|
|
72
84
|
## Or use any shell-capable coding agent
|
|
73
85
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: aero
|
|
3
|
-
description: AEO
|
|
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: "
|
|
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:
|
|
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 =
|
|
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:
|
|
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 (
|
|
700
|
-
url = defaultPathSerializer({ path:
|
|
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(
|
|
5385
|
+
async streamPost(path3, body, signal) {
|
|
5380
5386
|
await this.probeBasePath();
|
|
5381
|
-
const url = `${this.originUrl}/api/v1${
|
|
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,
|