@xera-ai/cli 0.12.3 → 0.13.1

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.
Files changed (2) hide show
  1. package/dist/index.js +454 -187
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -7,18 +7,265 @@ import { cac } from "cac";
7
7
  import pc4 from "picocolors";
8
8
 
9
9
  // src/commands/doctor.ts
10
- import { existsSync as existsSync2 } from "fs";
10
+ import { existsSync as existsSync6 } from "fs";
11
11
  import { NdjsonLogger, resolveArtifactPaths } from "@xera-ai/core";
12
12
  import pc from "picocolors";
13
13
 
14
14
  // src/checks.ts
15
- import { existsSync, readFileSync } from "fs";
16
- import { join } from "path";
15
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
16
+ import { join as join5 } from "path";
17
17
  import { loadConfig, readAuthState } from "@xera-ai/core";
18
18
  import { parse as parseYaml } from "yaml";
19
+
20
+ // src/editors/claude.ts
21
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
22
+ import { dirname, join } from "path";
23
+
24
+ // src/editors/frontmatter.ts
25
+ var FENCE = "---";
26
+ function parseFrontmatter(md) {
27
+ if (!md.startsWith(`${FENCE}
28
+ `)) {
29
+ return { frontmatter: { raw: "", fields: {} }, body: md };
30
+ }
31
+ const closeIdx = md.indexOf(`
32
+ ${FENCE}
33
+ `, FENCE.length + 1);
34
+ if (closeIdx < 0) {
35
+ return { frontmatter: { raw: "", fields: {} }, body: md };
36
+ }
37
+ const raw = md.slice(FENCE.length + 1, closeIdx);
38
+ const body = md.slice(closeIdx + `
39
+ ${FENCE}
40
+ `.length);
41
+ const fields = {};
42
+ const lines = raw.split(`
43
+ `);
44
+ for (let i = 0;i < lines.length; i++) {
45
+ const line = lines[i];
46
+ if (!line.trim())
47
+ continue;
48
+ const m = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
49
+ if (!m)
50
+ continue;
51
+ const key = m[1];
52
+ const value = m[2];
53
+ if (value === "|") {
54
+ const collected = [];
55
+ while (i + 1 < lines.length) {
56
+ const next = lines[i + 1];
57
+ if (/^ {2,}/.test(next)) {
58
+ collected.push(next.replace(/^ {2}/, ""));
59
+ i++;
60
+ } else if (next.trim() === "") {
61
+ collected.push("");
62
+ i++;
63
+ } else {
64
+ break;
65
+ }
66
+ }
67
+ while (collected.length && collected[collected.length - 1] === "")
68
+ collected.pop();
69
+ fields[key] = collected.join(`
70
+ `);
71
+ } else if (value === "true" || value === "false") {
72
+ fields[key] = value === "true";
73
+ } else if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
74
+ fields[key] = value.slice(1, -1).replace(/\\"/g, '"');
75
+ } else {
76
+ fields[key] = value;
77
+ }
78
+ }
79
+ return { frontmatter: { raw, fields }, body };
80
+ }
81
+ function serializeFrontmatter(fields) {
82
+ const lines = [FENCE];
83
+ for (const [key, value] of Object.entries(fields)) {
84
+ if (typeof value === "boolean") {
85
+ lines.push(`${key}: ${value}`);
86
+ } else if (Array.isArray(value)) {
87
+ lines.push(`${key}: [${value.join(", ")}]`);
88
+ } else if (value.includes(`
89
+ `)) {
90
+ lines.push(`${key}: |`);
91
+ for (const sub of value.split(`
92
+ `))
93
+ lines.push(` ${sub}`);
94
+ } else if (/[:#]/.test(value)) {
95
+ lines.push(`${key}: "${value.replace(/"/g, "\\\"")}"`);
96
+ } else {
97
+ lines.push(`${key}: ${value}`);
98
+ }
99
+ }
100
+ lines.push(FENCE);
101
+ return `${lines.join(`
102
+ `)}
103
+ `;
104
+ }
105
+
106
+ // src/editors/claude.ts
107
+ function renderSource(input) {
108
+ return serializeFrontmatter(input.frontmatter.fields) + input.body;
109
+ }
110
+ var claudeAdapter = {
111
+ name: "claude",
112
+ detect(cwd) {
113
+ return existsSync(join(cwd, ".claude"));
114
+ },
115
+ scaffoldSkill(cwd, input) {
116
+ const target = join(cwd, ".claude/skills", input.base, "SKILL.md");
117
+ mkdirSync(dirname(target), { recursive: true });
118
+ writeFileSync(target, renderSource(input));
119
+ },
120
+ scaffoldCommand(cwd, input) {
121
+ const target = join(cwd, ".claude/commands", `${input.base}.md`);
122
+ mkdirSync(dirname(target), { recursive: true });
123
+ writeFileSync(target, renderSource(input));
124
+ },
125
+ legacyMigrate(cwd, base) {
126
+ const flat = join(cwd, ".claude/skills", `${base}.md`);
127
+ const dir = join(cwd, ".claude/skills", base, "SKILL.md");
128
+ if (!existsSync(flat) || existsSync(dir))
129
+ return false;
130
+ const content = readFileSync(flat);
131
+ mkdirSync(dirname(dir), { recursive: true });
132
+ writeFileSync(dir, content);
133
+ unlinkSync(flat);
134
+ return true;
135
+ },
136
+ doctorChecks(cwd, requiredSkills) {
137
+ const skillsDir = join(cwd, ".claude/skills");
138
+ if (!existsSync(skillsDir)) {
139
+ return [{ name: "xera skills present (claude)", ok: false, message: "run `xera init`" }];
140
+ }
141
+ const missing = [];
142
+ const legacyFlat = [];
143
+ for (const base of requiredSkills) {
144
+ if (existsSync(join(skillsDir, base, "SKILL.md")))
145
+ continue;
146
+ if (existsSync(join(skillsDir, `${base}.md`)))
147
+ legacyFlat.push(base);
148
+ else
149
+ missing.push(base);
150
+ }
151
+ const check = {
152
+ name: "xera skills present (claude)",
153
+ ok: missing.length === 0 && legacyFlat.length === 0
154
+ };
155
+ if (missing.length) {
156
+ check.message = `missing: ${missing.map((b) => `${b}/SKILL.md`).join(", ")}`;
157
+ } else if (legacyFlat.length) {
158
+ check.message = `legacy flat layout \u2014 run \`xera init --update --editor claude\` to migrate`;
159
+ }
160
+ return [check];
161
+ }
162
+ };
163
+
164
+ // src/editors/codex.ts
165
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
166
+ import { dirname as dirname2, join as join2 } from "path";
167
+ var codexAdapter = {
168
+ name: "codex",
169
+ detect(cwd) {
170
+ return existsSync2(join2(cwd, ".agents"));
171
+ },
172
+ scaffoldSkill(cwd, input) {
173
+ const target = join2(cwd, ".agents/skills", input.base, "SKILL.md");
174
+ mkdirSync2(dirname2(target), { recursive: true });
175
+ writeFileSync2(target, serializeFrontmatter(input.frontmatter.fields) + input.body);
176
+ },
177
+ doctorChecks(cwd, requiredSkills) {
178
+ const skillsDir = join2(cwd, ".agents/skills");
179
+ const missing = requiredSkills.filter((b) => !existsSync2(join2(skillsDir, b, "SKILL.md")));
180
+ const check = {
181
+ name: "xera skills present (codex)",
182
+ ok: missing.length === 0
183
+ };
184
+ if (missing.length) {
185
+ check.message = `missing: ${missing.map((b) => `${b}/SKILL.md`).join(", ")} \u2014 run \`xera init --update --editor codex\``;
186
+ }
187
+ return [check];
188
+ }
189
+ };
190
+
191
+ // src/editors/cursor.ts
192
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
193
+ import { dirname as dirname3, join as join3 } from "path";
194
+ function ruleFrontmatter(input) {
195
+ const desc = input.frontmatter.fields.description;
196
+ if (desc === undefined) {
197
+ throw new Error(`Cursor scaffold requires 'description' in source frontmatter for ${input.base}`);
198
+ }
199
+ return { description: desc, alwaysApply: false };
200
+ }
201
+ function commandFrontmatter(input) {
202
+ const desc = input.frontmatter.fields.description;
203
+ if (desc === undefined) {
204
+ throw new Error(`Cursor scaffold requires 'description' in source frontmatter for ${input.base}`);
205
+ }
206
+ return { description: desc };
207
+ }
208
+ function write(target, content) {
209
+ mkdirSync3(dirname3(target), { recursive: true });
210
+ writeFileSync3(target, content);
211
+ }
212
+ var cursorAdapter = {
213
+ name: "cursor",
214
+ detect(cwd) {
215
+ return existsSync3(join3(cwd, ".cursor"));
216
+ },
217
+ scaffoldSkill(cwd, input) {
218
+ const path = join3(cwd, ".cursor/rules", input.base, "RULE.md");
219
+ write(path, serializeFrontmatter(ruleFrontmatter(input)) + input.body);
220
+ },
221
+ scaffoldCommand(cwd, input) {
222
+ const path = join3(cwd, ".cursor/commands", `${input.base}.md`);
223
+ write(path, serializeFrontmatter(commandFrontmatter(input)) + input.body);
224
+ },
225
+ doctorChecks(cwd, requiredSkills) {
226
+ const rulesDir = join3(cwd, ".cursor/rules");
227
+ const cmdsDir = join3(cwd, ".cursor/commands");
228
+ const missing = [];
229
+ for (const base of requiredSkills) {
230
+ if (!existsSync3(join3(rulesDir, base, "RULE.md")))
231
+ missing.push(`${base}/RULE.md`);
232
+ if (!existsSync3(join3(cmdsDir, `${base}.md`)))
233
+ missing.push(`commands/${base}.md`);
234
+ }
235
+ const check = {
236
+ name: "xera skills present (cursor)",
237
+ ok: missing.length === 0
238
+ };
239
+ if (missing.length)
240
+ check.message = `missing: ${missing.join(", ")} \u2014 run \`xera init --update --editor cursor\``;
241
+ return [check];
242
+ }
243
+ };
244
+
245
+ // src/editors/index.ts
246
+ var ALL_EDITORS = ["claude", "cursor", "codex"];
247
+ var editors = {
248
+ claude: claudeAdapter,
249
+ cursor: cursorAdapter,
250
+ codex: codexAdapter
251
+ };
252
+
253
+ // src/editors/detect.ts
254
+ import { existsSync as existsSync4 } from "fs";
255
+ import { join as join4 } from "path";
256
+ var MARKERS = {
257
+ claude: ".claude",
258
+ cursor: ".cursor",
259
+ codex: ".agents"
260
+ };
261
+ function detectEditors(cwd) {
262
+ return ALL_EDITORS.filter((name) => existsSync4(join4(cwd, MARKERS[name])));
263
+ }
264
+
265
+ // src/checks.ts
19
266
  function pushTicketChecks(checks, cwd, ticket, acFieldConfigured) {
20
- const ticketDir = join(cwd, ".xera", ticket);
21
- if (!existsSync(ticketDir)) {
267
+ const ticketDir = join5(cwd, ".xera", ticket);
268
+ if (!existsSync5(ticketDir)) {
22
269
  checks.push({
23
270
  name: `${ticket}: .xera/${ticket}/ exists`,
24
271
  ok: false,
@@ -26,8 +273,8 @@ function pushTicketChecks(checks, cwd, ticket, acFieldConfigured) {
26
273
  });
27
274
  return;
28
275
  }
29
- const giPath = join(ticketDir, "graph-input.json");
30
- if (!existsSync(giPath)) {
276
+ const giPath = join5(ticketDir, "graph-input.json");
277
+ if (!existsSync5(giPath)) {
31
278
  checks.push({
32
279
  name: `${ticket}: graph-input.json present`,
33
280
  ok: false,
@@ -35,7 +282,7 @@ function pushTicketChecks(checks, cwd, ticket, acFieldConfigured) {
35
282
  });
36
283
  } else {
37
284
  try {
38
- const data = JSON.parse(readFileSync(giPath, "utf8"));
285
+ const data = JSON.parse(readFileSync2(giPath, "utf8"));
39
286
  if (!Array.isArray(data.modifiesAreas)) {
40
287
  checks.push({
41
288
  name: `${ticket}: graph-input.json present`,
@@ -57,8 +304,8 @@ function pushTicketChecks(checks, cwd, ticket, acFieldConfigured) {
57
304
  });
58
305
  }
59
306
  }
60
- const storyPath = join(ticketDir, "story.md");
61
- if (!existsSync(storyPath)) {
307
+ const storyPath = join5(ticketDir, "story.md");
308
+ if (!existsSync5(storyPath)) {
62
309
  checks.push({
63
310
  name: `${ticket}: story.md acceptanceCriteria`,
64
311
  ok: false,
@@ -66,7 +313,7 @@ function pushTicketChecks(checks, cwd, ticket, acFieldConfigured) {
66
313
  });
67
314
  return;
68
315
  }
69
- const raw = readFileSync(storyPath, "utf8");
316
+ const raw = readFileSync2(storyPath, "utf8");
70
317
  const m = raw.match(/^---\n([\s\S]*?)\n---/);
71
318
  if (!m) {
72
319
  checks.push({
@@ -88,20 +335,29 @@ function pushTicketChecks(checks, cwd, ticket, acFieldConfigured) {
88
335
  return;
89
336
  }
90
337
  const ac = Array.isArray(fm.acceptanceCriteria) ? fm.acceptanceCriteria : [];
91
- if (ac.length === 0) {
92
- const hint = acFieldConfigured ? `jira.fields.acceptanceCriteria is configured but Jira returned no AC for this ticket \u2014 check the ticket in Jira` : `no AC in frontmatter; AC-level coverage will be empty. Set jira.fields.acceptanceCriteria in xera.config.ts if your project stores AC in a dedicated Jira field`;
93
- checks.push({
94
- name: `${ticket}: story.md acceptanceCriteria`,
95
- ok: false,
96
- message: hint
97
- });
98
- } else {
338
+ const source = fm.acceptanceCriteriaSource === "jira-field" || fm.acceptanceCriteriaSource === "body-extraction" || fm.acceptanceCriteriaSource === "none" ? fm.acceptanceCriteriaSource : undefined;
339
+ if (ac.length > 0) {
340
+ const suffix = source ? ` from ${source}` : "";
99
341
  checks.push({
100
342
  name: `${ticket}: story.md acceptanceCriteria`,
101
343
  ok: true,
102
- message: `${ac.length} AC item(s)`
344
+ message: `${ac.length} AC item(s)${suffix}`
103
345
  });
346
+ return;
347
+ }
348
+ let hint;
349
+ if (source === "none") {
350
+ hint = acFieldConfigured ? `jira.fields.acceptanceCriteria is configured but Jira returned no AC for this ticket, and /xera-fetch step 4 found no AC section in the body \u2014 add AC to the Jira ticket` : `AC not in Jira (no custom field configured) and /xera-fetch step 4 found no AC section in the description body \u2014 add AC/DoD to the Jira ticket, or edit story.md frontmatter manually`;
351
+ } else if (source === "body-extraction") {
352
+ hint = `acceptanceCriteriaSource: body-extraction but acceptanceCriteria is empty \u2014 re-run /xera-fetch ${ticket}`;
353
+ } else {
354
+ hint = acFieldConfigured ? `jira.fields.acceptanceCriteria is configured but Jira returned no AC for this ticket \u2014 check the ticket in Jira` : `no AC in frontmatter; AC-level coverage will be empty. Re-run /xera-fetch ${ticket} so step 4 can extract AC from the body (set jira.fields.acceptanceCriteria in xera.config.ts if your project uses a dedicated Jira field)`;
104
355
  }
356
+ checks.push({
357
+ name: `${ticket}: story.md acceptanceCriteria`,
358
+ ok: false,
359
+ message: hint
360
+ });
105
361
  }
106
362
  async function runChecks(cwd, opts = {}) {
107
363
  const checks = [];
@@ -153,10 +409,10 @@ async function runChecks(cwd, opts = {}) {
153
409
  });
154
410
  }
155
411
  }
156
- const httpAuthDir = join(cwd, ".xera", ".auth", "http");
412
+ const httpAuthDir = join5(cwd, ".xera", ".auth", "http");
157
413
  for (const role of Object.keys(cfg.http.auth.roles)) {
158
- const filePath = join(httpAuthDir, `${role}.json`);
159
- if (!existsSync(filePath)) {
414
+ const filePath = join5(httpAuthDir, `${role}.json`);
415
+ if (!existsSync5(filePath)) {
160
416
  checks.push({
161
417
  name: `http auth file present: ${role}`,
162
418
  ok: false,
@@ -215,7 +471,7 @@ async function runChecks(cwd, opts = {}) {
215
471
  });
216
472
  }
217
473
  } else {
218
- const open = existsSync(join(cwd, spec));
474
+ const open = existsSync5(join5(cwd, spec));
219
475
  const openCheck = open ? { name: "OpenAPI spec file present", ok: true } : { name: "OpenAPI spec file present", ok: false, message: `not found at ${spec}` };
220
476
  checks.push(openCheck);
221
477
  }
@@ -234,10 +490,10 @@ async function runChecks(cwd, opts = {}) {
234
490
  message: `${cfg.coverage.staleAfterDays}d is a very large window \u2014 coverage will be slow to react to drift`
235
491
  });
236
492
  }
237
- const snapPath = join(cwd, ".xera/graph/snapshot.json");
238
- if (existsSync(snapPath) && cfg.coverage.criticalAreas.length > 0) {
493
+ const snapPath = join5(cwd, ".xera/graph/snapshot.json");
494
+ if (existsSync5(snapPath) && cfg.coverage.criticalAreas.length > 0) {
239
495
  try {
240
- const snap = JSON.parse(readFileSync(snapPath, "utf8"));
496
+ const snap = JSON.parse(readFileSync2(snapPath, "utf8"));
241
497
  const known = new Set(Object.keys(snap.areas ?? {}));
242
498
  for (const slug of cfg.coverage.criticalAreas) {
243
499
  if (!known.has(slug)) {
@@ -250,9 +506,9 @@ async function runChecks(cwd, opts = {}) {
250
506
  }
251
507
  } catch {}
252
508
  }
253
- if (existsSync(snapPath)) {
509
+ if (existsSync5(snapPath)) {
254
510
  try {
255
- const snap = JSON.parse(readFileSync(snapPath, "utf8"));
511
+ const snap = JSON.parse(readFileSync2(snapPath, "utf8"));
256
512
  const acByTicket = {};
257
513
  for (const node of Object.values(snap.acNodes ?? {})) {
258
514
  acByTicket[node.ticketId] = (acByTicket[node.ticketId] ?? 0) + 1;
@@ -280,11 +536,11 @@ async function runChecks(cwd, opts = {}) {
280
536
  message: String(e.message)
281
537
  });
282
538
  }
283
- const envPath = join(cwd, ".env");
284
- if (!existsSync(envPath)) {
539
+ const envPath = join5(cwd, ".env");
540
+ if (!existsSync5(envPath)) {
285
541
  checks.push({ name: ".env present", ok: false, message: "copy from .env.example" });
286
542
  } else {
287
- const env = readFileSync(envPath, "utf8");
543
+ const env = readFileSync2(envPath, "utf8");
288
544
  checks.push({ name: "XERA_AUTH_KEY set", ok: /XERA_AUTH_KEY=[0-9a-fA-F]{64}/.test(env) });
289
545
  }
290
546
  try {
@@ -297,40 +553,26 @@ async function runChecks(cwd, opts = {}) {
297
553
  message: "run: bun add -D @playwright/test"
298
554
  });
299
555
  }
300
- const skillsDir = join(cwd, ".claude/skills");
301
- if (!existsSync(skillsDir)) {
302
- checks.push({ name: "xera skills present", ok: false, message: "run `xera init`" });
556
+ const REQUIRED_SKILLS = [
557
+ "xera-run",
558
+ "xera-fetch",
559
+ "xera-feature",
560
+ "xera-script",
561
+ "xera-exec",
562
+ "xera-report",
563
+ "xera-promote"
564
+ ];
565
+ const detected = detectEditors(cwd);
566
+ if (detected.length === 0) {
567
+ checks.push({
568
+ name: "xera editor integration present",
569
+ ok: false,
570
+ message: "run `xera init` (scaffolds for Claude Code, Cursor, and/or Codex)"
571
+ });
303
572
  } else {
304
- const required = [
305
- "xera-run",
306
- "xera-fetch",
307
- "xera-feature",
308
- "xera-script",
309
- "xera-exec",
310
- "xera-report",
311
- "xera-promote"
312
- ];
313
- const missing = [];
314
- const legacyFlat = [];
315
- for (const base of required) {
316
- if (existsSync(join(skillsDir, base, "SKILL.md")))
317
- continue;
318
- if (existsSync(join(skillsDir, `${base}.md`))) {
319
- legacyFlat.push(base);
320
- } else {
321
- missing.push(base);
322
- }
573
+ for (const name of detected) {
574
+ checks.push(...editors[name].doctorChecks(cwd, REQUIRED_SKILLS));
323
575
  }
324
- const skillsCheck = {
325
- name: "xera skills present",
326
- ok: missing.length === 0 && legacyFlat.length === 0
327
- };
328
- if (missing.length) {
329
- skillsCheck.message = `missing: ${missing.map((b) => `${b}/SKILL.md`).join(", ")}`;
330
- } else if (legacyFlat.length) {
331
- skillsCheck.message = `legacy flat layout in .claude/skills/ \u2014 run \`xera init --update\` to migrate to <name>/SKILL.md (Claude Code Skill tool requires the directory layout)`;
332
- }
333
- checks.push(skillsCheck);
334
576
  }
335
577
  return checks;
336
578
  }
@@ -340,7 +582,7 @@ async function doctorCommand(opts) {
340
582
  const cwd = process.cwd();
341
583
  if (opts.logs) {
342
584
  const paths = resolveArtifactPaths(cwd, opts.logs);
343
- if (!existsSync2(paths.logPath)) {
585
+ if (!existsSync6(paths.logPath)) {
344
586
  console.log(`No log at ${paths.logPath}`);
345
587
  return 0;
346
588
  }
@@ -366,25 +608,47 @@ async function doctorCommand(opts) {
366
608
  }
367
609
 
368
610
  // src/commands/init.ts
369
- import {
370
- appendFileSync,
371
- existsSync as existsSync3,
372
- mkdirSync as mkdirSync2,
373
- readdirSync as readdirSync2,
374
- readFileSync as readFileSync3,
375
- writeFileSync as writeFileSync2
376
- } from "fs";
611
+ import { appendFileSync, existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
377
612
  import { createRequire } from "module";
378
- import { dirname as dirname2, join as join3 } from "path";
613
+ import { join as join7 } from "path";
379
614
  import * as p from "@clack/prompts";
380
615
  import { generateKey } from "@xera-ai/core";
381
616
  import pc2 from "picocolors";
382
617
 
618
+ // src/editors/resolve.ts
619
+ function parseFlag(flag) {
620
+ if (flag === "all")
621
+ return [...ALL_EDITORS];
622
+ const parts = flag.split(",").map((s) => s.trim()).filter(Boolean);
623
+ if (parts.length === 0) {
624
+ throw new Error(`--editor: empty value. Valid: ${ALL_EDITORS.join(", ")} (or 'all').`);
625
+ }
626
+ const bad = parts.filter((p) => !ALL_EDITORS.includes(p));
627
+ if (bad.length) {
628
+ throw new Error(`--editor: unknown value(s) [${bad.join(", ")}]. Valid: ${ALL_EDITORS.join(", ")} (or 'all').`);
629
+ }
630
+ return parts;
631
+ }
632
+ async function resolveEditors(opts) {
633
+ if (opts.flag !== undefined)
634
+ return parseFlag(opts.flag);
635
+ const detected = detectEditors(opts.cwd);
636
+ if (opts.isUpdate)
637
+ return detected;
638
+ if (detected.length > 0)
639
+ return detected;
640
+ if (opts.isYes)
641
+ return [...ALL_EDITORS];
642
+ if (opts.prompt)
643
+ return opts.prompt();
644
+ return [...ALL_EDITORS];
645
+ }
646
+
383
647
  // src/scaffold.ts
384
- import { mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
385
- import { dirname, join as join2 } from "path";
648
+ import { mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
649
+ import { dirname as dirname4, join as join6 } from "path";
386
650
  import { fileURLToPath } from "url";
387
- var TEMPLATE_ROOT = join2(fileURLToPath(import.meta.url), "..", "..", "templates");
651
+ var TEMPLATE_ROOT = join6(fileURLToPath(import.meta.url), "..", "..", "templates");
388
652
  function render(tmpl, vars) {
389
653
  let out = tmpl;
390
654
  out = out.replace(/\{\{#each (\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_, k, block) => {
@@ -401,9 +665,9 @@ function render(tmpl, vars) {
401
665
  return out;
402
666
  }
403
667
  function scaffoldFile(targetPath, templateName, vars) {
404
- const tmpl = readFileSync2(join2(TEMPLATE_ROOT, templateName), "utf8");
405
- mkdirSync(dirname(targetPath), { recursive: true });
406
- writeFileSync(targetPath, render(tmpl, vars));
668
+ const tmpl = readFileSync3(join6(TEMPLATE_ROOT, templateName), "utf8");
669
+ mkdirSync4(dirname4(targetPath), { recursive: true });
670
+ writeFileSync4(targetPath, render(tmpl, vars));
407
671
  }
408
672
 
409
673
  // src/commands/init.ts
@@ -508,26 +772,26 @@ async function initCommand(opts) {
508
772
  authKey: generateKey()
509
773
  };
510
774
  const configTmpl = shape === "web" ? "xera.config.ts.tmpl" : shape === "api" ? "http-xera.config.ts.tmpl" : "mixed-xera.config.ts.tmpl";
511
- scaffoldFile(join3(cwd, "xera.config.ts"), configTmpl, vars);
775
+ scaffoldFile(join7(cwd, "xera.config.ts"), configTmpl, vars);
512
776
  const pwTmpl = shape === "api" ? "http-playwright.config.ts.tmpl" : "playwright.config.ts.tmpl";
513
- scaffoldFile(join3(cwd, "playwright.config.ts"), pwTmpl, vars);
514
- scaffoldFile(join3(cwd, "tsconfig.json"), "tsconfig.json.tmpl", vars);
777
+ scaffoldFile(join7(cwd, "playwright.config.ts"), pwTmpl, vars);
778
+ scaffoldFile(join7(cwd, "tsconfig.json"), "tsconfig.json.tmpl", vars);
515
779
  if (shape === "api") {
516
- scaffoldFile(join3(cwd, ".env.example"), "http-env.example.tmpl", vars);
780
+ scaffoldFile(join7(cwd, ".env.example"), "http-env.example.tmpl", vars);
517
781
  } else {
518
- scaffoldFile(join3(cwd, ".env.example"), "env.example.tmpl", vars);
782
+ scaffoldFile(join7(cwd, ".env.example"), "env.example.tmpl", vars);
519
783
  }
520
784
  if (wantsWeb || wantsHttp) {
521
- scaffoldFile(join3(cwd, "shared/auth-setup.ts"), "auth-setup.ts.tmpl", vars);
785
+ scaffoldFile(join7(cwd, "shared/auth-setup.ts"), "auth-setup.ts.tmpl", vars);
522
786
  }
523
- scaffoldFile(join3(cwd, ".github/workflows/xera-graph.yml"), "xera-graph.yml.template", vars);
787
+ scaffoldFile(join7(cwd, ".github/workflows/xera-graph.yml"), "xera-graph.yml.template", vars);
524
788
  if (wantsHttp && vars.openapiPath && !vars.openapiPath.startsWith("http")) {
525
- const openapiTarget = join3(cwd, vars.openapiPath);
526
- if (!existsSync3(openapiTarget)) {
789
+ const openapiTarget = join7(cwd, vars.openapiPath);
790
+ if (!existsSync7(openapiTarget)) {
527
791
  scaffoldFile(openapiTarget, "openapi.yaml.tmpl", vars);
528
792
  }
529
793
  }
530
- const gitignorePath = join3(cwd, ".gitignore");
794
+ const gitignorePath = join7(cwd, ".gitignore");
531
795
  const gitignoreAdditions = [
532
796
  "",
533
797
  "# xera",
@@ -539,33 +803,55 @@ async function initCommand(opts) {
539
803
  "node_modules/"
540
804
  ].join(`
541
805
  `);
542
- if (existsSync3(gitignorePath)) {
543
- const current = readFileSync3(gitignorePath, "utf8");
806
+ if (existsSync7(gitignorePath)) {
807
+ const current = readFileSync4(gitignorePath, "utf8");
544
808
  if (!current.includes("# xera"))
545
809
  appendFileSync(gitignorePath, gitignoreAdditions);
546
810
  } else {
547
- writeFileSync2(gitignorePath, `${gitignoreAdditions.trim()}
811
+ writeFileSync5(gitignorePath, `${gitignoreAdditions.trim()}
548
812
  `);
549
813
  }
814
+ const editorTargets = await resolveEditors({
815
+ flag: opts.editor,
816
+ cwd,
817
+ isUpdate: false,
818
+ isYes: opts.yes,
819
+ prompt: async () => {
820
+ const choice = await p.multiselect({
821
+ message: "Which editor(s) should xera scaffold for?",
822
+ options: [
823
+ { value: "claude", label: "Claude Code (.claude/skills/, .claude/commands/)" },
824
+ { value: "cursor", label: "Cursor (.cursor/rules/, .cursor/commands/)" },
825
+ { value: "codex", label: "OpenAI Codex CLI (.agents/skills/)" }
826
+ ],
827
+ initialValues: ["claude"],
828
+ required: true
829
+ });
830
+ if (typeof choice === "symbol")
831
+ cancel2();
832
+ return choice;
833
+ }
834
+ });
550
835
  const skillsPkgPath = require2.resolve("@xera-ai/skills/package.json");
551
- const skillsSrcDir = join3(skillsPkgPath, "..");
836
+ const skillsSrcDir = join7(skillsPkgPath, "..");
552
837
  const SKILL_IGNORE = new Set(["package.json", "version.json", "CHANGELOG.md"]);
553
838
  for (const name of readdirSync2(skillsSrcDir)) {
554
839
  if (SKILL_IGNORE.has(name))
555
840
  continue;
556
841
  if (!name.endsWith(".md"))
557
842
  continue;
558
- const content = readFileSync3(join3(skillsSrcDir, name));
843
+ const raw = readFileSync4(join7(skillsSrcDir, name), "utf8");
844
+ const { frontmatter, body } = parseFrontmatter(raw);
559
845
  const base = name.replace(/\.md$/, "");
560
- const skillFile = join3(cwd, ".claude/skills", base, "SKILL.md");
561
- mkdirSync2(dirname2(skillFile), { recursive: true });
562
- writeFileSync2(skillFile, content);
563
- const cmdFile = join3(cwd, ".claude/commands", name);
564
- mkdirSync2(dirname2(cmdFile), { recursive: true });
565
- writeFileSync2(cmdFile, content);
846
+ const skillInput = { base, body, frontmatter };
847
+ for (const editorName of editorTargets) {
848
+ const adapter = editors[editorName];
849
+ adapter.scaffoldSkill(cwd, skillInput);
850
+ adapter.scaffoldCommand?.(cwd, skillInput);
851
+ }
566
852
  }
567
- const pkgPath = join3(cwd, "package.json");
568
- const pkg = existsSync3(pkgPath) ? JSON.parse(readFileSync3(pkgPath, "utf8")) : { name: "xera-project", private: true, type: "module" };
853
+ const pkgPath = join7(cwd, "package.json");
854
+ const pkg = existsSync7(pkgPath) ? JSON.parse(readFileSync4(pkgPath, "utf8")) : { name: "xera-project", private: true, type: "module" };
569
855
  pkg.scripts = pkg.scripts ?? {};
570
856
  pkg.scripts["xera:fetch"] = "xera-internal fetch";
571
857
  pkg.scripts["xera:validate-feature"] = "xera-internal validate-feature";
@@ -601,7 +887,17 @@ async function initCommand(opts) {
601
887
  pkg.devDependencies["@playwright/test"] = "^1.60.0";
602
888
  pkg.devDependencies["@types/node"] = "^25.8.0";
603
889
  pkg.devDependencies["typescript"] = "^6.0.3";
604
- writeFileSync2(pkgPath, JSON.stringify(pkg, null, 2));
890
+ writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2));
891
+ const editorLines = editorTargets.map((e) => {
892
+ if (e === "claude")
893
+ return " Claude Code: /xera-run <TICKET>";
894
+ if (e === "cursor")
895
+ return " Cursor: /xera-run <TICKET> (slash menu)";
896
+ if (e === "codex")
897
+ return ' OpenAI Codex CLI: type "run xera for <TICKET>" \u2014 Codex picks up the xera-run skill';
898
+ return "";
899
+ }).join(`
900
+ `);
605
901
  const nextSteps = shape === "api" ? `
606
902
  Next:
607
903
  1) Copy .env.example to .env and set your auth credentials:
@@ -610,7 +906,7 @@ Next:
610
906
  2) Run pre-authentication:
611
907
  bun run xera:auth-setup
612
908
  3) Start testing:
613
- Open Claude Code in this directory and run: /xera-run <TICKET>
909
+ ${editorLines}
614
910
  ` : shape === "mixed" ? `
615
911
  Next:
616
912
  1) Copy .env.example to .env and set credentials (both web logins and API tokens):
@@ -618,7 +914,7 @@ Next:
618
914
  2) Run pre-authentication:
619
915
  bun run xera:auth-setup
620
916
  3) Start testing:
621
- Open Claude Code in this directory and run: /xera-run <TICKET>
917
+ ${editorLines}
622
918
  ` : `
623
919
  Next:
624
920
  1) Copy .env.example to .env and set your Jira credentials:
@@ -626,7 +922,7 @@ Next:
626
922
  2) Run pre-authentication:
627
923
  bun run xera:auth-setup
628
924
  3) Start testing:
629
- Open Claude Code in this directory and run: /xera-run <TICKET>
925
+ ${editorLines}
630
926
  `;
631
927
  p.note(nextSteps.trim(), "Next steps");
632
928
  p.outro(pc2.green("xera initialized!"));
@@ -635,24 +931,23 @@ Next:
635
931
  // src/commands/init-update.ts
636
932
  import {
637
933
  copyFileSync,
638
- existsSync as existsSync4,
639
- mkdirSync as mkdirSync3,
934
+ existsSync as existsSync8,
935
+ mkdirSync as mkdirSync5,
640
936
  readdirSync as readdirSync3,
641
- readFileSync as readFileSync4,
642
- unlinkSync,
643
- writeFileSync as writeFileSync3
937
+ readFileSync as readFileSync5,
938
+ writeFileSync as writeFileSync6
644
939
  } from "fs";
645
940
  import { createRequire as createRequire2 } from "module";
646
- import { dirname as dirname3, join as join4 } from "path";
941
+ import { join as join8 } from "path";
647
942
  import * as p2 from "@clack/prompts";
648
943
  import pc3 from "picocolors";
649
944
  var require3 = createRequire2(import.meta.url);
650
945
  var CLI_VERSION2 = require3("../package.json").version;
651
946
  function detectAdaptersFromConfig(cwd) {
652
- const configPath = join4(cwd, "xera.config.ts");
653
- if (!existsSync4(configPath))
947
+ const configPath = join8(cwd, "xera.config.ts");
948
+ if (!existsSync8(configPath))
654
949
  return null;
655
- const cfg = readFileSync4(configPath, "utf8");
950
+ const cfg = readFileSync5(configPath, "utf8");
656
951
  const m = cfg.match(/adapters:\s*\[([^\]]+)\]/);
657
952
  if (!m)
658
953
  return null;
@@ -703,7 +998,7 @@ ${roles.map((r) => ` ${r}: { envEmail: 'TEST_${r.toUpperCase().replace(/-
703
998
  ` web: {`,
704
999
  ` baseUrl: { staging: '${baseUrl}' },`,
705
1000
  ` defaultEnv: 'staging',`,
706
- authBlock + ` },`
1001
+ `${authBlock} },`
707
1002
  ].join(`
708
1003
  `);
709
1004
  }
@@ -729,12 +1024,12 @@ export const web = defineAuthSetup(async (page, _role, creds) => {
729
1024
  async function initUpdateCommand(opts) {
730
1025
  const cwd = process.cwd();
731
1026
  p2.intro(pc3.cyan("xera init --update"));
732
- const pkgPath = join4(cwd, "package.json");
733
- if (!existsSync4(pkgPath)) {
1027
+ const pkgPath = join8(cwd, "package.json");
1028
+ if (!existsSync8(pkgPath)) {
734
1029
  p2.cancel("No package.json found \u2014 run `xera init` first.");
735
1030
  process.exit(1);
736
1031
  }
737
- const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
1032
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
738
1033
  pkg.dependencies = pkg.dependencies ?? {};
739
1034
  pkg.dependencies["@xera-ai/core"] = `^${CLI_VERSION2}`;
740
1035
  pkg.dependencies["@xera-ai/prompts"] = `^${CLI_VERSION2}`;
@@ -754,79 +1049,47 @@ async function initUpdateCommand(opts) {
754
1049
  pkg.scripts["xera:impact-prepare"] = "xera-internal impact-prepare";
755
1050
  pkg.scripts["xera:heal-prepare"] = "xera-internal heal-prepare";
756
1051
  pkg.scripts["xera:disputes"] = "xera-internal disputes";
757
- writeFileSync3(pkgPath, JSON.stringify(pkg, null, 2));
758
- const wfDir = join4(cwd, ".github/workflows");
759
- mkdirSync3(wfDir, { recursive: true });
1052
+ writeFileSync6(pkgPath, JSON.stringify(pkg, null, 2));
1053
+ const wfDir = join8(cwd, ".github/workflows");
1054
+ mkdirSync5(wfDir, { recursive: true });
760
1055
  try {
761
1056
  const cliPkgPath = require3.resolve("@xera-ai/cli/package.json");
762
- const cliTplPath = join4(cliPkgPath, "..", "templates/xera-graph.yml.template");
763
- copyFileSync(cliTplPath, join4(wfDir, "xera-graph.yml"));
1057
+ const cliTplPath = join8(cliPkgPath, "..", "templates/xera-graph.yml.template");
1058
+ copyFileSync(cliTplPath, join8(wfDir, "xera-graph.yml"));
764
1059
  p2.log.info("scaffolded .github/workflows/xera-graph.yml");
765
1060
  } catch (_e) {
766
1061
  p2.log.warn("skipped xera-graph.yml scaffold (re-run `xera init` to create it)");
767
1062
  }
768
- const skillsSrc = require3.resolve("@xera-ai/skills/package.json");
769
- const newSkillsDir = join4(skillsSrc, "..");
770
- const SKILL_IGNORE = new Set(["package.json", "version.json", "CHANGELOG.md"]);
771
- for (const name of readdirSync3(newSkillsDir)) {
772
- if (SKILL_IGNORE.has(name))
773
- continue;
774
- if (!name.endsWith(".md"))
775
- continue;
776
- const newContent = readFileSync4(join4(newSkillsDir, name), "utf8");
777
- const base = name.replace(/\.md$/, "");
778
- const skillPath = join4(cwd, ".claude/skills", base, "SKILL.md");
779
- const legacyFlatSkillPath = join4(cwd, ".claude/skills", name);
780
- const cmdPath = join4(cwd, ".claude/commands", name);
781
- let migratedLegacy = false;
782
- if (existsSync4(legacyFlatSkillPath) && !existsSync4(skillPath)) {
783
- const legacyContent = readFileSync4(legacyFlatSkillPath, "utf8");
784
- mkdirSync3(dirname3(skillPath), { recursive: true });
785
- writeFileSync3(skillPath, legacyContent);
786
- unlinkSync(legacyFlatSkillPath);
787
- migratedLegacy = true;
788
- }
789
- const targets = [];
790
- for (const path of [skillPath, cmdPath]) {
791
- if (!existsSync4(path)) {
792
- targets.push({ path, state: "missing" });
793
- } else {
794
- const content = readFileSync4(path, "utf8");
795
- targets.push({ path, state: content === newContent ? "same" : "diff" });
796
- }
797
- }
798
- if (targets.every((s) => s.state === "missing")) {
799
- for (const { path } of targets) {
800
- mkdirSync3(dirname3(path), { recursive: true });
801
- writeFileSync3(path, newContent);
802
- }
803
- p2.log.info(`+ ${name}`);
804
- continue;
805
- }
806
- if (targets.every((s) => s.state === "same")) {
807
- if (migratedLegacy)
808
- p2.log.success(`migrated ${name} to .claude/skills/${base}/SKILL.md`);
809
- else
810
- p2.log.info(`= ${name}`);
811
- continue;
812
- }
813
- const choice = await p2.select({
814
- message: `${name} differs from package version`,
815
- options: [
816
- { value: "keep", label: "Keep local" },
817
- { value: "overwrite", label: "Overwrite with package version" }
818
- ]
819
- });
820
- if (choice === "overwrite") {
821
- for (const { path } of targets) {
822
- mkdirSync3(dirname3(path), { recursive: true });
823
- writeFileSync3(path, newContent);
1063
+ const editorTargets = await resolveEditors({
1064
+ flag: opts.editor,
1065
+ cwd,
1066
+ isUpdate: true,
1067
+ isYes: opts.yes
1068
+ });
1069
+ if (editorTargets.length === 0) {
1070
+ p2.log.warn("No editor integration detected in this project. Pass --editor claude|cursor|codex|all to add one.");
1071
+ } else {
1072
+ const skillsSrc = require3.resolve("@xera-ai/skills/package.json");
1073
+ const newSkillsDir = join8(skillsSrc, "..");
1074
+ const SKILL_IGNORE = new Set(["package.json", "version.json", "CHANGELOG.md"]);
1075
+ for (const name of readdirSync3(newSkillsDir)) {
1076
+ if (SKILL_IGNORE.has(name))
1077
+ continue;
1078
+ if (!name.endsWith(".md"))
1079
+ continue;
1080
+ const rawNew = readFileSync5(join8(newSkillsDir, name), "utf8");
1081
+ const { frontmatter, body } = parseFrontmatter(rawNew);
1082
+ const base = name.replace(/\.md$/, "");
1083
+ const skillInput = { base, body, frontmatter };
1084
+ for (const editorName of editorTargets) {
1085
+ const adapter = editors[editorName];
1086
+ const migrated = adapter.legacyMigrate?.(cwd, base) ?? false;
1087
+ if (migrated)
1088
+ p2.log.success(`migrated ${base} (${editorName}) to new layout`);
1089
+ adapter.scaffoldSkill(cwd, skillInput);
1090
+ adapter.scaffoldCommand?.(cwd, skillInput);
824
1091
  }
825
- p2.log.success(`overwrote ${name}`);
826
- } else {
827
- if (migratedLegacy)
828
- p2.log.success(`migrated ${name} to .claude/skills/${base}/SKILL.md`);
829
- p2.log.warn(`kept local ${name}`);
1092
+ p2.log.info(`refreshed ${base} across [${editorTargets.join(", ")}]`);
830
1093
  }
831
1094
  }
832
1095
  const hasShapeFlags = opts.apiBaseUrl !== undefined || opts.openapiPath !== undefined || opts.authStrategy !== undefined || opts.httpRoles !== undefined || opts.stagingUrl !== undefined || opts.authEnabled !== undefined || opts.roles !== undefined;
@@ -924,7 +1187,7 @@ async function main() {
924
1187
  cli.help();
925
1188
  cli.version(VERSION);
926
1189
  cli.usage("<command> [options]");
927
- cli.command("init", "Scaffold a new xera project in the current directory").option("--update", "Non-destructive refresh of an existing project").option("-y, --yes", "Accept all defaults (non-interactive)").option("--shape <shape>", "Project shape: web | api | mixed").option("--ju, --jira-base-url <url>", "Jira workspace URL").option("--pk, --project-keys <keys>", "Jira project key(s), comma-separated").option("--sf, --story-field <field>", "Jira field id for user story (default: description)").option("--ac, --ac-field <field>", "Jira field id for acceptance criteria").option("--su, --staging-url <url>", "Web app staging URL").option("--auth-enabled", "App requires login to test most pages").option("--no-auth-enabled", "App does not require login").option("--ro, --roles <roles>", "Test user roles, comma-separated (default: admin,regular)").option("--au, --api-base-url <url>", "API base URL").option("--op, --openapi-path <path>", "OpenAPI spec path or URL").option("--as, --auth-strategy <strategy>", `API auth strategy: ${VALID_AUTH_STRATEGIES.join(" | ")}`).option("--hr, --http-roles <roles>", "HTTP test roles, comma-separated (default: user)").example("xera init").example("xera init -y --shape web").example("xera init -y --shape api --pk MYPROJ --ju https://myco.atlassian.net --au https://api.staging.example.com --as bearer").example("xera init -y --shape mixed --pk PROJ --ju https://myco.atlassian.net --su https://staging.example.com --au https://api.staging.example.com").action(async (opts) => {
1190
+ cli.command("init", "Scaffold a new xera project in the current directory").option("--update", "Non-destructive refresh of an existing project").option("-y, --yes", "Accept all defaults (non-interactive)").option("--shape <shape>", "Project shape: web | api | mixed").option("--editor <list>", 'Editor(s) to scaffold: claude,cursor,codex or "all" (default: auto-detect or all)').option("--ju, --jira-base-url <url>", "Jira workspace URL").option("--pk, --project-keys <keys>", "Jira project key(s), comma-separated").option("--sf, --story-field <field>", "Jira field id for user story (default: description)").option("--ac, --ac-field <field>", "Jira field id for acceptance criteria").option("--su, --staging-url <url>", "Web app staging URL").option("--auth-enabled", "App requires login to test most pages").option("--no-auth-enabled", "App does not require login").option("--ro, --roles <roles>", "Test user roles, comma-separated (default: admin,regular)").option("--au, --api-base-url <url>", "API base URL").option("--op, --openapi-path <path>", "OpenAPI spec path or URL").option("--as, --auth-strategy <strategy>", `API auth strategy: ${VALID_AUTH_STRATEGIES.join(" | ")}`).option("--hr, --http-roles <roles>", "HTTP test roles, comma-separated (default: user)").example("xera init").example("xera init -y --shape web").example("xera init -y --shape web --editor claude,cursor").example("xera init -y --shape api --pk MYPROJ --ju https://myco.atlassian.net --au https://api.staging.example.com --as bearer").example("xera init -y --shape mixed --pk PROJ --ju https://myco.atlassian.net --su https://staging.example.com --au https://api.staging.example.com").action(async (opts) => {
928
1191
  if (opts.update) {
929
1192
  const updateOpts = { yes: !!opts.yes };
930
1193
  if (opts.shape !== undefined) {
@@ -957,6 +1220,8 @@ async function main() {
957
1220
  updateOpts.authEnabled = opts.authEnabled;
958
1221
  if (opts.roles !== undefined)
959
1222
  updateOpts.roles = opts.roles;
1223
+ if (opts.editor !== undefined)
1224
+ updateOpts.editor = opts.editor;
960
1225
  await initUpdateCommand(updateOpts);
961
1226
  return;
962
1227
  }
@@ -999,6 +1264,8 @@ async function main() {
999
1264
  initOpts.openapiPath = opts.openapiPath;
1000
1265
  if (opts.httpRoles !== undefined)
1001
1266
  initOpts.httpRoles = opts.httpRoles;
1267
+ if (opts.editor !== undefined)
1268
+ initOpts.editor = opts.editor;
1002
1269
  await initCommand(initOpts);
1003
1270
  });
1004
1271
  cli.command("doctor", "Run a health check").option("--strict <ticket>", "Treat ticket-specific checks as required").option("--logs <ticket>", "Pretty-print xera.log for a ticket").option("--usage", "Show token/usage summary from recent runs").action(async (opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xera-ai/cli",
3
- "version": "0.12.3",
3
+ "version": "0.13.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "xera": "./bin/xera"
@@ -15,8 +15,8 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "dependencies": {
18
- "@xera-ai/core": "^0.12.3",
19
- "@xera-ai/skills": "^0.12.3",
18
+ "@xera-ai/core": "^0.13.1",
19
+ "@xera-ai/skills": "^0.13.1",
20
20
  "@clack/prompts": "1.4.0",
21
21
  "cac": "7.0.0",
22
22
  "picocolors": "1.1.1",