@ahood/cli 0.11.0 → 0.12.1

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.
@@ -0,0 +1,80 @@
1
+ // Git remotes for project guides (ahood-cli#185): reading this repository's
2
+ // origin, and normalizing a remote to the `host/owner/repo` form the ahood
3
+ // server stores and matches on. normalizeProjectRemote is a line-for-line port
4
+ // of the server's lib/projects/normalize-project-remote.ts (ahood#778) -- the
5
+ // two must agree, or `project init` writes a remote the lookup never matches.
6
+ // tests/project-remote.test.ts runs the server's own test table against it.
7
+ import { execFileSync } from "node:child_process";
8
+ const HOST_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
9
+ const PATH_SEGMENT = /^[a-z0-9._-]+$/;
10
+ const ALLOWED_SCHEMES = ["https", "http", "ssh", "git"];
11
+ const MAX_PROJECT_REMOTE_LENGTH = 255;
12
+ const MAX_PATH_SEGMENTS = 8;
13
+ // Never echoes the input back: a rejected remote can still carry a
14
+ // credential (`https://user:token@...`).
15
+ const INVALID = {
16
+ ok: false,
17
+ reason: "not a git remote; expected host/owner/repo, e.g. github.com/owner/repo",
18
+ };
19
+ // Strips the scheme, any userinfo (`user:token@`), the port, a trailing
20
+ // `.git` and trailing slashes, then lowercases everything.
21
+ export function normalizeProjectRemote(raw) {
22
+ const input = raw.trim();
23
+ if (!input || input.length > MAX_PROJECT_REMOTE_LENGTH)
24
+ return INVALID;
25
+ if (/[\s?#\\]/.test(input))
26
+ return INVALID;
27
+ let rest;
28
+ const scheme = input.match(/^([a-z][a-z0-9+.-]*):\/\//i);
29
+ if (scheme) {
30
+ if (!ALLOWED_SCHEMES.includes(scheme[1].toLowerCase()))
31
+ return INVALID;
32
+ rest = input.slice(scheme[0].length);
33
+ }
34
+ else {
35
+ // scp-like form: [user@]host:path, where the path does not start with "/".
36
+ const scp = input.match(/^(?:[^@/:]+@)?([^@/:]+):(?!\/)(.+)$/);
37
+ rest = scp ? `${scp[1]}/${scp[2]}` : input;
38
+ }
39
+ const slash = rest.indexOf("/");
40
+ if (slash <= 0)
41
+ return INVALID;
42
+ let host = rest.slice(0, slash);
43
+ const at = host.lastIndexOf("@");
44
+ if (at !== -1)
45
+ host = host.slice(at + 1);
46
+ host = host.replace(/:\d+$/, "").toLowerCase();
47
+ const labels = host.split(".");
48
+ if (labels.length < 2 || !labels.every((label) => HOST_LABEL.test(label)))
49
+ return INVALID;
50
+ const path = rest
51
+ .slice(slash + 1)
52
+ .replace(/\/+$/, "")
53
+ .replace(/\.git$/i, "")
54
+ .replace(/\/+$/, "")
55
+ .toLowerCase();
56
+ const segments = path.split("/");
57
+ if (segments.length < 2 || segments.length > MAX_PATH_SEGMENTS)
58
+ return INVALID;
59
+ if (!segments.every((s) => s !== "." && s !== ".." && PATH_SEGMENT.test(s)))
60
+ return INVALID;
61
+ return { ok: true, value: `${host}/${segments.join("/")}` };
62
+ }
63
+ // The current repository's origin, normalized, or undefined when there is no
64
+ // git, no repository, no origin, or an origin that is not a shareable remote.
65
+ // The raw URL never leaves this function: it can carry a token.
66
+ export function detectProjectRemote() {
67
+ let raw;
68
+ try {
69
+ raw = execFileSync("git", ["remote", "get-url", "origin"], {
70
+ encoding: "utf8",
71
+ stdio: ["ignore", "pipe", "ignore"],
72
+ timeout: 5000,
73
+ });
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ const normalized = normalizeProjectRemote(raw);
79
+ return normalized.ok ? normalized.value : undefined;
80
+ }
package/dist/spec.js CHANGED
@@ -129,3 +129,12 @@ export function docPath(owner, doc) {
129
129
  validateSegment(doc, "skill", spec);
130
130
  return join(DOCS_ROOT, `${owner}@${doc}.md`);
131
131
  }
132
+ // A project guide installs to one fixed path per repository (ahood-cli#185),
133
+ // beside CLAUDE.md/AGENTS.md rather than inside them, so ahood never edits a
134
+ // file the user owns. Being shared, the path is claimed by exactly one pin:
135
+ // add.ts refuses a second guide while another is pinned.
136
+ export const PROJECT_GUIDE_PATH = join(".claude", "project.md");
137
+ // Where a pinned-file kind's install lives (kind-info.ts PINNED_FILE_KINDS).
138
+ export function pinnedFilePath(kind, owner, slug) {
139
+ return kind === "doc" ? docPath(owner, slug) : PROJECT_GUIDE_PATH;
140
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ahood
3
- description: How to use the ahood CLI (@ahood/cli), the registry for reusable AI-agent skills, subagent definitions (AGENT.md), MCP server manifests (server.json), and docs (DOC.md). Load BEFORE running any ahood command; before writing a non-trivial reusable skill, subagent, or MCP integration from scratch (search the registry first); when installing, updating, removing, publishing, sharing, or unpublishing one; when wiring ahood into an MCP host; or when setting up headless/CI access with AHOOD_TOKEN. Triggers - ahood, skill registry, is there a skill for this, install a skill, install an agent, add an MCP server, read a doc, publish a skill, ahood mcp, ahood doc, AHOOD_TOKEN.
3
+ description: How to use the ahood CLI (@ahood/cli), the registry for reusable AI-agent skills, subagent definitions (AGENT.md), MCP server manifests (server.json), docs (DOC.md), and project guides (PROJECT.md). Load BEFORE running any ahood command; before writing a non-trivial reusable skill, subagent, or MCP integration from scratch (search the registry first); when installing, updating, removing, publishing, sharing, or unpublishing one; when wiring ahood into an MCP host; or when setting up headless/CI access with AHOOD_TOKEN. Triggers - ahood, skill registry, is there a skill for this, install a skill, install an agent, add an MCP server, read a doc, project guide, publish a skill, ahood mcp, ahood doc, ahood project, AHOOD_TOKEN.
4
4
  ---
5
5
 
6
6
  # ahood
@@ -13,7 +13,7 @@ and only moves when someone runs an update.
13
13
  This guide ships inside the CLI itself (`ahood help useme`), so it matches the installed
14
14
  version (`ahood --version`). It needs no login or network to print.
15
15
 
16
- ## Four kinds, four command groups
16
+ ## Five kinds, five command groups
17
17
 
18
18
  | Kind | Root file | Installs to | Commands |
19
19
  |---|---|---|---|
@@ -21,23 +21,29 @@ version (`ahood --version`). It needs no login or network to print.
21
21
  | agent | `AGENT.md` | `.claude/agents/<owner>@<agent>.md` | `ahood agent <verb>` |
22
22
  | mcp | `server.json` | an entry merged into `.mcp.json` | `ahood mcp <verb>` |
23
23
  | doc | `DOC.md` | `.claude/docs/<owner>@<doc>.md` | `ahood doc <verb>` |
24
+ | project | `PROJECT.md` | `.claude/project.md` (one per repository) | `ahood project <verb>` |
24
25
 
25
- All four groups take the same verbs: `search`, `list`, `view`/`show`, `read`, `versions`,
26
+ All five groups take the same verbs: `search`, `list`, `view`/`show`, `read`, `versions`,
26
27
  `diff`, `add`, `update`, `outdated`, `remove`, `edit`, `unpublish`, `star`, `unstar`,
27
28
  `share`, `unshare`, `init`, `publish`.
28
29
 
29
- - `ahood agent <verb>`, `ahood mcp <verb>` and `ahood doc <verb>` act on their own kind only.
30
+ - `ahood agent <verb>`, `ahood mcp <verb>`, `ahood doc <verb>` and `ahood project <verb>` act on their own kind only.
30
31
  A target of another kind is refused (exit 2) before anything is downloaded, prompted for,
31
32
  written, or sent, and the error names the kind it found and the right command.
32
33
  - `ahood skill <verb>` is the **legacy, cross-kind** group in this CLI release: `skill search`
33
34
  and `skill list` include every kind, and `skill add`/`skill update` install whatever kind
34
- they resolve, except a doc: `skill add` refuses one and points at `ahood doc add`.
35
- Add `--kind skill` (or `agent`, `mcp`, `doc`) to any verb to scope it to one kind;
35
+ they resolve, except a doc or project guide: `skill add` refuses one and points at
36
+ `ahood doc add` / `ahood project add`.
37
+ Add `--kind skill` (or `agent`, `mcp`, `doc`, `project`) to any verb to scope it to one kind;
36
38
  `--kind all` spells out the legacy behavior. Prefer the kind-specific form in anything new --
37
39
  a future major version makes `ahood skill` skills-only.
38
40
  - A doc is a documentation page. `ahood doc read alice/setup-guide` prints it without
39
41
  installing anything; `ahood doc add alice/setup-guide` saves it as one pinned file under
40
42
  `.claude/docs/`, which `ahood doc update` and `ahood doc remove` manage like any other pin.
43
+ - A project guide is how to work in one repository: conventions, verification, traps. Run bare
44
+ `ahood project` inside a repository to list the guides naming its git remote;
45
+ `ahood project add alice/acme-api` saves one to `.claude/project.md` (pinned, one per
46
+ repository) and never touches `CLAUDE.md` or `AGENTS.md` -- point those at it yourself.
41
47
 
42
48
  ## Find, inspect, then install
43
49
 
@@ -87,7 +93,7 @@ ahood skill update --dry-run --json # preview, machine-readable
87
93
  ahood agent remove alice/code-reviewer # local uninstall; prompts unless --yes
88
94
  ```
89
95
 
90
- With no target, `agent`/`mcp`/`doc` `update` and `outdated` consider only installed pins of that
96
+ With no target, `agent`/`mcp`/`doc`/`project` `update` and `outdated` consider only installed pins of that
91
97
  kind; the legacy `skill update` covers every pin. Updates only affect the current project.
92
98
  An `.mcp.json` entry you edited by hand is refused rather than overwritten.
93
99
 
@@ -100,13 +106,14 @@ ahood skill init pdf-tools # ./pdf-tools/SKILL.md
100
106
  ahood agent init code-reviewer # ./code-reviewer/AGENT.md
101
107
  ahood mcp init github-server # ./github-server/server.json (placeholder URL)
102
108
  ahood doc init setup-guide # ./setup-guide/DOC.md
109
+ ahood project init acme-api # ./acme-api/PROJECT.md, `project:` filled from git origin
103
110
 
104
111
  ahood agent publish alice/code-reviewer@1.0.0 --path code-reviewer --name "Code Reviewer"
105
112
  ahood skill publish alice/pdf-tools@1.1.0 --path pdf-tools --changelog "Adds split"
106
113
  ```
107
114
 
108
115
  - `--name` is required only on the first publish, which creates the entry under your
109
- account. `agent publish`/`mcp publish`/`doc publish` imply the kind and refuse a contradictory `--kind`
116
+ account. `agent publish`/`mcp publish`/`doc publish`/`project publish` imply the kind and refuse a contradictory `--kind`
110
117
  or an existing entry of another kind before uploading anything.
111
118
  - **New entries are private.** Make one discoverable explicitly:
112
119
  `ahood skill edit alice/pdf-tools --visibility public`.
@@ -153,7 +160,7 @@ ahood skill publish alice/pdf-tools@1.1.0 --path pdf-tools --changelog "Adds spl
153
160
  3. **The hosted registry MCP endpoint**, `https://www.ahood.dev/api/mcp`: Streamable HTTP,
154
161
  stateless (POST only), `Authorization: Bearer <token>`. It has write tools too:
155
162
  `search_skills`, `get_skill`, `fetch_skill`, `read_skill`, `list_skill_versions`,
156
- `list_my_skills`, `search_collections`, `get_collection`, `create_skill`, `update_skill`,
163
+ `list_my_skills`, `search_collections`, `get_collection`, `get_project_guide`, `create_skill`, `update_skill`,
157
164
  `report_skill`, `delete_skill`, `yank_version`, and snap tools (`create_snap`,
158
165
  `list_snaps`, `get_snap`, `set_snap_tags`, `delete_snap`, `share_snap`, `unshare_snap`).
159
166
  A client that opens with a legacy-SSE `GET` gets a 405 -- use a Streamable HTTP client.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahood/cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
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",