@agent-native/skills 0.2.2 → 0.2.4

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 CHANGED
@@ -25,6 +25,7 @@ Options:
25
25
  --with-github-action Add .github/workflows/pr-visual-recap.yml when visual-recap is installed
26
26
  --force Overwrite a different existing PR Visual Recap workflow
27
27
  --no-mcp Install skill files only; skip registering the app's MCP server
28
+ --no-connect Register MCP where possible but skip inline browser/device authentication
28
29
  -y, --yes Use defaults in non-interactive mode
29
30
  --dry-run Print intended writes without changing files
30
31
  --json Print the result as JSON
@@ -118,6 +119,10 @@ export function parseSkillsCliArgs(argv) {
118
119
  out.mcp = false;
119
120
  else if (arg === "--mcp")
120
121
  out.mcp = true;
122
+ else if (arg === "--no-connect" || arg === "--skip-connect")
123
+ out.connect = false;
124
+ else if (arg === "--connect")
125
+ out.connect = true;
121
126
  else if (arg.startsWith("-"))
122
127
  throw new Error(`Unknown option: ${arg}`);
123
128
  else if (!out.source)
@@ -166,6 +171,8 @@ function toCoreSkillsArgv(parsed) {
166
171
  out.push("--force");
167
172
  if (parsed.mcp === false)
168
173
  out.push("--no-mcp");
174
+ if (parsed.connect === false)
175
+ out.push("--no-connect");
169
176
  if (parsed.updateInstructions === true)
170
177
  out.push("--update-instructions");
171
178
  if (parsed.updateInstructions === false)
@@ -229,6 +236,9 @@ export async function runSkillsCli(argv, options = {}) {
229
236
  source.cleanup?.();
230
237
  }
231
238
  }
239
+ const stdoutLog = parsed.printJson || options.log
240
+ ? options.log
241
+ : (message) => process.stdout.write(`${message}\n`);
232
242
  const result = await installSkills({
233
243
  source: skillSource,
234
244
  skillNames: parsed.skillNames,
@@ -243,7 +253,9 @@ export async function runSkillsCli(argv, options = {}) {
243
253
  instructionFiles: parsed.instructionFiles,
244
254
  withGithubAction: parsed.withGithubAction,
245
255
  force: parsed.force,
246
- log: parsed.printJson ? undefined : options.log,
256
+ connect: parsed.connect,
257
+ quiet: parsed.printJson,
258
+ log: parsed.printJson ? undefined : stdoutLog,
247
259
  isInteractive: options.isInteractive,
248
260
  promptSkills: options.promptSkills,
249
261
  promptClients: options.promptClients,
@@ -264,28 +276,10 @@ export async function runSkillsCli(argv, options = {}) {
264
276
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
265
277
  return;
266
278
  }
267
- const verb = parsed.dryRun ? "Would install" : "Installed";
268
- process.stdout.write([
269
- `${verb} ${result.skills.join(", ")} for ${result.clients.join(", ")} (${result.scope}).`,
270
- result.written.length
271
- ? `Skill files: ${result.written.join(", ")}`
272
- : "",
273
- result.instructionFiles.length
274
- ? `Managed instructions: ${result.instructionFiles.join(", ")}`
275
- : "",
276
- result.githubActionPath
277
- ? `PR Visual Recap workflow: ${result.githubActionPath}`
278
- : "",
279
- ...result.mcpServers.flatMap((server) => [
280
- `MCP server "${server.serverName}" ${parsed.dryRun ? "would be registered" : "registered"} for ${server.clients.join(", ")}${server.files.length ? `:\n ${server.files.join("\n ")}` : ""}`,
281
- ...server.guidance.map((line) => ` ${line}`),
282
- ]),
283
- parsed.dryRun
284
- ? ""
285
- : "Restart or reload selected agent clients if needed.",
286
- ]
287
- .filter(Boolean)
288
- .join("\n") + "\n");
279
+ await printInstallResult(result, {
280
+ baseDir: parsed.baseDir ?? options.baseDir ?? process.cwd(),
281
+ dryRun: parsed.dryRun,
282
+ });
289
283
  }
290
284
  catch (error) {
291
285
  telemetry.track("skills_cli failed", {
@@ -356,98 +350,126 @@ export async function installSkills(options) {
356
350
  });
357
351
  const scope = await resolveSelectedScope(options);
358
352
  options.telemetry?.track("skills_cli scope selected", { scope });
353
+ const skillNames = selected.map((skill) => skill.name);
354
+ const instructionBlocks = managedInstructionBlocksForSkills(skillNames);
355
+ const shouldUpdateInstructions = await shouldUpdateManagedInstructions(instructionBlocks, options);
356
+ const shouldWriteGithubAction = selected.some((skill) => skill.name === "visual-recap") &&
357
+ (options.withGithubAction ||
358
+ (await shouldPromptGithubAction(options, baseDir)));
359
+ const mcpApps = options.mcp === false ? [] : mcpAppsForSkills(skillNames);
360
+ const progress = await createInstallProgress(options, 1 +
361
+ (shouldUpdateInstructions ? 1 : 0) +
362
+ (shouldWriteGithubAction ? 1 : 0) +
363
+ mcpApps.length);
359
364
  const written = [];
360
- for (const client of clients) {
361
- const root = installRootForClient(client, scope, baseDir);
362
- for (const skill of selected) {
363
- const destination = path.join(root, skill.name);
364
- written.push(destination);
365
- if (!options.dryRun) {
366
- fs.rmSync(destination, { recursive: true, force: true });
367
- fs.mkdirSync(path.dirname(destination), { recursive: true });
368
- fs.cpSync(skill.dir, destination, { recursive: true });
365
+ let instructionFiles = [];
366
+ let githubActionPath;
367
+ const mcpServers = [];
368
+ try {
369
+ progress?.start("Installing skill files...");
370
+ for (const client of clients) {
371
+ const root = installRootForClient(client, scope, baseDir);
372
+ for (const skill of selected) {
373
+ const destination = path.join(root, skill.name);
374
+ written.push(destination);
375
+ if (!options.dryRun) {
376
+ fs.rmSync(destination, { recursive: true, force: true });
377
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
378
+ fs.cpSync(skill.dir, destination, { recursive: true });
379
+ }
369
380
  }
370
381
  }
371
- }
372
- options.telemetry?.track("skills_cli install completed", {
373
- skills: selected.map((skill) => skill.name).join(","),
374
- clients: clients.join(","),
375
- scope,
376
- writtenCount: written.length,
377
- dryRun: Boolean(options.dryRun),
378
- });
379
- const instructionFiles = await maybeUpdateInstructions(selected.map((skill) => skill.name), baseDir, options);
380
- if (instructionFiles.length) {
381
- options.telemetry?.track("skills_cli instructions updated", {
382
- fileCount: instructionFiles.length,
382
+ progress?.advance("Skill files installed");
383
+ options.telemetry?.track("skills_cli install completed", {
384
+ skills: skillNames.join(","),
385
+ clients: clients.join(","),
386
+ scope,
387
+ writtenCount: written.length,
388
+ dryRun: Boolean(options.dryRun),
383
389
  });
384
- }
385
- const githubActionPath = selected.some((skill) => skill.name === "visual-recap") &&
386
- (options.withGithubAction ||
387
- (await shouldPromptGithubAction(options, baseDir)))
388
- ? writePrVisualRecapWorkflow(baseDir, options)
389
- : undefined;
390
- if (githubActionPath) {
391
- options.telemetry?.track("skills_cli github action added");
392
- }
393
- // Register the hosted MCP server for app-backed skills (visual-plan /
394
- // visual-recap → Agent-Native Plan, assets, design-exploration) so the
395
- // agent can actually call them — not just read the SKILL.md. On by
396
- // default; `--no-mcp` installs the skill files only. One registration per
397
- // app, so visual-plan + visual-recap share a single "plan" server.
398
- const mcpServers = [];
399
- if (options.mcp !== false) {
400
- const mcpClients = clients.map((client) => client === "claude-code" ? "claude-code" : "codex");
401
- const seenApps = new Set();
402
- for (const skill of selected) {
403
- const app = resolveAppForSkill(skill.name);
404
- if (!app || seenApps.has(app.appId))
405
- continue;
406
- seenApps.add(app.appId);
407
- if (options.dryRun) {
408
- mcpServers.push({
409
- serverName: app.serverName,
410
- mcpUrl: app.mcpUrl,
411
- clients,
412
- files: [],
413
- authenticated: false,
414
- guidance: [],
390
+ if (shouldUpdateInstructions) {
391
+ progress?.message("Updating managed instructions...");
392
+ instructionFiles = writeManagedInstructions(instructionBlocks, baseDir, clients, scope, options);
393
+ progress?.advance("Managed instructions updated");
394
+ if (instructionFiles.length) {
395
+ options.telemetry?.track("skills_cli instructions updated", {
396
+ fileCount: instructionFiles.length,
415
397
  });
416
- continue;
417
398
  }
418
- const registration = await registerMcpServer({
419
- descriptor: {
420
- serverName: app.serverName,
421
- mcpUrl: app.mcpUrl,
422
- aliases: app.aliases,
423
- authMode: app.authMode,
424
- hostedUrl: app.hostedUrl,
425
- },
426
- clients: mcpClients,
427
- scope,
428
- baseDir,
429
- interactive: isInteractive(options),
430
- log,
431
- });
432
- mcpServers.push({
433
- serverName: app.serverName,
434
- mcpUrl: app.mcpUrl,
435
- clients,
436
- files: [...new Set(registration.written.map((entry) => entry.file))],
437
- authenticated: registration.authenticated,
438
- guidance: registration.guidance,
439
- });
440
- options.telemetry?.track("skills_cli mcp registered", {
441
- serverName: app.serverName,
442
- clients: clients.join(","),
443
- authenticated: registration.authenticated,
444
- });
445
399
  }
400
+ if (shouldWriteGithubAction) {
401
+ progress?.message("Writing PR Visual Recap workflow...");
402
+ githubActionPath = writePrVisualRecapWorkflow(baseDir, options);
403
+ progress?.advance("PR Visual Recap workflow ready");
404
+ if (githubActionPath) {
405
+ options.telemetry?.track("skills_cli github action added");
406
+ }
407
+ }
408
+ // Register the hosted MCP server for app-backed skills (visual-plan /
409
+ // visual-recap → Agent-Native Plan, assets, design-exploration) so the
410
+ // agent can actually call them — not just read the SKILL.md. On by
411
+ // default; `--no-mcp` installs the skill files only. One registration per
412
+ // app, so visual-plan + visual-recap share a single "plan" server.
413
+ if (mcpApps.length > 0) {
414
+ const mcpClients = clients.map((client) => client === "claude-code" ? "claude-code" : "codex");
415
+ for (const app of mcpApps) {
416
+ progress?.message(`Registering ${app.displayName} MCP server...`);
417
+ if (!options.dryRun) {
418
+ const registration = await registerMcpServer({
419
+ descriptor: {
420
+ serverName: app.serverName,
421
+ mcpUrl: app.mcpUrl,
422
+ aliases: app.aliases,
423
+ authMode: app.authMode,
424
+ hostedUrl: app.hostedUrl,
425
+ },
426
+ clients: mcpClients,
427
+ scope,
428
+ baseDir,
429
+ interactive: options.connect !== false && isInteractive(options),
430
+ log,
431
+ deviceFlowTimeoutMs: options.deviceFlowTimeoutMs,
432
+ });
433
+ mcpServers.push({
434
+ serverName: app.serverName,
435
+ mcpUrl: app.mcpUrl,
436
+ clients,
437
+ registeredClients: unique(registration.written.map((entry) => entry.client)),
438
+ files: [
439
+ ...new Set(registration.written.map((entry) => entry.file)),
440
+ ],
441
+ authenticated: registration.authenticated,
442
+ guidance: registration.guidance,
443
+ });
444
+ options.telemetry?.track("skills_cli mcp registered", {
445
+ serverName: app.serverName,
446
+ clients: clients.join(","),
447
+ authenticated: registration.authenticated,
448
+ });
449
+ }
450
+ else {
451
+ mcpServers.push({
452
+ serverName: app.serverName,
453
+ mcpUrl: app.mcpUrl,
454
+ clients,
455
+ registeredClients: clients,
456
+ files: [],
457
+ authenticated: false,
458
+ guidance: [],
459
+ });
460
+ }
461
+ progress?.advance(`${app.displayName} MCP server ready`);
462
+ }
463
+ }
464
+ progress?.stop("Installation complete");
465
+ }
466
+ catch (err) {
467
+ progress?.error("Installation failed");
468
+ throw err;
446
469
  }
447
- log(`Resolved ${selected.length} skill${selected.length === 1 ? "" : "s"} from ${source.root}.`);
448
470
  return {
449
471
  source: source.root,
450
- skills: selected.map((skill) => skill.name),
472
+ skills: skillNames,
451
473
  clients,
452
474
  scope,
453
475
  written,
@@ -475,6 +497,7 @@ function defaultArgs(command) {
475
497
  instructionFiles: [],
476
498
  withGithubAction: false,
477
499
  force: false,
500
+ connect: true,
478
501
  mcp: true,
479
502
  };
480
503
  }
@@ -510,6 +533,125 @@ function normalizeSkillName(value) {
510
533
  function unique(values) {
511
534
  return [...new Set(values)];
512
535
  }
536
+ function plural(count, singular, pluralForm = `${singular}s`) {
537
+ return `${count} ${count === 1 ? singular : pluralForm}`;
538
+ }
539
+ function shortenPathForOutput(file, baseDir) {
540
+ const resolved = path.resolve(file);
541
+ const home = process.env.HOME || os.homedir();
542
+ if (resolved === home || resolved.startsWith(`${home}${path.sep}`)) {
543
+ return `~${resolved.slice(home.length)}`;
544
+ }
545
+ const base = path.resolve(baseDir);
546
+ const relative = path.relative(base, resolved);
547
+ if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
548
+ return `.${path.sep}${relative}`;
549
+ }
550
+ return file;
551
+ }
552
+ function summarizePaths(files, baseDir, max = 4) {
553
+ const shortened = unique(files).map((file) => shortenPathForOutput(file, baseDir));
554
+ if (shortened.length <= max)
555
+ return shortened.join(", ");
556
+ return `${shortened.slice(0, max).join(", ")} +${shortened.length - max} more`;
557
+ }
558
+ async function createInstallProgress(options, max) {
559
+ if (options.quiet || !isInteractive(options) || max <= 0)
560
+ return null;
561
+ const clack = await import("@clack/prompts");
562
+ const progress = clack.progress({ max, indicator: "timer" });
563
+ let active = false;
564
+ return {
565
+ start(message) {
566
+ active = true;
567
+ progress.start(message);
568
+ },
569
+ message(message) {
570
+ if (active)
571
+ progress.message(message);
572
+ },
573
+ advance(message) {
574
+ if (active)
575
+ progress.advance(1, message);
576
+ },
577
+ stop(message) {
578
+ if (!active)
579
+ return;
580
+ progress.stop(message);
581
+ active = false;
582
+ },
583
+ error(message) {
584
+ if (!active)
585
+ return;
586
+ progress.error(message);
587
+ active = false;
588
+ },
589
+ };
590
+ }
591
+ function mcpAppsForSkills(skillNames) {
592
+ const apps = [];
593
+ const seen = new Set();
594
+ for (const skillName of skillNames) {
595
+ const app = resolveAppForSkill(skillName);
596
+ if (!app || seen.has(app.appId))
597
+ continue;
598
+ seen.add(app.appId);
599
+ apps.push(app);
600
+ }
601
+ return apps;
602
+ }
603
+ function mcpStatus(server, dryRun) {
604
+ if (dryRun)
605
+ return `would register for ${server.clients.join(", ")}`;
606
+ const registered = server.registeredClients;
607
+ const pending = server.clients.filter((client) => !registered.includes(client));
608
+ const parts = [];
609
+ if (registered.length > 0) {
610
+ parts.push(`${server.authenticated ? "registered and authenticated" : "registered"} for ${registered.join(", ")}`);
611
+ }
612
+ if (pending.length > 0) {
613
+ parts.push(`authentication pending for ${pending.join(", ")}`);
614
+ }
615
+ return (parts.join("; ") ||
616
+ `authentication pending for ${server.clients.join(", ")}`);
617
+ }
618
+ async function printInstallResult(result, options) {
619
+ const clack = await import("@clack/prompts");
620
+ const verb = options.dryRun ? "Would install" : "Installed";
621
+ const summary = [
622
+ `Skills ${result.skills.join(", ") || "none"}`,
623
+ `Agents ${result.clients.join(", ") || "none"}`,
624
+ `Scope ${result.scope}`,
625
+ result.written.length
626
+ ? `Skill folders ${plural(result.written.length, "folder")} (${summarizePaths(result.written, options.baseDir)})`
627
+ : "",
628
+ ].filter(Boolean);
629
+ clack.note(summary.join("\n"), verb);
630
+ if (result.instructionFiles.length) {
631
+ clack.note(summarizePaths(result.instructionFiles, options.baseDir), "Managed instructions");
632
+ }
633
+ if (result.githubActionPath) {
634
+ clack.note(shortenPathForOutput(result.githubActionPath, options.baseDir), "PR Visual Recap workflow");
635
+ }
636
+ if (result.mcpServers.length) {
637
+ const mcpLines = result.mcpServers.map((server) => {
638
+ const status = mcpStatus(server, options.dryRun);
639
+ const files = server.files.length
640
+ ? ` (${summarizePaths(server.files, options.baseDir, 2)})`
641
+ : "";
642
+ return `${server.serverName}: ${status}${files}`;
643
+ });
644
+ clack.note(mcpLines.join("\n"), "MCP");
645
+ const guidance = result.mcpServers.flatMap((server) => server.guidance);
646
+ if (guidance.length) {
647
+ clack.note(guidance.join("\n"), "Next steps");
648
+ }
649
+ }
650
+ if (!options.dryRun) {
651
+ clack.note("Restart or reload selected agent clients if needed.", "Reload");
652
+ }
653
+ clack.outro(`${options.dryRun ? "Dry run complete" : "All set"} ✅`);
654
+ }
513
655
  function compactPromptHint(value) {
514
656
  const hint = value?.replace(/\s+/g, " ").trim() ?? "";
515
657
  if (!hint)
@@ -716,14 +858,34 @@ function skillEntry(dir) {
716
858
  return null;
717
859
  const body = fs.readFileSync(skillFile, "utf-8");
718
860
  const frontmatter = body.match(/^---\n([\s\S]*?)\n---/);
719
- const name = frontmatter?.[1]
720
- ?.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]
721
- ?.trim() ?? path.basename(dir);
722
- const description = frontmatter?.[1]
723
- ?.match(/^description:\s*(?:>-\s*)?(.+)$/m)?.[1]
724
- ?.trim();
861
+ const name = frontmatterField(frontmatter?.[1], "name") ?? path.basename(dir);
862
+ const description = frontmatterField(frontmatter?.[1], "description");
725
863
  return { name: normalizeSkillName(name), dir, description };
726
864
  }
865
+ function frontmatterField(frontmatter, field) {
866
+ if (!frontmatter)
867
+ return undefined;
868
+ const lines = frontmatter.split(/\r?\n/);
869
+ for (let i = 0; i < lines.length; i += 1) {
870
+ const match = lines[i].match(new RegExp(`^${field}:\\s*(.*)$`));
871
+ if (!match)
872
+ continue;
873
+ const raw = match[1].trim();
874
+ if (raw === ">-" || raw === ">" || raw === "|-" || raw === "|") {
875
+ const block = [];
876
+ for (let j = i + 1; j < lines.length; j += 1) {
877
+ const line = lines[j];
878
+ if (line.trim() && !/^\s/.test(line))
879
+ break;
880
+ block.push(line.replace(/^\s+/, ""));
881
+ }
882
+ const value = raw.startsWith("|") ? block.join("\n") : block.join(" ");
883
+ return value.replace(/\s+/g, " ").trim() || undefined;
884
+ }
885
+ return raw.replace(/^["']|["']$/g, "").trim() || undefined;
886
+ }
887
+ return undefined;
888
+ }
727
889
  async function materializeSource(input) {
728
890
  const local = path.resolve(input);
729
891
  if (fs.existsSync(local))
@@ -798,70 +960,49 @@ function parseGitHubSource(input) {
798
960
  }
799
961
  function instructionContentForSkill(skillName) {
800
962
  if (skillName === "quick-recap") {
801
- return `## Quick Recap Status Block
802
-
803
- Every response that completes a unit of work must end with one final status line:
804
-
805
- \`\`\`md
806
- 🟢 Actual concise status sentence
807
- \`\`\`
808
-
809
- Use 🟢 when the requested work is finished. Use 🟡 when non-routine follow-up work or a manual step remains, and name that pending item. Use 🔴 only when blocked on user input. Keep the status line under 100 characters. Put the status line at the very end of the response. Do not add \`---\`, spacer lines, or any content after it.
810
-
811
- Examples:
812
-
813
- \`\`\`md
814
- 🟢 Updated quick recap docs with output examples
815
- \`\`\`
816
-
817
- \`\`\`md
818
- 🟡 Code updated, set PROVIDER_WEBHOOK_SECRET before testing webhooks
819
- \`\`\`
820
-
821
- \`\`\`md
822
- 🔴 Need the production API key to continue
823
- \`\`\``;
963
+ return "When writing final response status indicators, use the /quick-recap skill always.";
824
964
  }
825
965
  if (skillName === "efficient-fable") {
826
- return `## Efficient Fable
827
-
828
- When operating as Claude Fable or another explicitly Fable-class expensive model, preserve Fable for the judgment layer: decomposition, architecture and product tradeoffs, synthesis, risk calls, and final review. Delegate token-heavy research, coding, testing, file inventory, repetitive edits, and independent implementation slices to cheaper subagents when available. Write delegated prompts as self-contained handoff packets with objective, scope, out-of-scope areas, expected evidence, verification commands, and stop conditions. For testing, Fable should suggest the validation direction and important scripts or browser checks, then lighter agents can run them, reduce logs, collect screenshots, and report exact failures and likely causes. Treat delegated reports as leads: Fable should verify important cited files, failures, and high-risk diffs before relying on them. Do not make unsupported quality or speed guarantees; frame savings as workload-dependent.`;
966
+ return "When operating as Claude Fable, use the /efficient-fable skill always.";
829
967
  }
830
968
  if (skillName === "efficient-frontier") {
831
- return `## Efficient Frontier
832
-
833
- When running any high-cost frontier model on a codebase-heavy task, act as the orchestrator and reviewer. Split independent research, search, summarization, coding, and testing work into cheaper/faster subagents when the host supports them, then spend frontier-model tokens on the plan, tradeoffs, integration decisions, validation strategy, and final quality pass. Delegated prompts should be self-contained: objective, repo path, scope, out-of-scope areas, expected evidence, verification commands, and stop conditions. For testing-heavy work, the frontier model should choose the scripts or browser flows that matter while lighter agents run checks, reduce output, and return the concrete signal. Treat delegated findings as leads and verify important claims before presenting them as facts.`;
969
+ return "When using a high-cost frontier model for codebase-heavy work, use the /efficient-frontier skill always.";
834
970
  }
835
971
  if (skillName === "stay-within-limits") {
836
- return `## Stay Within Limits
837
-
838
- Before starting long-running or parallel agent work, and between waves, check current 5-hour and weekly usage limits with the host's usage tool. For Claude Code, use npx -y ccusage@latest blocks --active --json when no better first-party signal is available. Keep waves to at most 3 parallel subagents by default. If either active 5-hour or weekly window is at or above 95%, do not launch more work; pause until the window clears. When a wake/resume tool is available, schedule a self-contained wake prompt for min(3600, secondsUntilWindowClears), re-check the actual block/window on wake, reschedule if still over budget, and only continue when safely below the threshold. The wake prompt should restate the remaining plan, usage check, wave throttle, verification steps, and any delegation scope or stop conditions needed for the next wave. Check between waves, not mid-wave.`;
972
+ return "When long-running or parallel work needs usage-limit checks, use the /stay-within-limits skill always.";
973
+ }
974
+ if (skillName === "visual-plan") {
975
+ return "When work needs an interactive visual plan before implementation, use the /visual-plan skill always.";
976
+ }
977
+ if (skillName === "visual-recap") {
978
+ return "When a PR, branch, commit, or diff needs an interactive visual recap, use the /visual-recap skill always.";
839
979
  }
840
980
  return null;
841
981
  }
842
- async function maybeUpdateInstructions(skillNames, baseDir, options) {
843
- const blocks = skillNames
982
+ function managedInstructionBlocksForSkills(skillNames) {
983
+ return skillNames
844
984
  .map((name) => instructionContentForSkill(name))
845
985
  .filter((block) => Boolean(block));
986
+ }
987
+ async function shouldUpdateManagedInstructions(blocks, options) {
846
988
  if (blocks.length === 0)
847
- return [];
989
+ return false;
848
990
  let shouldUpdate = options.updateInstructions;
849
- if (shouldUpdate === undefined) {
850
- if (options.yes)
851
- shouldUpdate = true;
852
- else if (isInteractive(options)) {
853
- const prompt = options.promptUpdateInstructions ?? promptForUpdateInstructions;
854
- shouldUpdate = (await prompt()) === true;
855
- }
856
- else {
857
- shouldUpdate = false;
858
- }
859
- }
860
- if (!shouldUpdate)
991
+ if (shouldUpdate !== undefined)
992
+ return shouldUpdate;
993
+ if (options.yes)
994
+ return true;
995
+ if (!isInteractive(options))
996
+ return false;
997
+ const prompt = options.promptUpdateInstructions ?? promptForUpdateInstructions;
998
+ return (await prompt()) === true;
999
+ }
1000
+ function writeManagedInstructions(blocks, baseDir, clients, scope, options) {
1001
+ if (blocks.length === 0)
861
1002
  return [];
862
- const files = resolveInstructionFiles(baseDir, options.instructionFiles);
1003
+ const files = resolveInstructionFiles(baseDir, options.instructionFiles, clients, scope);
863
1004
  const content = `${MANAGED_INSTRUCTIONS_START}
864
- ${blocks.join("\n\n")}
1005
+ ${blocks.join("\n")}
865
1006
  ${MANAGED_INSTRUCTIONS_END}`;
866
1007
  for (const file of files) {
867
1008
  if (options.dryRun)
@@ -870,10 +1011,30 @@ ${MANAGED_INSTRUCTIONS_END}`;
870
1011
  }
871
1012
  return files;
872
1013
  }
873
- function resolveInstructionFiles(baseDir, explicit) {
1014
+ async function maybeUpdateInstructions(skillNames, baseDir, options) {
1015
+ const blocks = managedInstructionBlocksForSkills(skillNames);
1016
+ const clients = options.clients?.length ? options.clients : CLIENTS;
1017
+ const scope = options.scope ?? "user";
1018
+ if (!(await shouldUpdateManagedInstructions(blocks, options)))
1019
+ return [];
1020
+ return writeManagedInstructions(blocks, baseDir, clients, scope, options);
1021
+ }
1022
+ function resolveInstructionFiles(baseDir, explicit, clients, scope) {
874
1023
  if (explicit && explicit.length > 0) {
875
1024
  return explicit.map((file) => path.resolve(baseDir, file));
876
1025
  }
1026
+ if (scope === "user") {
1027
+ const home = process.env.HOME || os.homedir();
1028
+ const files = [];
1029
+ if (clients.includes("codex")) {
1030
+ const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
1031
+ files.push(path.join(codexHome, "AGENTS.md"));
1032
+ }
1033
+ if (clients.includes("claude-code")) {
1034
+ files.push(path.join(home, ".claude", "CLAUDE.md"));
1035
+ }
1036
+ return unique(files);
1037
+ }
877
1038
  const candidates = ["AGENTS.md", "CLAUDE.md"].map((file) => path.join(baseDir, file));
878
1039
  const existing = candidates.filter((file) => fs.existsSync(file));
879
1040
  return existing.length > 0 ? existing : [path.join(baseDir, "AGENTS.md")];