@ahood/cli 0.5.1 → 0.6.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 +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/list.js +5 -1
- package/dist/commands/mcp.js +7 -0
- package/dist/commands/publish.js +52 -13
- package/dist/commands/read.js +26 -8
- package/dist/commands/remove.js +44 -5
- package/dist/commands/search.js +17 -11
- 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 +8 -3
- package/dist/commands/view.js +8 -4
- package/dist/commands/whoami.js +58 -33
- package/dist/config.js +3 -2
- package/dist/exit-code.js +9 -0
- package/dist/flags.js +2 -1
- package/dist/help.js +95 -18
- package/dist/index.js +2 -0
- package/dist/lockfile.js +16 -7
- package/dist/mcp/safe-tool.js +46 -0
- package/dist/mcp/server.js +20 -0
- package/dist/mcp/tools.js +61 -0
- package/dist/secret-prompt.js +74 -0
- package/dist/spec.js +33 -9
- package/dist/usage-error.js +8 -0
- package/package.json +4 -2
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ahood/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "CLI for ahood, a registry for installing and publishing Claude Code skills.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/AlexKay28/ahood-cli",
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
"sync-readme": "node scripts/sync-readme.mjs"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
36
|
+
"tar-stream": "^3.2.1",
|
|
37
|
+
"zod": "^4.5.4"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
40
|
"@types/node": "^20",
|