@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/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 Error(`Invalid ${kind} "${value}" in "${spec}" -- must start with a letter/digit and contain only letters, digits, ".", "_", or "-".`);
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 Error(`Invalid ${kind} in "${spec.slice(0, 60)}${spec.length > 60 ? "..." : ""}" -- ${kind} segment is too long (${value.length} characters; must be at most ${maxLength}).`);
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 Error(usage);
30
+ throw new UsageError(usage);
30
31
  const parts = spec.split("/");
31
32
  if (parts.length !== 2 || !parts[0] || !parts[1])
32
- throw new Error(usage);
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 Error(usage);
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 Error(`Missing version after "@" in "${spec}".\n${usage}`);
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 Error(`Invalid version "${version}" in "${spec}" -- expected "latest" or a semver like 1.2.3.\n${usage}`);
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 Error(`${flag} must be a valid http:// or https:// URL (got "${value}").`);
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 Error(`${flag} must use http:// or https:// (got scheme "${url.protocol}" from "${value}").`);
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.5.1",
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
- "tar-stream": "^3.2.1"
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",