@agent-native/skills 0.0.0-beta-20260819155526

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/index.js ADDED
@@ -0,0 +1,1335 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { docsUrl } from "@agent-native/core/shared";
8
+ import { resolveAppForSkill } from "./built-in-apps.js";
9
+ import { registerMcpServer } from "./connect.js";
10
+ import { createCliTelemetry } from "./telemetry.js";
11
+ const HELP = `@agent-native/skills
12
+
13
+ Usage:
14
+ npx @agent-native/skills@latest add [options]
15
+ npx @agent-native/skills@latest list
16
+ npx @agent-native/skills@latest status [skill|scaffold] [options]
17
+ npx @agent-native/skills@latest update [skill|scaffold] [options]
18
+
19
+ Options:
20
+ --skill <name> Install only this skill (repeatable)
21
+ --client, -a <client> codex, claude-code, cowork, pi, cursor, opencode, github-copilot, or all
22
+ (default: all; repeatable or comma-separated)
23
+ --scope <user|project> Install globally or into the current project (default: user)
24
+ -g, --global Alias for --scope user
25
+ --project Alias for --scope project
26
+ --update-instructions Add managed AGENTS.md / CLAUDE.md instructions when useful
27
+ --no-update-instructions Skip managed instruction file updates
28
+ --instructions-file <path> File to receive managed instructions (repeatable)
29
+ --with-github-action Add .github/workflows/pr-visual-recap.yml when visual-recap is installed
30
+ --mode <mode> App-backed skill mode: hosted, local-files, or self-hosted
31
+ --mcp-url <url> Self-hosted app/MCP URL for app-backed skills
32
+ --force Overwrite a different existing PR Visual Recap workflow
33
+ --no-mcp Install skill files only; skip registering the app's MCP server
34
+ --no-connect Register MCP where possible but skip inline browser/device authentication
35
+ -y, --yes Use defaults in non-interactive mode
36
+ --dry-run Print intended writes without changing files
37
+ --json Print the result as JSON
38
+
39
+ App-backed skills (visual-plan, visual-recap, visualize-repo, content) register
40
+ their hosted MCP server in your agent config by default so the agent can
41
+ actually use them. Use --no-mcp to skip that and copy the files only.
42
+ For visual-plan/visual-recap/visualize-repo/content, choose --mode local-files
43
+ for local-file workflows, or --mode self-hosted --mcp-url <url> for your own app.
44
+
45
+ Examples:
46
+ npx @agent-native/skills@latest add
47
+ npx @agent-native/skills@latest add --skill quick-recap
48
+ npx @agent-native/skills@latest add --skill visual-recap --with-github-action
49
+ npx @agent-native/skills@latest update scaffold --project
50
+ `;
51
+ const CLIENTS = [
52
+ "codex",
53
+ "claude-code",
54
+ "cowork",
55
+ "pi",
56
+ "cursor",
57
+ "opencode",
58
+ "github-copilot",
59
+ ];
60
+ const DEFAULT_SKILLS_SOURCE = "BuilderIO/skills";
61
+ const MANAGED_INSTRUCTIONS_START = "<!-- BEGIN @agent-native/skills -->";
62
+ const MANAGED_INSTRUCTIONS_END = "<!-- END @agent-native/skills -->";
63
+ export function parseSkillsCliArgs(argv) {
64
+ const first = argv[0];
65
+ if (!first || first === "help" || first === "--help" || first === "-h") {
66
+ return defaultArgs("help");
67
+ }
68
+ const command = first === "list" ||
69
+ first === "status" ||
70
+ first === "update" ||
71
+ first === "add"
72
+ ? first
73
+ : "add";
74
+ const args = first === "add" ||
75
+ first === "list" ||
76
+ first === "status" ||
77
+ first === "update"
78
+ ? argv.slice(1)
79
+ : argv;
80
+ const out = defaultArgs(command);
81
+ for (let i = 0; i < args.length; i += 1) {
82
+ const arg = args[i];
83
+ const eat = (flag) => {
84
+ if (arg === flag) {
85
+ const next = args[++i];
86
+ if (!next || next.startsWith("-")) {
87
+ throw new Error(`Missing value for ${flag}.`);
88
+ }
89
+ return next;
90
+ }
91
+ if (arg.startsWith(`${flag}=`)) {
92
+ const value = arg.slice(flag.length + 1);
93
+ if (!value)
94
+ throw new Error(`Missing value for ${flag}.`);
95
+ return value;
96
+ }
97
+ return undefined;
98
+ };
99
+ let value;
100
+ if ((value = eat("--skill")) !== undefined)
101
+ out.skillNames.push(value);
102
+ else if ((value = eat("-s")) !== undefined)
103
+ out.skillNames.push(value);
104
+ else if ((value = eat("--client")) !== undefined)
105
+ out.clients.push(...normalizeClients(value));
106
+ else if ((value = eat("--agent")) !== undefined)
107
+ out.clients.push(...normalizeClients(value));
108
+ else if ((value = eat("-a")) !== undefined)
109
+ out.clients.push(...normalizeClients(value));
110
+ else if ((value = eat("--scope")) !== undefined) {
111
+ out.scope = parseScope(value);
112
+ out.scopeExplicit = true;
113
+ }
114
+ else if ((value = eat("--instructions-file")) !== undefined)
115
+ out.instructionFiles.push(value);
116
+ else if ((value = eat("--cwd")) !== undefined)
117
+ out.baseDir = value;
118
+ else if (arg === "-g" || arg === "--global") {
119
+ out.scope = "user";
120
+ out.scopeExplicit = true;
121
+ }
122
+ else if (arg === "--project") {
123
+ out.scope = "project";
124
+ out.scopeExplicit = true;
125
+ }
126
+ else if (arg === "--copy") {
127
+ // Compatibility with the open `skills` CLI. This installer always copies.
128
+ out.copySource = true;
129
+ }
130
+ else if (arg === "-y" || arg === "--yes")
131
+ out.yes = true;
132
+ else if (arg === "--dry-run")
133
+ out.dryRun = true;
134
+ else if (arg === "--json")
135
+ out.printJson = true;
136
+ else if (arg === "--quiet")
137
+ out.quiet = true;
138
+ else if (arg === "--update-instructions")
139
+ out.updateInstructions = true;
140
+ else if (arg === "--no-update-instructions")
141
+ out.updateInstructions = false;
142
+ else if (arg === "--with-github-action" || arg === "--with-github-actions")
143
+ out.withGithubAction = true;
144
+ else if ((value = eat("--mode")) !== undefined) {
145
+ out.planMode = parsePlanMode(value);
146
+ }
147
+ else if ((value = eat("--mcp-url")) !== undefined) {
148
+ out.mcpUrl = value;
149
+ }
150
+ else if (arg === "--force")
151
+ out.force = true;
152
+ else if (arg === "--no-mcp")
153
+ out.mcp = false;
154
+ else if (arg === "--mcp")
155
+ out.mcp = true;
156
+ else if (arg === "--no-connect" || arg === "--skip-connect")
157
+ out.connect = false;
158
+ else if (arg === "--connect")
159
+ out.connect = true;
160
+ else if (arg.startsWith("-"))
161
+ throw new Error(`Unknown option: ${arg}`);
162
+ else if (!out.source)
163
+ out.source = arg;
164
+ else
165
+ throw new Error(`Unexpected argument: ${arg}`);
166
+ }
167
+ if (out.command === "add" &&
168
+ out.source &&
169
+ out.source !== DEFAULT_SKILLS_SOURCE &&
170
+ !out.copySource) {
171
+ throw new Error(`Unexpected argument: ${out.source}. @agent-native/skills installs the BuilderIO skills collection; use --skill <name> to choose a skill.`);
172
+ }
173
+ if (out.command === "add" &&
174
+ out.source === DEFAULT_SKILLS_SOURCE &&
175
+ !out.copySource) {
176
+ out.source = undefined;
177
+ }
178
+ out.skillNames = unique(out.skillNames.map(normalizeSkillName));
179
+ out.clients = unique(out.clients);
180
+ out.instructionFiles = unique(out.instructionFiles);
181
+ if (out.planMode && out.mcpUrl && out.planMode !== "self-hosted") {
182
+ throw new Error("--mcp-url can only be used with --mode self-hosted.");
183
+ }
184
+ if (out.mcpUrl && !out.planMode)
185
+ out.planMode = "self-hosted";
186
+ return out;
187
+ }
188
+ /**
189
+ * Translate this package's parsed args into the argv shape `@agent-native/core`
190
+ * skills expects. Core takes a single positional target + compatible flags; we
191
+ * forward one explicit skill as that target and let core's interactive picker
192
+ * handle 0-or-many selections.
193
+ */
194
+ function toCoreSkillsArgv(parsed) {
195
+ const out = [parsed.command];
196
+ if (parsed.command === "add") {
197
+ if (parsed.copySource && parsed.source && parsed.skillNames.length > 0) {
198
+ out.push(parsed.source);
199
+ for (const skill of parsed.skillNames)
200
+ out.push("--skill", skill);
201
+ }
202
+ else if (parsed.skillNames.length === 1) {
203
+ out.push(parsed.skillNames[0]);
204
+ }
205
+ else if (parsed.skillNames.length > 1) {
206
+ const appIds = unique(parsed.skillNames
207
+ .map((name) => resolveAppForSkill(name)?.appId)
208
+ .filter((appId) => Boolean(appId)));
209
+ if (appIds.length === 1 &&
210
+ parsed.skillNames.every((name) => resolveAppForSkill(name))) {
211
+ out.push(appIds[0]);
212
+ }
213
+ else {
214
+ for (const skill of parsed.skillNames)
215
+ out.push("--skill", skill);
216
+ }
217
+ }
218
+ }
219
+ else if (parsed.command === "status" || parsed.command === "update") {
220
+ if (parsed.source)
221
+ out.push(parsed.source);
222
+ else if (parsed.skillNames.length === 1)
223
+ out.push(parsed.skillNames[0]);
224
+ }
225
+ if ((parsed.command === "add" ||
226
+ parsed.command === "status" ||
227
+ parsed.command === "update") &&
228
+ parsed.clients.length) {
229
+ out.push("--client", parsed.clients.join(","));
230
+ }
231
+ if ((parsed.command === "add" ||
232
+ parsed.command === "status" ||
233
+ parsed.command === "update") &&
234
+ parsed.scopeExplicit) {
235
+ out.push("--scope", parsed.scope);
236
+ }
237
+ if (parsed.command === "add" && parsed.yes)
238
+ out.push("--yes");
239
+ if (parsed.dryRun)
240
+ out.push("--dry-run");
241
+ if (parsed.printJson)
242
+ out.push("--json");
243
+ if (parsed.command === "add" && parsed.withGithubAction)
244
+ out.push("--with-github-action");
245
+ if (parsed.command === "add" && parsed.planMode)
246
+ out.push("--mode", parsed.planMode);
247
+ if (parsed.command === "add" && parsed.mcpUrl)
248
+ out.push("--mcp-url", parsed.mcpUrl);
249
+ if (parsed.command === "add" && parsed.force)
250
+ out.push("--force");
251
+ if (parsed.command === "add" && parsed.mcp === false)
252
+ out.push("--no-mcp");
253
+ if (parsed.command === "add" && parsed.connect === false)
254
+ out.push("--no-connect");
255
+ if (parsed.command === "add" && parsed.updateInstructions === true)
256
+ out.push("--update-instructions");
257
+ if (parsed.command === "add" && parsed.updateInstructions === false)
258
+ out.push("--no-update-instructions");
259
+ return out;
260
+ }
261
+ function shouldLoadPublicCatalog(parsed) {
262
+ if (parsed.command === "list")
263
+ return true;
264
+ if (parsed.command !== "add")
265
+ return false;
266
+ if (parsed.copySource && parsed.source)
267
+ return true;
268
+ if (parsed.skillNames.length === 0)
269
+ return true;
270
+ return parsed.skillNames.some((name) => !isCoreDelegatedSkill(name));
271
+ }
272
+ const REWIND_SKILL_TARGETS = new Set([
273
+ "rewind",
274
+ "screen-memory",
275
+ "clips-rewind",
276
+ "agent-native-rewind",
277
+ ]);
278
+ function isRewindSkillTarget(skillName) {
279
+ return REWIND_SKILL_TARGETS.has(skillName.trim().toLowerCase());
280
+ }
281
+ function isCoreDelegatedSkill(skillName) {
282
+ // Rewind uses Core's local Screen Memory installer, not the standalone
283
+ // package's hosted MCP descriptor path.
284
+ return (isRewindSkillTarget(skillName) || Boolean(resolveAppForSkill(skillName)));
285
+ }
286
+ const HIDDEN_STANDALONE_BUILT_INS = [
287
+ "assets",
288
+ "content",
289
+ "design-exploration",
290
+ "visual-edit",
291
+ "context-xray",
292
+ ];
293
+ function shouldShowDelegatedStartupProgress(parsed, options) {
294
+ return (parsed.command !== "help" &&
295
+ !parsed.printJson &&
296
+ !parsed.quiet &&
297
+ cliInteractive(parsed, options));
298
+ }
299
+ export async function runSkillsCli(argv, options = {}) {
300
+ const parsed = parseSkillsCliArgs(argv);
301
+ // `@agent-native/skills` normally uses the exact same core flow as
302
+ // `agent-native skills`; it only passes a broader public skill catalog.
303
+ // AGENT_NATIVE_SKILLS_DIRECT=1 is set by core when it shells out to this
304
+ // package as a headless file-copy worker for public/plain skill repos. That
305
+ // direct mode is intentionally non-user-facing prompt plumbing.
306
+ if (process.env.AGENT_NATIVE_SKILLS_DIRECT !== "1") {
307
+ if (parsed.command === "help") {
308
+ process.stdout.write(`${HELP}\n`);
309
+ return;
310
+ }
311
+ if (shouldShowDelegatedStartupProgress(parsed, options)) {
312
+ process.stderr.write("Preparing Agent Native skills...\n");
313
+ }
314
+ const loadedSource = shouldLoadPublicCatalog(parsed)
315
+ ? await materializeSource(parsed.source ?? DEFAULT_SKILLS_SOURCE)
316
+ : null;
317
+ try {
318
+ const { runSkills } = (await import("@agent-native/core/cli/skills"));
319
+ const publicSkillEntries = loadedSource
320
+ ? discoverSkills(loadedSource.root).map((entry) => ({
321
+ name: entry.name,
322
+ description: entry.description,
323
+ }))
324
+ : [];
325
+ await runSkills(toCoreSkillsArgv(parsed), {
326
+ log: options.log,
327
+ isInteractive: options.isInteractive,
328
+ baseDir: parsed.baseDir ?? options.baseDir,
329
+ catalogMode: "all",
330
+ hiddenBuiltInSkillTargets: HIDDEN_STANDALONE_BUILT_INS,
331
+ publicSkillSource: loadedSource?.root ?? parsed.source ?? DEFAULT_SKILLS_SOURCE,
332
+ publicSkillEntries,
333
+ promptSkills: options.promptSkills
334
+ ? async (context) => options.promptSkills?.({
335
+ initialSkills: context.initialTargets,
336
+ options: context.options,
337
+ }) ?? null
338
+ : undefined,
339
+ promptClients: options.promptClients,
340
+ promptScope: options.promptScope,
341
+ promptGithubAction: options.promptGithubAction
342
+ ? async (context) => options.promptGithubAction?.({
343
+ workflowPath: context.workflowPath,
344
+ setupCommand: context.setupCommand,
345
+ docsUrl: context.docsUrl,
346
+ }) ?? null
347
+ : undefined,
348
+ promptPlanMode: options.promptPlanMode,
349
+ promptPlanMcpUrl: options.promptPlanMcpUrl,
350
+ promptUpdateInstructions: options.promptUpdateInstructions,
351
+ });
352
+ return;
353
+ }
354
+ finally {
355
+ loadedSource?.cleanup?.();
356
+ }
357
+ }
358
+ const startedAt = Date.now();
359
+ const telemetry = createCliTelemetry({
360
+ cli: "skills-installer",
361
+ cliVersion: readCliVersion(),
362
+ command: parsed.command,
363
+ interactive: cliInteractive(parsed, options),
364
+ });
365
+ try {
366
+ if (parsed.command === "help") {
367
+ process.stdout.write(`${HELP}\n`);
368
+ return;
369
+ }
370
+ telemetry.track("skills_cli started");
371
+ const skillSource = parsed.source ?? DEFAULT_SKILLS_SOURCE;
372
+ if (parsed.command === "list") {
373
+ const source = await materializeSource(skillSource);
374
+ try {
375
+ const skills = discoverSkills(source.root);
376
+ telemetry.track("skills_cli skills listed", {
377
+ availableCount: skills.length,
378
+ available: skills.map((skill) => skill.name).join(","),
379
+ });
380
+ if (parsed.printJson) {
381
+ process.stdout.write(`${JSON.stringify(skills, null, 2)}\n`);
382
+ return;
383
+ }
384
+ for (const skill of skills) {
385
+ process.stdout.write(`${skill.name}${skill.description ? ` - ${skill.description}` : ""}\n`);
386
+ }
387
+ return;
388
+ }
389
+ finally {
390
+ source.cleanup?.();
391
+ }
392
+ }
393
+ const stdoutLog = parsed.printJson || options.log
394
+ ? options.log
395
+ : (message) => process.stdout.write(`${message}\n`);
396
+ const result = await installSkills({
397
+ source: skillSource,
398
+ skillNames: parsed.skillNames,
399
+ clients: parsed.clients,
400
+ // Leave scope undefined unless the user passed --scope/-g/--project so the
401
+ // installer can prompt for it interactively.
402
+ scope: parsed.scopeExplicit ? parsed.scope : undefined,
403
+ baseDir: parsed.baseDir ?? options.baseDir,
404
+ yes: parsed.yes,
405
+ dryRun: parsed.dryRun,
406
+ updateInstructions: parsed.updateInstructions,
407
+ instructionFiles: parsed.instructionFiles,
408
+ withGithubAction: parsed.withGithubAction,
409
+ force: parsed.force,
410
+ connect: parsed.connect,
411
+ quiet: parsed.printJson || parsed.quiet,
412
+ log: parsed.printJson || parsed.quiet ? undefined : stdoutLog,
413
+ isInteractive: options.isInteractive,
414
+ promptSkills: options.promptSkills,
415
+ promptClients: options.promptClients,
416
+ promptScope: options.promptScope,
417
+ promptUpdateInstructions: options.promptUpdateInstructions,
418
+ promptGithubAction: options.promptGithubAction,
419
+ promptPlanMode: options.promptPlanMode,
420
+ promptPlanMcpUrl: options.promptPlanMcpUrl,
421
+ telemetry,
422
+ mcp: parsed.mcp,
423
+ planMode: parsed.planMode,
424
+ mcpUrl: parsed.mcpUrl,
425
+ });
426
+ telemetry.track("skills_cli completed", {
427
+ skills: result.skills.join(","),
428
+ clients: result.clients.join(","),
429
+ scope: result.scope,
430
+ dryRun: result.dryRun,
431
+ durationMs: Date.now() - startedAt,
432
+ });
433
+ if (parsed.printJson) {
434
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
435
+ return;
436
+ }
437
+ if (parsed.quiet)
438
+ return;
439
+ await printInstallResult(result, {
440
+ baseDir: parsed.baseDir ?? options.baseDir ?? process.cwd(),
441
+ dryRun: parsed.dryRun,
442
+ });
443
+ }
444
+ catch (error) {
445
+ telemetry.track("skills_cli failed", {
446
+ command: parsed.command,
447
+ error: error instanceof Error ? error.message : String(error),
448
+ durationMs: Date.now() - startedAt,
449
+ });
450
+ telemetry.captureException(error, {
451
+ handled: false,
452
+ tags: { source: "skills-command", command: parsed.command },
453
+ extra: { durationMs: Date.now() - startedAt },
454
+ });
455
+ throw error;
456
+ }
457
+ finally {
458
+ await telemetry.flush();
459
+ }
460
+ }
461
+ function readCliVersion() {
462
+ try {
463
+ const here = path.dirname(fileURLToPath(import.meta.url));
464
+ // dist/index.js → ../package.json
465
+ const pkg = JSON.parse(fs.readFileSync(path.resolve(here, "../package.json"), "utf8"));
466
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
467
+ }
468
+ catch {
469
+ return "unknown";
470
+ }
471
+ }
472
+ function cliInteractive(parsed, options) {
473
+ if (parsed.yes)
474
+ return false;
475
+ if (options.isInteractive)
476
+ return options.isInteractive();
477
+ if (process.env.CI === "true")
478
+ return false;
479
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
480
+ }
481
+ export async function installSkills(options) {
482
+ const baseDir = path.resolve(options.baseDir ?? process.cwd());
483
+ const log = options.log ?? (() => { });
484
+ const sourceInput = options.source ?? DEFAULT_SKILLS_SOURCE;
485
+ const source = await materializeSource(sourceInput);
486
+ try {
487
+ const entries = discoverSkills(source.root);
488
+ if (entries.length === 0) {
489
+ throw new Error(`No skills found in ${sourceInput}. Expected skills/*/SKILL.md.`);
490
+ }
491
+ const preselected = (options.skillNames ?? []).length > 0;
492
+ const selected = await resolveSelectedSkills(entries, options);
493
+ options.telemetry?.track("skills_cli skills selected", {
494
+ selected: selected.map((skill) => skill.name).join(","),
495
+ selectedCount: selected.length,
496
+ selectedAll: selected.length === entries.length,
497
+ preselected,
498
+ });
499
+ const skillNames = selected.map((skill) => skill.name);
500
+ const modeAwareApp = selectedModeAwareApp(skillNames);
501
+ const planMode = await resolvePlanModeForSkills(skillNames, options);
502
+ const planMcpOverride = await resolvePlanMcpOverrideForMode(planMode, options, modeAwareApp?.appId);
503
+ const clients = await resolveSelectedClients(options);
504
+ options.telemetry?.track("skills_cli clients selected", {
505
+ clients: clients.join(","),
506
+ clientCount: clients.length,
507
+ });
508
+ const scope = await resolveSelectedScope(options);
509
+ options.telemetry?.track("skills_cli scope selected", { scope });
510
+ const instructionBlocks = managedInstructionBlocksForSkills(skillNames);
511
+ const shouldUpdateInstructions = await shouldUpdateManagedInstructions(instructionBlocks, options);
512
+ const shouldWriteGithubAction = await shouldWritePrVisualRecapWorkflow(selected.some((skill) => skill.name === "visual-recap"), options, baseDir);
513
+ const mcpApps = options.mcp === false || planMode === "local-files"
514
+ ? []
515
+ : mcpAppsForSkills(skillNames, planMcpOverride);
516
+ const skillFileClients = clients.filter(supportsSkillFiles);
517
+ if (skillFileClients.length === 0 && mcpApps.length === 0) {
518
+ throw new Error("Claude Cowork is MCP-only for Agent Native skills. Choose Codex, Claude Code, Pi, Cursor, OpenCode, or GitHub Copilot for local skill files, or install an app-backed skill with MCP enabled.");
519
+ }
520
+ const progress = await createInstallProgress(options, 1 +
521
+ (shouldUpdateInstructions ? 1 : 0) +
522
+ (shouldWriteGithubAction ? 1 : 0) +
523
+ mcpApps.length);
524
+ const written = [];
525
+ let instructionFiles = [];
526
+ let githubActionPath;
527
+ const mcpServers = [];
528
+ try {
529
+ progress?.start("Installing skill files...");
530
+ for (const root of unique(skillFileClients.map((client) => installRootForClient(client, scope, baseDir)))) {
531
+ for (const skill of selected) {
532
+ const destination = path.join(root, skill.name);
533
+ written.push(destination);
534
+ if (!options.dryRun) {
535
+ fs.rmSync(destination, { recursive: true, force: true });
536
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
537
+ fs.cpSync(skill.dir, destination, { recursive: true });
538
+ }
539
+ }
540
+ }
541
+ progress?.advance("Skill files installed");
542
+ options.telemetry?.track("skills_cli install completed", {
543
+ skills: skillNames.join(","),
544
+ clients: clients.join(","),
545
+ scope,
546
+ writtenCount: written.length,
547
+ dryRun: Boolean(options.dryRun),
548
+ });
549
+ if (shouldUpdateInstructions) {
550
+ progress?.message("Updating managed instructions...");
551
+ instructionFiles = writeManagedInstructions(instructionBlocks, baseDir, skillFileClients, scope, options);
552
+ progress?.advance("Managed instructions updated");
553
+ if (instructionFiles.length) {
554
+ options.telemetry?.track("skills_cli instructions updated", {
555
+ fileCount: instructionFiles.length,
556
+ });
557
+ }
558
+ }
559
+ if (shouldWriteGithubAction) {
560
+ progress?.message("Writing PR Visual Recap workflow...");
561
+ githubActionPath = writePrVisualRecapWorkflow(baseDir, options);
562
+ progress?.advance("PR Visual Recap workflow ready");
563
+ if (githubActionPath) {
564
+ options.telemetry?.track("skills_cli github action added");
565
+ }
566
+ }
567
+ // Register the hosted MCP server for app-backed skills (visual-plan /
568
+ // visual-recap → Agent-Native Plan) so the agent can actually call them,
569
+ // not just read the SKILL.md. On by default; `--no-mcp` installs the
570
+ // skill files only. One registration per app, so visual-plan +
571
+ // visual-recap share a single "plan" server.
572
+ if (mcpApps.length > 0) {
573
+ const mcpClients = clients
574
+ .map(skillClientToMcpClient)
575
+ .filter((client) => Boolean(client));
576
+ if (mcpClients.length === 0) {
577
+ throw new Error("MCP setup supports Claude Code, Codex, Cursor, OpenCode, GitHub Copilot / VS Code, or Claude Cowork. Use --mode local-files or --no-mcp for Pi.");
578
+ }
579
+ for (const app of mcpApps) {
580
+ progress?.message(`Registering ${app.displayName} MCP server...`);
581
+ if (!options.dryRun) {
582
+ const registration = await registerMcpServer({
583
+ descriptor: {
584
+ serverName: app.serverName,
585
+ mcpUrl: app.mcpUrl,
586
+ aliases: app.aliases,
587
+ authMode: app.authMode,
588
+ hostedUrl: app.hostedUrl,
589
+ },
590
+ clients: mcpClients,
591
+ scope,
592
+ baseDir,
593
+ interactive: options.connect !== false && isInteractive(options),
594
+ log,
595
+ deviceFlowTimeoutMs: options.deviceFlowTimeoutMs,
596
+ });
597
+ mcpServers.push({
598
+ serverName: app.serverName,
599
+ mcpUrl: app.mcpUrl,
600
+ clients,
601
+ registeredClients: unique(registration.written.map((entry) => entry.client)),
602
+ files: [
603
+ ...new Set(registration.written.map((entry) => entry.file)),
604
+ ],
605
+ authenticated: registration.authenticated,
606
+ guidance: registration.guidance,
607
+ });
608
+ options.telemetry?.track("skills_cli mcp registered", {
609
+ serverName: app.serverName,
610
+ clients: clients.join(","),
611
+ authenticated: registration.authenticated,
612
+ });
613
+ }
614
+ else {
615
+ mcpServers.push({
616
+ serverName: app.serverName,
617
+ mcpUrl: app.mcpUrl,
618
+ clients,
619
+ registeredClients: clients,
620
+ files: [],
621
+ authenticated: false,
622
+ guidance: [],
623
+ });
624
+ }
625
+ progress?.advance(`${app.displayName} MCP server ready`);
626
+ }
627
+ }
628
+ progress?.stop("Installation complete");
629
+ }
630
+ catch (err) {
631
+ progress?.error("Installation failed");
632
+ throw err;
633
+ }
634
+ return {
635
+ source: source.root,
636
+ skills: skillNames,
637
+ clients,
638
+ scope,
639
+ written,
640
+ instructionFiles,
641
+ githubActionPath,
642
+ mcpServers,
643
+ planMode,
644
+ dryRun: Boolean(options.dryRun),
645
+ };
646
+ }
647
+ finally {
648
+ source.cleanup?.();
649
+ }
650
+ }
651
+ function defaultArgs(command) {
652
+ return {
653
+ command,
654
+ copySource: false,
655
+ skillNames: [],
656
+ clients: [],
657
+ scope: "user",
658
+ scopeExplicit: false,
659
+ yes: false,
660
+ dryRun: false,
661
+ printJson: false,
662
+ instructionFiles: [],
663
+ withGithubAction: false,
664
+ force: false,
665
+ connect: true,
666
+ mcp: true,
667
+ quiet: false,
668
+ };
669
+ }
670
+ function selectedModeAwareApp(skillNames) {
671
+ return skillNames
672
+ .map((skillName) => resolveAppForSkill(skillName))
673
+ .find((app) => app?.appId === "visual-plans" || app?.appId === "content");
674
+ }
675
+ async function resolvePlanModeForSkills(skillNames, options) {
676
+ const modeAwareApp = selectedModeAwareApp(skillNames);
677
+ if (options.planMode && !modeAwareApp) {
678
+ throw new Error("--mode only applies to visual-plan / visual-recap / visualize-repo / content.");
679
+ }
680
+ if (options.mcpUrl && !modeAwareApp) {
681
+ throw new Error("--mcp-url only applies to visual-plan / visual-recap / visualize-repo / content.");
682
+ }
683
+ if (!modeAwareApp)
684
+ return undefined;
685
+ if (options.mcpUrl && !options.planMode)
686
+ return "self-hosted";
687
+ if (options.planMode)
688
+ return options.planMode;
689
+ return modeAwareApp.appId === "visual-plans" ? "hosted" : undefined;
690
+ }
691
+ function resolvePlanMcpUrlOverride(input) {
692
+ let parsed;
693
+ try {
694
+ parsed = new URL(input);
695
+ }
696
+ catch {
697
+ throw new Error(`--mcp-url must be a valid URL (got "${input}").`);
698
+ }
699
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
700
+ throw new Error("--mcp-url must use http:// or https://.");
701
+ }
702
+ const publicMcpSuffix = "/mcp";
703
+ const legacyMcpSuffix = "/_agent-native/mcp";
704
+ const trimmedPath = parsed.pathname.replace(/\/+$/, "");
705
+ const mcpSuffix = trimmedPath.endsWith(legacyMcpSuffix)
706
+ ? legacyMcpSuffix
707
+ : trimmedPath.endsWith(publicMcpSuffix)
708
+ ? publicMcpSuffix
709
+ : undefined;
710
+ const appPath = mcpSuffix
711
+ ? trimmedPath.slice(0, -mcpSuffix.length)
712
+ : trimmedPath;
713
+ const mcpPath = `${appPath || ""}${publicMcpSuffix}`;
714
+ return {
715
+ hostedUrl: `${parsed.origin}${appPath}`,
716
+ mcpUrl: `${parsed.origin}${mcpPath}`,
717
+ };
718
+ }
719
+ async function resolvePlanMcpOverrideForMode(planMode, options, appId) {
720
+ if (planMode === "local-files" && options.mcpUrl) {
721
+ throw new Error("--mode local-files cannot be combined with --mcp-url.");
722
+ }
723
+ if (planMode !== "self-hosted")
724
+ return undefined;
725
+ if (!options.mcpUrl) {
726
+ throw new Error("--mode self-hosted requires --mcp-url <url> in direct mode.");
727
+ }
728
+ if (!appId) {
729
+ throw new Error("--mode self-hosted requires an app-backed skill.");
730
+ }
731
+ return { appId, ...resolvePlanMcpUrlOverride(options.mcpUrl) };
732
+ }
733
+ function parsePlanMode(value) {
734
+ if (value === "hosted" || value === "local-files" || value === "self-hosted")
735
+ return value;
736
+ throw new Error('--mode must be "hosted", "local-files", or "self-hosted".');
737
+ }
738
+ function parseScope(value) {
739
+ if (value === "user" || value === "project")
740
+ return value;
741
+ throw new Error("--scope must be user or project.");
742
+ }
743
+ function normalizeClients(value) {
744
+ return value.split(",").flatMap((raw) => {
745
+ const client = raw.trim().toLowerCase();
746
+ if (!client)
747
+ return [];
748
+ if (client === "all")
749
+ return CLIENTS;
750
+ if (client === "codex")
751
+ return ["codex"];
752
+ if (client === "cowork" || client === "claude-cowork") {
753
+ return ["cowork"];
754
+ }
755
+ if (client === "pi")
756
+ return ["pi"];
757
+ if (client === "cursor")
758
+ return ["cursor"];
759
+ if (client === "opencode" || client === "open-code") {
760
+ return ["opencode"];
761
+ }
762
+ if (client === "github-copilot" ||
763
+ client === "copilot" ||
764
+ client === "vscode" ||
765
+ client === "vs-code") {
766
+ return ["github-copilot"];
767
+ }
768
+ if (client === "claude" ||
769
+ client === "claude-code" ||
770
+ client === "claude-code-cli") {
771
+ return ["claude-code"];
772
+ }
773
+ throw new Error(`Unsupported client "${raw}". Use codex, claude-code, cowork, pi, cursor, opencode, github-copilot, or all.`);
774
+ });
775
+ }
776
+ function skillClientToMcpClient(client) {
777
+ if (client === "pi")
778
+ return null;
779
+ if (client === "claude-code")
780
+ return "claude-code";
781
+ return client;
782
+ }
783
+ function supportsSkillFiles(client) {
784
+ return client !== "cowork";
785
+ }
786
+ function normalizeSkillName(value) {
787
+ const normalized = value.trim().toLowerCase();
788
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(normalized)) {
789
+ throw new Error(`Invalid skill name "${value}".`);
790
+ }
791
+ return normalized;
792
+ }
793
+ function unique(values) {
794
+ return [...new Set(values)];
795
+ }
796
+ function plural(count, singular, pluralForm = `${singular}s`) {
797
+ return `${count} ${count === 1 ? singular : pluralForm}`;
798
+ }
799
+ function shortenPathForOutput(file, baseDir) {
800
+ const resolved = path.resolve(file);
801
+ const home = process.env.HOME || os.homedir();
802
+ if (resolved === home || resolved.startsWith(`${home}${path.sep}`)) {
803
+ return `~${resolved.slice(home.length)}`;
804
+ }
805
+ const base = path.resolve(baseDir);
806
+ const relative = path.relative(base, resolved);
807
+ if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
808
+ return `.${path.sep}${relative}`;
809
+ }
810
+ return file;
811
+ }
812
+ function summarizePaths(files, baseDir, max = 4) {
813
+ const shortened = unique(files).map((file) => shortenPathForOutput(file, baseDir));
814
+ if (shortened.length <= max)
815
+ return shortened.join(", ");
816
+ return `${shortened.slice(0, max).join(", ")} +${shortened.length - max} more`;
817
+ }
818
+ async function createInstallProgress(options, max) {
819
+ if (options.quiet || !isInteractive(options) || max <= 0)
820
+ return null;
821
+ const clack = await import("@clack/prompts");
822
+ const progress = clack.progress({ max, indicator: "timer" });
823
+ let active = false;
824
+ return {
825
+ start(message) {
826
+ active = true;
827
+ progress.start(message);
828
+ },
829
+ message(message) {
830
+ if (active)
831
+ progress.message(message);
832
+ },
833
+ advance(message) {
834
+ if (active)
835
+ progress.advance(1, message);
836
+ },
837
+ stop(message) {
838
+ if (!active)
839
+ return;
840
+ progress.stop(message);
841
+ active = false;
842
+ },
843
+ error(message) {
844
+ if (!active)
845
+ return;
846
+ progress.error(message);
847
+ active = false;
848
+ },
849
+ };
850
+ }
851
+ function mcpAppsForSkills(skillNames, planOverride) {
852
+ const apps = [];
853
+ const seen = new Set();
854
+ for (const skillName of skillNames) {
855
+ const app = resolveAppForSkill(skillName);
856
+ if (!app || seen.has(app.appId))
857
+ continue;
858
+ seen.add(app.appId);
859
+ if (app.appId === planOverride?.appId) {
860
+ apps.push({
861
+ ...app,
862
+ hostedUrl: planOverride.hostedUrl,
863
+ mcpUrl: planOverride.mcpUrl,
864
+ });
865
+ }
866
+ else {
867
+ apps.push(app);
868
+ }
869
+ }
870
+ return apps;
871
+ }
872
+ function mcpStatus(server, dryRun) {
873
+ if (dryRun)
874
+ return `would register for ${server.clients.join(", ")}`;
875
+ const registered = server.registeredClients;
876
+ const pending = server.clients.filter((client) => !registered.includes(client));
877
+ const parts = [];
878
+ if (registered.length > 0) {
879
+ parts.push(`${server.authenticated ? "registered and authenticated" : "registered"} for ${registered.join(", ")}`);
880
+ }
881
+ if (pending.length > 0) {
882
+ parts.push(`authentication pending for ${pending.join(", ")}`);
883
+ }
884
+ return (parts.join("; ") ||
885
+ `authentication pending for ${server.clients.join(", ")}`);
886
+ }
887
+ async function printInstallResult(result, options) {
888
+ const clack = await import("@clack/prompts");
889
+ const verb = options.dryRun ? "Would install" : "Installed";
890
+ const summary = [
891
+ `Skills ${result.skills.join(", ") || "none"}`,
892
+ `Agents ${result.clients.join(", ") || "none"}`,
893
+ `Scope ${result.scope}`,
894
+ result.planMode ? `Install mode ${result.planMode}` : "",
895
+ result.written.length
896
+ ? `Skill folders ${plural(result.written.length, "folder")} (${summarizePaths(result.written, options.baseDir)})`
897
+ : "",
898
+ ].filter(Boolean);
899
+ clack.note(summary.join("\n"), verb);
900
+ if (result.instructionFiles.length) {
901
+ clack.note(summarizePaths(result.instructionFiles, options.baseDir), "Managed instructions");
902
+ }
903
+ if (result.githubActionPath) {
904
+ clack.note(shortenPathForOutput(result.githubActionPath, options.baseDir), "PR Visual Recap workflow");
905
+ }
906
+ if (result.mcpServers.length) {
907
+ const mcpLines = result.mcpServers.map((server) => {
908
+ const status = mcpStatus(server, options.dryRun);
909
+ const files = server.files.length
910
+ ? ` (${summarizePaths(server.files, options.baseDir, 2)})`
911
+ : "";
912
+ return `${server.serverName}: ${status}${files}`;
913
+ });
914
+ clack.note(mcpLines.join("\n"), "MCP");
915
+ const guidance = result.mcpServers.flatMap((server) => server.guidance);
916
+ if (guidance.length) {
917
+ clack.note(guidance.join("\n"), "Next steps");
918
+ }
919
+ }
920
+ if (result.planMode === "local-files" &&
921
+ result.skills.some((skill) => ["visual-plan", "visual-recap", "visualize-repo"].includes(skill))) {
922
+ clack.note([
923
+ "No sharing, all local.",
924
+ "Run: npx @agent-native/core@latest plan blocks --out plan-blocks.md",
925
+ "Check: npx @agent-native/core@latest plan local check --dir plans/<slug>",
926
+ "Serve: npx @agent-native/core@latest plan local serve --dir plans/<slug> --open",
927
+ "Verify: npx @agent-native/core@latest plan local verify --dir plans/<slug>",
928
+ ].join("\n"), "Local Plan files");
929
+ }
930
+ if (!options.dryRun) {
931
+ clack.note("Restart or reload selected agent clients if needed.", "Reload");
932
+ }
933
+ clack.outro(`${options.dryRun ? "Dry run complete" : "All set"} ✅`);
934
+ }
935
+ async function resolveSelectedSkills(entries, options) {
936
+ const byName = new Map(entries.map((entry) => [entry.name, entry]));
937
+ const requested = unique((options.skillNames ?? []).map(normalizeSkillName));
938
+ if (requested.length > 0) {
939
+ const missing = requested.filter((name) => !byName.has(name));
940
+ if (missing.length > 0) {
941
+ throw new Error(`Unknown skill${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}. Available: ${entries
942
+ .map((entry) => entry.name)
943
+ .join(", ")}.`);
944
+ }
945
+ return requested.map((name) => byName.get(name));
946
+ }
947
+ return entries;
948
+ }
949
+ async function resolveSelectedScope(options) {
950
+ if (options.scope)
951
+ return options.scope;
952
+ return "user";
953
+ }
954
+ async function resolveSelectedClients(options) {
955
+ const requested = unique(options.clients ?? []);
956
+ if (requested.length > 0)
957
+ return requested;
958
+ return CLIENTS;
959
+ }
960
+ function isInteractive(options) {
961
+ if (options.isInteractive)
962
+ return options.isInteractive();
963
+ if (process.env.CI === "true")
964
+ return false;
965
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
966
+ }
967
+ function installRootForClient(client, scope, baseDir) {
968
+ const home = process.env.HOME || os.homedir();
969
+ if (scope === "project") {
970
+ if (client !== "claude-code") {
971
+ return path.join(baseDir, ".agents", "skills");
972
+ }
973
+ return path.join(baseDir, ".claude", "skills");
974
+ }
975
+ if (client === "codex") {
976
+ return process.env.CODEX_HOME
977
+ ? path.join(process.env.CODEX_HOME, "skills")
978
+ : path.join(home, ".codex", "skills");
979
+ }
980
+ if (client === "pi") {
981
+ return path.join(home, ".agents", "skills");
982
+ }
983
+ if (client === "cursor" ||
984
+ client === "opencode" ||
985
+ client === "github-copilot") {
986
+ return path.join(home, ".agents", "skills");
987
+ }
988
+ return path.join(home, ".claude", "skills");
989
+ }
990
+ function discoverSkills(root) {
991
+ const skillsRoot = resolveSkillsRoot(root);
992
+ const directSkill = path.join(skillsRoot, "SKILL.md");
993
+ if (fs.existsSync(directSkill)) {
994
+ const entry = skillEntry(skillsRoot);
995
+ return entry ? [entry] : [];
996
+ }
997
+ const entries = fs
998
+ .readdirSync(skillsRoot, { withFileTypes: true })
999
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
1000
+ .map((entry) => skillEntry(path.join(skillsRoot, entry.name)))
1001
+ .filter((entry) => Boolean(entry));
1002
+ return entries.sort((a, b) => a.name.localeCompare(b.name));
1003
+ }
1004
+ function resolveSkillsRoot(root) {
1005
+ for (const manifestPath of [
1006
+ path.join(root, ".codex-plugin", "plugin.json"),
1007
+ path.join(root, ".claude-plugin", "plugin.json"),
1008
+ ]) {
1009
+ if (!fs.existsSync(manifestPath))
1010
+ continue;
1011
+ try {
1012
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1013
+ if (typeof manifest.skills === "string") {
1014
+ const resolved = path.resolve(root, manifest.skills);
1015
+ if (fs.existsSync(resolved))
1016
+ return resolved;
1017
+ }
1018
+ }
1019
+ catch { }
1020
+ }
1021
+ const conventional = path.join(root, "skills");
1022
+ if (fs.existsSync(conventional))
1023
+ return conventional;
1024
+ return root;
1025
+ }
1026
+ function skillEntry(dir) {
1027
+ const skillFile = path.join(dir, "SKILL.md");
1028
+ if (!fs.existsSync(skillFile))
1029
+ return null;
1030
+ const body = fs.readFileSync(skillFile, "utf-8");
1031
+ const frontmatter = body.match(/^---\n([\s\S]*?)\n---/);
1032
+ const name = frontmatterField(frontmatter?.[1], "name") ?? path.basename(dir);
1033
+ const description = frontmatterField(frontmatter?.[1], "description");
1034
+ return { name: normalizeSkillName(name), dir, description };
1035
+ }
1036
+ function frontmatterField(frontmatter, field) {
1037
+ if (!frontmatter)
1038
+ return undefined;
1039
+ const lines = frontmatter.split(/\r?\n/);
1040
+ for (let i = 0; i < lines.length; i += 1) {
1041
+ const match = lines[i].match(new RegExp(`^${field}:\\s*(.*)$`));
1042
+ if (!match)
1043
+ continue;
1044
+ const raw = match[1].trim();
1045
+ if (raw === ">-" || raw === ">" || raw === "|-" || raw === "|") {
1046
+ const block = [];
1047
+ for (let j = i + 1; j < lines.length; j += 1) {
1048
+ const line = lines[j];
1049
+ if (line.trim() && !/^\s/.test(line))
1050
+ break;
1051
+ block.push(line.replace(/^\s+/, ""));
1052
+ }
1053
+ const value = raw.startsWith("|") ? block.join("\n") : block.join(" ");
1054
+ return value.replace(/\s+/g, " ").trim() || undefined;
1055
+ }
1056
+ return raw.replace(/^["']|["']$/g, "").trim() || undefined;
1057
+ }
1058
+ return undefined;
1059
+ }
1060
+ async function materializeSource(input) {
1061
+ const local = path.resolve(input);
1062
+ if (fs.existsSync(local))
1063
+ return { root: local };
1064
+ const parsed = parseGitHubSource(input);
1065
+ if (!parsed) {
1066
+ throw new Error(`Skill source not found: ${input}. Use a local path, GitHub owner/repo, or GitHub URL.`);
1067
+ }
1068
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agent-native-skills-"));
1069
+ const archive = path.join(tmpRoot, "source.tgz");
1070
+ const ref = parsed.ref ?? "main";
1071
+ const url = `https://codeload.github.com/${parsed.owner}/${parsed.repo}/tar.gz/${encodeURIComponent(ref)}`;
1072
+ const response = await fetch(url, {
1073
+ headers: { "user-agent": "@agent-native/skills" },
1074
+ });
1075
+ if (!response.ok) {
1076
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
1077
+ throw new Error(`Could not download ${parsed.owner}/${parsed.repo}@${ref}: HTTP ${response.status}.`);
1078
+ }
1079
+ fs.writeFileSync(archive, Buffer.from(await response.arrayBuffer()));
1080
+ const extractDir = path.join(tmpRoot, "extract");
1081
+ fs.mkdirSync(extractDir, { recursive: true });
1082
+ const extracted = spawnSync("tar", ["-xzf", archive, "-C", extractDir], {
1083
+ stdio: "pipe",
1084
+ encoding: "utf-8",
1085
+ });
1086
+ if (extracted.status !== 0) {
1087
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
1088
+ throw new Error(`Could not extract ${input}: ${extracted.stderr || extracted.stdout}`);
1089
+ }
1090
+ const [first] = fs
1091
+ .readdirSync(extractDir, { withFileTypes: true })
1092
+ .filter((entry) => entry.isDirectory());
1093
+ if (!first) {
1094
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
1095
+ throw new Error(`Downloaded archive for ${input} did not contain a repo directory.`);
1096
+ }
1097
+ return {
1098
+ root: path.join(extractDir, first.name),
1099
+ cleanup: () => fs.rmSync(tmpRoot, { recursive: true, force: true }),
1100
+ };
1101
+ }
1102
+ function parseGitHubSource(input) {
1103
+ const shorthand = input.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(?:#(.+))?$/);
1104
+ if (shorthand) {
1105
+ return {
1106
+ owner: shorthand[1],
1107
+ repo: shorthand[2].replace(/\.git$/, ""),
1108
+ ref: shorthand[3],
1109
+ };
1110
+ }
1111
+ let url;
1112
+ try {
1113
+ url = new URL(input);
1114
+ }
1115
+ catch {
1116
+ return null;
1117
+ }
1118
+ if (url.hostname !== "github.com")
1119
+ return null;
1120
+ const parts = url.pathname.split("/").filter(Boolean);
1121
+ if (parts.length < 2)
1122
+ return null;
1123
+ const treeIndex = parts.indexOf("tree");
1124
+ return {
1125
+ owner: parts[0],
1126
+ repo: parts[1].replace(/\.git$/, ""),
1127
+ ref: treeIndex >= 0 && parts[treeIndex + 1]
1128
+ ? parts.slice(treeIndex + 1).join("/")
1129
+ : url.hash.slice(1) || undefined,
1130
+ };
1131
+ }
1132
+ function instructionContentForSkill(skillName) {
1133
+ if (skillName === "quick-recap") {
1134
+ return "When writing final response status indicators, use the /quick-recap skill always.";
1135
+ }
1136
+ if (skillName === "efficient-fable") {
1137
+ return "When operating as Claude Fable, use the /efficient-fable skill always.";
1138
+ }
1139
+ if (skillName === "efficient-frontier") {
1140
+ return "When using a high-cost frontier model for codebase-heavy work, use the /efficient-frontier skill always.";
1141
+ }
1142
+ if (skillName === "stay-within-limits") {
1143
+ return "When long-running or parallel work needs usage-limit checks, use the /stay-within-limits skill always.";
1144
+ }
1145
+ if (skillName === "visual-plan") {
1146
+ return "When work needs an interactive visual plan before implementation, use the /visual-plan skill always.";
1147
+ }
1148
+ if (skillName === "visual-recap") {
1149
+ return "When a PR, branch, commit, or diff needs an interactive visual recap, use the /visual-recap skill always.";
1150
+ }
1151
+ if (skillName === "visualize-repo") {
1152
+ return "When a repository needs local visual docs or a navigable Plan-backed repo viewer, use the /visualize-repo skill always.";
1153
+ }
1154
+ return null;
1155
+ }
1156
+ function managedInstructionBlocksForSkills(skillNames) {
1157
+ return skillNames
1158
+ .map((name) => instructionContentForSkill(name))
1159
+ .filter((block) => Boolean(block));
1160
+ }
1161
+ async function shouldUpdateManagedInstructions(blocks, options) {
1162
+ if (blocks.length === 0)
1163
+ return false;
1164
+ let shouldUpdate = options.updateInstructions;
1165
+ if (shouldUpdate !== undefined)
1166
+ return shouldUpdate;
1167
+ if (options.yes)
1168
+ return true;
1169
+ return false;
1170
+ }
1171
+ function writeManagedInstructions(blocks, baseDir, clients, scope, options) {
1172
+ if (blocks.length === 0)
1173
+ return [];
1174
+ const files = resolveInstructionFiles(baseDir, options.instructionFiles, clients, scope);
1175
+ const content = `${MANAGED_INSTRUCTIONS_START}
1176
+ ${blocks.join("\n")}
1177
+ ${MANAGED_INSTRUCTIONS_END}`;
1178
+ for (const file of files) {
1179
+ if (options.dryRun)
1180
+ continue;
1181
+ upsertManagedBlock(file, content);
1182
+ }
1183
+ return files;
1184
+ }
1185
+ async function maybeUpdateInstructions(skillNames, baseDir, options) {
1186
+ const blocks = managedInstructionBlocksForSkills(skillNames);
1187
+ const clients = options.clients?.length ? options.clients : CLIENTS;
1188
+ const scope = options.scope ?? "user";
1189
+ if (!(await shouldUpdateManagedInstructions(blocks, options)))
1190
+ return [];
1191
+ return writeManagedInstructions(blocks, baseDir, clients, scope, options);
1192
+ }
1193
+ function resolveInstructionFiles(baseDir, explicit, clients, scope) {
1194
+ if (explicit && explicit.length > 0) {
1195
+ return explicit.map((file) => path.resolve(baseDir, file));
1196
+ }
1197
+ const instructionClients = clients.filter(supportsSkillFiles);
1198
+ if (instructionClients.length === 0)
1199
+ return [];
1200
+ if (scope === "user") {
1201
+ const home = process.env.HOME || os.homedir();
1202
+ const files = [];
1203
+ if (instructionClients.includes("codex")) {
1204
+ const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
1205
+ files.push(path.join(codexHome, "AGENTS.md"));
1206
+ }
1207
+ if (instructionClients.includes("claude-code")) {
1208
+ files.push(path.join(home, ".claude", "CLAUDE.md"));
1209
+ }
1210
+ return unique(files);
1211
+ }
1212
+ const candidates = ["AGENTS.md", "CLAUDE.md"].map((file) => path.join(baseDir, file));
1213
+ const existing = candidates.filter((file) => fs.existsSync(file));
1214
+ return existing.length > 0 ? existing : [path.join(baseDir, "AGENTS.md")];
1215
+ }
1216
+ function stripManagedInstructionBlocks(text) {
1217
+ const pattern = new RegExp(`${escapeRegExp(MANAGED_INSTRUCTIONS_START)}[\\s\\S]*?${escapeRegExp(MANAGED_INSTRUCTIONS_END)}`, "g");
1218
+ return text.replace(pattern, "");
1219
+ }
1220
+ function upsertManagedBlock(file, block) {
1221
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1222
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : "";
1223
+ const firstAt = existing.indexOf(MANAGED_INSTRUCTIONS_START);
1224
+ let next;
1225
+ if (firstAt >= 0) {
1226
+ // Collapse EVERY managed block into a single fresh one re-inserted where the
1227
+ // first one began, so repeated installs or pre-existing duplicates never
1228
+ // leave the same instructions written more than once.
1229
+ const before = existing.slice(0, firstAt).trimEnd();
1230
+ const after = stripManagedInstructionBlocks(existing.slice(firstAt)).trimStart();
1231
+ next =
1232
+ (before ? `${before}\n\n` : "") +
1233
+ `${block.trimEnd()}\n` +
1234
+ (after ? `\n${after}` : "");
1235
+ }
1236
+ else {
1237
+ next = `${existing.trimEnd()}${existing.trim() ? "\n\n" : ""}${block.trimEnd()}\n`;
1238
+ }
1239
+ fs.writeFileSync(file, next, "utf-8");
1240
+ }
1241
+ function escapeRegExp(value) {
1242
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1243
+ }
1244
+ const PR_VISUAL_RECAP_DOCS_URL = docsUrl("pr-visual-recap");
1245
+ function prVisualRecapWorkflowPath(baseDir) {
1246
+ return path.join(baseDir, ".github", "workflows", "pr-visual-recap.yml");
1247
+ }
1248
+ function prVisualRecapSetupCommand() {
1249
+ return "npx @agent-native/core@latest recap setup";
1250
+ }
1251
+ async function promptForGithubAction(context) {
1252
+ const clack = await import("@clack/prompts");
1253
+ const result = await clack.confirm({
1254
+ message: "Optional: add automatic PR Visual Recaps? (GitHub Action)\n" +
1255
+ " Posts a human-friendly recap on every pull request.\n" +
1256
+ ` Learn more: ${context.docsUrl}\n` +
1257
+ ` Writes ${context.workflowPath}; ${context.setupCommand} finishes the GitHub secrets.`,
1258
+ initialValue: false,
1259
+ });
1260
+ if (clack.isCancel(result)) {
1261
+ clack.cancel("Skipped PR Visual Recap workflow.");
1262
+ return null;
1263
+ }
1264
+ return Boolean(result);
1265
+ }
1266
+ async function shouldWritePrVisualRecapWorkflow(hasVisualRecap, options, baseDir) {
1267
+ if (!hasVisualRecap)
1268
+ return false;
1269
+ if (options.withGithubAction)
1270
+ return true;
1271
+ if (fs.existsSync(prVisualRecapWorkflowPath(baseDir)))
1272
+ return false;
1273
+ if (options.yes || !isInteractive(options))
1274
+ return false;
1275
+ const prompt = options.promptGithubAction ?? promptForGithubAction;
1276
+ const choice = await prompt({
1277
+ workflowPath: path.join(".github", "workflows", "pr-visual-recap.yml"),
1278
+ setupCommand: prVisualRecapSetupCommand(),
1279
+ docsUrl: PR_VISUAL_RECAP_DOCS_URL,
1280
+ });
1281
+ return choice === true;
1282
+ }
1283
+ const PR_VISUAL_RECAP_REUSABLE_WORKFLOW = `name: PR Visual Recap
1284
+
1285
+ on:
1286
+ pull_request:
1287
+ types: [opened, synchronize, reopened, ready_for_review, labeled, closed]
1288
+
1289
+ permissions:
1290
+ contents: read
1291
+
1292
+ concurrency:
1293
+ group: pr-visual-recap-\${{ github.event.pull_request.number }}
1294
+ cancel-in-progress: true
1295
+
1296
+ jobs:
1297
+ visual-recap:
1298
+ if: github.event.action != 'labeled' || vars.VISUAL_RECAP_REQUIRED_LABELS != ''
1299
+ permissions:
1300
+ checks: write
1301
+ contents: read
1302
+ issues: write
1303
+ pull-requests: read
1304
+ uses: BuilderIO/agent-native/.github/workflows/pr-visual-recap-reusable.yml@main
1305
+ with:
1306
+ skill-source: repo
1307
+ runs-on: \${{ vars.VISUAL_RECAP_RUNS_ON || '"ubuntu-latest"' }}
1308
+ gate-runs-on: \${{ vars.VISUAL_RECAP_GATE_RUNS_ON || 'ubuntu-latest' }}
1309
+ required-labels: \${{ vars.VISUAL_RECAP_REQUIRED_LABELS || '' }}
1310
+ secrets:
1311
+ PLAN_RECAP_TOKEN: \${{ secrets.PLAN_RECAP_TOKEN }}
1312
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
1313
+ OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}
1314
+ PLAN_RECAP_APP_URL: \${{ secrets.PLAN_RECAP_APP_URL }}
1315
+ `;
1316
+ function writePrVisualRecapWorkflow(baseDir, options) {
1317
+ const file = path.join(baseDir, ".github", "workflows", "pr-visual-recap.yml");
1318
+ if (options.dryRun)
1319
+ return file;
1320
+ if (fs.existsSync(file)) {
1321
+ const current = fs.readFileSync(file, "utf-8");
1322
+ if (current === PR_VISUAL_RECAP_REUSABLE_WORKFLOW)
1323
+ return file;
1324
+ if (!options.force) {
1325
+ throw new Error(`${file} already exists and differs. Re-run with --force to overwrite it.`);
1326
+ }
1327
+ }
1328
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1329
+ fs.writeFileSync(file, PR_VISUAL_RECAP_REUSABLE_WORKFLOW, "utf-8");
1330
+ return file;
1331
+ }
1332
+ export function createInstallId() {
1333
+ return randomUUID();
1334
+ }
1335
+ //# sourceMappingURL=index.js.map