@ahood/cli 0.5.0 → 0.5.2
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 +6 -3
- package/dist/commands/add.js +265 -9
- package/dist/commands/completion.js +2 -1
- package/dist/commands/edit.js +5 -4
- package/dist/commands/group.js +8 -7
- package/dist/commands/init.js +8 -7
- package/dist/commands/publish.js +52 -13
- package/dist/commands/read.js +2 -1
- package/dist/commands/remove.js +44 -5
- package/dist/commands/search.js +5 -4
- package/dist/commands/share.js +5 -4
- package/dist/commands/star.js +2 -2
- package/dist/commands/token.js +4 -3
- package/dist/commands/unpublish.js +5 -4
- package/dist/commands/update.js +20 -3
- package/dist/commands/versions.js +3 -2
- package/dist/commands/view.js +4 -3
- package/dist/commands/whoami.js +9 -3
- package/dist/config.js +3 -2
- package/dist/exit-code.js +9 -0
- package/dist/flags.js +2 -1
- package/dist/help.js +87 -18
- package/dist/lockfile.js +16 -7
- package/dist/secret-prompt.js +74 -0
- package/dist/spec.js +33 -9
- package/dist/usage-error.js +8 -0
- package/package.json +1 -1
package/dist/help.js
CHANGED
|
@@ -71,7 +71,11 @@ export const SKILL_COMMANDS_HELP = [
|
|
|
71
71
|
{
|
|
72
72
|
usage: "ahood skill add <owner>/<skill>[@version]",
|
|
73
73
|
summary: "Install a skill into .claude/skills/, pinned in the lockfile.",
|
|
74
|
-
desc: "Install a skill into .claude/skills/, pinned in the lockfile."
|
|
74
|
+
desc: "Install a skill into .claude/skills/, pinned in the lockfile. An artifact published with --kind agent " +
|
|
75
|
+
"installs instead as a single file at .claude/agents/<owner>@<skill>.md (Claude Code's own subagent " +
|
|
76
|
+
"loader scans .claude/agents/*.md as flat files), not a .claude/skills/ directory. An artifact published " +
|
|
77
|
+
"with --kind mcp installs by merging an entry into .mcp.json instead, prompting for any secret " +
|
|
78
|
+
"environment variables its server.json declares that aren't already set in your shell.",
|
|
75
79
|
examples: ["ahood skill add alice/pdf-tools", "ahood skill add alice/pdf-tools@1.2.0"],
|
|
76
80
|
},
|
|
77
81
|
{
|
|
@@ -143,20 +147,23 @@ export const SKILL_COMMANDS_HELP = [
|
|
|
143
147
|
examples: ["ahood skill init pdf-tools", "ahood skill init"],
|
|
144
148
|
},
|
|
145
149
|
{
|
|
146
|
-
usage: "ahood skill publish <owner>/<skill>@<version> [--path <dir>] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--json]",
|
|
147
|
-
summary: "Publish a new version of a skill from a folder containing SKILL.md, creating the skill first if it doesn't already exist.",
|
|
148
|
-
desc: "Publish a new version of a skill from a folder containing SKILL.md. If the
|
|
149
|
-
"creates it first -- pass --name
|
|
150
|
+
usage: "ahood skill publish <owner>/<skill>@<version> [--path <dir>] [--kind skill|agent|mcp] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--json]",
|
|
151
|
+
summary: "Publish a new version of a skill, agent, or mcp server manifest from a folder containing SKILL.md, AGENT.md, or server.json, creating the skill first if it doesn't already exist.",
|
|
152
|
+
desc: "Publish a new version of a skill, agent, or mcp server manifest from a folder containing SKILL.md, AGENT.md, or server.json. If the artifact " +
|
|
153
|
+
"doesn't exist yet, this creates it first -- pass --name to set its display name, and --kind agent or --kind mcp if " +
|
|
154
|
+
"publishing an AGENT.md or server.json (auto-detected from the folder contents when only one of SKILL.md/AGENT.md/server.json is present). " +
|
|
155
|
+
"--name is required when creating; optionally also pass --tagline/--tags/--license/--homepage/--repository. " +
|
|
150
156
|
"Processing happens server-side after upload; this command polls and reports the final published/failed status. " +
|
|
151
157
|
"The legacy form `ahood skill publish <path> --owner <owner> --slug <skill> --version <x.y.z>` is still accepted. " +
|
|
152
158
|
"Every flag also accepts the --flag=value form, needed when a value itself starts with --.",
|
|
153
159
|
flags: [
|
|
154
|
-
"--
|
|
160
|
+
"--kind skill|agent|mcp What kind of artifact this is. Auto-detected from SKILL.md/AGENT.md/server.json when omitted.",
|
|
161
|
+
"--name <text> Required only when creating the artifact on this publish.",
|
|
155
162
|
"--tagline <text> Short one-line description, used only when creating.",
|
|
156
163
|
"--tags <comma,separated> Initial tags, used only when creating.",
|
|
157
164
|
"--license <id> An SPDX license identifier, e.g. MIT, used only when creating.",
|
|
158
|
-
"--homepage <url> The
|
|
159
|
-
"--repository <url> The
|
|
165
|
+
"--homepage <url> The artifact's homepage URL, used only when creating.",
|
|
166
|
+
"--repository <url> The artifact's source repository URL, used only when creating.",
|
|
160
167
|
"--json Suppress human progress lines; print one {version,status,...} JSON object on completion (or {error} on failure).",
|
|
161
168
|
],
|
|
162
169
|
examples: [
|
|
@@ -278,10 +285,74 @@ export function formatCommandHelp(entry) {
|
|
|
278
285
|
lines.push("", "Examples:", ...entry.examples.map((e) => ` ${e}`));
|
|
279
286
|
return lines.join("\n");
|
|
280
287
|
}
|
|
288
|
+
const FALLBACK_TERMINAL_WIDTH = 80;
|
|
289
|
+
// Piped/redirected output (a script capturing `ahood skill --help`, a test)
|
|
290
|
+
// has no TTY and so no column count -- falls back to a fixed width rather
|
|
291
|
+
// than wrapping to whatever width happened to be inherited, so scripted use
|
|
292
|
+
// gets stable, repeatable output.
|
|
293
|
+
function terminalWidth() {
|
|
294
|
+
const columns = process.stdout.columns;
|
|
295
|
+
return columns && columns > 20 ? columns : FALLBACK_TERMINAL_WIDTH;
|
|
296
|
+
}
|
|
297
|
+
// Greedy word wrap -- doesn't split words, so a single word longer than
|
|
298
|
+
// `width` is left on its own overlength line rather than being cut mid-word.
|
|
299
|
+
function wrapText(text, width) {
|
|
300
|
+
if (width < 10)
|
|
301
|
+
return [text];
|
|
302
|
+
const words = text.split(" ");
|
|
303
|
+
const lines = [];
|
|
304
|
+
let current = "";
|
|
305
|
+
for (const word of words) {
|
|
306
|
+
const candidate = current ? `${current} ${word}` : word;
|
|
307
|
+
if (candidate.length > width && current) {
|
|
308
|
+
lines.push(current);
|
|
309
|
+
current = word;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
current = candidate;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (current)
|
|
316
|
+
lines.push(current);
|
|
317
|
+
return lines;
|
|
318
|
+
}
|
|
319
|
+
// Longest usage string an aligned label column will stretch to accommodate
|
|
320
|
+
// -- past this, one outlier (e.g. `skill publish`'s usage line, with every
|
|
321
|
+
// flag spelled out) would otherwise push every other row's description
|
|
322
|
+
// column out with it, which is what made `ahood skill --help` wrap to
|
|
323
|
+
// 300+ characters per line with no relation to the terminal's actual width
|
|
324
|
+
// (ahood-cli#81). A row whose usage is longer than this gets its summary on
|
|
325
|
+
// its own indented line instead of sharing the row.
|
|
326
|
+
const MAX_LABEL_COLUMN = 50;
|
|
327
|
+
// Renders a two-column command table (usage on the left, one-line summary on
|
|
328
|
+
// the right, aligned and wrapped to the terminal width) -- shared by
|
|
329
|
+
// formatSkillHelp/formatGroupHelp/formatHelp so all three `--help` listings
|
|
330
|
+
// wrap the same way.
|
|
331
|
+
function formatCommandTable(rows) {
|
|
332
|
+
const longest = Math.max(...rows.map((r) => r.usage.length));
|
|
333
|
+
const labelWidth = Math.min(longest, MAX_LABEL_COLUMN);
|
|
334
|
+
const columnWidth = labelWidth + 4; // 2-space left margin + 2-space gutter after the label
|
|
335
|
+
const summaryWidth = Math.max(20, terminalWidth() - columnWidth);
|
|
336
|
+
const indent = " ".repeat(columnWidth);
|
|
337
|
+
const lines = [];
|
|
338
|
+
for (const row of rows) {
|
|
339
|
+
const summaryLines = wrapText(row.summary, summaryWidth);
|
|
340
|
+
if (row.usage.length <= labelWidth) {
|
|
341
|
+
lines.push(` ${row.usage.padEnd(labelWidth + 2)}${summaryLines[0]}`);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
lines.push(` ${row.usage}`);
|
|
345
|
+
lines.push(`${indent}${summaryLines[0]}`);
|
|
346
|
+
}
|
|
347
|
+
for (const continuation of summaryLines.slice(1)) {
|
|
348
|
+
lines.push(`${indent}${continuation}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return lines;
|
|
352
|
+
}
|
|
281
353
|
// `ahood skill --help` -- the group-level listing for every skill verb.
|
|
282
354
|
export function formatSkillHelp() {
|
|
283
|
-
const
|
|
284
|
-
const lines = SKILL_COMMANDS_HELP.map((c) => ` ${usageWithAliases(c).padEnd(width + 2)}${c.summary}`);
|
|
355
|
+
const lines = formatCommandTable(SKILL_COMMANDS_HELP.map((c) => ({ usage: usageWithAliases(c), summary: c.summary })));
|
|
285
356
|
return [
|
|
286
357
|
"ahood skill -- manage skills in the ahood registry",
|
|
287
358
|
"",
|
|
@@ -293,8 +364,7 @@ export function formatSkillHelp() {
|
|
|
293
364
|
}
|
|
294
365
|
// `ahood group --help` -- the group-level listing for every group verb.
|
|
295
366
|
export function formatGroupHelp() {
|
|
296
|
-
const
|
|
297
|
-
const lines = GROUP_COMMANDS_HELP.map((c) => ` ${usageWithAliases(c).padEnd(width + 2)}${c.summary}`);
|
|
367
|
+
const lines = formatCommandTable(GROUP_COMMANDS_HELP.map((c) => ({ usage: usageWithAliases(c), summary: c.summary })));
|
|
298
368
|
return [
|
|
299
369
|
"ahood group -- create private groups, invite members, and share skills with them",
|
|
300
370
|
"",
|
|
@@ -312,12 +382,11 @@ export function formatHelp() {
|
|
|
312
382
|
const skillGroupSummary = "Search, install, and publish skills -- run `ahood skill --help` for the full list.";
|
|
313
383
|
const groupGroupUsage = "ahood group <command>";
|
|
314
384
|
const groupGroupSummary = "Create private groups and share skills with them -- run `ahood group --help` for the full list.";
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
];
|
|
385
|
+
const lines = formatCommandTable([
|
|
386
|
+
...TOP_LEVEL_COMMANDS_HELP.map((c) => ({ usage: usageWithAliases(c), summary: c.summary })),
|
|
387
|
+
{ usage: skillGroupUsage, summary: skillGroupSummary },
|
|
388
|
+
{ usage: groupGroupUsage, summary: groupGroupSummary },
|
|
389
|
+
]);
|
|
321
390
|
return [
|
|
322
391
|
"ahood -- CLI for the ahood skills registry (https://ahood.vercel.app)",
|
|
323
392
|
"",
|
package/dist/lockfile.js
CHANGED
|
@@ -15,20 +15,29 @@ export function readLockfile(path) {
|
|
|
15
15
|
throw new Error(`Lockfile at ${path} is corrupted and could not be parsed as JSON. Fix or delete it before continuing.`);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
// Generalized so callers other than the lockfile itself (e.g. add.ts's
|
|
19
|
+
// .mcp.json merge, which sits right next to the lockfile and can hold other
|
|
20
|
+
// servers' secrets) get the same crash-safety guarantee instead of a bare
|
|
21
|
+
// writeFileSync that risks truncating the file on interruption.
|
|
22
|
+
export function writeJsonFileAtomic(path, data) {
|
|
19
23
|
mkdirSync(dirname(path), { recursive: true });
|
|
20
24
|
// Write-then-rename so a process interrupted mid-write never leaves a
|
|
21
|
-
// truncated
|
|
22
|
-
//
|
|
25
|
+
// truncated file on disk -- a reader always sees either the old or the new
|
|
26
|
+
// complete content, never a partial one.
|
|
23
27
|
const tmpPath = `${path}.tmp-${process.pid}-${process.hrtime.bigint()}`;
|
|
24
|
-
writeFileSync(tmpPath, JSON.stringify(
|
|
28
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2) + "\n");
|
|
25
29
|
renameSync(tmpPath, path);
|
|
26
30
|
}
|
|
31
|
+
function writeLockfile(path, lockfile) {
|
|
32
|
+
writeJsonFileAtomic(path, lockfile);
|
|
33
|
+
}
|
|
27
34
|
// Simple advisory lock via mkdir's atomicity (EEXIST on a second caller),
|
|
28
|
-
// so two concurrent `ahood add`/`remove` invocations against the same
|
|
35
|
+
// so two concurrent `ahood skill add`/`remove` invocations against the same
|
|
29
36
|
// project don't race a read-modify-write and silently drop one another's
|
|
30
|
-
// entry.
|
|
31
|
-
|
|
37
|
+
// entry. Exported so other same-directory JSON files with the same
|
|
38
|
+
// read-modify-write shape (e.g. add.ts's .mcp.json merge) can reuse it
|
|
39
|
+
// instead of duplicating the pattern.
|
|
40
|
+
export function withLock(path, fn) {
|
|
32
41
|
mkdirSync(dirname(path), { recursive: true });
|
|
33
42
|
const lockDir = `${path}.lock`;
|
|
34
43
|
const deadline = Date.now() + 5000;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
// Masked secret-value prompt, extending confirm.ts's TTY-safe "close" race
|
|
3
|
+
// (see confirm.ts's own comment): the same closed-stdin-never-resolves
|
|
4
|
+
// problem, and the same fix -- a "close" listener rejects if it fires
|
|
5
|
+
// before an answer is captured. Kept as its own module rather than folded
|
|
6
|
+
// into confirm.ts because that module's contract is yes/no, not a typed
|
|
7
|
+
// value, and because masking requires raw-mode keystroke handling that a
|
|
8
|
+
// plain rl.question() call cannot do (readline always echoes verbatim when
|
|
9
|
+
// its input is a TTY).
|
|
10
|
+
export function promptSecret(promptText) {
|
|
11
|
+
const stdin = process.stdin;
|
|
12
|
+
if (!stdin.isTTY) {
|
|
13
|
+
// Non-interactive stdin (piped input, CI, or these tests' fake
|
|
14
|
+
// Readable): fall back to an ordinary line-buffered question. There is
|
|
15
|
+
// no keystroke stream to mask in this mode -- the whole line arrives as
|
|
16
|
+
// one chunk -- and masking would only hide a value that's already
|
|
17
|
+
// sitting unmasked in whatever produced the pipe.
|
|
18
|
+
const rl = createInterface({ input: stdin, output: process.stdout });
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
let settled = false;
|
|
21
|
+
rl.once("close", () => {
|
|
22
|
+
if (settled)
|
|
23
|
+
return;
|
|
24
|
+
settled = true;
|
|
25
|
+
reject(new Error("Prompt was not answered (stdin closed)."));
|
|
26
|
+
});
|
|
27
|
+
rl.question(promptText, (answer) => {
|
|
28
|
+
settled = true;
|
|
29
|
+
rl.close();
|
|
30
|
+
resolve(answer);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
process.stdout.write(promptText);
|
|
35
|
+
stdin.setRawMode(true);
|
|
36
|
+
stdin.resume();
|
|
37
|
+
stdin.setEncoding("utf8");
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
let value = "";
|
|
40
|
+
function cleanup() {
|
|
41
|
+
stdin.removeListener("data", onData);
|
|
42
|
+
stdin.setRawMode(false);
|
|
43
|
+
stdin.pause();
|
|
44
|
+
}
|
|
45
|
+
function onData(chunk) {
|
|
46
|
+
for (const char of chunk) {
|
|
47
|
+
if (char === "\n" || char === "\r") {
|
|
48
|
+
cleanup();
|
|
49
|
+
process.stdout.write("\n");
|
|
50
|
+
resolve(value);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (char === "\u0003") {
|
|
54
|
+
// Ctrl-C
|
|
55
|
+
cleanup();
|
|
56
|
+
process.stdout.write("\n");
|
|
57
|
+
reject(new Error("Prompt cancelled (Ctrl-C)."));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (char === "\u007f" || char === "\b") {
|
|
61
|
+
// Backspace/delete
|
|
62
|
+
if (value.length > 0) {
|
|
63
|
+
value = value.slice(0, -1);
|
|
64
|
+
process.stdout.write("\b \b");
|
|
65
|
+
}
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
value += char;
|
|
69
|
+
process.stdout.write("*");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
stdin.on("data", onData);
|
|
73
|
+
});
|
|
74
|
+
}
|
package/dist/spec.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { UsageError } from "./usage-error.js";
|
|
2
3
|
// Owner and skill segments become filesystem path components (.claude/skills/<owner>/<skill>)
|
|
3
4
|
// and URL path segments, so both are restricted to a safe charset that cannot contain "..",
|
|
4
5
|
// "/", "\", or a leading dot -- this is what closes the path-traversal hole in add/remove
|
|
@@ -17,19 +18,19 @@ export const SEMVER_RE = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-
|
|
|
17
18
|
const MAX_SEGMENT_LENGTH = { owner: 32, skill: 64 };
|
|
18
19
|
export function validateSegment(value, kind, spec) {
|
|
19
20
|
if (!SEGMENT_RE.test(value) || value === "." || value === "..") {
|
|
20
|
-
throw new
|
|
21
|
+
throw new UsageError(`Invalid ${kind} "${value}" in "${spec}" -- must start with a letter/digit and contain only letters, digits, ".", "_", or "-".`);
|
|
21
22
|
}
|
|
22
23
|
const maxLength = MAX_SEGMENT_LENGTH[kind];
|
|
23
24
|
if (value.length > maxLength) {
|
|
24
|
-
throw new
|
|
25
|
+
throw new UsageError(`Invalid ${kind} in "${spec.slice(0, 60)}${spec.length > 60 ? "..." : ""}" -- ${kind} segment is too long (${value.length} characters; must be at most ${maxLength}).`);
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
export function parseOwnerSkill(spec, usage) {
|
|
28
29
|
if (!spec)
|
|
29
|
-
throw new
|
|
30
|
+
throw new UsageError(usage);
|
|
30
31
|
const parts = spec.split("/");
|
|
31
32
|
if (parts.length !== 2 || !parts[0] || !parts[1])
|
|
32
|
-
throw new
|
|
33
|
+
throw new UsageError(usage);
|
|
33
34
|
const [owner, skill] = parts;
|
|
34
35
|
validateSegment(owner, "owner", spec);
|
|
35
36
|
validateSegment(skill, "skill", spec);
|
|
@@ -37,15 +38,15 @@ export function parseOwnerSkill(spec, usage) {
|
|
|
37
38
|
}
|
|
38
39
|
export function parseOwnerSkillVersion(spec, usage) {
|
|
39
40
|
if (!spec)
|
|
40
|
-
throw new
|
|
41
|
+
throw new UsageError(usage);
|
|
41
42
|
const atIndex = spec.lastIndexOf("@");
|
|
42
43
|
const ownerSkill = atIndex > 0 ? spec.slice(0, atIndex) : spec;
|
|
43
44
|
const version = atIndex > 0 ? spec.slice(atIndex + 1) : "latest";
|
|
44
45
|
if (atIndex > 0 && version === "") {
|
|
45
|
-
throw new
|
|
46
|
+
throw new UsageError(`Missing version after "@" in "${spec}".\n${usage}`);
|
|
46
47
|
}
|
|
47
48
|
if (version !== "latest" && !SEMVER_RE.test(version)) {
|
|
48
|
-
throw new
|
|
49
|
+
throw new UsageError(`Invalid version "${version}" in "${spec}" -- expected "latest" or a semver like 1.2.3.\n${usage}`);
|
|
49
50
|
}
|
|
50
51
|
const { owner, skill } = parseOwnerSkill(ownerSkill, usage);
|
|
51
52
|
return { owner, skill, version };
|
|
@@ -67,10 +68,10 @@ export function validateExternalUrl(value, flag) {
|
|
|
67
68
|
url = new URL(value);
|
|
68
69
|
}
|
|
69
70
|
catch {
|
|
70
|
-
throw new
|
|
71
|
+
throw new UsageError(`${flag} must be a valid http:// or https:// URL (got "${value}").`);
|
|
71
72
|
}
|
|
72
73
|
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
73
|
-
throw new
|
|
74
|
+
throw new UsageError(`${flag} must use http:// or https:// (got scheme "${url.protocol}" from "${value}").`);
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
export const SKILLS_ROOT = join(".claude", "skills");
|
|
@@ -78,3 +79,26 @@ export const LOCKFILE_PATH = join(".claude", "skills.lock.json");
|
|
|
78
79
|
export function skillDir(owner, skill) {
|
|
79
80
|
return join(SKILLS_ROOT, owner, skill);
|
|
80
81
|
}
|
|
82
|
+
// Claude Code's own subagent loader scans .claude/agents/*.md as flat files
|
|
83
|
+
// (non-recursive), unlike .claude/skills/<owner>/<skill>/ which nests by
|
|
84
|
+
// owner -- a bare .claude/agents/<skill>.md would let two different owners'
|
|
85
|
+
// same-named agent collide and silently overwrite each other. Joining the
|
|
86
|
+
// owner into the filename with "@" keeps the layout flat (so Claude Code
|
|
87
|
+
// still finds it) while staying collision-resistant: SEGMENT_RE's character
|
|
88
|
+
// class is exactly [a-z0-9._-] (case-insensitive), which does not include
|
|
89
|
+
// "@" -- so "@" can never appear inside a valid owner or skill segment, and
|
|
90
|
+
// the joined filename always contains exactly one "@", at the boundary
|
|
91
|
+
// between the two segments. That makes the join unambiguous for any pair of
|
|
92
|
+
// SEGMENT_RE-valid values, including ones containing hyphens (e.g.
|
|
93
|
+
// owner="al-ice"/skill="bob" joins to "al-ice@bob.md", which cannot also be
|
|
94
|
+
// produced by any other valid owner/skill pair). "@" is a valid filename
|
|
95
|
+
// character on Linux, macOS, and Windows.
|
|
96
|
+
export const AGENTS_ROOT = join(".claude", "agents");
|
|
97
|
+
export function agentPath(owner, skill) {
|
|
98
|
+
return join(AGENTS_ROOT, `${owner}@${skill}.md`);
|
|
99
|
+
}
|
|
100
|
+
// .mcp.json lives at the project root -- Claude Code's own convention for
|
|
101
|
+
// MCP server configuration (unlike .claude/skills or .claude/agents, this
|
|
102
|
+
// file isn't under .claude/ at all, since it's a convention MCP clients
|
|
103
|
+
// broadly share, not one specific to this CLI).
|
|
104
|
+
export const MCP_CONFIG_PATH = ".mcp.json";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Thrown for a usage/validation error caught locally, before any network
|
|
2
|
+
// call -- a missing/malformed argument, an invalid flag value, a bad
|
|
3
|
+
// owner/skill spec, etc. Kept distinct from a plain Error so exit-code.ts's
|
|
4
|
+
// exitCodeFor() can map it to exit code 2 ("usage or validation error", per
|
|
5
|
+
// the README's exit-code table) instead of falling through to the generic 1
|
|
6
|
+
// ("general error") that every other thrown Error gets (ahood-cli#80).
|
|
7
|
+
export class UsageError extends Error {
|
|
8
|
+
}
|
package/package.json
CHANGED