@agent-native/skills 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connect.d.ts +1 -0
- package/dist/connect.d.ts.map +1 -1
- package/dist/connect.js +37 -15
- package/dist/connect.js.map +1 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +467 -209
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,6 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import readline from "node:readline/promises";
|
|
7
6
|
import { fileURLToPath } from "node:url";
|
|
8
7
|
import { resolveAppForSkill } from "./built-in-apps.js";
|
|
9
8
|
import { registerMcpServer } from "./connect.js";
|
|
@@ -26,6 +25,7 @@ Options:
|
|
|
26
25
|
--with-github-action Add .github/workflows/pr-visual-recap.yml when visual-recap is installed
|
|
27
26
|
--force Overwrite a different existing PR Visual Recap workflow
|
|
28
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
|
|
29
29
|
-y, --yes Use defaults in non-interactive mode
|
|
30
30
|
--dry-run Print intended writes without changing files
|
|
31
31
|
--json Print the result as JSON
|
|
@@ -119,6 +119,10 @@ export function parseSkillsCliArgs(argv) {
|
|
|
119
119
|
out.mcp = false;
|
|
120
120
|
else if (arg === "--mcp")
|
|
121
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;
|
|
122
126
|
else if (arg.startsWith("-"))
|
|
123
127
|
throw new Error(`Unknown option: ${arg}`);
|
|
124
128
|
else if (!out.source)
|
|
@@ -167,6 +171,8 @@ function toCoreSkillsArgv(parsed) {
|
|
|
167
171
|
out.push("--force");
|
|
168
172
|
if (parsed.mcp === false)
|
|
169
173
|
out.push("--no-mcp");
|
|
174
|
+
if (parsed.connect === false)
|
|
175
|
+
out.push("--no-connect");
|
|
170
176
|
if (parsed.updateInstructions === true)
|
|
171
177
|
out.push("--update-instructions");
|
|
172
178
|
if (parsed.updateInstructions === false)
|
|
@@ -230,6 +236,9 @@ export async function runSkillsCli(argv, options = {}) {
|
|
|
230
236
|
source.cleanup?.();
|
|
231
237
|
}
|
|
232
238
|
}
|
|
239
|
+
const stdoutLog = parsed.printJson || options.log
|
|
240
|
+
? options.log
|
|
241
|
+
: (message) => process.stdout.write(`${message}\n`);
|
|
233
242
|
const result = await installSkills({
|
|
234
243
|
source: skillSource,
|
|
235
244
|
skillNames: parsed.skillNames,
|
|
@@ -244,8 +253,15 @@ export async function runSkillsCli(argv, options = {}) {
|
|
|
244
253
|
instructionFiles: parsed.instructionFiles,
|
|
245
254
|
withGithubAction: parsed.withGithubAction,
|
|
246
255
|
force: parsed.force,
|
|
247
|
-
|
|
256
|
+
connect: parsed.connect,
|
|
257
|
+
quiet: parsed.printJson,
|
|
258
|
+
log: parsed.printJson ? undefined : stdoutLog,
|
|
248
259
|
isInteractive: options.isInteractive,
|
|
260
|
+
promptSkills: options.promptSkills,
|
|
261
|
+
promptClients: options.promptClients,
|
|
262
|
+
promptScope: options.promptScope,
|
|
263
|
+
promptUpdateInstructions: options.promptUpdateInstructions,
|
|
264
|
+
promptGithubAction: options.promptGithubAction,
|
|
249
265
|
telemetry,
|
|
250
266
|
mcp: parsed.mcp,
|
|
251
267
|
});
|
|
@@ -260,28 +276,10 @@ export async function runSkillsCli(argv, options = {}) {
|
|
|
260
276
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
261
277
|
return;
|
|
262
278
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
? `Skill files: ${result.written.join(", ")}`
|
|
268
|
-
: "",
|
|
269
|
-
result.instructionFiles.length
|
|
270
|
-
? `Managed instructions: ${result.instructionFiles.join(", ")}`
|
|
271
|
-
: "",
|
|
272
|
-
result.githubActionPath
|
|
273
|
-
? `PR Visual Recap workflow: ${result.githubActionPath}`
|
|
274
|
-
: "",
|
|
275
|
-
...result.mcpServers.flatMap((server) => [
|
|
276
|
-
`MCP server "${server.serverName}" ${parsed.dryRun ? "would be registered" : "registered"} for ${server.clients.join(", ")}${server.files.length ? `:\n ${server.files.join("\n ")}` : ""}`,
|
|
277
|
-
...server.guidance.map((line) => ` ${line}`),
|
|
278
|
-
]),
|
|
279
|
-
parsed.dryRun
|
|
280
|
-
? ""
|
|
281
|
-
: "Restart or reload selected agent clients if needed.",
|
|
282
|
-
]
|
|
283
|
-
.filter(Boolean)
|
|
284
|
-
.join("\n") + "\n");
|
|
279
|
+
await printInstallResult(result, {
|
|
280
|
+
baseDir: parsed.baseDir ?? options.baseDir ?? process.cwd(),
|
|
281
|
+
dryRun: parsed.dryRun,
|
|
282
|
+
});
|
|
285
283
|
}
|
|
286
284
|
catch (error) {
|
|
287
285
|
telemetry.track("skills_cli failed", {
|
|
@@ -352,98 +350,126 @@ export async function installSkills(options) {
|
|
|
352
350
|
});
|
|
353
351
|
const scope = await resolveSelectedScope(options);
|
|
354
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);
|
|
355
364
|
const written = [];
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
}
|
|
365
380
|
}
|
|
366
381
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
});
|
|
375
|
-
const instructionFiles = await maybeUpdateInstructions(selected.map((skill) => skill.name), baseDir, options);
|
|
376
|
-
if (instructionFiles.length) {
|
|
377
|
-
options.telemetry?.track("skills_cli instructions updated", {
|
|
378
|
-
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),
|
|
379
389
|
});
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
(
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
options.telemetry?.track("skills_cli github action added");
|
|
388
|
-
}
|
|
389
|
-
// Register the hosted MCP server for app-backed skills (visual-plan /
|
|
390
|
-
// visual-recap → Agent-Native Plan, assets, design-exploration) so the
|
|
391
|
-
// agent can actually call them — not just read the SKILL.md. On by
|
|
392
|
-
// default; `--no-mcp` installs the skill files only. One registration per
|
|
393
|
-
// app, so visual-plan + visual-recap share a single "plan" server.
|
|
394
|
-
const mcpServers = [];
|
|
395
|
-
if (options.mcp !== false) {
|
|
396
|
-
const mcpClients = clients.map((client) => client === "claude-code" ? "claude-code" : "codex");
|
|
397
|
-
const seenApps = new Set();
|
|
398
|
-
for (const skill of selected) {
|
|
399
|
-
const app = resolveAppForSkill(skill.name);
|
|
400
|
-
if (!app || seenApps.has(app.appId))
|
|
401
|
-
continue;
|
|
402
|
-
seenApps.add(app.appId);
|
|
403
|
-
if (options.dryRun) {
|
|
404
|
-
mcpServers.push({
|
|
405
|
-
serverName: app.serverName,
|
|
406
|
-
mcpUrl: app.mcpUrl,
|
|
407
|
-
clients,
|
|
408
|
-
files: [],
|
|
409
|
-
authenticated: false,
|
|
410
|
-
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,
|
|
411
397
|
});
|
|
412
|
-
continue;
|
|
413
398
|
}
|
|
414
|
-
const registration = await registerMcpServer({
|
|
415
|
-
descriptor: {
|
|
416
|
-
serverName: app.serverName,
|
|
417
|
-
mcpUrl: app.mcpUrl,
|
|
418
|
-
aliases: app.aliases,
|
|
419
|
-
authMode: app.authMode,
|
|
420
|
-
hostedUrl: app.hostedUrl,
|
|
421
|
-
},
|
|
422
|
-
clients: mcpClients,
|
|
423
|
-
scope,
|
|
424
|
-
baseDir,
|
|
425
|
-
interactive: isInteractive(options),
|
|
426
|
-
log,
|
|
427
|
-
});
|
|
428
|
-
mcpServers.push({
|
|
429
|
-
serverName: app.serverName,
|
|
430
|
-
mcpUrl: app.mcpUrl,
|
|
431
|
-
clients,
|
|
432
|
-
files: [...new Set(registration.written.map((entry) => entry.file))],
|
|
433
|
-
authenticated: registration.authenticated,
|
|
434
|
-
guidance: registration.guidance,
|
|
435
|
-
});
|
|
436
|
-
options.telemetry?.track("skills_cli mcp registered", {
|
|
437
|
-
serverName: app.serverName,
|
|
438
|
-
clients: clients.join(","),
|
|
439
|
-
authenticated: registration.authenticated,
|
|
440
|
-
});
|
|
441
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;
|
|
442
469
|
}
|
|
443
|
-
log(`Resolved ${selected.length} skill${selected.length === 1 ? "" : "s"} from ${source.root}.`);
|
|
444
470
|
return {
|
|
445
471
|
source: source.root,
|
|
446
|
-
skills:
|
|
472
|
+
skills: skillNames,
|
|
447
473
|
clients,
|
|
448
474
|
scope,
|
|
449
475
|
written,
|
|
@@ -471,6 +497,7 @@ function defaultArgs(command) {
|
|
|
471
497
|
instructionFiles: [],
|
|
472
498
|
withGithubAction: false,
|
|
473
499
|
force: false,
|
|
500
|
+
connect: true,
|
|
474
501
|
mcp: true,
|
|
475
502
|
};
|
|
476
503
|
}
|
|
@@ -506,6 +533,157 @@ function normalizeSkillName(value) {
|
|
|
506
533
|
function unique(values) {
|
|
507
534
|
return [...new Set(values)];
|
|
508
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
|
+
}
|
|
655
|
+
function compactPromptHint(value) {
|
|
656
|
+
const hint = value?.replace(/\s+/g, " ").trim() ?? "";
|
|
657
|
+
if (!hint)
|
|
658
|
+
return "Skill from BuilderIO/skills.";
|
|
659
|
+
if (hint.length <= 96)
|
|
660
|
+
return hint;
|
|
661
|
+
return `${hint.slice(0, 93).trimEnd()}...`;
|
|
662
|
+
}
|
|
663
|
+
function skillPromptOptions(entries) {
|
|
664
|
+
return entries.map((entry) => ({
|
|
665
|
+
value: entry.name,
|
|
666
|
+
label: entry.name,
|
|
667
|
+
hint: compactPromptHint(entry.description),
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
async function promptForSkills(context) {
|
|
671
|
+
const clack = await import("@clack/prompts");
|
|
672
|
+
const result = await clack.multiselect({
|
|
673
|
+
message: "Which skills do you want to install?\n" +
|
|
674
|
+
" (space toggles, enter confirms)",
|
|
675
|
+
options: context.options,
|
|
676
|
+
initialValues: context.initialSkills,
|
|
677
|
+
required: true,
|
|
678
|
+
});
|
|
679
|
+
if (clack.isCancel(result)) {
|
|
680
|
+
clack.cancel("Cancelled.");
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
if (!Array.isArray(result))
|
|
684
|
+
return [];
|
|
685
|
+
return result.filter((value) => typeof value === "string");
|
|
686
|
+
}
|
|
509
687
|
async function resolveSelectedSkills(entries, options) {
|
|
510
688
|
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
|
511
689
|
const requested = unique((options.skillNames ?? []).map(normalizeSkillName));
|
|
@@ -520,47 +698,87 @@ async function resolveSelectedSkills(entries, options) {
|
|
|
520
698
|
}
|
|
521
699
|
if (!isInteractive(options) || options.yes)
|
|
522
700
|
return entries;
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
].join("\n"));
|
|
528
|
-
const trimmed = answer.trim();
|
|
529
|
-
if (!trimmed)
|
|
530
|
-
return entries;
|
|
531
|
-
const selectedNames = trimmed
|
|
532
|
-
.split(",")
|
|
533
|
-
.map((part) => part.trim())
|
|
534
|
-
.filter(Boolean)
|
|
535
|
-
.map((part) => {
|
|
536
|
-
const asNumber = Number(part);
|
|
537
|
-
if (Number.isInteger(asNumber) &&
|
|
538
|
-
asNumber >= 1 &&
|
|
539
|
-
asNumber <= entries.length) {
|
|
540
|
-
return entries[asNumber - 1].name;
|
|
541
|
-
}
|
|
542
|
-
return normalizeSkillName(part);
|
|
701
|
+
const prompt = options.promptSkills ?? promptForSkills;
|
|
702
|
+
const selectedNames = await prompt({
|
|
703
|
+
initialSkills: entries.map((entry) => entry.name),
|
|
704
|
+
options: skillPromptOptions(entries),
|
|
543
705
|
});
|
|
706
|
+
if (!selectedNames || selectedNames.length === 0) {
|
|
707
|
+
throw new Error("Cancelled.");
|
|
708
|
+
}
|
|
544
709
|
return resolveSelectedSkills(entries, {
|
|
545
710
|
...options,
|
|
546
711
|
skillNames: selectedNames,
|
|
547
712
|
});
|
|
548
713
|
}
|
|
714
|
+
async function promptForScope(context) {
|
|
715
|
+
const clack = await import("@clack/prompts");
|
|
716
|
+
const result = await clack.select({
|
|
717
|
+
message: "Where do you want to install these skills?",
|
|
718
|
+
options: [
|
|
719
|
+
{
|
|
720
|
+
value: "project",
|
|
721
|
+
label: "Project",
|
|
722
|
+
hint: "This repo only (.agents / .claude in the current directory)",
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
value: "user",
|
|
726
|
+
label: "User",
|
|
727
|
+
hint: "Your home directory (~/.codex, ~/.claude), across projects",
|
|
728
|
+
},
|
|
729
|
+
],
|
|
730
|
+
initialValue: context.initialScope,
|
|
731
|
+
});
|
|
732
|
+
if (clack.isCancel(result)) {
|
|
733
|
+
clack.cancel("Cancelled.");
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
return result === "user" ? "user" : "project";
|
|
737
|
+
}
|
|
549
738
|
async function resolveSelectedScope(options) {
|
|
550
739
|
if (options.scope)
|
|
551
740
|
return options.scope;
|
|
552
741
|
if (!isInteractive(options) || options.yes)
|
|
553
742
|
return "user";
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
743
|
+
const prompt = options.promptScope ?? promptForScope;
|
|
744
|
+
const selected = await prompt({ initialScope: "project" });
|
|
745
|
+
if (!selected)
|
|
746
|
+
throw new Error("Cancelled.");
|
|
747
|
+
return selected;
|
|
748
|
+
}
|
|
749
|
+
function clientPromptOptions() {
|
|
750
|
+
return [
|
|
751
|
+
{
|
|
752
|
+
value: "codex",
|
|
753
|
+
label: "Codex",
|
|
754
|
+
hint: "Install into Codex skill directories",
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
value: "claude-code",
|
|
758
|
+
label: "Claude Code",
|
|
759
|
+
hint: "Install into Claude Code skill directories",
|
|
760
|
+
},
|
|
761
|
+
];
|
|
762
|
+
}
|
|
763
|
+
function normalizePromptClients(values) {
|
|
764
|
+
if (!Array.isArray(values))
|
|
765
|
+
return [];
|
|
766
|
+
return unique(values.filter((value) => value === "codex" || value === "claude-code"));
|
|
767
|
+
}
|
|
768
|
+
async function promptForClients(context) {
|
|
769
|
+
const clack = await import("@clack/prompts");
|
|
770
|
+
const result = await clack.multiselect({
|
|
771
|
+
message: "Install these skills for which local agents?\n" +
|
|
772
|
+
" (space toggles, enter confirms)",
|
|
773
|
+
options: context.options,
|
|
774
|
+
initialValues: context.initialClients,
|
|
775
|
+
required: true,
|
|
776
|
+
});
|
|
777
|
+
if (clack.isCancel(result)) {
|
|
778
|
+
clack.cancel("Cancelled.");
|
|
779
|
+
return null;
|
|
780
|
+
}
|
|
781
|
+
return normalizePromptClients(result);
|
|
564
782
|
}
|
|
565
783
|
async function resolveSelectedClients(options) {
|
|
566
784
|
const requested = unique(options.clients ?? []);
|
|
@@ -568,9 +786,14 @@ async function resolveSelectedClients(options) {
|
|
|
568
786
|
return requested;
|
|
569
787
|
if (!isInteractive(options) || options.yes)
|
|
570
788
|
return CLIENTS;
|
|
571
|
-
const
|
|
572
|
-
const
|
|
573
|
-
|
|
789
|
+
const prompt = options.promptClients ?? promptForClients;
|
|
790
|
+
const selected = await prompt({
|
|
791
|
+
initialClients: CLIENTS,
|
|
792
|
+
options: clientPromptOptions(),
|
|
793
|
+
});
|
|
794
|
+
if (!selected || selected.length === 0)
|
|
795
|
+
throw new Error("Cancelled.");
|
|
796
|
+
return selected;
|
|
574
797
|
}
|
|
575
798
|
function isInteractive(options) {
|
|
576
799
|
if (options.isInteractive)
|
|
@@ -579,18 +802,6 @@ function isInteractive(options) {
|
|
|
579
802
|
return false;
|
|
580
803
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
581
804
|
}
|
|
582
|
-
async function promptLine(question) {
|
|
583
|
-
const rl = readline.createInterface({
|
|
584
|
-
input: process.stdin,
|
|
585
|
-
output: process.stdout,
|
|
586
|
-
});
|
|
587
|
-
try {
|
|
588
|
-
return await rl.question(question);
|
|
589
|
-
}
|
|
590
|
-
finally {
|
|
591
|
-
rl.close();
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
805
|
function installRootForClient(client, scope, baseDir) {
|
|
595
806
|
const home = process.env.HOME || os.homedir();
|
|
596
807
|
if (scope === "project") {
|
|
@@ -647,14 +858,34 @@ function skillEntry(dir) {
|
|
|
647
858
|
return null;
|
|
648
859
|
const body = fs.readFileSync(skillFile, "utf-8");
|
|
649
860
|
const frontmatter = body.match(/^---\n([\s\S]*?)\n---/);
|
|
650
|
-
const name = frontmatter?.[1]
|
|
651
|
-
|
|
652
|
-
?.trim() ?? path.basename(dir);
|
|
653
|
-
const description = frontmatter?.[1]
|
|
654
|
-
?.match(/^description:\s*(?:>-\s*)?(.+)$/m)?.[1]
|
|
655
|
-
?.trim();
|
|
861
|
+
const name = frontmatterField(frontmatter?.[1], "name") ?? path.basename(dir);
|
|
862
|
+
const description = frontmatterField(frontmatter?.[1], "description");
|
|
656
863
|
return { name: normalizeSkillName(name), dir, description };
|
|
657
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
|
+
}
|
|
658
889
|
async function materializeSource(input) {
|
|
659
890
|
const local = path.resolve(input);
|
|
660
891
|
if (fs.existsSync(local))
|
|
@@ -729,70 +960,49 @@ function parseGitHubSource(input) {
|
|
|
729
960
|
}
|
|
730
961
|
function instructionContentForSkill(skillName) {
|
|
731
962
|
if (skillName === "quick-recap") {
|
|
732
|
-
return
|
|
733
|
-
|
|
734
|
-
Every response that completes a unit of work must end with one final status line:
|
|
735
|
-
|
|
736
|
-
\`\`\`md
|
|
737
|
-
🟢 Actual concise status sentence
|
|
738
|
-
\`\`\`
|
|
739
|
-
|
|
740
|
-
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.
|
|
741
|
-
|
|
742
|
-
Examples:
|
|
743
|
-
|
|
744
|
-
\`\`\`md
|
|
745
|
-
🟢 Updated quick recap docs with output examples
|
|
746
|
-
\`\`\`
|
|
747
|
-
|
|
748
|
-
\`\`\`md
|
|
749
|
-
🟡 Code updated, set PROVIDER_WEBHOOK_SECRET before testing webhooks
|
|
750
|
-
\`\`\`
|
|
751
|
-
|
|
752
|
-
\`\`\`md
|
|
753
|
-
🔴 Need the production API key to continue
|
|
754
|
-
\`\`\``;
|
|
963
|
+
return "When writing final response status indicators, use the /quick-recap skill always.";
|
|
755
964
|
}
|
|
756
965
|
if (skillName === "efficient-fable") {
|
|
757
|
-
return
|
|
758
|
-
|
|
759
|
-
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.";
|
|
760
967
|
}
|
|
761
968
|
if (skillName === "efficient-frontier") {
|
|
762
|
-
return
|
|
763
|
-
|
|
764
|
-
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.";
|
|
765
970
|
}
|
|
766
971
|
if (skillName === "stay-within-limits") {
|
|
767
|
-
return
|
|
768
|
-
|
|
769
|
-
|
|
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.";
|
|
770
979
|
}
|
|
771
980
|
return null;
|
|
772
981
|
}
|
|
773
|
-
|
|
774
|
-
|
|
982
|
+
function managedInstructionBlocksForSkills(skillNames) {
|
|
983
|
+
return skillNames
|
|
775
984
|
.map((name) => instructionContentForSkill(name))
|
|
776
985
|
.filter((block) => Boolean(block));
|
|
986
|
+
}
|
|
987
|
+
async function shouldUpdateManagedInstructions(blocks, options) {
|
|
777
988
|
if (blocks.length === 0)
|
|
778
|
-
return
|
|
989
|
+
return false;
|
|
779
990
|
let shouldUpdate = options.updateInstructions;
|
|
780
|
-
if (shouldUpdate
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
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)
|
|
792
1002
|
return [];
|
|
793
|
-
const files = resolveInstructionFiles(baseDir, options.instructionFiles);
|
|
1003
|
+
const files = resolveInstructionFiles(baseDir, options.instructionFiles, clients, scope);
|
|
794
1004
|
const content = `${MANAGED_INSTRUCTIONS_START}
|
|
795
|
-
${blocks.join("\n
|
|
1005
|
+
${blocks.join("\n")}
|
|
796
1006
|
${MANAGED_INSTRUCTIONS_END}`;
|
|
797
1007
|
for (const file of files) {
|
|
798
1008
|
if (options.dryRun)
|
|
@@ -801,10 +1011,30 @@ ${MANAGED_INSTRUCTIONS_END}`;
|
|
|
801
1011
|
}
|
|
802
1012
|
return files;
|
|
803
1013
|
}
|
|
804
|
-
function
|
|
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) {
|
|
805
1023
|
if (explicit && explicit.length > 0) {
|
|
806
1024
|
return explicit.map((file) => path.resolve(baseDir, file));
|
|
807
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
|
+
}
|
|
808
1038
|
const candidates = ["AGENTS.md", "CLAUDE.md"].map((file) => path.join(baseDir, file));
|
|
809
1039
|
const existing = candidates.filter((file) => fs.existsSync(file));
|
|
810
1040
|
return existing.length > 0 ? existing : [path.join(baseDir, "AGENTS.md")];
|
|
@@ -829,8 +1059,36 @@ async function shouldPromptGithubAction(options, baseDir) {
|
|
|
829
1059
|
if (fs.existsSync(path.join(baseDir, ".github", "workflows", "pr-visual-recap.yml"))) {
|
|
830
1060
|
return false;
|
|
831
1061
|
}
|
|
832
|
-
const
|
|
833
|
-
return
|
|
1062
|
+
const prompt = options.promptGithubAction ?? promptForGithubAction;
|
|
1063
|
+
return ((await prompt({
|
|
1064
|
+
workflowPath: path.join(".github", "workflows", "pr-visual-recap.yml"),
|
|
1065
|
+
})) === true);
|
|
1066
|
+
}
|
|
1067
|
+
async function promptForUpdateInstructions() {
|
|
1068
|
+
const clack = await import("@clack/prompts");
|
|
1069
|
+
const result = await clack.confirm({
|
|
1070
|
+
message: "Add managed AGENTS.md / CLAUDE.md instructions for always-on behavior?",
|
|
1071
|
+
initialValue: true,
|
|
1072
|
+
});
|
|
1073
|
+
if (clack.isCancel(result)) {
|
|
1074
|
+
clack.cancel("Skipped instruction update.");
|
|
1075
|
+
return null;
|
|
1076
|
+
}
|
|
1077
|
+
return Boolean(result);
|
|
1078
|
+
}
|
|
1079
|
+
async function promptForGithubAction(context) {
|
|
1080
|
+
const clack = await import("@clack/prompts");
|
|
1081
|
+
const result = await clack.confirm({
|
|
1082
|
+
message: "Optional: add automatic PR Visual Recaps? (GitHub Action)\n" +
|
|
1083
|
+
" Posts a human-friendly recap on every pull request.\n" +
|
|
1084
|
+
` Writes ${context.workflowPath}.`,
|
|
1085
|
+
initialValue: false,
|
|
1086
|
+
});
|
|
1087
|
+
if (clack.isCancel(result)) {
|
|
1088
|
+
clack.cancel("Skipped PR Visual Recap workflow.");
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
return Boolean(result);
|
|
834
1092
|
}
|
|
835
1093
|
const PR_VISUAL_RECAP_REUSABLE_WORKFLOW = `name: PR Visual Recap
|
|
836
1094
|
|