@ahood/cli 0.10.0 → 0.11.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 +38 -12
- package/dist/commands/add.js +52 -13
- package/dist/commands/completion.js +3 -2
- package/dist/commands/diff.js +20 -13
- package/dist/commands/group.js +1 -1
- package/dist/commands/init.js +23 -1
- package/dist/commands/publish.js +21 -7
- package/dist/commands/remove.js +8 -3
- package/dist/commands/update.js +5 -0
- package/dist/config.js +7 -2
- package/dist/help.js +40 -19
- package/dist/index.js +10 -5
- package/dist/kind-info.js +11 -4
- package/dist/kinds.js +5 -2
- package/dist/lockfile.js +7 -1
- package/dist/spec.js +13 -0
- package/dist/useme/SKILL.md +17 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ahood
|
|
2
2
|
|
|
3
|
-
**The command-line client for [ahood](https://ahood.
|
|
3
|
+
**The command-line client for [ahood](https://www.ahood.dev) — a registry for installing and publishing [Claude Code](https://claude.com/claude-code) Skills.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@ahood/cli)
|
|
6
6
|
[](https://www.npmjs.com/package/@ahood/cli)
|
|
@@ -80,7 +80,7 @@ Requires Node.js 18 or later.
|
|
|
80
80
|
| Variable | Purpose |
|
|
81
81
|
| --- | --- |
|
|
82
82
|
| `AHOOD_TOKEN` | A personal API token (create one with `ahood token create <name>` after logging in once). When set, every command uses it instead of the stored browser-login credentials — the standard way to authenticate in CI or any non-interactive environment. |
|
|
83
|
-
| `AHOOD_API_URL` | Overrides the registry endpoint (default: `https://ahood.
|
|
83
|
+
| `AHOOD_API_URL` | Overrides the registry endpoint (default: `https://www.ahood.dev`). Must be `https://`, except for `localhost`/`127.0.0.1` or a `.test`/`.invalid`/`.example`/`.localhost` host, which may use plain `http://` for local development. |
|
|
84
84
|
| `XDG_CONFIG_HOME` | If set, browser-login credentials are stored under `$XDG_CONFIG_HOME/ahood/credentials.json` instead of the default `~/.config/ahood/credentials.json`. |
|
|
85
85
|
|
|
86
86
|
Paths `ahood` reads and writes in your project. The lockfile is meant to be committed, same as `package-lock.json`:
|
|
@@ -178,7 +178,7 @@ $ ahood group create "Design Team" --description "Shared skills for the design t
|
|
|
178
178
|
Created group Design Team (design-team).
|
|
179
179
|
|
|
180
180
|
$ ahood group invite-link design-team
|
|
181
|
-
https://ahood.
|
|
181
|
+
https://www.ahood.dev/groups/join?token=abc123...
|
|
182
182
|
Expires: 2026-09-14T00:00:00.000Z
|
|
183
183
|
This link (and its token) is shown only this once -- save it now, it cannot be retrieved again.
|
|
184
184
|
|
|
@@ -189,15 +189,16 @@ Shared alice/pdf-tools with design-team.
|
|
|
189
189
|
Sharing is additive -- it doesn't change `alice/pdf-tools`'s own public/private visibility, it just makes it visible to everyone in `design-team` too. On the other side, whoever received the link runs `ahood group join <the link>` to join.
|
|
190
190
|
</details>
|
|
191
191
|
|
|
192
|
-
## Kinds: skills, agents,
|
|
192
|
+
## Kinds: skills, agents, MCP servers, and docs
|
|
193
193
|
|
|
194
|
-
The registry holds
|
|
194
|
+
The registry holds four kinds of artifact, each with its own command group. All four take the same verbs (`search`, `list`, `view`/`show`, `read`, `versions`, `diff`, `add`, `update`, `outdated`, `remove`, `edit`, `unpublish`, `star`, `unstar`, `share`, `unshare`, `init`, `publish`), run by the same code:
|
|
195
195
|
|
|
196
196
|
| Kind | Root file | `add` installs to | Group |
|
|
197
197
|
| --- | --- | --- | --- |
|
|
198
198
|
| skill | `SKILL.md` | `.claude/skills/<owner>@<skill>/` | `ahood skill <verb>` |
|
|
199
199
|
| agent | `AGENT.md` | `.claude/agents/<owner>@<agent>.md` | `ahood agent <verb>` |
|
|
200
200
|
| mcp | `server.json` | an entry merged into `.mcp.json` | `ahood mcp <verb>` |
|
|
201
|
+
| doc | `DOC.md` | `.claude/docs/<owner>@<doc>.md` | `ahood doc <verb>` |
|
|
201
202
|
|
|
202
203
|
```
|
|
203
204
|
$ ahood agent search review
|
|
@@ -205,18 +206,20 @@ $ ahood agent init code-reviewer # ./code-reviewer/AGENT.md
|
|
|
205
206
|
$ ahood agent publish alice/code-reviewer@1.0.0 --path code-reviewer --name "Code Reviewer"
|
|
206
207
|
$ ahood mcp init github-server # ./github-server/server.json, a safe placeholder starter
|
|
207
208
|
$ ahood mcp add alice/github-server
|
|
209
|
+
$ ahood doc read alice/setup-guide # print a doc without installing it
|
|
210
|
+
$ ahood doc add alice/setup-guide # ./.claude/docs/alice@setup-guide.md, pinned
|
|
208
211
|
$ ahood agent add alice/pdf-tools
|
|
209
212
|
alice/pdf-tools is a skill, not an agent -- refusing to install it. Use `ahood skill add alice/pdf-tools` instead.
|
|
210
213
|
```
|
|
211
214
|
|
|
212
|
-
- **`ahood agent` and `ahood
|
|
213
|
-
- **Missing or unexpected kind metadata is refused, never guessed.** If the registry's answer carries no kind (or one this CLI doesn't handle), a kind-scoped command stops with exit `1` and points at the legacy form. `remove` reads the kind from the project's own files (the skill directory, the agent file,
|
|
214
|
-
- **`ahood skill` is legacy and cross-kind in this release.** `ahood skill search` and `ahood skill list` include every kind, and `ahood skill add`/`update`/`remove`/... act on whatever kind the target is, exactly as before -- including `--json` output and exit codes -- so existing scripts keep working. Add `--kind skill|agent|mcp` to any `ahood skill` verb (except `publish`, whose `--kind` keeps its original meaning) to scope it, or `--kind all` to spell out the cross-kind behavior.
|
|
215
|
-
- **
|
|
215
|
+
- **`ahood agent`, `ahood mcp` and `ahood doc` are strict.** A target of another kind is refused -- exit code `2`, naming the kind it found -- before anything is downloaded, prompted for, written, or sent. That covers `add`, `update`, `remove`, `edit`, `unpublish`, `share`/`unshare`, `star`/`unstar`, and `publish` (an existing entry of another kind is refused before anything is packed or uploaded); the read-only verbs refuse too, rather than show the wrong kind. `search` asks the registry for that kind only, `list` keeps only that kind of your own entries, and `update`/`outdated` with no argument consider only installed pins of that kind. `agent publish`/`mcp publish`/`doc publish` imply their kind and reject a contradictory `--kind`.
|
|
216
|
+
- **Missing or unexpected kind metadata is refused, never guessed.** If the registry's answer carries no kind (or one this CLI doesn't handle), a kind-scoped command stops with exit `1` and points at the legacy form. `remove` reads the kind from the project's own files (the skill directory, the agent file, the mcp fingerprint in the lockfile, or a doc pin's recorded `kind`) and asks the registry only when those can't tell; if neither can, nothing is removed.
|
|
217
|
+
- **`ahood skill` is legacy and cross-kind in this release.** `ahood skill search` and `ahood skill list` include every kind, and `ahood skill add`/`update`/`remove`/... act on whatever kind the target is, exactly as before -- including `--json` output and exit codes -- so existing scripts keep working. The one exception is a doc: `ahood skill add` refuses it and points at `ahood doc add`. Add `--kind skill|agent|mcp|doc` to any `ahood skill` verb (except `publish`, whose `--kind` keeps its original meaning) to scope it, or `--kind all` to spell out the cross-kind behavior.
|
|
218
|
+
- **Docs install as one pinned file.** `ahood doc add` saves a doc's `DOC.md` to `.claude/docs/<owner>@<doc>.md` and pins it in `.claude/skills.lock.json` with `"kind": "doc"`. It refuses to overwrite a file at that path that isn't pinned as that doc. `ahood doc update`/`outdated`/`remove` manage those pins like any other kind. Kind dispatch is table-driven (`src/kind-info.ts`).
|
|
216
219
|
|
|
217
220
|
**Migration plan for a strict `ahood skill`.** A future major version will make `ahood skill <verb>` skills-only by default, matching `agent` and `mcp`. Until then: (1) this release adds the strict groups and `--kind`, and labels `ahood skill` as legacy in `--help`; (2) a following minor release prints a stderr deprecation note when an `ahood skill` command actually touches a non-skill entry (stdout and `--json` unchanged); (3) the major release flips the default, keeping `ahood skill <verb> --kind all` as the explicit cross-kind escape hatch. Scripts that want today's behavior forever can pass `--kind all` now.
|
|
218
221
|
|
|
219
|
-
**`ahood mcp` has two meanings.** With no arguments, or as `ahood mcp serve` (the preferred spelling), it starts this CLI's local, read-only MCP server over stdio -- unchanged, so existing MCP host configurations keep working with no edits. With a registry verb it manages MCP server manifests. Anything else (`ahood mcp srve`, `ahood mcp --flag`) fails with usage and a suggestion instead of starting a server. Neither is the hosted registry MCP endpoint at `https://ahood.
|
|
222
|
+
**`ahood mcp` has two meanings.** With no arguments, or as `ahood mcp serve` (the preferred spelling), it starts this CLI's local, read-only MCP server over stdio -- unchanged, so existing MCP host configurations keep working with no edits. With a registry verb it manages MCP server manifests. Anything else (`ahood mcp srve`, `ahood mcp --flag`) fails with usage and a suggestion instead of starting a server. Neither is the hosted registry MCP endpoint at `https://www.ahood.dev/api/mcp`.
|
|
220
223
|
|
|
221
224
|
## Commands
|
|
222
225
|
|
|
@@ -257,7 +260,7 @@ alice/pdf-tools is a skill, not an agent -- refusing to install it. Use `ahood s
|
|
|
257
260
|
| `ahood skill share <owner>/<skill> --group <group>` | Share a skill you own with a group, without changing its public/private visibility. |
|
|
258
261
|
| `ahood skill unshare <owner>/<skill> --group <group>` | Stop sharing a skill you own with a group. |
|
|
259
262
|
| `ahood skill init [name]` | Scaffold a new skill folder with a minimal, valid SKILL.md. |
|
|
260
|
-
| `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>] [--changelog <text>] [--json]` | Publish a new version of a skill, agent,
|
|
263
|
+
| `ahood skill publish <owner>/<skill>@<version> [--path <dir>] [--kind skill\|agent\|mcp\|doc] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--changelog <text>] [--json]` | Publish a new version of a skill, agent, mcp server manifest, or doc from a folder containing SKILL.md, AGENT.md, server.json, or DOC.md, creating the skill first if it doesn't already exist. |
|
|
261
264
|
|
|
262
265
|
### Agent
|
|
263
266
|
|
|
@@ -307,6 +310,29 @@ alice/pdf-tools is a skill, not an agent -- refusing to install it. Use `ahood s
|
|
|
307
310
|
| `ahood mcp init [name]` | Scaffold a new MCP server manifest folder with a minimal, valid server.json. |
|
|
308
311
|
| `ahood mcp publish <owner>/<server>@<version> [--path <dir>] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--changelog <text>] [--json]` | Publish a new version of an MCP server manifest from a folder containing server.json, creating it first if it doesn't exist yet. |
|
|
309
312
|
|
|
313
|
+
### Doc
|
|
314
|
+
|
|
315
|
+
| Command | What it does |
|
|
316
|
+
| --- | --- |
|
|
317
|
+
| `ahood doc search <query> [--json] [--limit <n>]` | Search published docs. |
|
|
318
|
+
| `ahood doc view\|show <owner>/<doc> [--json] [--web]` | Show a single doc's details without installing it (alias: ahood doc show). |
|
|
319
|
+
| `ahood doc read <owner>/<doc> [--json]` | Print a published doc's DOC.md, without installing it. |
|
|
320
|
+
| `ahood doc versions <owner>/<doc> [--json]` | List a doc's published-version history. |
|
|
321
|
+
| `ahood doc diff <owner>/<doc> <versionA> <versionB> [--json]` | Show what changed between two published versions of a doc. |
|
|
322
|
+
| `ahood doc list [--json]` | List your own docs, public and private. |
|
|
323
|
+
| `ahood doc add <owner>/<doc>[@version]` | Install a doc into .claude/docs/<owner>@<doc>.md, pinned in the lockfile. |
|
|
324
|
+
| `ahood doc update [<owner>/<doc> ...] [--dry-run] [--json]` | Move doc pins forward to the latest version. |
|
|
325
|
+
| `ahood doc outdated [<owner>/<doc> ...] [--json]` | Read-only staleness check for installed docs. |
|
|
326
|
+
| `ahood doc remove <owner>/<doc> [--yes]` | Uninstall and unpin a doc (local only, prompts unless --yes is passed). |
|
|
327
|
+
| `ahood doc edit <owner>/<doc> [--tagline] [--tags] [--license] [--visibility] [--homepage] [--repository]` | Update a doc you own, changing only the flags you pass. |
|
|
328
|
+
| `ahood doc unpublish <owner>/<doc>[@version] [--yes]` | Delete a doc from the registry for every consumer, or yank one version (prompts unless --yes is passed). |
|
|
329
|
+
| `ahood doc star <owner>/<doc>` | Star a doc. |
|
|
330
|
+
| `ahood doc unstar <owner>/<doc>` | Remove your star from a doc. |
|
|
331
|
+
| `ahood doc share <owner>/<doc> --group <group>` | Share a doc you own with a group, without changing its visibility. |
|
|
332
|
+
| `ahood doc unshare <owner>/<doc> --group <group>` | Stop sharing a doc you own with a group. |
|
|
333
|
+
| `ahood doc init [name]` | Scaffold a new doc folder with a minimal, valid DOC.md. |
|
|
334
|
+
| `ahood doc publish <owner>/<doc>@<version> [--path <dir>] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--changelog <text>] [--json]` | Publish a new version of a doc from a folder containing DOC.md, creating it first if it doesn't exist yet. |
|
|
335
|
+
|
|
310
336
|
### Group
|
|
311
337
|
|
|
312
338
|
| Command | What it does |
|
|
@@ -391,7 +417,7 @@ npm test # run the vitest suite
|
|
|
391
417
|
|
|
392
418
|
If you add or change a command, update `src/help.ts`'s `COMMANDS_HELP` and then run `npm run sync-readme` to regenerate the table above — CI fails (`node scripts/sync-readme.mjs --check`) if it's out of sync.
|
|
393
419
|
|
|
394
|
-
Full reference, including personal API tokens, the public REST API, and the MCP server: **https://ahood.
|
|
420
|
+
Full reference, including personal API tokens, the public REST API, and the MCP server: **https://www.ahood.dev/docs**
|
|
395
421
|
|
|
396
422
|
## License
|
|
397
423
|
|
package/dist/commands/add.js
CHANGED
|
@@ -4,12 +4,12 @@ import { basename, dirname, join, resolve, sep } from "node:path";
|
|
|
4
4
|
import * as tarStream from "tar-stream";
|
|
5
5
|
import { gunzipSync } from "node:zlib";
|
|
6
6
|
import { apiFetch, apiJson, ApiError, sanitizeErrorMessage } from "../http.js";
|
|
7
|
-
import { LOCKFILE_PATH, parseOwnerSkillVersion, skillDir, agentPath, AGENTS_ROOT, MCP_CONFIG_PATH } from "../spec.js";
|
|
8
|
-
import { readLockfile, withLock, writeJsonFileAtomic, writeLockfileEntryVerifyingChecksum, LockfileChecksumConflictError, } from "../lockfile.js";
|
|
7
|
+
import { LOCKFILE_PATH, parseOwnerSkillVersion, skillDir, agentPath, docPath, AGENTS_ROOT, MCP_CONFIG_PATH } from "../spec.js";
|
|
8
|
+
import { readLockfile, withLock, writeFileAtomic, writeJsonFileAtomic, writeLockfileEntryVerifyingChecksum, LockfileChecksumConflictError, } from "../lockfile.js";
|
|
9
9
|
import { promptSecret } from "../secret-prompt.js";
|
|
10
10
|
import { sanitizeForTerminal } from "../terminal-safe.js";
|
|
11
11
|
import { UsageError } from "../usage-error.js";
|
|
12
|
-
import { assertKind } from "../kinds.js";
|
|
12
|
+
import { assertKind, strictKind } from "../kinds.js";
|
|
13
13
|
const USAGE = "Usage: ahood skill add <owner>/<skill>[@version]";
|
|
14
14
|
const MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024; // 50 MB compressed
|
|
15
15
|
const MAX_EXTRACTED_BYTES = 200 * 1024 * 1024; // 200 MB decompressed
|
|
@@ -865,6 +865,33 @@ export async function downloadVerifiedArchive(owner, skill, meta) {
|
|
|
865
865
|
}
|
|
866
866
|
return buffer;
|
|
867
867
|
}
|
|
868
|
+
// `ahood doc add` (ahood-cli#182): only DOC.md is extracted, written
|
|
869
|
+
// atomically to .claude/docs/<owner>@<doc>.md, and pinned with kind "doc" so
|
|
870
|
+
// remove/update can tell the pin apart without asking the registry. If the
|
|
871
|
+
// pin can't be written, the file goes back to what it was: the previous
|
|
872
|
+
// version's content on an update, nothing on a fresh install.
|
|
873
|
+
async function installDoc(owner, skill, meta, buffer) {
|
|
874
|
+
const key = `${owner}/${skill}`;
|
|
875
|
+
const content = await extractSingleFileContent(buffer, "DOC.md");
|
|
876
|
+
const destPath = docPath(owner, skill);
|
|
877
|
+
const previous = existsSync(destPath) ? readFileSync(destPath) : undefined;
|
|
878
|
+
writeFileAtomic(destPath, content);
|
|
879
|
+
try {
|
|
880
|
+
writeLockfileEntryVerifyingChecksum(LOCKFILE_PATH, key, {
|
|
881
|
+
version: meta.version,
|
|
882
|
+
checksum_sha256: meta.checksum_sha256,
|
|
883
|
+
kind: "doc",
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
rollbackOrWarn(() => (previous === undefined ? rmSync(destPath, { force: true }) : writeFileAtomic(destPath, previous)), `WARNING: could not restore ${destPath} after ${key}'s install failed -- it may not match its ${LOCKFILE_PATH} pin. Run \`ahood doc remove ${key}\` to clean it up.`);
|
|
888
|
+
if (error instanceof LockfileChecksumConflictError) {
|
|
889
|
+
throw new Error(checksumConflictMessage(key, meta, error.existing));
|
|
890
|
+
}
|
|
891
|
+
throw error;
|
|
892
|
+
}
|
|
893
|
+
console.log(`Installed ${key}@${meta.version} to ${destPath}`);
|
|
894
|
+
}
|
|
868
895
|
export async function add(args, scope) {
|
|
869
896
|
const spec = args[0];
|
|
870
897
|
if (!spec)
|
|
@@ -876,16 +903,24 @@ export async function add(args, scope) {
|
|
|
876
903
|
// refused here, before the download, the lockfile and every write below.
|
|
877
904
|
// Both metadata routes report the entry's kind, so this costs no request.
|
|
878
905
|
assertKind(scope, key, meta.kind, "add", "install");
|
|
879
|
-
//
|
|
880
|
-
//
|
|
881
|
-
// used to fall through to the skill path and extract a doc into
|
|
906
|
+
// Legacy cross-kind `ahood skill add` of a doc stays refused (ahood-cli#175):
|
|
907
|
+
// it used to fall through to the skill path and extract a doc into
|
|
882
908
|
// .claude/skills/<owner>@<slug>/ with no SKILL.md -- not a working skill,
|
|
883
|
-
// with nothing telling the user.
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
909
|
+
// with nothing telling the user. Docs install only through the doc scope
|
|
910
|
+
// (`ahood doc add`, ahood-cli#182), which writes the one flat file below.
|
|
911
|
+
// Refused before the download, the lockfile and any write. Entries with NO
|
|
912
|
+
// kind keep the legacy skill behaviour.
|
|
913
|
+
const docInstall = meta.kind === "doc" && strictKind(scope) === "doc";
|
|
914
|
+
if (meta.kind === "doc" && !docInstall) {
|
|
915
|
+
throw new UsageError(`${key} is a doc (a documentation page), not a skill. ` +
|
|
916
|
+
`Install it with \`ahood doc add ${key}\`, or read it without installing with \`ahood doc read ${key}\`.`);
|
|
917
|
+
}
|
|
918
|
+
// No-overwrite: a file already at the doc's path that this project's
|
|
919
|
+
// lockfile does not pin as this doc was not written by ahood, so it is
|
|
920
|
+
// left alone. Checked before the download, like every refusal above.
|
|
921
|
+
if (docInstall && existsSync(docPath(owner, skill)) && readLockfile(LOCKFILE_PATH)[key]?.kind !== "doc") {
|
|
922
|
+
throw new Error(`${docPath(owner, skill)} already exists and is not pinned as ${key} in ${LOCKFILE_PATH} -- refusing to overwrite it. ` +
|
|
923
|
+
"Move or delete it first.");
|
|
889
924
|
}
|
|
890
925
|
// A lockfile entry records the checksum this exact version was installed
|
|
891
926
|
// with; if a later fetch of the "same" version disagrees, either the
|
|
@@ -912,7 +947,7 @@ export async function add(args, scope) {
|
|
|
912
947
|
// manifest can never actually land on disk there -- the warning would be
|
|
913
948
|
// both mislabeled ("skill") and about a risk that can't materialize
|
|
914
949
|
// (ahood-cli final review finding #5).
|
|
915
|
-
if (meta.kind !== "agent" && meta.kind !== "mcp" && meta.manifest.some((f) => f.path.startsWith("scripts/"))) {
|
|
950
|
+
if (meta.kind !== "agent" && meta.kind !== "mcp" && meta.kind !== "doc" && meta.manifest.some((f) => f.path.startsWith("scripts/"))) {
|
|
916
951
|
console.warn("WARNING: this skill includes a scripts/ directory. Review its contents before use.");
|
|
917
952
|
}
|
|
918
953
|
const buffer = await downloadVerifiedArchive(owner, skill, meta);
|
|
@@ -945,6 +980,10 @@ export async function add(args, scope) {
|
|
|
945
980
|
await installMcpEntry(owner, skill, meta, buffer);
|
|
946
981
|
return;
|
|
947
982
|
}
|
|
983
|
+
if (docInstall) {
|
|
984
|
+
await installDoc(owner, skill, meta, buffer);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
948
987
|
// Flat, owner@skill-joined directory name so Claude Code's own one-level-deep
|
|
949
988
|
// project-skill scan actually discovers it (see skillDir's comment in spec.ts).
|
|
950
989
|
const destDir = skillDir(owner, skill);
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { TOP_LEVEL_COMMANDS_HELP, SKILL_COMMANDS_HELP, AGENT_COMMANDS_HELP, MCP_COMMANDS_HELP, GROUP_COMMANDS_HELP, SNAP_COMMANDS_HELP, COMMAND_ALIASES, } from "../help.js";
|
|
1
|
+
import { TOP_LEVEL_COMMANDS_HELP, SKILL_COMMANDS_HELP, AGENT_COMMANDS_HELP, MCP_COMMANDS_HELP, DOC_COMMANDS_HELP, GROUP_COMMANDS_HELP, SNAP_COMMANDS_HELP, COMMAND_ALIASES, } from "../help.js";
|
|
2
2
|
import { UsageError } from "../usage-error.js";
|
|
3
3
|
const USAGE = "Usage: ahood completion <bash|zsh|fish>";
|
|
4
4
|
// Every entity noun with its own verb list, in the order completion offers
|
|
5
|
-
// them. Registry nouns (skill, agent, mcp -- ahood-cli#172) come first; group
|
|
5
|
+
// them. Registry nouns (skill, agent, mcp -- ahood-cli#172; doc -- #182) come first; group
|
|
6
6
|
// and snap were already real nouns but had no completion at all before.
|
|
7
7
|
const NOUNS = [
|
|
8
8
|
["skill", SKILL_COMMANDS_HELP],
|
|
9
9
|
["agent", AGENT_COMMANDS_HELP],
|
|
10
10
|
["mcp", MCP_COMMANDS_HELP],
|
|
11
|
+
["doc", DOC_COMMANDS_HELP],
|
|
11
12
|
["group", GROUP_COMMANDS_HELP],
|
|
12
13
|
["snap", SNAP_COMMANDS_HELP],
|
|
13
14
|
];
|
package/dist/commands/diff.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createPatch } from "diff";
|
|
|
2
2
|
import { fetchVersionMeta, downloadVerifiedArchive, extractSingleFileContent } from "./add.js";
|
|
3
3
|
import { parseOwnerSkill, SEMVER_RE } from "../spec.js";
|
|
4
4
|
import { UsageError } from "../usage-error.js";
|
|
5
|
-
import { assertKind } from "../kinds.js";
|
|
5
|
+
import { assertKind, isCliKind, KINDS } from "../kinds.js";
|
|
6
6
|
const USAGE = "Usage: ahood skill diff <owner>/<skill> <versionA> <versionB> [--json]";
|
|
7
7
|
// Both versions must be explicit semver here, unlike `add`'s
|
|
8
8
|
// parseOwnerSkillVersion which lets "latest" default the version -- a diff
|
|
@@ -20,12 +20,16 @@ function validateVersion(version, spec) {
|
|
|
20
20
|
// SKILL.md's presence from the manifest we already fetched -- checked
|
|
21
21
|
// up front so a missing file produces a clean message instead of
|
|
22
22
|
// extractSingleFileContent's generic "not found in the downloaded archive".
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
//
|
|
24
|
+
// The file is the entry's root document (ahood-cli#182): SKILL.md for a skill
|
|
25
|
+
// or an entry with no kind (unchanged), AGENT.md, server.json or DOC.md for
|
|
26
|
+
// the other kinds, which have no SKILL.md to diff.
|
|
27
|
+
async function fetchSkillMdContent(owner, skill, meta, rootDoc) {
|
|
28
|
+
if (!meta.manifest.some((f) => f.path === rootDoc || f.path === `./${rootDoc}`)) {
|
|
29
|
+
throw new Error(`${owner}/${skill}@${meta.version} has no ${rootDoc} in its manifest.`);
|
|
26
30
|
}
|
|
27
31
|
const buffer = await downloadVerifiedArchive(owner, skill, meta);
|
|
28
|
-
const content = await extractSingleFileContent(buffer,
|
|
32
|
+
const content = await extractSingleFileContent(buffer, rootDoc);
|
|
29
33
|
return content.toString("utf-8");
|
|
30
34
|
}
|
|
31
35
|
// Compares two versions' manifests by path. VersionMeta.manifest only
|
|
@@ -35,14 +39,14 @@ async function fetchSkillMdContent(owner, skill, meta) {
|
|
|
35
39
|
// diffed above) -- there is no way to tell whether some other common path's
|
|
36
40
|
// *content* differs without downloading and comparing it too, which is out
|
|
37
41
|
// of scope for this command (see the resolution-plan comment on issue #88).
|
|
38
|
-
function diffManifests(manifestA, manifestB, skillMdChanged) {
|
|
42
|
+
function diffManifests(manifestA, manifestB, skillMdChanged, rootDoc) {
|
|
39
43
|
const pathsA = new Set(manifestA.map((f) => f.path));
|
|
40
44
|
const pathsB = new Set(manifestB.map((f) => f.path));
|
|
41
45
|
const added = [...pathsB].filter((p) => !pathsA.has(p)).sort();
|
|
42
46
|
const removed = [...pathsA].filter((p) => !pathsB.has(p)).sort();
|
|
43
47
|
const changed = [];
|
|
44
|
-
if (skillMdChanged && pathsA.has(
|
|
45
|
-
changed.push(
|
|
48
|
+
if (skillMdChanged && pathsA.has(rootDoc) && pathsB.has(rootDoc)) {
|
|
49
|
+
changed.push(rootDoc);
|
|
46
50
|
}
|
|
47
51
|
return { added, removed, changed };
|
|
48
52
|
}
|
|
@@ -61,13 +65,16 @@ export async function diff(args, scope) {
|
|
|
61
65
|
]);
|
|
62
66
|
// Both version responses carry the entry's kind; checked before either archive is downloaded.
|
|
63
67
|
assertKind(scope, `${owner}/${skill}`, metaA.kind, "diff", "diff");
|
|
68
|
+
const rootDoc = isCliKind(metaA.kind) ? KINDS[metaA.kind].rootDoc : "SKILL.md";
|
|
64
69
|
const [contentA, contentB] = await Promise.all([
|
|
65
|
-
fetchSkillMdContent(owner, skill, metaA),
|
|
66
|
-
fetchSkillMdContent(owner, skill, metaB),
|
|
70
|
+
fetchSkillMdContent(owner, skill, metaA, rootDoc),
|
|
71
|
+
fetchSkillMdContent(owner, skill, metaB, rootDoc),
|
|
67
72
|
]);
|
|
68
|
-
|
|
73
|
+
// The --json key stays `skillmd_diff` whatever the root document is, so
|
|
74
|
+
// the output shape never changes.
|
|
75
|
+
const skillmdDiff = createPatch(rootDoc, contentA, contentB, versionA, versionB);
|
|
69
76
|
const skillMdChanged = contentA !== contentB;
|
|
70
|
-
const manifest = diffManifests(metaA.manifest, metaB.manifest, skillMdChanged);
|
|
77
|
+
const manifest = diffManifests(metaA.manifest, metaB.manifest, skillMdChanged, rootDoc);
|
|
71
78
|
if (jsonOutput) {
|
|
72
79
|
console.log(JSON.stringify({ skillmd_diff: skillMdChanged ? skillmdDiff : "", manifest }));
|
|
73
80
|
return;
|
|
@@ -78,7 +85,7 @@ export async function diff(args, scope) {
|
|
|
78
85
|
console.log(skillmdDiff);
|
|
79
86
|
}
|
|
80
87
|
else {
|
|
81
|
-
console.log(
|
|
88
|
+
console.log(`${rootDoc} is unchanged between these versions.`);
|
|
82
89
|
}
|
|
83
90
|
const summaryParts = [];
|
|
84
91
|
if (manifest.added.length > 0)
|
package/dist/commands/group.js
CHANGED
|
@@ -85,7 +85,7 @@ export async function inviteLink(args) {
|
|
|
85
85
|
console.log(`Expires: ${expiresAt}`);
|
|
86
86
|
console.log("This link (and its token) is shown only this once -- save it now, it cannot be retrieved again.");
|
|
87
87
|
}
|
|
88
|
-
// Accepts either a full join URL (https://ahood.
|
|
88
|
+
// Accepts either a full join URL (https://www.ahood.dev/groups/join?token=...)
|
|
89
89
|
// or a bare raw token, for convenience -- whatever a user pastes from
|
|
90
90
|
// `ahood group invite-link` or forwards from someone else. Anything that
|
|
91
91
|
// doesn't parse as a URL, or parses but has no `token` query param, is
|
package/dist/commands/init.js
CHANGED
|
@@ -102,10 +102,30 @@ function buildServerJson(name) {
|
|
|
102
102
|
return `${JSON.stringify(manifest, null, 2)}
|
|
103
103
|
`;
|
|
104
104
|
}
|
|
105
|
+
// DOC.md, for `ahood doc init` (ahood-cli#182). The backend's publish
|
|
106
|
+
// validator (lib/publish/parse-doc-frontmatter.ts in the ahood repo) is the
|
|
107
|
+
// same hand-rolled flat `key: value` reader as AGENT.md's, minus the skills
|
|
108
|
+
// list: the file must START with a `---` line, the closing `---` line must be
|
|
109
|
+
// followed by a newline, and `name` and `description` must be non-empty. So
|
|
110
|
+
// the same rules as buildAgentMd apply: nothing above the opening `---`, and
|
|
111
|
+
// no colon in a comment line inside the frontmatter.
|
|
112
|
+
function buildDocMd(name) {
|
|
113
|
+
return `---
|
|
114
|
+
name: ${name}
|
|
115
|
+
description: TODO -- one line saying what this doc covers and when an agent should read it.
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
# ${name}
|
|
119
|
+
|
|
120
|
+
TODO -- write the documentation here. Plain Markdown; this whole file, frontmatter
|
|
121
|
+
included, is what \`ahood doc read\` prints and \`ahood doc add\` saves.
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
105
124
|
const TEMPLATES = {
|
|
106
125
|
skill: buildSkillMd,
|
|
107
126
|
agent: buildAgentMd,
|
|
108
127
|
mcp: buildServerJson,
|
|
128
|
+
doc: buildDocMd,
|
|
109
129
|
};
|
|
110
130
|
const NEXT_STEPS = {
|
|
111
131
|
skill: ["Fill in the description, then flesh out the ## Instructions section."],
|
|
@@ -115,7 +135,9 @@ const NEXT_STEPS = {
|
|
|
115
135
|
'"remotes" with one npm package: "packages": [{"registry_type": "npm", "runtime_hint": "npx",',
|
|
116
136
|
'"identifier": "<npm-package>", "version": "<x.y.z>"}] (plus optional "environment_variables").',
|
|
117
137
|
],
|
|
138
|
+
doc: ["Fill in the description, then write the doc below the frontmatter."],
|
|
118
139
|
};
|
|
140
|
+
const PLACEHOLDERS = { skill: "<skill>", agent: "<agent>", mcp: "<server>", doc: "<doc>" };
|
|
119
141
|
// Falls back to "my-skill" when the directory name itself isn't usable as a
|
|
120
142
|
// bare YAML scalar (empty, or starting with a character like "-" or "@" that
|
|
121
143
|
// would need quoting) -- e.g. running `ahood skill init` with no name directly in
|
|
@@ -180,6 +202,6 @@ export async function init(args, scope) {
|
|
|
180
202
|
console.log(`Created ${docPath}`);
|
|
181
203
|
for (const line of NEXT_STEPS[kind])
|
|
182
204
|
console.log(line);
|
|
183
|
-
const placeholder = kind
|
|
205
|
+
const placeholder = PLACEHOLDERS[kind];
|
|
184
206
|
console.log(`Run \`ahood ${strict ?? "skill"} publish <owner>/${placeholder}@<version>${name ? ` --path ${name}` : ""}\` when ready.`);
|
|
185
207
|
}
|
package/dist/commands/publish.js
CHANGED
|
@@ -47,7 +47,7 @@ async function pollVersionStatus(owner, skill, version) {
|
|
|
47
47
|
}
|
|
48
48
|
throw new Error(`Still processing after ${POLL_TIMEOUT_MS / 1000}s -- check \`ahood skill view ${owner}/${skill}\` later for the result.`);
|
|
49
49
|
}
|
|
50
|
-
const USAGE = "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>] [--changelog <text>] [--json]\n" +
|
|
50
|
+
const USAGE = "Usage: ahood skill publish <owner>/<skill>@<version> [--path <dir>] [--kind skill|agent|mcp|doc] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--changelog <text>] [--json]\n" +
|
|
51
51
|
" or: ahood skill publish <path> --owner <owner> --slug <skill> --version <x.y.z> [--json]";
|
|
52
52
|
// Matched by ENTRY NAME at every depth, not by path prefix, so a nested
|
|
53
53
|
// `vendor/thing/.git` is skipped the same as a top-level one. Not a
|
|
@@ -264,8 +264,8 @@ function parsePublishArgs(args) {
|
|
|
264
264
|
if (!SEMVER_RE.test(version)) {
|
|
265
265
|
throw new UsageError(`--version must be a semver like 1.2.3 (got "${version}").\n${USAGE}`);
|
|
266
266
|
}
|
|
267
|
-
if (kind !== undefined && kind !== "skill" && kind !== "agent" && kind !== "mcp") {
|
|
268
|
-
throw new UsageError(`--kind must be "skill", "agent", or "
|
|
267
|
+
if (kind !== undefined && kind !== "skill" && kind !== "agent" && kind !== "mcp" && kind !== "doc") {
|
|
268
|
+
throw new UsageError(`--kind must be "skill", "agent", "mcp", or "doc" (got "${kind}").\n${USAGE}`);
|
|
269
269
|
}
|
|
270
270
|
// Validated here, before any network call (including the tar/gzip of the
|
|
271
271
|
// skill directory) -- these only ever reach the server via
|
|
@@ -377,11 +377,17 @@ export async function publish(args, scope) {
|
|
|
377
377
|
const skillMdPath = join(path, "SKILL.md");
|
|
378
378
|
const agentMdPath = join(path, "AGENT.md");
|
|
379
379
|
const serverJsonPath = join(path, "server.json");
|
|
380
|
+
const docMdPath = join(path, "DOC.md");
|
|
380
381
|
const hasSkillMd = existsSync(skillMdPath);
|
|
381
382
|
const hasAgentMd = existsSync(agentMdPath);
|
|
382
383
|
const hasServerJson = existsSync(serverJsonPath);
|
|
384
|
+
const hasDocMd = existsSync(docMdPath);
|
|
383
385
|
let resolvedKind = kind;
|
|
384
|
-
if (kind === "
|
|
386
|
+
if (kind === "doc") {
|
|
387
|
+
if (!hasDocMd)
|
|
388
|
+
throw new Error(`No DOC.md found at ${docMdPath} -- ${via} must point at a folder containing DOC.md.`);
|
|
389
|
+
}
|
|
390
|
+
else if (kind === "agent") {
|
|
385
391
|
if (!hasAgentMd)
|
|
386
392
|
throw new Error(`No AGENT.md found at ${agentMdPath} -- ${via} must point at a folder containing AGENT.md.`);
|
|
387
393
|
}
|
|
@@ -395,12 +401,17 @@ export async function publish(args, scope) {
|
|
|
395
401
|
}
|
|
396
402
|
else {
|
|
397
403
|
// kind === undefined -- infer from whichever root file(s) are actually present
|
|
398
|
-
const present = [
|
|
404
|
+
const present = [
|
|
405
|
+
hasSkillMd && "SKILL.md",
|
|
406
|
+
hasAgentMd && "AGENT.md",
|
|
407
|
+
hasServerJson && "server.json",
|
|
408
|
+
hasDocMd && "DOC.md",
|
|
409
|
+
].filter(Boolean);
|
|
399
410
|
if (present.length > 1) {
|
|
400
|
-
throw new Error(`Multiple root files found at ${path} (${present.join(", ")}) -- pass --kind skill, --kind agent, or --kind
|
|
411
|
+
throw new Error(`Multiple root files found at ${path} (${present.join(", ")}) -- pass --kind skill, --kind agent, --kind mcp, or --kind doc to disambiguate.`);
|
|
401
412
|
}
|
|
402
413
|
if (present.length === 0) {
|
|
403
|
-
throw new Error(`No SKILL.md, AGENT.md,
|
|
414
|
+
throw new Error(`No SKILL.md, AGENT.md, server.json, or DOC.md found at ${path} -- publish must point at a skill, agent, mcp, or doc folder's root.`);
|
|
404
415
|
}
|
|
405
416
|
if (hasAgentMd) {
|
|
406
417
|
resolvedKind = "agent"; // inferred: only AGENT.md present, no --kind needed
|
|
@@ -408,6 +419,9 @@ export async function publish(args, scope) {
|
|
|
408
419
|
else if (hasServerJson) {
|
|
409
420
|
resolvedKind = "mcp"; // inferred: only server.json present, no --kind needed
|
|
410
421
|
}
|
|
422
|
+
else if (hasDocMd) {
|
|
423
|
+
resolvedKind = "doc"; // inferred: only DOC.md present (ahood-cli#182)
|
|
424
|
+
}
|
|
411
425
|
// else: hasSkillMd only -- resolvedKind stays undefined (unchanged existing behavior, server defaults to 'skill')
|
|
412
426
|
}
|
|
413
427
|
// ahood-cli#93: fires for an explicit --kind skill, and for the
|
package/dist/commands/remove.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, rmSync } from "node:fs";
|
|
2
2
|
import { confirm } from "../confirm.js";
|
|
3
3
|
import { readLockfile, removeLockfileEntry, withLock, writeJsonFileAtomic } from "../lockfile.js";
|
|
4
|
-
import { LOCKFILE_PATH, parseOwnerSkill, skillDir, agentPath, MCP_CONFIG_PATH } from "../spec.js";
|
|
4
|
+
import { LOCKFILE_PATH, parseOwnerSkill, skillDir, agentPath, docPath, MCP_CONFIG_PATH } from "../spec.js";
|
|
5
5
|
import { readMcpConfig, matchesMcpConfigHash } from "./add.js";
|
|
6
6
|
import { UsageError } from "../usage-error.js";
|
|
7
7
|
import { assertKind, ensureRemoteKind, KindMismatchError, describeKind, localInstalledKind, strictKind, } from "../kinds.js";
|
|
@@ -46,6 +46,9 @@ export async function remove(args, scope) {
|
|
|
46
46
|
// never a directory under .claude/skills/ -- distinct from the dir check above.
|
|
47
47
|
const agentFile = agentPath(owner, skill);
|
|
48
48
|
const agentExisted = existsSync(agentFile);
|
|
49
|
+
// A doc installs as one flat file under .claude/docs/ (ahood-cli#182).
|
|
50
|
+
const docFile = docPath(owner, skill);
|
|
51
|
+
const docExisted = existsSync(docFile);
|
|
49
52
|
const lockEntry = readLockfile(LOCKFILE_PATH)[key];
|
|
50
53
|
const hadLockfileEntry = lockEntry !== undefined;
|
|
51
54
|
// A pin whose install left nothing on disk is the defining shape of an
|
|
@@ -55,8 +58,8 @@ export async function remove(args, scope) {
|
|
|
55
58
|
// before that field did (lockfile.ts: "absent for any mcp entry installed
|
|
56
59
|
// before this field existed") and it is computed HERE, above the .mcp.json
|
|
57
60
|
// read -- so it survives the case where that read throws (ahood-cli#143).
|
|
58
|
-
const hadNoOnDiskFootprint = !dirExisted && !agentExisted && hadLockfileEntry;
|
|
59
|
-
if (!dirExisted && !agentExisted && !hadLockfileEntry) {
|
|
61
|
+
const hadNoOnDiskFootprint = !dirExisted && !agentExisted && !docExisted && lockEntry?.kind !== "doc" && hadLockfileEntry;
|
|
62
|
+
if (!dirExisted && !agentExisted && !docExisted && !hadLockfileEntry) {
|
|
60
63
|
console.error(`${key} was not installed -- nothing to remove.`);
|
|
61
64
|
process.exitCode = 1;
|
|
62
65
|
return;
|
|
@@ -93,6 +96,8 @@ export async function remove(args, scope) {
|
|
|
93
96
|
rmSync(dir, { recursive: true, force: true });
|
|
94
97
|
if (agentExisted)
|
|
95
98
|
rmSync(agentFile, { force: true });
|
|
99
|
+
if (docExisted)
|
|
100
|
+
rmSync(docFile, { force: true });
|
|
96
101
|
// An mcp-kind install has no directory or agent file on disk -- its only
|
|
97
102
|
// footprint is the lockfile entry (cleared below, once this block has run)
|
|
98
103
|
// and a live entry in .mcp.json (which add.ts's installMcpEntry merged in,
|
package/dist/commands/update.js
CHANGED
|
@@ -214,8 +214,13 @@ export async function update(args, scope) {
|
|
|
214
214
|
}
|
|
215
215
|
// no @version -- resolves to latest again. A strict scope is passed on so
|
|
216
216
|
// add() re-checks the kind it resolves itself before writing anything.
|
|
217
|
+
// A doc pin under legacy `ahood skill update` is moved forward through
|
|
218
|
+
// the doc scope (ahood-cli#182): legacy add refuses docs, but legacy
|
|
219
|
+
// update has always covered every installed pin.
|
|
217
220
|
if (kind)
|
|
218
221
|
await add([ownerSlashSkill], scope);
|
|
222
|
+
else if (meta.kind === "doc")
|
|
223
|
+
await add([ownerSlashSkill], { noun: "doc", kind: "doc" });
|
|
219
224
|
else
|
|
220
225
|
await add([ownerSlashSkill]);
|
|
221
226
|
}
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { UsageError } from "./usage-error.js";
|
|
2
|
-
|
|
2
|
+
// www.ahood.dev since ahood#661: production Clerk only accepts ahood.dev
|
|
3
|
+
// origins, so web pages on the old host redirect here. The API still answers on
|
|
4
|
+
// https://ahood.vercel.app (CLIs <= 0.10.0 default to it), so an override naming
|
|
5
|
+
// that legacy host is official too and isn't warned about.
|
|
6
|
+
const DEFAULT_API_URL = "https://www.ahood.dev";
|
|
7
|
+
const LEGACY_API_URL = "https://ahood.vercel.app";
|
|
3
8
|
let warnedAboutOverride = false;
|
|
4
9
|
// AHOOD_API_URL is trusted with the bearer token on every request (see
|
|
5
10
|
// http.ts's apiFetch), so a scheme/host is validated rather than accepted
|
|
@@ -29,7 +34,7 @@ export function getApiUrl() {
|
|
|
29
34
|
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLocalHost)) {
|
|
30
35
|
throw new UsageError(`AHOOD_API_URL must use https:// (http:// is only allowed for localhost/test hosts); got "${configured}".`);
|
|
31
36
|
}
|
|
32
|
-
if (trimmed !== DEFAULT_API_URL && !warnedAboutOverride) {
|
|
37
|
+
if (trimmed !== DEFAULT_API_URL && trimmed !== LEGACY_API_URL && !warnedAboutOverride) {
|
|
33
38
|
console.error(`warning: using non-default API endpoint from AHOOD_API_URL: ${trimmed}`);
|
|
34
39
|
warnedAboutOverride = true;
|
|
35
40
|
}
|
package/dist/help.js
CHANGED
|
@@ -121,7 +121,8 @@ export const SKILL_COMMANDS_HELP = [
|
|
|
121
121
|
"installs instead as a single file at .claude/agents/<owner>@<skill>.md (Claude Code's own subagent " +
|
|
122
122
|
"loader scans .claude/agents/*.md as flat files), not a .claude/skills/ directory. An artifact published " +
|
|
123
123
|
"with --kind mcp installs by merging an entry into .mcp.json instead, prompting for any secret " +
|
|
124
|
-
"environment variables its server.json declares that aren't already set in your shell."
|
|
124
|
+
"environment variables its server.json declares that aren't already set in your shell. A doc is refused " +
|
|
125
|
+
"here; install it with `ahood doc add`.",
|
|
125
126
|
examples: ["ahood skill add alice/pdf-tools", "ahood skill add alice/pdf-tools@1.2.0"],
|
|
126
127
|
},
|
|
127
128
|
{
|
|
@@ -206,17 +207,17 @@ export const SKILL_COMMANDS_HELP = [
|
|
|
206
207
|
examples: ["ahood skill init pdf-tools", "ahood skill init"],
|
|
207
208
|
},
|
|
208
209
|
{
|
|
209
|
-
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>] [--changelog <text>] [--json]",
|
|
210
|
-
summary: "Publish a new version of a skill, agent,
|
|
211
|
-
desc: "Publish a new version of a skill, agent,
|
|
212
|
-
"doesn't exist yet, this creates it first -- pass --name to set its display name, and --kind agent or --kind
|
|
213
|
-
"publishing an AGENT.md
|
|
210
|
+
usage: "ahood skill publish <owner>/<skill>@<version> [--path <dir>] [--kind skill|agent|mcp|doc] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--changelog <text>] [--json]",
|
|
211
|
+
summary: "Publish a new version of a skill, agent, mcp server manifest, or doc from a folder containing SKILL.md, AGENT.md, server.json, or DOC.md, creating the skill first if it doesn't already exist.",
|
|
212
|
+
desc: "Publish a new version of a skill, agent, mcp server manifest, or doc from a folder containing SKILL.md, AGENT.md, server.json, or DOC.md. If the artifact " +
|
|
213
|
+
"doesn't exist yet, this creates it first -- pass --name to set its display name, and --kind agent, --kind mcp, or --kind doc if " +
|
|
214
|
+
"publishing an AGENT.md, server.json, or DOC.md (auto-detected from the folder contents when only one of SKILL.md/AGENT.md/server.json/DOC.md is present). " +
|
|
214
215
|
"--name is required when creating; optionally also pass --tagline/--tags/--license/--homepage/--repository. " +
|
|
215
216
|
"Processing happens server-side after upload; this command polls and reports the final published/failed status. " +
|
|
216
217
|
"The legacy form `ahood skill publish <path> --owner <owner> --slug <skill> --version <x.y.z>` is still accepted. " +
|
|
217
218
|
"Every flag also accepts the --flag=value form, needed when a value itself starts with --.",
|
|
218
219
|
flags: [
|
|
219
|
-
"--kind skill|agent|mcp
|
|
220
|
+
"--kind skill|agent|mcp|doc What kind of artifact this is. Auto-detected from SKILL.md/AGENT.md/server.json/DOC.md when omitted.",
|
|
220
221
|
"--name <text> Required only when creating the artifact on this publish.",
|
|
221
222
|
"--tagline <text> Short one-line description, used only when creating.",
|
|
222
223
|
"--tags <comma,separated> Initial tags, used only when creating.",
|
|
@@ -240,15 +241,21 @@ export const SKILL_COMMANDS_HELP = [
|
|
|
240
241
|
// rather than hand-copied twice, so the two lists can't drift from each other.
|
|
241
242
|
// SKILL_COMMANDS_HELP above stays hand-written: it documents the legacy,
|
|
242
243
|
// cross-kind behavior `ahood skill` keeps in this release.
|
|
244
|
+
const PLACEHOLDER = { agent: "<agent>", mcp: "<server>", doc: "<doc>" };
|
|
245
|
+
const EXAMPLE = {
|
|
246
|
+
agent: { slug: "alice/code-reviewer", name: "Code Reviewer" },
|
|
247
|
+
mcp: { slug: "alice/github-server", name: "GitHub Server" },
|
|
248
|
+
doc: { slug: "alice/setup-guide", name: "Setup Guide" },
|
|
249
|
+
};
|
|
243
250
|
function registryCommandsHelp(kind) {
|
|
244
251
|
const info = KINDS[kind];
|
|
245
|
-
const n = kind
|
|
252
|
+
const n = PLACEHOLDER[kind];
|
|
246
253
|
const p = `ahood ${kind}`;
|
|
247
254
|
const label = info.label;
|
|
248
255
|
const a = info.article;
|
|
249
256
|
const plural = info.plural;
|
|
250
257
|
const doc = info.rootDoc;
|
|
251
|
-
const ex = kind
|
|
258
|
+
const ex = EXAMPLE[kind].slug;
|
|
252
259
|
const kindRefusal = `Refused, before anything else happens, when the target is not ${a} ${label}.`;
|
|
253
260
|
return [
|
|
254
261
|
{
|
|
@@ -378,11 +385,12 @@ function registryCommandsHelp(kind) {
|
|
|
378
385
|
"--changelog <text> What changed in this version.",
|
|
379
386
|
"--json Print one {version,status,...} JSON object on completion (or {error} on failure).",
|
|
380
387
|
],
|
|
381
|
-
examples: [`${p} publish ${ex}@1.0.0 --name "${kind
|
|
388
|
+
examples: [`${p} publish ${ex}@1.0.0 --name "${EXAMPLE[kind].name}"`],
|
|
382
389
|
},
|
|
383
390
|
];
|
|
384
391
|
}
|
|
385
392
|
export const AGENT_COMMANDS_HELP = registryCommandsHelp("agent");
|
|
393
|
+
export const DOC_COMMANDS_HELP = registryCommandsHelp("doc");
|
|
386
394
|
// `ahood mcp` is two things (ADR 0007 in the ahood repo separates the hosted
|
|
387
395
|
// registry MCP endpoint from this CLI's local one): with no verb, or with
|
|
388
396
|
// `serve`, it is the local, read-only stdio MCP server that MCP hosts spawn;
|
|
@@ -396,7 +404,7 @@ export const MCP_SERVE_HELP = [
|
|
|
396
404
|
"skill_versions, skill_list, skill_outdated, and whoami as MCP tools -- the same data ahood's --json " +
|
|
397
405
|
"commands already return, reachable as typed tool calls instead of parsed CLI output. Meant to be " +
|
|
398
406
|
"launched by an MCP-aware agent host (e.g. Claude Code's MCP server configuration), not run interactively. " +
|
|
399
|
-
"Read-only by design. This is not the hosted registry MCP endpoint (https://ahood.
|
|
407
|
+
"Read-only by design. This is not the hosted registry MCP endpoint (https://www.ahood.dev/api/mcp), which " +
|
|
400
408
|
"is a separate server with write tools.",
|
|
401
409
|
examples: ['claude mcp add ahood -- ahood mcp serve', "npx @ahood/cli@latest mcp serve"],
|
|
402
410
|
},
|
|
@@ -445,10 +453,10 @@ export const GROUP_COMMANDS_HELP = [
|
|
|
445
453
|
{
|
|
446
454
|
usage: "ahood group join <invite-url-or-token>",
|
|
447
455
|
summary: "Join a group using an invite link or its raw token.",
|
|
448
|
-
desc: "Join a group using an invite link (e.g. https://ahood.
|
|
456
|
+
desc: "Join a group using an invite link (e.g. https://www.ahood.dev/groups/join?token=...) or its bare " +
|
|
449
457
|
"raw token -- either form works.",
|
|
450
458
|
examples: [
|
|
451
|
-
"ahood group join https://ahood.
|
|
459
|
+
"ahood group join https://www.ahood.dev/groups/join?token=abc123",
|
|
452
460
|
"ahood group join abc123",
|
|
453
461
|
],
|
|
454
462
|
},
|
|
@@ -585,6 +593,7 @@ const ENTITY_COMMANDS_HELP = {
|
|
|
585
593
|
skill: SKILL_COMMANDS_HELP,
|
|
586
594
|
agent: AGENT_COMMANDS_HELP,
|
|
587
595
|
mcp: MCP_COMMANDS_HELP,
|
|
596
|
+
doc: DOC_COMMANDS_HELP,
|
|
588
597
|
group: GROUP_COMMANDS_HELP,
|
|
589
598
|
snap: SNAP_COMMANDS_HELP,
|
|
590
599
|
};
|
|
@@ -709,8 +718,8 @@ export function formatSkillHelp() {
|
|
|
709
718
|
"Legacy, cross-kind: in this release `ahood skill <verb>` still acts on any registry kind",
|
|
710
719
|
"(skills, agents, MCP server manifests) -- `skill list`/`skill search` include every kind, and",
|
|
711
720
|
"`skill add`/`skill update` install whatever kind they resolve. Scripts relying on that keep",
|
|
712
|
-
"working. For one kind only, use `ahood agent
|
|
713
|
-
"`--kind skill|agent|mcp` to any verb below (`--kind all` spells out the legacy behavior).",
|
|
721
|
+
"working. For one kind only, use `ahood agent|mcp|doc <verb>`, or add",
|
|
722
|
+
"`--kind skill|agent|mcp|doc` to any verb below (`--kind all` spells out the legacy behavior).",
|
|
714
723
|
"A future major version will make `ahood skill` skills-only.",
|
|
715
724
|
"",
|
|
716
725
|
"Commands:",
|
|
@@ -745,17 +754,26 @@ export function formatMcpHelp() {
|
|
|
745
754
|
" ahood mcp / ahood mcp serve start this CLI's local, read-only MCP server over stdio",
|
|
746
755
|
" (what MCP host configs launch; `serve` is the preferred spelling)",
|
|
747
756
|
" ahood mcp <verb> manage mcp-kind registry entries (installs merge into .mcp.json)",
|
|
748
|
-
"Neither is the hosted registry MCP endpoint, https://ahood.
|
|
757
|
+
"Neither is the hosted registry MCP endpoint, https://www.ahood.dev/api/mcp.",
|
|
749
758
|
"Registry verbs act on MCP server manifests only; another kind is refused before any side effect.",
|
|
750
759
|
], MCP_COMMANDS_HELP, "mcp");
|
|
751
760
|
}
|
|
752
761
|
// One entry point for every registry noun's group help, so dispatch and
|
|
753
762
|
// `ahood help <noun>` can't disagree about which listing a noun gets.
|
|
763
|
+
export function formatDocHelp() {
|
|
764
|
+
return formatRegistryKindHelp("ahood doc -- docs (DOC.md) in the ahood registry", [
|
|
765
|
+
"Every verb here acts on docs only: a target of another kind is refused before anything",
|
|
766
|
+
"is downloaded, prompted for, written, or sent. Installs land in .claude/docs/<owner>@<doc>.md.",
|
|
767
|
+
"To look at a doc without installing it, use `ahood doc read`.",
|
|
768
|
+
], DOC_COMMANDS_HELP, "doc");
|
|
769
|
+
}
|
|
754
770
|
export function formatKindHelp(noun) {
|
|
755
771
|
if (noun === "agent")
|
|
756
772
|
return formatAgentHelp();
|
|
757
773
|
if (noun === "mcp")
|
|
758
774
|
return formatMcpHelp();
|
|
775
|
+
if (noun === "doc")
|
|
776
|
+
return formatDocHelp();
|
|
759
777
|
return formatSkillHelp();
|
|
760
778
|
}
|
|
761
779
|
// `ahood group --help` -- the group-level listing for every group verb.
|
|
@@ -793,6 +811,8 @@ export function formatHelp() {
|
|
|
793
811
|
const agentGroupSummary = "The same verbs, for agent definitions only -- run `ahood agent --help` for the full list.";
|
|
794
812
|
const mcpGroupUsage = "ahood mcp <command>";
|
|
795
813
|
const mcpGroupSummary = "The same verbs, for MCP server manifests only; bare `ahood mcp` or `ahood mcp serve` starts the local MCP server.";
|
|
814
|
+
const docGroupUsage = "ahood doc <command>";
|
|
815
|
+
const docGroupSummary = "The same verbs, for docs (DOC.md) only -- run `ahood doc --help` for the full list.";
|
|
796
816
|
const groupGroupUsage = "ahood group <command>";
|
|
797
817
|
const groupGroupSummary = "Create private groups and share skills with them -- run `ahood group --help` for the full list.";
|
|
798
818
|
const snapGroupUsage = "ahood snap <command>";
|
|
@@ -802,11 +822,12 @@ export function formatHelp() {
|
|
|
802
822
|
{ usage: skillGroupUsage, summary: skillGroupSummary },
|
|
803
823
|
{ usage: agentGroupUsage, summary: agentGroupSummary },
|
|
804
824
|
{ usage: mcpGroupUsage, summary: mcpGroupSummary },
|
|
825
|
+
{ usage: docGroupUsage, summary: docGroupSummary },
|
|
805
826
|
{ usage: groupGroupUsage, summary: groupGroupSummary },
|
|
806
827
|
{ usage: snapGroupUsage, summary: snapGroupSummary },
|
|
807
828
|
]);
|
|
808
829
|
return [
|
|
809
|
-
"ahood -- CLI for the ahood skills registry (https://ahood.
|
|
830
|
+
"ahood -- CLI for the ahood skills registry (https://www.ahood.dev)",
|
|
810
831
|
"",
|
|
811
832
|
"Quick start:",
|
|
812
833
|
" ahood login",
|
|
@@ -823,7 +844,7 @@ export function formatHelp() {
|
|
|
823
844
|
"",
|
|
824
845
|
"Run `ahood <command> --help` (or `ahood help <command>`) for a single command's flags and examples.",
|
|
825
846
|
"Run `ahood skill --help` for the full list of skill commands.",
|
|
826
|
-
"Run `ahood agent --help` / `ahood mcp --help` for the kind-scoped agent and
|
|
847
|
+
"Run `ahood agent --help` / `ahood mcp --help` / `ahood doc --help` for the kind-scoped agent, MCP and doc commands.",
|
|
827
848
|
"Run `ahood group --help` for the full list of group commands.",
|
|
828
849
|
"Run `ahood snap --help` for the full list of snap commands.",
|
|
829
850
|
"Run `ahood --version` to print the installed CLI version.",
|
|
@@ -832,6 +853,6 @@ export function formatHelp() {
|
|
|
832
853
|
" 4 authentication required or rejected, 5 not found,",
|
|
833
854
|
" 6 network/transport error or upstream server (5xx) error.",
|
|
834
855
|
"",
|
|
835
|
-
"Full reference: https://ahood.
|
|
856
|
+
"Full reference: https://www.ahood.dev/docs",
|
|
836
857
|
].join("\n");
|
|
837
858
|
}
|
package/dist/index.js
CHANGED
|
@@ -96,6 +96,7 @@ const COMMANDS = {
|
|
|
96
96
|
mcp: dispatchMcp,
|
|
97
97
|
skill: dispatchSkill,
|
|
98
98
|
agent: dispatchAgent,
|
|
99
|
+
doc: dispatchDoc,
|
|
99
100
|
group: dispatchGroup,
|
|
100
101
|
snap: dispatchSnap,
|
|
101
102
|
};
|
|
@@ -107,8 +108,9 @@ const COMMANDS = {
|
|
|
107
108
|
// here just means "a command that owns its own sub-verb dispatch", the same
|
|
108
109
|
// sense "skill" already used before "group" (the entity) existed. "snap"
|
|
109
110
|
// joins this set for the exact same reason (ahood-cli#107).
|
|
110
|
-
// "agent" and "mcp" join it as registry nouns (ahood-cli#172)
|
|
111
|
-
|
|
111
|
+
// "agent" and "mcp" join it as registry nouns (ahood-cli#172), "doc" too
|
|
112
|
+
// (ahood-cli#182).
|
|
113
|
+
const GROUP_COMMANDS = new Set(["skill", "agent", "mcp", "doc", "group", "snap"]);
|
|
112
114
|
function levenshtein(a, b) {
|
|
113
115
|
const dp = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
114
116
|
for (let i = 0; i <= a.length; i++)
|
|
@@ -146,7 +148,7 @@ function closestGroupVerb(input) {
|
|
|
146
148
|
function closestSnapVerb(input) {
|
|
147
149
|
return closestOf(input, Object.keys(SNAP_VERBS));
|
|
148
150
|
}
|
|
149
|
-
// `ahood <noun> <verb> [...]` for every registry noun (skill, agent, mcp) --
|
|
151
|
+
// `ahood <noun> <verb> [...]` for every registry noun (skill, agent, mcp, doc) --
|
|
150
152
|
// owns its own help handling at both the noun level (`ahood agent` /
|
|
151
153
|
// `ahood agent --help`) and the per-verb level (`ahood agent <verb> --help`),
|
|
152
154
|
// since neither should fall through to main()'s generic top-level --help
|
|
@@ -210,6 +212,9 @@ async function dispatchSkill(args) {
|
|
|
210
212
|
async function dispatchAgent(args) {
|
|
211
213
|
await dispatchRegistry("agent", args);
|
|
212
214
|
}
|
|
215
|
+
async function dispatchDoc(args) {
|
|
216
|
+
await dispatchRegistry("doc", args);
|
|
217
|
+
}
|
|
213
218
|
// `ahood mcp` keeps its original contract byte-for-byte: with NO arguments it
|
|
214
219
|
// is the local stdio MCP server that existing host configs launch -- nothing
|
|
215
220
|
// is printed and nothing else runs first. `ahood mcp serve` is the same
|
|
@@ -312,7 +317,7 @@ async function main() {
|
|
|
312
317
|
process.stdout.write(content);
|
|
313
318
|
return;
|
|
314
319
|
}
|
|
315
|
-
if (sub === "skill" || sub === "agent" || sub === "mcp") {
|
|
320
|
+
if (sub === "skill" || sub === "agent" || sub === "mcp" || sub === "doc") {
|
|
316
321
|
const verb = args[1];
|
|
317
322
|
const entry = verb ? findCommandHelp(sub, verb) : undefined;
|
|
318
323
|
console.log(entry ? formatCommandHelp(entry) : formatKindHelp(sub));
|
|
@@ -394,4 +399,4 @@ if (isEntrypoint) {
|
|
|
394
399
|
main();
|
|
395
400
|
}
|
|
396
401
|
// Exported for tests only -- the CLI itself only ever calls main() above.
|
|
397
|
-
export { main, dispatchSkill, dispatchAgent, dispatchMcp, dispatchRegistry, dispatchGroup, dispatchSnap, COMMANDS, SKILL_COMMANDS, GROUP_VERBS, SNAP_VERBS };
|
|
402
|
+
export { main, dispatchSkill, dispatchAgent, dispatchDoc, dispatchMcp, dispatchRegistry, dispatchGroup, dispatchSnap, COMMANDS, SKILL_COMMANDS, GROUP_VERBS, SNAP_VERBS };
|
package/dist/kind-info.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
// The registry stores four artifact kinds (the backend's
|
|
8
8
|
// lib/skills/validation.ts ALLOWED_KINDS: skill, agent, mcp, doc), and a
|
|
9
9
|
// kind is fixed when an entry is created -- PATCH's allow-list has no `kind`
|
|
10
|
-
// -- so the kind an entry reports now is the kind it has always had.
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// -- so the kind an entry reports now is the kind it has always had. Each
|
|
11
|
+
// kind has a noun group (`ahood skill|agent|mcp|doc <verb>`), all run by one
|
|
12
|
+
// shared dispatcher. `doc` joined as the fourth in ahood-cli#182: it installs
|
|
13
|
+
// as one flat Markdown file, its DOC.md, the same shape an agent has.
|
|
14
14
|
export const KINDS = {
|
|
15
15
|
skill: {
|
|
16
16
|
label: "skill",
|
|
@@ -33,6 +33,13 @@ export const KINDS = {
|
|
|
33
33
|
rootDoc: "server.json",
|
|
34
34
|
installsTo: "an entry in .mcp.json",
|
|
35
35
|
},
|
|
36
|
+
doc: {
|
|
37
|
+
label: "doc",
|
|
38
|
+
article: "a",
|
|
39
|
+
plural: "docs",
|
|
40
|
+
rootDoc: "DOC.md",
|
|
41
|
+
installsTo: ".claude/docs/<owner>@<doc>.md",
|
|
42
|
+
},
|
|
36
43
|
};
|
|
37
44
|
export const CLI_KINDS = Object.keys(KINDS);
|
|
38
45
|
export function isCliKind(value) {
|
package/dist/kinds.js
CHANGED
|
@@ -62,6 +62,9 @@ export async function ensureRemoteKind(scope, owner, skill, verb, action) {
|
|
|
62
62
|
// skill whose directory was deleted by hand) is undefined: unknown, and the
|
|
63
63
|
// caller must ask the registry rather than guess.
|
|
64
64
|
export function localInstalledKind(owner, skill, entry) {
|
|
65
|
+
// A doc pin records its kind (ahood-cli#182), the one kind that does.
|
|
66
|
+
if (entry?.kind === "doc")
|
|
67
|
+
return "doc";
|
|
65
68
|
const hasDir = existsSync(skillDir(owner, skill));
|
|
66
69
|
const hasAgentFile = existsSync(agentPath(owner, skill));
|
|
67
70
|
if (hasDir && !hasAgentFile)
|
|
@@ -102,8 +105,8 @@ export function extractKindFlag(args) {
|
|
|
102
105
|
// Resolves the scope for `ahood <noun> <verb> [--kind <k>]`.
|
|
103
106
|
// skill noun, no --kind or --kind all -> legacy (cross-kind)
|
|
104
107
|
// skill noun, --kind <k> -> strict <k>
|
|
105
|
-
// agent/mcp noun, no --kind or --kind <same> -> strict <noun>
|
|
106
|
-
// agent/mcp noun, any other --kind -> usage error (contradiction)
|
|
108
|
+
// agent/mcp/doc noun, no --kind or --kind <same> -> strict <noun>
|
|
109
|
+
// agent/mcp/doc noun, any other --kind -> usage error (contradiction)
|
|
107
110
|
export function resolveScope(noun, kindFlag) {
|
|
108
111
|
const valid = [...CLI_KINDS, "all"].join(", ");
|
|
109
112
|
if (kindFlag !== undefined && kindFlag !== "all" && !isCliKind(kindFlag)) {
|
package/dist/lockfile.js
CHANGED
|
@@ -146,6 +146,12 @@ function resolveWriteTarget(path) {
|
|
|
146
146
|
// file it is creating must get what it asked for regardless of the ambient
|
|
147
147
|
// umask.
|
|
148
148
|
export function writeJsonFileAtomic(path, data, mode) {
|
|
149
|
+
writeFileAtomic(path, JSON.stringify(data, null, 2) + "\n", mode);
|
|
150
|
+
}
|
|
151
|
+
// The same write-then-rename for raw content. A doc install (ahood-cli#182)
|
|
152
|
+
// writes its DOC.md through this, so an interrupted install never leaves a
|
|
153
|
+
// truncated doc behind.
|
|
154
|
+
export function writeFileAtomic(path, content, mode) {
|
|
149
155
|
// Resolved before the mkdir so the directory that actually gets created is
|
|
150
156
|
// the resolved target's, not the link's -- a link set up ahead of the first
|
|
151
157
|
// install can point somewhere that doesn't exist yet -- and so that every
|
|
@@ -189,7 +195,7 @@ export function writeJsonFileAtomic(path, data, mode) {
|
|
|
189
195
|
// 0600 from the moment of creation: for .mcp.json this temp file holds
|
|
190
196
|
// resolved MCP server secrets, and it must never be world-readable, not
|
|
191
197
|
// even for the instant before the rename (ahood-cli#119).
|
|
192
|
-
writeFileSync(tmpPath,
|
|
198
|
+
writeFileSync(tmpPath, content, { mode: 0o600 });
|
|
193
199
|
renameSync(tmpPath, dest);
|
|
194
200
|
try {
|
|
195
201
|
chmodSync(dest, finalMode);
|
package/dist/spec.js
CHANGED
|
@@ -116,3 +116,16 @@ export function agentPath(owner, skill) {
|
|
|
116
116
|
// file isn't under .claude/ at all, since it's a convention MCP clients
|
|
117
117
|
// broadly share, not one specific to this CLI).
|
|
118
118
|
export const MCP_CONFIG_PATH = ".mcp.json";
|
|
119
|
+
// A doc installs as one flat file, .claude/docs/<owner>@<doc>.md
|
|
120
|
+
// (ahood-cli#182), the same "@"-joined shape as agentPath above. Unlike
|
|
121
|
+
// agentPath, both segments are validated again here: callers have already
|
|
122
|
+
// parsed them through parseOwnerSkill, but this is the path a doc install
|
|
123
|
+
// writes to, so the SEGMENT_RE containment is enforced where the path is
|
|
124
|
+
// built too. `alice/..` can never resolve outside .claude/docs/.
|
|
125
|
+
export const DOCS_ROOT = join(".claude", "docs");
|
|
126
|
+
export function docPath(owner, doc) {
|
|
127
|
+
const spec = `${owner}/${doc}`;
|
|
128
|
+
validateSegment(owner, "owner", spec);
|
|
129
|
+
validateSegment(doc, "skill", spec);
|
|
130
|
+
return join(DOCS_ROOT, `${owner}@${doc}.md`);
|
|
131
|
+
}
|
package/dist/useme/SKILL.md
CHANGED
|
@@ -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),
|
|
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.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# ahood
|
|
@@ -13,28 +13,31 @@ 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
|
-
##
|
|
16
|
+
## Four kinds, four command groups
|
|
17
17
|
|
|
18
18
|
| Kind | Root file | Installs to | Commands |
|
|
19
19
|
|---|---|---|---|
|
|
20
20
|
| skill | `SKILL.md` | `.claude/skills/<owner>@<skill>/` | `ahood skill <verb>` |
|
|
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
|
+
| doc | `DOC.md` | `.claude/docs/<owner>@<doc>.md` | `ahood doc <verb>` |
|
|
23
24
|
|
|
24
|
-
All
|
|
25
|
+
All four groups take the same verbs: `search`, `list`, `view`/`show`, `read`, `versions`,
|
|
25
26
|
`diff`, `add`, `update`, `outdated`, `remove`, `edit`, `unpublish`, `star`, `unstar`,
|
|
26
27
|
`share`, `unshare`, `init`, `publish`.
|
|
27
28
|
|
|
28
|
-
- `ahood agent <verb>` and `ahood
|
|
29
|
-
kind is refused (exit 2) before anything is downloaded, prompted for,
|
|
30
|
-
and the error names the kind it found and the right command.
|
|
29
|
+
- `ahood agent <verb>`, `ahood mcp <verb>` and `ahood doc <verb>` act on their own kind only.
|
|
30
|
+
A target of another kind is refused (exit 2) before anything is downloaded, prompted for,
|
|
31
|
+
written, or sent, and the error names the kind it found and the right command.
|
|
31
32
|
- `ahood skill <verb>` is the **legacy, cross-kind** group in this CLI release: `skill search`
|
|
32
33
|
and `skill list` include every kind, and `skill add`/`skill update` install whatever kind
|
|
33
|
-
they resolve
|
|
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;
|
|
34
36
|
`--kind all` spells out the legacy behavior. Prefer the kind-specific form in anything new --
|
|
35
37
|
a future major version makes `ahood skill` skills-only.
|
|
36
|
-
-
|
|
37
|
-
|
|
38
|
+
- A doc is a documentation page. `ahood doc read alice/setup-guide` prints it without
|
|
39
|
+
installing anything; `ahood doc add alice/setup-guide` saves it as one pinned file under
|
|
40
|
+
`.claude/docs/`, which `ahood doc update` and `ahood doc remove` manage like any other pin.
|
|
38
41
|
|
|
39
42
|
## Find, inspect, then install
|
|
40
43
|
|
|
@@ -84,7 +87,7 @@ ahood skill update --dry-run --json # preview, machine-readable
|
|
|
84
87
|
ahood agent remove alice/code-reviewer # local uninstall; prompts unless --yes
|
|
85
88
|
```
|
|
86
89
|
|
|
87
|
-
With no target, `agent`/`mcp` `update` and `outdated` consider only installed pins of that
|
|
90
|
+
With no target, `agent`/`mcp`/`doc` `update` and `outdated` consider only installed pins of that
|
|
88
91
|
kind; the legacy `skill update` covers every pin. Updates only affect the current project.
|
|
89
92
|
An `.mcp.json` entry you edited by hand is refused rather than overwritten.
|
|
90
93
|
|
|
@@ -96,13 +99,14 @@ Scaffold a valid starter, edit it, then publish an immutable semver version:
|
|
|
96
99
|
ahood skill init pdf-tools # ./pdf-tools/SKILL.md
|
|
97
100
|
ahood agent init code-reviewer # ./code-reviewer/AGENT.md
|
|
98
101
|
ahood mcp init github-server # ./github-server/server.json (placeholder URL)
|
|
102
|
+
ahood doc init setup-guide # ./setup-guide/DOC.md
|
|
99
103
|
|
|
100
104
|
ahood agent publish alice/code-reviewer@1.0.0 --path code-reviewer --name "Code Reviewer"
|
|
101
105
|
ahood skill publish alice/pdf-tools@1.1.0 --path pdf-tools --changelog "Adds split"
|
|
102
106
|
```
|
|
103
107
|
|
|
104
108
|
- `--name` is required only on the first publish, which creates the entry under your
|
|
105
|
-
account. `agent publish`/`mcp publish` imply the kind and refuse a contradictory `--kind`
|
|
109
|
+
account. `agent publish`/`mcp publish`/`doc publish` imply the kind and refuse a contradictory `--kind`
|
|
106
110
|
or an existing entry of another kind before uploading anything.
|
|
107
111
|
- **New entries are private.** Make one discoverable explicitly:
|
|
108
112
|
`ahood skill edit alice/pdf-tools --visibility public`.
|
|
@@ -146,7 +150,7 @@ ahood skill publish alice/pdf-tools@1.1.0 --path pdf-tools --changelog "Adds spl
|
|
|
146
150
|
CLI's own credential. Example: `claude mcp add ahood -- ahood mcp serve`.
|
|
147
151
|
2. **`ahood mcp <verb>`**: registry commands for MCP server *manifests* (`server.json`) --
|
|
148
152
|
installing one merges a server entry into `.mcp.json`. This does not start anything.
|
|
149
|
-
3. **The hosted registry MCP endpoint**, `https://ahood.
|
|
153
|
+
3. **The hosted registry MCP endpoint**, `https://www.ahood.dev/api/mcp`: Streamable HTTP,
|
|
150
154
|
stateless (POST only), `Authorization: Bearer <token>`. It has write tools too:
|
|
151
155
|
`search_skills`, `get_skill`, `fetch_skill`, `read_skill`, `list_skill_versions`,
|
|
152
156
|
`list_my_skills`, `search_collections`, `get_collection`, `create_skill`, `update_skill`,
|
|
@@ -167,4 +171,4 @@ changes. `ahood skill add alexkay/ahood` is a separate, optional thing: it insta
|
|
|
167
171
|
registry-published ahood skill into this project as a pinned skill, on its own release
|
|
168
172
|
schedule. Neither is required for the other.
|
|
169
173
|
|
|
170
|
-
Full reference: `ahood --help`, `ahood <group> --help`, and https://ahood.
|
|
174
|
+
Full reference: `ahood --help`, `ahood <group> --help`, and https://www.ahood.dev/docs.
|
package/package.json
CHANGED