@agent-native/skills 0.1.0
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/README.md +15 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +633 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +80 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +699 -0
- package/dist/install.js.map +1 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,633 @@
|
|
|
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 readline from "node:readline/promises";
|
|
7
|
+
const HELP = `@agent-native/skills
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
npx @agent-native/skills add [options]
|
|
11
|
+
npx @agent-native/skills list
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--skill <name> Install only this skill (repeatable)
|
|
15
|
+
--client, -a <client> codex, claude-code, or all (repeatable or comma-separated)
|
|
16
|
+
--scope <user|project> Install globally or into the current project (default: user)
|
|
17
|
+
-g, --global Alias for --scope user
|
|
18
|
+
--project Alias for --scope project
|
|
19
|
+
--update-instructions Add managed AGENTS.md / CLAUDE.md instructions when useful
|
|
20
|
+
--no-update-instructions Skip managed instruction file updates
|
|
21
|
+
--instructions-file <path> File to receive managed instructions (repeatable)
|
|
22
|
+
--with-github-action Add .github/workflows/pr-visual-recap.yml when visual-recap is installed
|
|
23
|
+
--force Overwrite a different existing PR Visual Recap workflow
|
|
24
|
+
-y, --yes Use defaults in non-interactive mode
|
|
25
|
+
--dry-run Print intended writes without changing files
|
|
26
|
+
--json Print the result as JSON
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
npx @agent-native/skills add
|
|
30
|
+
npx @agent-native/skills add --skill quick-recap
|
|
31
|
+
npx @agent-native/skills add --skill visual-recap --with-github-action
|
|
32
|
+
`;
|
|
33
|
+
const CLIENTS = ["codex", "claude-code"];
|
|
34
|
+
const DEFAULT_SKILLS_SOURCE = "BuilderIO/skills";
|
|
35
|
+
const MANAGED_INSTRUCTIONS_START = "<!-- BEGIN @agent-native/skills -->";
|
|
36
|
+
const MANAGED_INSTRUCTIONS_END = "<!-- END @agent-native/skills -->";
|
|
37
|
+
export function parseSkillsCliArgs(argv) {
|
|
38
|
+
const first = argv[0];
|
|
39
|
+
if (!first || first === "help" || first === "--help" || first === "-h") {
|
|
40
|
+
return defaultArgs("help");
|
|
41
|
+
}
|
|
42
|
+
const command = first === "list" ? "list" : "add";
|
|
43
|
+
const args = first === "add" || first === "list" ? argv.slice(1) : argv;
|
|
44
|
+
const out = defaultArgs(command);
|
|
45
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
46
|
+
const arg = args[i];
|
|
47
|
+
const eat = (flag) => {
|
|
48
|
+
if (arg === flag) {
|
|
49
|
+
const next = args[++i];
|
|
50
|
+
if (!next || next.startsWith("-")) {
|
|
51
|
+
throw new Error(`Missing value for ${flag}.`);
|
|
52
|
+
}
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
55
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
56
|
+
const value = arg.slice(flag.length + 1);
|
|
57
|
+
if (!value)
|
|
58
|
+
throw new Error(`Missing value for ${flag}.`);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
};
|
|
63
|
+
let value;
|
|
64
|
+
if ((value = eat("--skill")) !== undefined)
|
|
65
|
+
out.skillNames.push(value);
|
|
66
|
+
else if ((value = eat("-s")) !== undefined)
|
|
67
|
+
out.skillNames.push(value);
|
|
68
|
+
else if ((value = eat("--client")) !== undefined)
|
|
69
|
+
out.clients.push(...normalizeClients(value));
|
|
70
|
+
else if ((value = eat("--agent")) !== undefined)
|
|
71
|
+
out.clients.push(...normalizeClients(value));
|
|
72
|
+
else if ((value = eat("-a")) !== undefined)
|
|
73
|
+
out.clients.push(...normalizeClients(value));
|
|
74
|
+
else if ((value = eat("--scope")) !== undefined)
|
|
75
|
+
out.scope = parseScope(value);
|
|
76
|
+
else if ((value = eat("--instructions-file")) !== undefined)
|
|
77
|
+
out.instructionFiles.push(value);
|
|
78
|
+
else if ((value = eat("--cwd")) !== undefined)
|
|
79
|
+
out.baseDir = value;
|
|
80
|
+
else if (arg === "-g" || arg === "--global")
|
|
81
|
+
out.scope = "user";
|
|
82
|
+
else if (arg === "--project")
|
|
83
|
+
out.scope = "project";
|
|
84
|
+
else if (arg === "--copy") {
|
|
85
|
+
// Compatibility with the open `skills` CLI. This installer always copies.
|
|
86
|
+
out.copySource = true;
|
|
87
|
+
}
|
|
88
|
+
else if (arg === "-y" || arg === "--yes")
|
|
89
|
+
out.yes = true;
|
|
90
|
+
else if (arg === "--dry-run")
|
|
91
|
+
out.dryRun = true;
|
|
92
|
+
else if (arg === "--json")
|
|
93
|
+
out.printJson = true;
|
|
94
|
+
else if (arg === "--update-instructions")
|
|
95
|
+
out.updateInstructions = true;
|
|
96
|
+
else if (arg === "--no-update-instructions")
|
|
97
|
+
out.updateInstructions = false;
|
|
98
|
+
else if (arg === "--with-github-action" || arg === "--with-github-actions")
|
|
99
|
+
out.withGithubAction = true;
|
|
100
|
+
else if (arg === "--force")
|
|
101
|
+
out.force = true;
|
|
102
|
+
else if (arg.startsWith("-"))
|
|
103
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
104
|
+
else if (!out.source)
|
|
105
|
+
out.source = arg;
|
|
106
|
+
else
|
|
107
|
+
throw new Error(`Unexpected argument: ${arg}`);
|
|
108
|
+
}
|
|
109
|
+
if (out.source && out.source !== DEFAULT_SKILLS_SOURCE && !out.copySource) {
|
|
110
|
+
throw new Error(`Unexpected argument: ${out.source}. @agent-native/skills installs the BuilderIO skills collection; use --skill <name> to choose a skill.`);
|
|
111
|
+
}
|
|
112
|
+
if (out.source === DEFAULT_SKILLS_SOURCE && !out.copySource) {
|
|
113
|
+
out.source = undefined;
|
|
114
|
+
}
|
|
115
|
+
out.skillNames = unique(out.skillNames.map(normalizeSkillName));
|
|
116
|
+
out.clients = unique(out.clients);
|
|
117
|
+
out.instructionFiles = unique(out.instructionFiles);
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
export async function runSkillsCli(argv, options = {}) {
|
|
121
|
+
const parsed = parseSkillsCliArgs(argv);
|
|
122
|
+
if (parsed.command === "help") {
|
|
123
|
+
process.stdout.write(`${HELP}\n`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const skillSource = parsed.source ?? DEFAULT_SKILLS_SOURCE;
|
|
127
|
+
if (parsed.command === "list") {
|
|
128
|
+
const source = await materializeSource(skillSource);
|
|
129
|
+
try {
|
|
130
|
+
const skills = discoverSkills(source.root);
|
|
131
|
+
if (parsed.printJson) {
|
|
132
|
+
process.stdout.write(`${JSON.stringify(skills, null, 2)}\n`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
for (const skill of skills) {
|
|
136
|
+
process.stdout.write(`${skill.name}${skill.description ? ` - ${skill.description}` : ""}\n`);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
source.cleanup?.();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const result = await installSkills({
|
|
145
|
+
source: skillSource,
|
|
146
|
+
skillNames: parsed.skillNames,
|
|
147
|
+
clients: parsed.clients,
|
|
148
|
+
scope: parsed.scope,
|
|
149
|
+
baseDir: parsed.baseDir ?? options.baseDir,
|
|
150
|
+
yes: parsed.yes,
|
|
151
|
+
dryRun: parsed.dryRun,
|
|
152
|
+
updateInstructions: parsed.updateInstructions,
|
|
153
|
+
instructionFiles: parsed.instructionFiles,
|
|
154
|
+
withGithubAction: parsed.withGithubAction,
|
|
155
|
+
force: parsed.force,
|
|
156
|
+
log: parsed.printJson ? undefined : options.log,
|
|
157
|
+
isInteractive: options.isInteractive,
|
|
158
|
+
});
|
|
159
|
+
if (parsed.printJson) {
|
|
160
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const verb = parsed.dryRun ? "Would install" : "Installed";
|
|
164
|
+
process.stdout.write([
|
|
165
|
+
`${verb} ${result.skills.join(", ")} for ${result.clients.join(", ")} (${result.scope}).`,
|
|
166
|
+
result.written.length ? `Skill files: ${result.written.join(", ")}` : "",
|
|
167
|
+
result.instructionFiles.length
|
|
168
|
+
? `Managed instructions: ${result.instructionFiles.join(", ")}`
|
|
169
|
+
: "",
|
|
170
|
+
result.githubActionPath
|
|
171
|
+
? `PR Visual Recap workflow: ${result.githubActionPath}`
|
|
172
|
+
: "",
|
|
173
|
+
parsed.dryRun
|
|
174
|
+
? ""
|
|
175
|
+
: "Restart or reload selected agent clients if needed.",
|
|
176
|
+
]
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.join("\n") + "\n");
|
|
179
|
+
}
|
|
180
|
+
export async function installSkills(options) {
|
|
181
|
+
const baseDir = path.resolve(options.baseDir ?? process.cwd());
|
|
182
|
+
const log = options.log ?? (() => { });
|
|
183
|
+
const sourceInput = options.source ?? DEFAULT_SKILLS_SOURCE;
|
|
184
|
+
const source = await materializeSource(sourceInput);
|
|
185
|
+
try {
|
|
186
|
+
const entries = discoverSkills(source.root);
|
|
187
|
+
if (entries.length === 0) {
|
|
188
|
+
throw new Error(`No skills found in ${sourceInput}. Expected skills/*/SKILL.md.`);
|
|
189
|
+
}
|
|
190
|
+
const selected = await resolveSelectedSkills(entries, options);
|
|
191
|
+
const clients = await resolveSelectedClients(options);
|
|
192
|
+
const scope = options.scope ?? "user";
|
|
193
|
+
const written = [];
|
|
194
|
+
for (const client of clients) {
|
|
195
|
+
const root = installRootForClient(client, scope, baseDir);
|
|
196
|
+
for (const skill of selected) {
|
|
197
|
+
const destination = path.join(root, skill.name);
|
|
198
|
+
written.push(destination);
|
|
199
|
+
if (!options.dryRun) {
|
|
200
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
201
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
202
|
+
fs.cpSync(skill.dir, destination, { recursive: true });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const instructionFiles = await maybeUpdateInstructions(selected.map((skill) => skill.name), baseDir, options);
|
|
207
|
+
const githubActionPath = selected.some((skill) => skill.name === "visual-recap") &&
|
|
208
|
+
(options.withGithubAction ||
|
|
209
|
+
(await shouldPromptGithubAction(options, baseDir)))
|
|
210
|
+
? writePrVisualRecapWorkflow(baseDir, options)
|
|
211
|
+
: undefined;
|
|
212
|
+
log(`Resolved ${selected.length} skill${selected.length === 1 ? "" : "s"} from ${source.root}.`);
|
|
213
|
+
return {
|
|
214
|
+
source: source.root,
|
|
215
|
+
skills: selected.map((skill) => skill.name),
|
|
216
|
+
clients,
|
|
217
|
+
scope,
|
|
218
|
+
written,
|
|
219
|
+
instructionFiles,
|
|
220
|
+
githubActionPath,
|
|
221
|
+
dryRun: Boolean(options.dryRun),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
source.cleanup?.();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function defaultArgs(command) {
|
|
229
|
+
return {
|
|
230
|
+
command,
|
|
231
|
+
copySource: false,
|
|
232
|
+
skillNames: [],
|
|
233
|
+
clients: [],
|
|
234
|
+
scope: "user",
|
|
235
|
+
yes: false,
|
|
236
|
+
dryRun: false,
|
|
237
|
+
printJson: false,
|
|
238
|
+
instructionFiles: [],
|
|
239
|
+
withGithubAction: false,
|
|
240
|
+
force: false,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function parseScope(value) {
|
|
244
|
+
if (value === "user" || value === "project")
|
|
245
|
+
return value;
|
|
246
|
+
throw new Error("--scope must be user or project.");
|
|
247
|
+
}
|
|
248
|
+
function normalizeClients(value) {
|
|
249
|
+
return value.split(",").flatMap((raw) => {
|
|
250
|
+
const client = raw.trim().toLowerCase();
|
|
251
|
+
if (!client)
|
|
252
|
+
return [];
|
|
253
|
+
if (client === "all")
|
|
254
|
+
return CLIENTS;
|
|
255
|
+
if (client === "codex")
|
|
256
|
+
return ["codex"];
|
|
257
|
+
if (client === "claude" ||
|
|
258
|
+
client === "claude-code" ||
|
|
259
|
+
client === "claude-code-cli") {
|
|
260
|
+
return ["claude-code"];
|
|
261
|
+
}
|
|
262
|
+
throw new Error(`Unsupported client "${raw}". Use codex, claude-code, or all.`);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
function normalizeSkillName(value) {
|
|
266
|
+
const normalized = value.trim().toLowerCase();
|
|
267
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(normalized)) {
|
|
268
|
+
throw new Error(`Invalid skill name "${value}".`);
|
|
269
|
+
}
|
|
270
|
+
return normalized;
|
|
271
|
+
}
|
|
272
|
+
function unique(values) {
|
|
273
|
+
return [...new Set(values)];
|
|
274
|
+
}
|
|
275
|
+
async function resolveSelectedSkills(entries, options) {
|
|
276
|
+
const byName = new Map(entries.map((entry) => [entry.name, entry]));
|
|
277
|
+
const requested = unique((options.skillNames ?? []).map(normalizeSkillName));
|
|
278
|
+
if (requested.length > 0) {
|
|
279
|
+
const missing = requested.filter((name) => !byName.has(name));
|
|
280
|
+
if (missing.length > 0) {
|
|
281
|
+
throw new Error(`Unknown skill${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}. Available: ${entries
|
|
282
|
+
.map((entry) => entry.name)
|
|
283
|
+
.join(", ")}.`);
|
|
284
|
+
}
|
|
285
|
+
return requested.map((name) => byName.get(name));
|
|
286
|
+
}
|
|
287
|
+
if (!isInteractive(options) || options.yes)
|
|
288
|
+
return entries;
|
|
289
|
+
const answer = await promptLine([
|
|
290
|
+
"Which skills do you want to install?",
|
|
291
|
+
...entries.map((entry, index) => ` ${index + 1}. ${entry.name}${entry.description ? ` - ${entry.description}` : ""}`),
|
|
292
|
+
"Enter numbers or names separated by commas, or press Enter for all: ",
|
|
293
|
+
].join("\n"));
|
|
294
|
+
const trimmed = answer.trim();
|
|
295
|
+
if (!trimmed)
|
|
296
|
+
return entries;
|
|
297
|
+
const selectedNames = trimmed
|
|
298
|
+
.split(",")
|
|
299
|
+
.map((part) => part.trim())
|
|
300
|
+
.filter(Boolean)
|
|
301
|
+
.map((part) => {
|
|
302
|
+
const asNumber = Number(part);
|
|
303
|
+
if (Number.isInteger(asNumber) &&
|
|
304
|
+
asNumber >= 1 &&
|
|
305
|
+
asNumber <= entries.length) {
|
|
306
|
+
return entries[asNumber - 1].name;
|
|
307
|
+
}
|
|
308
|
+
return normalizeSkillName(part);
|
|
309
|
+
});
|
|
310
|
+
return resolveSelectedSkills(entries, {
|
|
311
|
+
...options,
|
|
312
|
+
skillNames: selectedNames,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async function resolveSelectedClients(options) {
|
|
316
|
+
const requested = unique(options.clients ?? []);
|
|
317
|
+
if (requested.length > 0)
|
|
318
|
+
return requested;
|
|
319
|
+
if (!isInteractive(options) || options.yes)
|
|
320
|
+
return ["codex"];
|
|
321
|
+
const answer = await promptLine("Install for which clients? Enter codex, claude-code, or all [codex]: ");
|
|
322
|
+
const trimmed = answer.trim();
|
|
323
|
+
return trimmed ? unique(normalizeClients(trimmed)) : ["codex"];
|
|
324
|
+
}
|
|
325
|
+
function isInteractive(options) {
|
|
326
|
+
if (options.isInteractive)
|
|
327
|
+
return options.isInteractive();
|
|
328
|
+
if (process.env.CI === "true")
|
|
329
|
+
return false;
|
|
330
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
331
|
+
}
|
|
332
|
+
async function promptLine(question) {
|
|
333
|
+
const rl = readline.createInterface({
|
|
334
|
+
input: process.stdin,
|
|
335
|
+
output: process.stdout,
|
|
336
|
+
});
|
|
337
|
+
try {
|
|
338
|
+
return await rl.question(question);
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
rl.close();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function installRootForClient(client, scope, baseDir) {
|
|
345
|
+
const home = process.env.HOME || os.homedir();
|
|
346
|
+
if (scope === "project") {
|
|
347
|
+
return client === "codex"
|
|
348
|
+
? path.join(baseDir, ".agents", "skills")
|
|
349
|
+
: path.join(baseDir, ".claude", "skills");
|
|
350
|
+
}
|
|
351
|
+
if (client === "codex") {
|
|
352
|
+
return process.env.CODEX_HOME
|
|
353
|
+
? path.join(process.env.CODEX_HOME, "skills")
|
|
354
|
+
: path.join(home, ".codex", "skills");
|
|
355
|
+
}
|
|
356
|
+
return path.join(home, ".claude", "skills");
|
|
357
|
+
}
|
|
358
|
+
function discoverSkills(root) {
|
|
359
|
+
const skillsRoot = resolveSkillsRoot(root);
|
|
360
|
+
const directSkill = path.join(skillsRoot, "SKILL.md");
|
|
361
|
+
if (fs.existsSync(directSkill)) {
|
|
362
|
+
const entry = skillEntry(skillsRoot);
|
|
363
|
+
return entry ? [entry] : [];
|
|
364
|
+
}
|
|
365
|
+
const entries = fs
|
|
366
|
+
.readdirSync(skillsRoot, { withFileTypes: true })
|
|
367
|
+
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
|
368
|
+
.map((entry) => skillEntry(path.join(skillsRoot, entry.name)))
|
|
369
|
+
.filter((entry) => Boolean(entry));
|
|
370
|
+
return entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
371
|
+
}
|
|
372
|
+
function resolveSkillsRoot(root) {
|
|
373
|
+
for (const manifestPath of [
|
|
374
|
+
path.join(root, ".codex-plugin", "plugin.json"),
|
|
375
|
+
path.join(root, ".claude-plugin", "plugin.json"),
|
|
376
|
+
]) {
|
|
377
|
+
if (!fs.existsSync(manifestPath))
|
|
378
|
+
continue;
|
|
379
|
+
try {
|
|
380
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
381
|
+
if (typeof manifest.skills === "string") {
|
|
382
|
+
const resolved = path.resolve(root, manifest.skills);
|
|
383
|
+
if (fs.existsSync(resolved))
|
|
384
|
+
return resolved;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
catch { }
|
|
388
|
+
}
|
|
389
|
+
const conventional = path.join(root, "skills");
|
|
390
|
+
if (fs.existsSync(conventional))
|
|
391
|
+
return conventional;
|
|
392
|
+
return root;
|
|
393
|
+
}
|
|
394
|
+
function skillEntry(dir) {
|
|
395
|
+
const skillFile = path.join(dir, "SKILL.md");
|
|
396
|
+
if (!fs.existsSync(skillFile))
|
|
397
|
+
return null;
|
|
398
|
+
const body = fs.readFileSync(skillFile, "utf-8");
|
|
399
|
+
const frontmatter = body.match(/^---\n([\s\S]*?)\n---/);
|
|
400
|
+
const name = frontmatter?.[1]
|
|
401
|
+
?.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]
|
|
402
|
+
?.trim() ?? path.basename(dir);
|
|
403
|
+
const description = frontmatter?.[1]
|
|
404
|
+
?.match(/^description:\s*(?:>-\s*)?(.+)$/m)?.[1]
|
|
405
|
+
?.trim();
|
|
406
|
+
return { name: normalizeSkillName(name), dir, description };
|
|
407
|
+
}
|
|
408
|
+
async function materializeSource(input) {
|
|
409
|
+
const local = path.resolve(input);
|
|
410
|
+
if (fs.existsSync(local))
|
|
411
|
+
return { root: local };
|
|
412
|
+
const parsed = parseGitHubSource(input);
|
|
413
|
+
if (!parsed) {
|
|
414
|
+
throw new Error(`Skill source not found: ${input}. Use a local path, GitHub owner/repo, or GitHub URL.`);
|
|
415
|
+
}
|
|
416
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "agent-native-skills-"));
|
|
417
|
+
const archive = path.join(tmpRoot, "source.tgz");
|
|
418
|
+
const ref = parsed.ref ?? "main";
|
|
419
|
+
const url = `https://codeload.github.com/${parsed.owner}/${parsed.repo}/tar.gz/${encodeURIComponent(ref)}`;
|
|
420
|
+
const response = await fetch(url, {
|
|
421
|
+
headers: { "user-agent": "@agent-native/skills" },
|
|
422
|
+
});
|
|
423
|
+
if (!response.ok) {
|
|
424
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
425
|
+
throw new Error(`Could not download ${parsed.owner}/${parsed.repo}@${ref}: HTTP ${response.status}.`);
|
|
426
|
+
}
|
|
427
|
+
fs.writeFileSync(archive, Buffer.from(await response.arrayBuffer()));
|
|
428
|
+
const extractDir = path.join(tmpRoot, "extract");
|
|
429
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
430
|
+
const extracted = spawnSync("tar", ["-xzf", archive, "-C", extractDir], {
|
|
431
|
+
stdio: "pipe",
|
|
432
|
+
encoding: "utf-8",
|
|
433
|
+
});
|
|
434
|
+
if (extracted.status !== 0) {
|
|
435
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
436
|
+
throw new Error(`Could not extract ${input}: ${extracted.stderr || extracted.stdout}`);
|
|
437
|
+
}
|
|
438
|
+
const [first] = fs
|
|
439
|
+
.readdirSync(extractDir, { withFileTypes: true })
|
|
440
|
+
.filter((entry) => entry.isDirectory());
|
|
441
|
+
if (!first) {
|
|
442
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
443
|
+
throw new Error(`Downloaded archive for ${input} did not contain a repo directory.`);
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
root: path.join(extractDir, first.name),
|
|
447
|
+
cleanup: () => fs.rmSync(tmpRoot, { recursive: true, force: true }),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function parseGitHubSource(input) {
|
|
451
|
+
const shorthand = input.match(/^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(?:#(.+))?$/);
|
|
452
|
+
if (shorthand) {
|
|
453
|
+
return {
|
|
454
|
+
owner: shorthand[1],
|
|
455
|
+
repo: shorthand[2].replace(/\.git$/, ""),
|
|
456
|
+
ref: shorthand[3],
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
let url;
|
|
460
|
+
try {
|
|
461
|
+
url = new URL(input);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
if (url.hostname !== "github.com")
|
|
467
|
+
return null;
|
|
468
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
469
|
+
if (parts.length < 2)
|
|
470
|
+
return null;
|
|
471
|
+
const treeIndex = parts.indexOf("tree");
|
|
472
|
+
return {
|
|
473
|
+
owner: parts[0],
|
|
474
|
+
repo: parts[1].replace(/\.git$/, ""),
|
|
475
|
+
ref: treeIndex >= 0 && parts[treeIndex + 1]
|
|
476
|
+
? parts.slice(treeIndex + 1).join("/")
|
|
477
|
+
: url.hash.slice(1) || undefined,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function instructionContentForSkill(skillName) {
|
|
481
|
+
if (skillName === "quick-recap") {
|
|
482
|
+
return `## Quick Recap Status Block
|
|
483
|
+
|
|
484
|
+
Every response that completes a unit of work must end with one final status line:
|
|
485
|
+
|
|
486
|
+
\`\`\`md
|
|
487
|
+
🟢 Actual concise status sentence
|
|
488
|
+
\`\`\`
|
|
489
|
+
|
|
490
|
+
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.
|
|
491
|
+
|
|
492
|
+
Examples:
|
|
493
|
+
|
|
494
|
+
\`\`\`md
|
|
495
|
+
🟢 Updated quick recap docs with output examples
|
|
496
|
+
\`\`\`
|
|
497
|
+
|
|
498
|
+
\`\`\`md
|
|
499
|
+
🟡 Code updated, set PROVIDER_WEBHOOK_SECRET before testing webhooks
|
|
500
|
+
\`\`\`
|
|
501
|
+
|
|
502
|
+
\`\`\`md
|
|
503
|
+
🔴 Need the production API key to continue
|
|
504
|
+
\`\`\``;
|
|
505
|
+
}
|
|
506
|
+
if (skillName === "efficient-fable") {
|
|
507
|
+
return `## Efficient Fable
|
|
508
|
+
|
|
509
|
+
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.`;
|
|
510
|
+
}
|
|
511
|
+
if (skillName === "efficient-frontier") {
|
|
512
|
+
return `## Efficient Frontier
|
|
513
|
+
|
|
514
|
+
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.`;
|
|
515
|
+
}
|
|
516
|
+
if (skillName === "stay-within-limits") {
|
|
517
|
+
return `## Stay Within Limits
|
|
518
|
+
|
|
519
|
+
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.`;
|
|
520
|
+
}
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
async function maybeUpdateInstructions(skillNames, baseDir, options) {
|
|
524
|
+
const blocks = skillNames
|
|
525
|
+
.map((name) => instructionContentForSkill(name))
|
|
526
|
+
.filter((block) => Boolean(block));
|
|
527
|
+
if (blocks.length === 0)
|
|
528
|
+
return [];
|
|
529
|
+
let shouldUpdate = options.updateInstructions;
|
|
530
|
+
if (shouldUpdate === undefined) {
|
|
531
|
+
if (options.yes)
|
|
532
|
+
shouldUpdate = true;
|
|
533
|
+
else if (isInteractive(options)) {
|
|
534
|
+
const answer = await promptLine("Add managed AGENTS.md / CLAUDE.md instructions for always-on behavior? [Y/n] ");
|
|
535
|
+
shouldUpdate = !/^n/i.test(answer.trim());
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
shouldUpdate = false;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (!shouldUpdate)
|
|
542
|
+
return [];
|
|
543
|
+
const files = resolveInstructionFiles(baseDir, options.instructionFiles);
|
|
544
|
+
const content = `${MANAGED_INSTRUCTIONS_START}
|
|
545
|
+
${blocks.join("\n\n")}
|
|
546
|
+
${MANAGED_INSTRUCTIONS_END}`;
|
|
547
|
+
for (const file of files) {
|
|
548
|
+
if (options.dryRun)
|
|
549
|
+
continue;
|
|
550
|
+
upsertManagedBlock(file, content);
|
|
551
|
+
}
|
|
552
|
+
return files;
|
|
553
|
+
}
|
|
554
|
+
function resolveInstructionFiles(baseDir, explicit) {
|
|
555
|
+
if (explicit && explicit.length > 0) {
|
|
556
|
+
return explicit.map((file) => path.resolve(baseDir, file));
|
|
557
|
+
}
|
|
558
|
+
const candidates = ["AGENTS.md", "CLAUDE.md"].map((file) => path.join(baseDir, file));
|
|
559
|
+
const existing = candidates.filter((file) => fs.existsSync(file));
|
|
560
|
+
return existing.length > 0 ? existing : [path.join(baseDir, "AGENTS.md")];
|
|
561
|
+
}
|
|
562
|
+
function upsertManagedBlock(file, block) {
|
|
563
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
564
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf-8") : "";
|
|
565
|
+
const pattern = new RegExp(`${escapeRegExp(MANAGED_INSTRUCTIONS_START)}[\\s\\S]*?${escapeRegExp(MANAGED_INSTRUCTIONS_END)}`);
|
|
566
|
+
const next = pattern.test(existing)
|
|
567
|
+
? existing.replace(pattern, block)
|
|
568
|
+
: `${existing.trimEnd()}${existing.trim() ? "\n\n" : ""}${block}\n`;
|
|
569
|
+
fs.writeFileSync(file, next, "utf-8");
|
|
570
|
+
}
|
|
571
|
+
function escapeRegExp(value) {
|
|
572
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
573
|
+
}
|
|
574
|
+
async function shouldPromptGithubAction(options, baseDir) {
|
|
575
|
+
if (options.withGithubAction)
|
|
576
|
+
return true;
|
|
577
|
+
if (options.yes || !isInteractive(options))
|
|
578
|
+
return false;
|
|
579
|
+
if (fs.existsSync(path.join(baseDir, ".github", "workflows", "pr-visual-recap.yml"))) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
const answer = await promptLine("Add the optional PR Visual Recap GitHub Action? [y/N] ");
|
|
583
|
+
return /^y/i.test(answer.trim());
|
|
584
|
+
}
|
|
585
|
+
const PR_VISUAL_RECAP_REUSABLE_WORKFLOW = `name: PR Visual Recap
|
|
586
|
+
|
|
587
|
+
on:
|
|
588
|
+
pull_request:
|
|
589
|
+
types: [opened, synchronize, reopened, ready_for_review]
|
|
590
|
+
|
|
591
|
+
permissions:
|
|
592
|
+
contents: read
|
|
593
|
+
|
|
594
|
+
concurrency:
|
|
595
|
+
group: pr-visual-recap-\${{ github.event.pull_request.number }}
|
|
596
|
+
cancel-in-progress: true
|
|
597
|
+
|
|
598
|
+
jobs:
|
|
599
|
+
visual-recap:
|
|
600
|
+
permissions:
|
|
601
|
+
checks: write
|
|
602
|
+
contents: read
|
|
603
|
+
issues: write
|
|
604
|
+
pull-requests: read
|
|
605
|
+
uses: BuilderIO/agent-native/.github/workflows/pr-visual-recap-reusable.yml@main
|
|
606
|
+
with:
|
|
607
|
+
skill-source: repo
|
|
608
|
+
secrets:
|
|
609
|
+
PLAN_RECAP_TOKEN: \${{ secrets.PLAN_RECAP_TOKEN }}
|
|
610
|
+
ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
|
|
611
|
+
OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}
|
|
612
|
+
PLAN_RECAP_APP_URL: \${{ secrets.PLAN_RECAP_APP_URL }}
|
|
613
|
+
`;
|
|
614
|
+
function writePrVisualRecapWorkflow(baseDir, options) {
|
|
615
|
+
const file = path.join(baseDir, ".github", "workflows", "pr-visual-recap.yml");
|
|
616
|
+
if (options.dryRun)
|
|
617
|
+
return file;
|
|
618
|
+
if (fs.existsSync(file)) {
|
|
619
|
+
const current = fs.readFileSync(file, "utf-8");
|
|
620
|
+
if (current === PR_VISUAL_RECAP_REUSABLE_WORKFLOW)
|
|
621
|
+
return file;
|
|
622
|
+
if (!options.force) {
|
|
623
|
+
throw new Error(`${file} already exists and differs. Re-run with --force to overwrite it.`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
627
|
+
fs.writeFileSync(file, PR_VISUAL_RECAP_REUSABLE_WORKFLOW, "utf-8");
|
|
628
|
+
return file;
|
|
629
|
+
}
|
|
630
|
+
export function createInstallId() {
|
|
631
|
+
return randomUUID();
|
|
632
|
+
}
|
|
633
|
+
//# sourceMappingURL=index.js.map
|