@ainyc/canonry 3.2.7 → 3.3.3

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-7R5NSWF7.js";
20
+ } from "./chunk-GH5ILITH.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-GY4MNUTI.js";
51
+ } from "./chunk-NCGX6UI4.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) {
@@ -2012,9 +2015,9 @@ async function gaConnect(project, opts) {
2012
2015
  propertyId: opts.propertyId
2013
2016
  };
2014
2017
  if (opts.keyFile) {
2015
- const fs9 = await import("fs");
2018
+ const fs10 = await import("fs");
2016
2019
  try {
2017
- const content = fs9.readFileSync(opts.keyFile, "utf-8");
2020
+ const content = fs10.readFileSync(opts.keyFile, "utf-8");
2018
2021
  JSON.parse(content);
2019
2022
  body.keyJson = content;
2020
2023
  } catch (e) {
@@ -5878,13 +5881,307 @@ Usage: canonry settings provider ${name} --api-key <key> [--model <model>] [--ma
5878
5881
  }
5879
5882
  ];
5880
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
+
5881
6178
  // src/commands/snapshot.ts
5882
- import fs5 from "fs";
5883
- import path4 from "path";
6179
+ import fs6 from "fs";
6180
+ import path5 from "path";
5884
6181
 
5885
6182
  // src/snapshot-pdf.ts
5886
- import fs4 from "fs";
5887
- import path3 from "path";
6183
+ import fs5 from "fs";
6184
+ import path4 from "path";
5888
6185
  import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
5889
6186
  var PAGE_WIDTH = 612;
5890
6187
  var PAGE_HEIGHT = 792;
@@ -6093,9 +6390,9 @@ async function writeSnapshotPdf(report, outputPath) {
6093
6390
  renderCompetitors(pdf, report);
6094
6391
  renderQueries(pdf, report);
6095
6392
  const bytes = await doc.save();
6096
- const resolvedPath = path3.resolve(outputPath);
6097
- fs4.mkdirSync(path3.dirname(resolvedPath), { recursive: true });
6098
- fs4.writeFileSync(resolvedPath, bytes);
6393
+ const resolvedPath = path4.resolve(outputPath);
6394
+ fs5.mkdirSync(path4.dirname(resolvedPath), { recursive: true });
6395
+ fs5.writeFileSync(resolvedPath, bytes);
6099
6396
  return resolvedPath;
6100
6397
  }
6101
6398
  function renderCover(pdf, report) {
@@ -6253,9 +6550,9 @@ Markdown saved: ${savedMdPath}`);
6253
6550
  PDF saved: ${savedPdfPath}`);
6254
6551
  }
6255
6552
  function writeSnapshotMarkdown(report, outputPath) {
6256
- const resolvedPath = path4.resolve(outputPath);
6257
- fs5.mkdirSync(path4.dirname(resolvedPath), { recursive: true });
6258
- 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");
6259
6556
  return resolvedPath;
6260
6557
  }
6261
6558
  function formatSnapshotMarkdown(report) {
@@ -6910,7 +7207,7 @@ var CONTENT_CLI_COMMANDS = [
6910
7207
 
6911
7208
  // src/commands/bootstrap.ts
6912
7209
  import crypto from "crypto";
6913
- import path5 from "path";
7210
+ import path6 from "path";
6914
7211
  import { eq as eq2 } from "drizzle-orm";
6915
7212
 
6916
7213
  // ../config/src/index.ts
@@ -7057,7 +7354,7 @@ async function bootstrapCommand(_opts) {
7057
7354
  );
7058
7355
  }
7059
7356
  const configDir = getConfigDir();
7060
- const databasePath = env.databasePath || path5.join(configDir, "data.db");
7357
+ const databasePath = env.databasePath || path6.join(configDir, "data.db");
7061
7358
  const existing = configExists();
7062
7359
  const existingConfig = existing ? loadConfig() : void 0;
7063
7360
  let rawApiKey;
@@ -7127,10 +7424,10 @@ async function bootstrapCommand(_opts) {
7127
7424
 
7128
7425
  // src/commands/daemon.ts
7129
7426
  import { spawn } from "child_process";
7130
- import fs6 from "fs";
7131
- import path6 from "path";
7427
+ import fs7 from "fs";
7428
+ import path7 from "path";
7132
7429
  function getPidPath() {
7133
- return path6.join(getConfigDir(), "canonry.pid");
7430
+ return path7.join(getConfigDir(), "canonry.pid");
7134
7431
  }
7135
7432
  function isProcessAlive(pid) {
7136
7433
  try {
@@ -7157,8 +7454,8 @@ async function waitForReady(host, port, maxMs = 1e4) {
7157
7454
  async function startDaemon(opts) {
7158
7455
  const pidPath = getPidPath();
7159
7456
  const format = opts.format ?? "text";
7160
- if (fs6.existsSync(pidPath)) {
7161
- 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);
7162
7459
  if (!isNaN(existingPid) && isProcessAlive(existingPid)) {
7163
7460
  throw new CliError({
7164
7461
  code: "DAEMON_ALREADY_RUNNING",
@@ -7169,9 +7466,9 @@ async function startDaemon(opts) {
7169
7466
  }
7170
7467
  });
7171
7468
  }
7172
- fs6.unlinkSync(pidPath);
7469
+ fs7.unlinkSync(pidPath);
7173
7470
  }
7174
- const cliPath = path6.resolve(new URL(import.meta.url).pathname);
7471
+ const cliPath = path7.resolve(new URL(import.meta.url).pathname);
7175
7472
  const inSourceMode = new URL(import.meta.url).pathname.endsWith(".ts");
7176
7473
  const args = inSourceMode ? ["--import", "tsx", cliPath, "serve"] : [cliPath, "serve"];
7177
7474
  if (opts.port) args.push("--port", opts.port);
@@ -7190,10 +7487,10 @@ async function startDaemon(opts) {
7190
7487
  });
7191
7488
  }
7192
7489
  const configDir = getConfigDir();
7193
- if (!fs6.existsSync(configDir)) {
7194
- fs6.mkdirSync(configDir, { recursive: true });
7490
+ if (!fs7.existsSync(configDir)) {
7491
+ fs7.mkdirSync(configDir, { recursive: true });
7195
7492
  }
7196
- fs6.writeFileSync(pidPath, String(child.pid), "utf-8");
7493
+ fs7.writeFileSync(pidPath, String(child.pid), "utf-8");
7197
7494
  const port = opts.port ?? "4100";
7198
7495
  const host = opts.host ?? "127.0.0.1";
7199
7496
  if (format !== "json") {
@@ -7202,7 +7499,7 @@ async function startDaemon(opts) {
7202
7499
  const ready = await waitForReady(host, port);
7203
7500
  if (!ready) {
7204
7501
  try {
7205
- fs6.unlinkSync(pidPath);
7502
+ fs7.unlinkSync(pidPath);
7206
7503
  } catch {
7207
7504
  }
7208
7505
  throw new CliError({
@@ -7234,7 +7531,7 @@ async function startDaemon(opts) {
7234
7531
  }
7235
7532
  function stopDaemon(format = "text") {
7236
7533
  const pidPath = getPidPath();
7237
- if (!fs6.existsSync(pidPath)) {
7534
+ if (!fs7.existsSync(pidPath)) {
7238
7535
  if (format === "json") {
7239
7536
  console.log(JSON.stringify({
7240
7537
  stopped: false,
@@ -7245,7 +7542,7 @@ function stopDaemon(format = "text") {
7245
7542
  console.log("Canonry is not running (no PID file found)");
7246
7543
  return;
7247
7544
  }
7248
- const pid = parseInt(fs6.readFileSync(pidPath, "utf-8").trim(), 10);
7545
+ const pid = parseInt(fs7.readFileSync(pidPath, "utf-8").trim(), 10);
7249
7546
  if (isNaN(pid)) {
7250
7547
  if (format === "json") {
7251
7548
  console.log(JSON.stringify({
@@ -7256,7 +7553,7 @@ function stopDaemon(format = "text") {
7256
7553
  } else {
7257
7554
  console.error("Invalid PID file. Removing it.");
7258
7555
  }
7259
- fs6.unlinkSync(pidPath);
7556
+ fs7.unlinkSync(pidPath);
7260
7557
  return;
7261
7558
  }
7262
7559
  if (!isProcessAlive(pid)) {
@@ -7270,12 +7567,12 @@ function stopDaemon(format = "text") {
7270
7567
  } else {
7271
7568
  console.log(`Canonry is not running (stale PID: ${pid}). Cleaning up.`);
7272
7569
  }
7273
- fs6.unlinkSync(pidPath);
7570
+ fs7.unlinkSync(pidPath);
7274
7571
  return;
7275
7572
  }
7276
7573
  try {
7277
7574
  process.kill(pid, "SIGTERM");
7278
- fs6.unlinkSync(pidPath);
7575
+ fs7.unlinkSync(pidPath);
7279
7576
  if (format === "json") {
7280
7577
  console.log(JSON.stringify({
7281
7578
  stopped: true,
@@ -7299,9 +7596,9 @@ function stopDaemon(format = "text") {
7299
7596
 
7300
7597
  // src/commands/init.ts
7301
7598
  import crypto2 from "crypto";
7302
- import fs7 from "fs";
7599
+ import fs8 from "fs";
7303
7600
  import readline from "readline";
7304
- import path7 from "path";
7601
+ import path8 from "path";
7305
7602
  function prompt(question) {
7306
7603
  const rl = readline.createInterface({
7307
7604
  input: process.stdin,
@@ -7319,6 +7616,12 @@ var DEFAULT_QUOTA = {
7319
7616
  maxRequestsPerMinute: 10,
7320
7617
  maxRequestsPerDay: 500
7321
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
+ }
7322
7625
  var DEFAULT_AGENT_MODELS = {
7323
7626
  anthropic: "anthropic/claude-sonnet-4-6",
7324
7627
  openai: "openai/gpt-4o",
@@ -7347,8 +7650,8 @@ async function initCommand(opts) {
7347
7650
  return void 0;
7348
7651
  }
7349
7652
  const configDir = getConfigDir();
7350
- if (!fs7.existsSync(configDir)) {
7351
- fs7.mkdirSync(configDir, { recursive: true });
7653
+ if (!fs8.existsSync(configDir)) {
7654
+ fs8.mkdirSync(configDir, { recursive: true });
7352
7655
  }
7353
7656
  const bootstrapEnv = getBootstrapEnv(process.env, {
7354
7657
  GEMINI_API_KEY: opts?.geminiKey,
@@ -7463,7 +7766,7 @@ async function initCommand(opts) {
7463
7766
  const rawApiKey = `cnry_${crypto2.randomBytes(16).toString("hex")}`;
7464
7767
  const keyHash = crypto2.createHash("sha256").update(rawApiKey).digest("hex");
7465
7768
  const keyPrefix = rawApiKey.slice(0, 9);
7466
- const databasePath = path7.join(configDir, "data.db");
7769
+ const databasePath = path8.join(configDir, "data.db");
7467
7770
  const db = createClient(databasePath);
7468
7771
  migrate(db);
7469
7772
  db.insert(apiKeys).values({
@@ -7482,6 +7785,20 @@ async function initCommand(opts) {
7482
7785
  google
7483
7786
  });
7484
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
+ }
7485
7802
  if (format === "json") {
7486
7803
  console.log(JSON.stringify({
7487
7804
  initialized: true,
@@ -7490,7 +7807,9 @@ async function initCommand(opts) {
7490
7807
  apiUrl: `http://localhost:${process.env.CANONRY_PORT || "4100"}`,
7491
7808
  apiKey: rawApiKey,
7492
7809
  providers: providerNames,
7493
- googleConfigured: !!google
7810
+ googleConfigured: !!google,
7811
+ skills: skillsSummary,
7812
+ skillsTip
7494
7813
  }, null, 2));
7495
7814
  } else {
7496
7815
  console.log(`
@@ -7498,6 +7817,13 @@ Config saved to ${getConfigPath()}`);
7498
7817
  console.log(`Database created at ${databasePath}`);
7499
7818
  console.log(`API key: ${rawApiKey}`);
7500
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}`);
7501
7827
  }
7502
7828
  let agentLLM;
7503
7829
  const agentProvider = opts?.agentProvider;
@@ -7762,7 +8088,7 @@ function applyServerEnv(values) {
7762
8088
  var SYSTEM_CLI_COMMANDS = [
7763
8089
  {
7764
8090
  path: ["init"],
7765
- 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]",
7766
8092
  options: {
7767
8093
  force: { type: "boolean", short: "f", default: false },
7768
8094
  "gemini-key": stringOption(),
@@ -7773,7 +8099,9 @@ var SYSTEM_CLI_COMMANDS = [
7773
8099
  "local-model": stringOption(),
7774
8100
  "local-key": stringOption(),
7775
8101
  "google-client-id": stringOption(),
7776
- "google-client-secret": stringOption()
8102
+ "google-client-secret": stringOption(),
8103
+ "skip-skills": { type: "boolean" },
8104
+ "skills-dir": stringOption()
7777
8105
  },
7778
8106
  allowPositionals: false,
7779
8107
  run: async (input) => {
@@ -7788,6 +8116,8 @@ var SYSTEM_CLI_COMMANDS = [
7788
8116
  localKey: getString(input.values, "local-key"),
7789
8117
  googleClientId: getString(input.values, "google-client-id"),
7790
8118
  googleClientSecret: getString(input.values, "google-client-secret"),
8119
+ skipSkills: getBoolean(input.values, "skip-skills"),
8120
+ skillsDir: getString(input.values, "skills-dir"),
7791
8121
  format: input.format
7792
8122
  });
7793
8123
  }
@@ -7884,7 +8214,7 @@ var SYSTEM_CLI_COMMANDS = [
7884
8214
  ];
7885
8215
 
7886
8216
  // src/cli-commands/wordpress.ts
7887
- import fs8 from "fs";
8217
+ import fs9 from "fs";
7888
8218
 
7889
8219
  // src/commands/wordpress.ts
7890
8220
  function getClient18() {
@@ -8120,12 +8450,12 @@ async function wordpressSetMeta(project, body) {
8120
8450
  printPageDetail(result);
8121
8451
  }
8122
8452
  async function wordpressBulkSetMeta(project, opts) {
8123
- const fs9 = await import("fs/promises");
8124
- const path8 = await import("path");
8125
- 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);
8126
8456
  let raw;
8127
8457
  try {
8128
- raw = await fs9.readFile(filePath, "utf8");
8458
+ raw = await fs10.readFile(filePath, "utf8");
8129
8459
  } catch {
8130
8460
  throw new CliError({
8131
8461
  code: "FILE_READ_ERROR",
@@ -8222,13 +8552,13 @@ async function wordpressSetSchema(project, body) {
8222
8552
  printManualAssist(`Schema update for "${body.slug}"`, result);
8223
8553
  }
8224
8554
  async function wordpressSchemaDeploy(project, opts) {
8225
- const fs9 = await import("fs/promises");
8226
- const path8 = await import("path");
8555
+ const fs10 = await import("fs/promises");
8556
+ const path9 = await import("path");
8227
8557
  const yaml = await import("yaml").catch(() => null);
8228
- const filePath = path8.resolve(opts.profile);
8558
+ const filePath = path9.resolve(opts.profile);
8229
8559
  let raw;
8230
8560
  try {
8231
- raw = await fs9.readFile(filePath, "utf8");
8561
+ raw = await fs10.readFile(filePath, "utf8");
8232
8562
  } catch {
8233
8563
  throw new CliError({
8234
8564
  code: "FILE_READ_ERROR",
@@ -8333,13 +8663,13 @@ async function wordpressOnboard(project, opts) {
8333
8663
  }
8334
8664
  let profileData;
8335
8665
  if (opts.profile) {
8336
- const fs9 = await import("fs/promises");
8337
- const path8 = await import("path");
8666
+ const fs10 = await import("fs/promises");
8667
+ const path9 = await import("path");
8338
8668
  const yaml = await import("yaml").catch(() => null);
8339
- const filePath = path8.resolve(opts.profile);
8669
+ const filePath = path9.resolve(opts.profile);
8340
8670
  let raw;
8341
8671
  try {
8342
- raw = await fs9.readFile(filePath, "utf8");
8672
+ raw = await fs10.readFile(filePath, "utf8");
8343
8673
  } catch {
8344
8674
  throw new CliError({
8345
8675
  code: "FILE_READ_ERROR",
@@ -8488,7 +8818,7 @@ function resolveContent(input, command, usage, options) {
8488
8818
  }
8489
8819
  if (contentFile) {
8490
8820
  try {
8491
- return fs8.readFileSync(contentFile, "utf-8");
8821
+ return fs9.readFileSync(contentFile, "utf-8");
8492
8822
  } catch (error) {
8493
8823
  const message = error instanceof Error ? error.message : String(error);
8494
8824
  throw usageError(`Error: could not read --content-file "${contentFile}": ${message}`, {
@@ -9422,6 +9752,7 @@ var REGISTERED_CLI_COMMANDS = [
9422
9752
  ...KEYWORD_CLI_COMMANDS,
9423
9753
  ...COMPETITOR_CLI_COMMANDS,
9424
9754
  ...SETTINGS_CLI_COMMANDS,
9755
+ ...SKILLS_CLI_COMMANDS,
9425
9756
  ...SNAPSHOT_CLI_COMMANDS,
9426
9757
  ...RUN_CLI_COMMANDS,
9427
9758
  ...OPERATOR_CLI_COMMANDS,
@@ -9451,6 +9782,7 @@ Setup:
9451
9782
  bootstrap Bootstrap config/database from env vars
9452
9783
  serve Start the local server (foreground)
9453
9784
  start / stop Start/stop as a background daemon
9785
+ skills List or install bundled agent skills (claude/codex)
9454
9786
 
9455
9787
  Projects:
9456
9788
  project Create, update, list, show, delete projects