@ahood/cli 0.8.8 → 0.10.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 +92 -6
- package/dist/commands/add.js +147 -54
- package/dist/commands/completion.js +61 -37
- package/dist/commands/diff.js +4 -1
- package/dist/commands/edit.js +3 -1
- package/dist/commands/init.js +90 -10
- package/dist/commands/list.js +11 -5
- package/dist/commands/login.js +63 -3
- package/dist/commands/mcp.js +6 -3
- package/dist/commands/publish.js +35 -5
- package/dist/commands/read.js +30 -2
- package/dist/commands/remove.js +83 -12
- package/dist/commands/search.js +12 -4
- package/dist/commands/share.js +5 -2
- package/dist/commands/snap.js +140 -35
- package/dist/commands/star.js +5 -2
- package/dist/commands/unpublish.js +5 -1
- package/dist/commands/update.js +109 -11
- package/dist/commands/versions.js +3 -1
- package/dist/commands/view.js +4 -1
- package/dist/commands/whoami.js +50 -9
- package/dist/flags.js +100 -8
- package/dist/help.js +292 -15
- package/dist/http.js +137 -45
- package/dist/index.js +106 -22
- package/dist/kind-info.js +40 -0
- package/dist/kinds.js +122 -0
- package/dist/lockfile.js +397 -38
- package/dist/mcp/safe-tool.js +14 -2
- package/dist/spec.js +8 -5
- package/dist/terminal-safe.js +62 -0
- package/dist/useme/SKILL.md +170 -0
- package/dist/useme.js +14 -0
- package/package.json +3 -2
package/dist/commands/whoami.js
CHANGED
|
@@ -4,14 +4,22 @@ import { resolveToken } from "../credentials.js";
|
|
|
4
4
|
// still authenticate?", which is already settled by the time this runs.
|
|
5
5
|
// A failure here (network blip, an otherwise-valid token hitting a 500 on
|
|
6
6
|
// this specific route, etc.) must never turn a successful auth check into a
|
|
7
|
-
// command failure -- so
|
|
8
|
-
// the plain "Authenticated..." message instead.
|
|
7
|
+
// command failure -- so most errors are swallowed and callers fall back to
|
|
8
|
+
// the plain "Authenticated..." message instead. The one exception is a 404,
|
|
9
|
+
// which carries a `provisioning` flag worth reading rather than discarding:
|
|
10
|
+
// see ProfileOutcome above.
|
|
9
11
|
async function fetchProfile() {
|
|
10
12
|
try {
|
|
11
|
-
|
|
13
|
+
const profile = await apiJson("/api/v1/profile");
|
|
14
|
+
return { profile };
|
|
12
15
|
}
|
|
13
|
-
catch {
|
|
14
|
-
|
|
16
|
+
catch (error) {
|
|
17
|
+
if (error instanceof ApiError && error.status === 404) {
|
|
18
|
+
const body = error.body;
|
|
19
|
+
const provisioning = !!body && typeof body === "object" && body.provisioning === true;
|
|
20
|
+
return { profileStatus: provisioning ? "provisioning" : "not_found" };
|
|
21
|
+
}
|
|
22
|
+
return {};
|
|
15
23
|
}
|
|
16
24
|
}
|
|
17
25
|
// Pure auth-status check: no console output, no process.exitCode -- callers
|
|
@@ -38,13 +46,13 @@ export async function checkAuth() {
|
|
|
38
46
|
await apiJson("/api/v1/auth/tokens");
|
|
39
47
|
// A session-backed caller (only reachable if this ever runs against a
|
|
40
48
|
// cookie-bearing client) -- the token list came back.
|
|
41
|
-
const
|
|
42
|
-
return { authenticated: true, mode: "session",
|
|
49
|
+
const profileOutcome = await fetchProfile();
|
|
50
|
+
return { authenticated: true, mode: "session", ...profileOutcome };
|
|
43
51
|
}
|
|
44
52
|
catch (error) {
|
|
45
53
|
if (error instanceof ApiError && error.status === 403) {
|
|
46
|
-
const
|
|
47
|
-
return { authenticated: true, mode: "token",
|
|
54
|
+
const profileOutcome = await fetchProfile();
|
|
55
|
+
return { authenticated: true, mode: "token", ...profileOutcome };
|
|
48
56
|
}
|
|
49
57
|
if (error instanceof ApiError && error.status === 401) {
|
|
50
58
|
return { authenticated: false, reason: "invalid_token" };
|
|
@@ -89,6 +97,39 @@ export async function whoami(args = []) {
|
|
|
89
97
|
process.exitCode = 1;
|
|
90
98
|
return;
|
|
91
99
|
}
|
|
100
|
+
const asToken = result.mode === "token";
|
|
101
|
+
if (result.profileStatus === "not_found") {
|
|
102
|
+
// Distinct from "provisioning" below: the server saw a 404 for this
|
|
103
|
+
// caller's profile and did NOT flag it as the expected provisioning
|
|
104
|
+
// window (ahood#340) -- authentication is genuinely fine (we got this
|
|
105
|
+
// far), but something about the account's profile is actually wrong
|
|
106
|
+
// rather than merely not-created-yet. Exit 5 ("Not found"), the same
|
|
107
|
+
// code exitCodeFor() already gives an ApiError 404 elsewhere -- this is
|
|
108
|
+
// that same class of failure, just decided locally since checkAuth()
|
|
109
|
+
// never throws.
|
|
110
|
+
if (wantsJson)
|
|
111
|
+
console.log(JSON.stringify({ authenticated: true, mode: result.mode, profileStatus: "not_found" }));
|
|
112
|
+
else {
|
|
113
|
+
console.error(`Authenticated${asToken ? " with a personal API token" : ""}, but no profile was found for this account -- ` +
|
|
114
|
+
"this is not the usual just-signed-up delay, so something is actually wrong. Try `ahood login` again, and " +
|
|
115
|
+
"if it keeps happening, contact support.");
|
|
116
|
+
}
|
|
117
|
+
process.exitCode = 5;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (result.profileStatus === "provisioning") {
|
|
121
|
+
// The expected, self-resolving window between signup and the profile
|
|
122
|
+
// row being created (ahood#336, ahood#340) -- not a failure of any kind,
|
|
123
|
+
// so this stays exit 0. "Refresh this page" (the web copy's hint) makes
|
|
124
|
+
// no sense here; re-running the command is the CLI-native equivalent.
|
|
125
|
+
if (wantsJson)
|
|
126
|
+
console.log(JSON.stringify({ authenticated: true, mode: result.mode, profileStatus: "provisioning" }));
|
|
127
|
+
else {
|
|
128
|
+
console.log(`Authenticated${asToken ? " with a personal API token" : ""}. Your account is still being set up -- ` +
|
|
129
|
+
"this finishes on its own and needs nothing from you. Run `ahood whoami` again in a bit to see your profile.");
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
92
133
|
if (wantsJson) {
|
|
93
134
|
console.log(JSON.stringify({ authenticated: true, mode: result.mode, ...result.profile }));
|
|
94
135
|
return;
|
package/dist/flags.js
CHANGED
|
@@ -11,22 +11,114 @@ import { UsageError } from "./usage-error.js";
|
|
|
11
11
|
// value, verbatim, even if it starts with "--" -- this is
|
|
12
12
|
// the unambiguous escape hatch for values that genuinely
|
|
13
13
|
// start with "--" (e.g. a tagline like "--fast and cheap").
|
|
14
|
+
// A repeated value flag is refused rather than resolved. `flagValue` used to
|
|
15
|
+
// return the FIRST match and stop, while the stripping below skipped EVERY
|
|
16
|
+
// occurrence's neighbour, so `snap search foo --tags a --tags bar` searched for
|
|
17
|
+
// "foo" tagged "a" and deleted "bar" from the query outright -- the user got
|
|
18
|
+
// results for a command they did not type, at exit 0 (ahood-cli#136). Both
|
|
19
|
+
// last-wins and first-wins are silent guesses about which fragment of a
|
|
20
|
+
// command reassembled from shell history is the stale one; the CLI already
|
|
21
|
+
// refuses flags it doesn't recognize, and refusing here is the only option that
|
|
22
|
+
// cannot discard a value the user typed. The cost is one retry with the
|
|
23
|
+
// duplicate deleted, which is also the edit the user has to make anyway.
|
|
24
|
+
function assertFirstOccurrence(seen, flag) {
|
|
25
|
+
if (seen.has(flag)) {
|
|
26
|
+
throw new UsageError(`${flag} given more than once. Pass it once -- repeating it would silently discard one of the values` +
|
|
27
|
+
// Safe to say unconditionally for --tags: every --tags in this CLI
|
|
28
|
+
// (publish/edit/snap create/list/search) is one comma-separated value,
|
|
29
|
+
// and "it accumulates" is the misconception that produces the repeat.
|
|
30
|
+
(flag === "--tags" ? `; pass multiple tags as one comma-separated value (--tags a,b)` : "") +
|
|
31
|
+
`.`);
|
|
32
|
+
}
|
|
33
|
+
seen.add(flag);
|
|
34
|
+
}
|
|
14
35
|
export function flagValue(args, flag) {
|
|
15
36
|
const prefix = `${flag}=`;
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
let found;
|
|
16
39
|
for (let i = 0; i < args.length; i++) {
|
|
17
40
|
const arg = args[i];
|
|
18
41
|
if (arg.startsWith(prefix)) {
|
|
19
|
-
|
|
42
|
+
assertFirstOccurrence(seen, flag);
|
|
43
|
+
found = arg.slice(prefix.length);
|
|
44
|
+
continue;
|
|
20
45
|
}
|
|
21
46
|
if (arg === flag) {
|
|
22
47
|
const value = args[i + 1];
|
|
23
48
|
if (value === undefined || value.startsWith("--")) {
|
|
24
49
|
throw new UsageError(`${flag} requires a value.`);
|
|
25
50
|
}
|
|
26
|
-
|
|
51
|
+
assertFirstOccurrence(seen, flag);
|
|
52
|
+
found = value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
// Returns the tokens left over after stripping the flags a command declares:
|
|
58
|
+
// `booleanFlags` stand alone (--json), `valueFlags` consume the following token
|
|
59
|
+
// and also accept the --flag=value form. Whatever remains is something the
|
|
60
|
+
// command does NOT accept -- an unrecognized "--" flag or a stray positional --
|
|
61
|
+
// and it's the caller's job to reject it (or, for a query/id command, to
|
|
62
|
+
// interpret it as the positional).
|
|
63
|
+
//
|
|
64
|
+
// Lives here rather than inline in each command because hand-rolling this check
|
|
65
|
+
// per command is exactly how `snap list` ended up with no check at all while
|
|
66
|
+
// its sibling `snap search` had one, so `snap list --tag ci` (singular typo)
|
|
67
|
+
// issued an UNFILTERED request and printed every snap as though it were the
|
|
68
|
+
// filtered set (ahood-cli#135). parseSearchQuery below is now a caller of this
|
|
69
|
+
// helper rather than a second copy of it (folded in by ahood-cli#136, whose bug
|
|
70
|
+
// existed in both copies); tagsSnap still carries its own inline copy -- fold it
|
|
71
|
+
// in when the queued snap-parsing fixes (ahood-cli#137/#138) next touch it,
|
|
72
|
+
// rather than growing a third variant again.
|
|
73
|
+
export function unrecognizedArgs(args, booleanFlags, valueFlags) {
|
|
74
|
+
return unrecognizedIndices(args, booleanFlags, valueFlags).map((i) => args[i]);
|
|
75
|
+
}
|
|
76
|
+
// The same stripping rules as unrecognizedArgs, reporting WHERE the leftovers
|
|
77
|
+
// sat rather than what they were -- and the primitive the two share, so there
|
|
78
|
+
// is still exactly one copy of those rules (the whole point of #135's helper;
|
|
79
|
+
// a second hand-rolled variant is the mistake it exists to stop).
|
|
80
|
+
//
|
|
81
|
+
// `snap create` needs the positions, not just the tokens: its leftovers are the
|
|
82
|
+
// words of a freeform note, so "starts with --" is the only thing it can reject
|
|
83
|
+
// outright, and the one remaining signal that a bare token was misplaced is that
|
|
84
|
+
// the leftovers sit on BOTH sides of a flag the command consumed -- which is
|
|
85
|
+
// precisely `snap create "note" --tags deploy bugfix` folding "bugfix" into the
|
|
86
|
+
// note body (ahood-cli#134). Positions are the only way to see that gap.
|
|
87
|
+
export function unrecognizedIndices(args, booleanFlags, valueFlags) {
|
|
88
|
+
const indices = [];
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
// Which index (if any) the value flag just seen actually consumed. The test
|
|
91
|
+
// used to be `valueFlags.includes(args[i - 1])`, which stripped the neighbour
|
|
92
|
+
// of EVERY occurrence -- so a repeated --tags ate a word nothing had read
|
|
93
|
+
// (ahood-cli#136). Tracking the consuming occurrence means exactly one token
|
|
94
|
+
// per accepted flag disappears, and assertFirstOccurrence refuses the repeat
|
|
95
|
+
// with the same message flagValue gives, so a caller that reaches this helper
|
|
96
|
+
// first can't report the duplicate as "Unknown flag: --tags" instead.
|
|
97
|
+
let consumedAt = -1;
|
|
98
|
+
for (let i = 0; i < args.length; i++) {
|
|
99
|
+
const arg = args[i];
|
|
100
|
+
if (i === consumedAt)
|
|
101
|
+
continue;
|
|
102
|
+
if (booleanFlags.includes(arg))
|
|
103
|
+
continue;
|
|
104
|
+
if (valueFlags.includes(arg)) {
|
|
105
|
+
assertFirstOccurrence(seen, arg);
|
|
106
|
+
// Same swallow-protection as flagValue: a following "--" token is not
|
|
107
|
+
// this flag's value, it's the next flag, so leave it to be judged on its
|
|
108
|
+
// own (flagValue throws "requires a value" for it first in every caller).
|
|
109
|
+
const next = args[i + 1];
|
|
110
|
+
if (next !== undefined && !next.startsWith("--"))
|
|
111
|
+
consumedAt = i + 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const equalsForm = valueFlags.find((f) => arg.startsWith(`${f}=`));
|
|
115
|
+
if (equalsForm !== undefined) {
|
|
116
|
+
assertFirstOccurrence(seen, equalsForm);
|
|
117
|
+
continue;
|
|
27
118
|
}
|
|
119
|
+
indices.push(i);
|
|
28
120
|
}
|
|
29
|
-
return
|
|
121
|
+
return indices;
|
|
30
122
|
}
|
|
31
123
|
// Shared by every "<verb> <query> [--json] [--limit <n>]" command (skill
|
|
32
124
|
// search, snap search) -- factored out after this exact logic was
|
|
@@ -43,11 +135,11 @@ export function flagValue(args, flag) {
|
|
|
43
135
|
// a flag that command doesn't implement instead of silently ignoring it and
|
|
44
136
|
// returning unfiltered results.
|
|
45
137
|
export function parseSearchQuery(args, usage, valueFlags = []) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
138
|
+
// Delegated to unrecognizedArgs rather than re-filtering here: this was the
|
|
139
|
+
// second hand-rolled copy of those stripping rules, and it carried the same
|
|
140
|
+
// repeated-flag bug (ahood-cli#136) that the first one did -- which is the
|
|
141
|
+
// duplication hazard #135's helper was extracted to end.
|
|
142
|
+
const queryParts = unrecognizedArgs(args, ["--json"], ["--limit", ...valueFlags]);
|
|
51
143
|
const unknownFlag = queryParts.find((a) => a.startsWith("--"));
|
|
52
144
|
if (unknownFlag)
|
|
53
145
|
throw new UsageError(`Unknown flag: ${unknownFlag}\n${usage}`);
|
package/dist/help.js
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
// Canonical command list for terminal --help output. Kept in sync by hand
|
|
2
|
+
// with cli/README.md's table and app/docs/page.tsx's COMMANDS array (three
|
|
3
|
+
// copies of the same list, none of which can literally import from this
|
|
4
|
+
// file -- README.md is plain markdown and the docs page is a separate
|
|
5
|
+
// Next.js app) -- a comment in each of those two places points back here so
|
|
6
|
+
// a future command addition doesn't update only one.
|
|
7
|
+
//
|
|
8
|
+
// The command surface is split into two tiers, gh-style:
|
|
9
|
+
// - TOP_LEVEL_COMMANDS_HELP: account/auth-scoped commands that stay flat
|
|
10
|
+
// (login, logout, whoami, token, completion) -- same reasoning
|
|
11
|
+
// `gh auth login` isn't `gh account login`.
|
|
12
|
+
// - SKILL_COMMANDS_HELP: every registry verb, reached as `ahood skill
|
|
13
|
+
// <verb>` -- legacy and cross-kind in this release (ahood-cli#172).
|
|
14
|
+
// - AGENT_COMMANDS_HELP / MCP_COMMANDS_HELP: the same verbs, kind-scoped,
|
|
15
|
+
// generated from one template (registryCommandsHelp); MCP's list also
|
|
16
|
+
// carries `ahood mcp` / `ahood mcp serve`, the local stdio server.
|
|
17
|
+
// - GROUP_COMMANDS_HELP / SNAP_COMMANDS_HELP: the other entity nouns.
|
|
18
|
+
//
|
|
19
|
+
// `summary` is a genuinely one-sentence blurb used only by the top-level
|
|
20
|
+
// `ahood --help` / `ahood skill --help` listings. `desc` is the full
|
|
21
|
+
// (possibly multi-sentence) description rendered in full by `ahood help
|
|
22
|
+
// <command>` / `ahood skill <verb> --help`. Keep `summary` self-contained --
|
|
23
|
+
// it must not depend on the reader having also seen `desc`.
|
|
24
|
+
import { KINDS } from "./kind-info.js";
|
|
1
25
|
export const TOP_LEVEL_COMMANDS_HELP = [
|
|
2
26
|
{
|
|
3
27
|
usage: "ahood login",
|
|
@@ -27,12 +51,15 @@ export const TOP_LEVEL_COMMANDS_HELP = [
|
|
|
27
51
|
examples: ["ahood completion bash >> ~/.bashrc"],
|
|
28
52
|
},
|
|
29
53
|
{
|
|
30
|
-
usage: "ahood
|
|
31
|
-
summary: "
|
|
32
|
-
desc: "
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
54
|
+
usage: "ahood help useme",
|
|
55
|
+
summary: "Print the bundled ahood SKILL.md for an AI agent -- raw, offline, no login needed.",
|
|
56
|
+
desc: "Print the ahood self-skill bundled with this exact CLI version: a complete SKILL.md teaching an AI agent " +
|
|
57
|
+
"(or a human) when and how to use ahood. Stdout is the raw file and nothing else -- no banner, no colors -- " +
|
|
58
|
+
"so it can be pasted into an agent's context, redirected into a file, or read directly by an agent with " +
|
|
59
|
+
"shell access. Works offline and without credentials, and never writes to your project. It is not the " +
|
|
60
|
+
"same as `ahood skill add alexkay/ahood`, which installs the registry-published copy as a separate, " +
|
|
61
|
+
"pinned project skill.",
|
|
62
|
+
examples: ["ahood help useme", "ahood help useme > ahood-SKILL.md"],
|
|
36
63
|
},
|
|
37
64
|
];
|
|
38
65
|
export const SKILL_COMMANDS_HELP = [
|
|
@@ -206,6 +233,182 @@ export const SKILL_COMMANDS_HELP = [
|
|
|
206
233
|
],
|
|
207
234
|
},
|
|
208
235
|
];
|
|
236
|
+
// Kind-scoped registry verbs (ahood-cli#172): `ahood agent <verb>` and
|
|
237
|
+
// `ahood mcp <verb>` take the same verbs as `ahood skill <verb>`, run by the
|
|
238
|
+
// same handlers, but strictly for their own kind -- a target of another kind
|
|
239
|
+
// is refused before any side effect. Generated from one template per verb
|
|
240
|
+
// rather than hand-copied twice, so the two lists can't drift from each other.
|
|
241
|
+
// SKILL_COMMANDS_HELP above stays hand-written: it documents the legacy,
|
|
242
|
+
// cross-kind behavior `ahood skill` keeps in this release.
|
|
243
|
+
function registryCommandsHelp(kind) {
|
|
244
|
+
const info = KINDS[kind];
|
|
245
|
+
const n = kind === "agent" ? "<agent>" : "<server>";
|
|
246
|
+
const p = `ahood ${kind}`;
|
|
247
|
+
const label = info.label;
|
|
248
|
+
const a = info.article;
|
|
249
|
+
const plural = info.plural;
|
|
250
|
+
const doc = info.rootDoc;
|
|
251
|
+
const ex = kind === "agent" ? "alice/code-reviewer" : "alice/github-server";
|
|
252
|
+
const kindRefusal = `Refused, before anything else happens, when the target is not ${a} ${label}.`;
|
|
253
|
+
return [
|
|
254
|
+
{
|
|
255
|
+
usage: `${p} search <query> [--json] [--limit <n>]`,
|
|
256
|
+
summary: `Search published ${plural}.`,
|
|
257
|
+
desc: `Search published ${plural} only (the registry's ?kind=${kind} filter). Same output and --json shape as \`ahood skill search\`.`,
|
|
258
|
+
flags: ["--json Emit the raw result objects instead of formatted lines.", "--limit <n> Cap the number of results."],
|
|
259
|
+
examples: [`${p} search github`],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
usage: `${p} view <owner>/${n} [--json] [--web]`,
|
|
263
|
+
summary: `Show a single ${label}'s details without installing it (alias: ${p} show).`,
|
|
264
|
+
desc: `Show a single ${label}'s details without installing it. Alias: ${p} show. ${kindRefusal}`,
|
|
265
|
+
flags: ["--json Emit the raw object instead of formatted lines.", "--web Open its page in your browser instead of printing."],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
usage: `${p} read <owner>/${n} [--json]`,
|
|
269
|
+
summary: `Print a published ${label}'s ${doc}, without installing it.`,
|
|
270
|
+
desc: `Print the ${doc} of a published ${label} verbatim, without installing it -- read it before you add it. ${kindRefusal}`,
|
|
271
|
+
flags: ["--json Emit {version, content} as a single line instead of the raw content."],
|
|
272
|
+
examples: [`${p} read ${ex}`],
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
usage: `${p} versions <owner>/${n} [--json]`,
|
|
276
|
+
summary: `List a ${label}'s published-version history.`,
|
|
277
|
+
desc: `List a ${label}'s published-version history: version, changelog, size, and publish date. ${kindRefusal}`,
|
|
278
|
+
flags: ["--json Emit the raw version objects instead of formatted text."],
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
usage: `${p} diff <owner>/${n} <versionA> <versionB> [--json]`,
|
|
282
|
+
summary: `Show what changed between two published versions of ${a} ${label}.`,
|
|
283
|
+
desc: `Show what changed between two published versions, exactly like \`ahood skill diff\`. Both versions must be explicit semver. ${kindRefusal}`,
|
|
284
|
+
flags: ["--json Emit {skillmd_diff, manifest: {added, removed, changed}} instead of formatted text."],
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
usage: `${p} list [--json]`,
|
|
288
|
+
summary: `List your own ${plural}, public and private.`,
|
|
289
|
+
desc: `List your own ${plural}, public and private. The registry returns every entry you own; this keeps only the ${kind} ones ` +
|
|
290
|
+
"(an entry with no kind in the response is left out, with a warning on stderr). --json keeps `ahood skill list --json`'s shape.",
|
|
291
|
+
flags: ["--json Emit the raw objects instead of formatted lines."],
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
usage: `${p} add <owner>/${n}[@version]`,
|
|
295
|
+
summary: `Install ${a} ${label} into ${info.installsTo}, pinned in the lockfile.`,
|
|
296
|
+
desc: `Install ${a} ${label} into ${info.installsTo}, pinned in .claude/skills.lock.json. ` +
|
|
297
|
+
(kind === "mcp" ? "Prompts for any secret environment variables its server.json declares that aren't already set. " : "") +
|
|
298
|
+
`Refused before anything is downloaded or written when the target is not ${a} ${label}.`,
|
|
299
|
+
examples: [`${p} add ${ex}`, `${p} add ${ex}@1.2.0`],
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
usage: `${p} update [<owner>/${n} ...] [--dry-run] [--json]`,
|
|
303
|
+
summary: `Move ${label} pins forward to the latest version.`,
|
|
304
|
+
desc: `Move lockfile pins forward to the latest version. With no argument, considers only the installed ${plural} ` +
|
|
305
|
+
`in this project -- other kinds' pins are left alone. A named target that is not ${a} ${label} is refused and counted as a failure.`,
|
|
306
|
+
flags: [
|
|
307
|
+
"--dry-run Preview current vs. latest version (and the changelog) without installing anything.",
|
|
308
|
+
"--json With --dry-run, emit structured preview objects instead of a formatted table.",
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
usage: `${p} outdated [<owner>/${n} ...] [--json]`,
|
|
313
|
+
summary: `Read-only staleness check for installed ${plural}.`,
|
|
314
|
+
desc: `Read-only staleness check -- equivalent to \`${p} update --dry-run\`. With no argument, checks only the installed ${plural}.`,
|
|
315
|
+
flags: ["--json Emit structured preview objects instead of a formatted table."],
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
usage: `${p} remove <owner>/${n} [--yes]`,
|
|
319
|
+
summary: `Uninstall and unpin ${a} ${label} (local only, prompts unless --yes is passed).`,
|
|
320
|
+
desc: `Uninstall and unpin ${a} ${label} from this project (local only). Refused, before the prompt and before anything is deleted, ` +
|
|
321
|
+
`when the install is another kind; the kind is read from the project's own files, and from the registry only when they can't tell.`,
|
|
322
|
+
flags: ["--yes Skip the interactive confirmation, for scripts/CI."],
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
usage: `${p} edit <owner>/${n} [--tagline] [--tags] [--license] [--visibility] [--homepage] [--repository]`,
|
|
326
|
+
summary: `Update ${a} ${label} you own, changing only the flags you pass.`,
|
|
327
|
+
desc: `Update ${a} ${label} you own; same flags as \`ahood skill edit\`. ${kindRefusal}`,
|
|
328
|
+
flags: [
|
|
329
|
+
"--tagline <text> Short one-line description.",
|
|
330
|
+
"--tags <comma,separated> Replaces the tag list.",
|
|
331
|
+
"--license <id> An SPDX license identifier, e.g. MIT.",
|
|
332
|
+
"--visibility public|private Who can see and install it.",
|
|
333
|
+
"--homepage <url> Homepage URL.",
|
|
334
|
+
"--repository <url> Source repository URL.",
|
|
335
|
+
],
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
usage: `${p} unpublish <owner>/${n}[@version] [--yes]`,
|
|
339
|
+
summary: `Delete ${a} ${label} from the registry for every consumer, or yank one version (prompts unless --yes is passed).`,
|
|
340
|
+
desc: `Without @version: delete ${a} ${label} from the registry for every consumer. With @version: yank that version. ` +
|
|
341
|
+
`Refused before the prompt when the target is not ${a} ${label}.`,
|
|
342
|
+
flags: ["--yes Skip the interactive confirmation, for scripts/CI."],
|
|
343
|
+
},
|
|
344
|
+
{ usage: `${p} star <owner>/${n}`, summary: `Star ${a} ${label}.`, desc: `Star ${a} ${label}. ${kindRefusal}` },
|
|
345
|
+
{ usage: `${p} unstar <owner>/${n}`, summary: `Remove your star from ${a} ${label}.`, desc: `Remove your star from ${a} ${label}. ${kindRefusal}` },
|
|
346
|
+
{
|
|
347
|
+
usage: `${p} share <owner>/${n} --group <group>`,
|
|
348
|
+
summary: `Share ${a} ${label} you own with a group, without changing its visibility.`,
|
|
349
|
+
desc: `Share ${a} ${label} you own with a group you belong to. ${kindRefusal}`,
|
|
350
|
+
flags: ["--group <group> The group's slug (from `ahood group list`)."],
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
usage: `${p} unshare <owner>/${n} --group <group>`,
|
|
354
|
+
summary: `Stop sharing ${a} ${label} you own with a group.`,
|
|
355
|
+
desc: `Stop sharing ${a} ${label} you own with a group. ${kindRefusal}`,
|
|
356
|
+
flags: ["--group <group> The group's slug."],
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
usage: `${p} init [name]`,
|
|
360
|
+
summary: `Scaffold a new ${label} folder with a minimal, valid ${doc}.`,
|
|
361
|
+
desc: `Scaffold a minimal ${doc} that passes the registry's publish validation: ./<name>/${doc} with a name, or ./${doc} without. ` +
|
|
362
|
+
(kind === "mcp"
|
|
363
|
+
? "The starter points at a reserved example.com URL, so it can never install anyone's real server until you edit it. "
|
|
364
|
+
: "") +
|
|
365
|
+
`Refuses to overwrite an existing ${doc}, or to add one to a folder that already holds another kind's root document. ` +
|
|
366
|
+
"Names are slug-normalized and must stay inside the current directory.",
|
|
367
|
+
examples: [`${p} init ${ex.split("/")[1]}`, `${p} init`],
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
usage: `${p} publish <owner>/${n}@<version> [--path <dir>] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--changelog <text>] [--json]`,
|
|
371
|
+
summary: `Publish a new version of ${a} ${label} from a folder containing ${doc}, creating it first if it doesn't exist yet.`,
|
|
372
|
+
desc: `Publish a new version from a folder containing ${doc}; the kind is implied, and a contradictory --kind is refused. ` +
|
|
373
|
+
`If the entry already exists as another kind, this stops before anything is packed or uploaded. ` +
|
|
374
|
+
"If it doesn't exist yet, it is created first -- pass --name for that. Otherwise the same flags and --json output as `ahood skill publish`.",
|
|
375
|
+
flags: [
|
|
376
|
+
"--path <dir> Folder to publish (default: the current directory).",
|
|
377
|
+
"--name <text> Required only when creating the entry on this publish.",
|
|
378
|
+
"--changelog <text> What changed in this version.",
|
|
379
|
+
"--json Print one {version,status,...} JSON object on completion (or {error} on failure).",
|
|
380
|
+
],
|
|
381
|
+
examples: [`${p} publish ${ex}@1.0.0 --name "${kind === "agent" ? "Code Reviewer" : "GitHub Server"}"`],
|
|
382
|
+
},
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
export const AGENT_COMMANDS_HELP = registryCommandsHelp("agent");
|
|
386
|
+
// `ahood mcp` is two things (ADR 0007 in the ahood repo separates the hosted
|
|
387
|
+
// registry MCP endpoint from this CLI's local one): with no verb, or with
|
|
388
|
+
// `serve`, it is the local, read-only stdio MCP server that MCP hosts spawn;
|
|
389
|
+
// with a registry verb it manages mcp-kind registry entries (server.json
|
|
390
|
+
// manifests). Both are listed here so `ahood mcp --help` explains both.
|
|
391
|
+
export const MCP_SERVE_HELP = [
|
|
392
|
+
{
|
|
393
|
+
usage: "ahood mcp serve",
|
|
394
|
+
summary: "Start the local, read-only ahood MCP server over stdio (preferred spelling).",
|
|
395
|
+
desc: "Start a Model Context Protocol server over stdio, exposing skill_search, skill_view, skill_read, " +
|
|
396
|
+
"skill_versions, skill_list, skill_outdated, and whoami as MCP tools -- the same data ahood's --json " +
|
|
397
|
+
"commands already return, reachable as typed tool calls instead of parsed CLI output. Meant to be " +
|
|
398
|
+
"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.vercel.app/api/mcp), which " +
|
|
400
|
+
"is a separate server with write tools.",
|
|
401
|
+
examples: ['claude mcp add ahood -- ahood mcp serve', "npx @ahood/cli@latest mcp serve"],
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
usage: "ahood mcp",
|
|
405
|
+
summary: "Same as `ahood mcp serve`, kept byte-for-byte so existing MCP host configs keep working.",
|
|
406
|
+
desc: "With no arguments at all, `ahood mcp` starts the same stdio server as `ahood mcp serve` -- nothing is printed " +
|
|
407
|
+
"to stdout before the protocol starts, and no login or network is needed to start it. Any other first word must " +
|
|
408
|
+
"be `serve`, `--help`, or a registry verb below; anything unrecognized fails with usage instead of starting a server.",
|
|
409
|
+
},
|
|
410
|
+
];
|
|
411
|
+
export const MCP_COMMANDS_HELP = [...MCP_SERVE_HELP, ...registryCommandsHelp("mcp")];
|
|
209
412
|
// Every group-entity verb, all reached as `ahood group <verb>` -- mirrors
|
|
210
413
|
// SKILL_COMMANDS_HELP above, just for the "Groups" feature: private groups,
|
|
211
414
|
// shareable invite links, and sharing your own skills with a group without
|
|
@@ -283,14 +486,17 @@ export const SNAP_COMMANDS_HELP = [
|
|
|
283
486
|
summary: "Capture a new private snap, from an argument or piped stdin.",
|
|
284
487
|
desc: "Capture a new private snap. Pass the content as an argument, or omit it and pipe content on stdin " +
|
|
285
488
|
"(e.g. `echo \"...\" | ahood snap create`, or a piped heredoc) -- useful for capturing a whole session's " +
|
|
286
|
-
"worth of freeform text at once. Prints the new snap's id on success."
|
|
489
|
+
"worth of freeform text at once. Prints the new snap's id on success. An unrecognized flag is refused " +
|
|
490
|
+
"rather than folded into the note; pass content that itself starts with `--` after a bare `--`.",
|
|
287
491
|
flags: [
|
|
288
492
|
"--tags <comma,separated> Attach tags to the new snap.",
|
|
289
493
|
"--json Emit {id, created_at} instead of just the bare id.",
|
|
494
|
+
"-- End of options: everything after it is content, verbatim.",
|
|
290
495
|
],
|
|
291
496
|
examples: [
|
|
292
497
|
'ahood snap create "Debugged the flaky CI step, root cause was a race in the cache key." --tags deploy,bugfix',
|
|
293
498
|
'echo "..." | ahood snap create',
|
|
499
|
+
'ahood snap create -- "--json broke the parser"',
|
|
294
500
|
],
|
|
295
501
|
},
|
|
296
502
|
{
|
|
@@ -346,13 +552,24 @@ export const SNAP_COMMANDS_HELP = [
|
|
|
346
552
|
flags: ["--yes Skip the interactive confirmation, for scripts/CI."],
|
|
347
553
|
},
|
|
348
554
|
{
|
|
349
|
-
usage: "ahood snap tags <id> [
|
|
350
|
-
summary: "
|
|
351
|
-
desc: "
|
|
352
|
-
"
|
|
353
|
-
"
|
|
354
|
-
|
|
355
|
-
|
|
555
|
+
usage: "ahood snap tags <id> [tag ...] [--clear] [--json]",
|
|
556
|
+
summary: "Print a snap's tags, or replace them with the tags given.",
|
|
557
|
+
desc: "With no tag list, prints the snap's current tags and changes nothing. With a tag list, replaces the " +
|
|
558
|
+
"full set -- this is not a merge, so any existing tags not listed are dropped. Tags may be given as " +
|
|
559
|
+
"separate arguments, comma-separated, or both; quote a tag that contains a space. Clearing every tag " +
|
|
560
|
+
"is explicit: pass --clear or an empty string. Each tag prints quoted, so one multi-word tag never " +
|
|
561
|
+
"looks like two. No confirmation prompt: unlike remove/unshare this only touches metadata, never the " +
|
|
562
|
+
"snap's content or shareability, and the tags it replaces can be read back first.",
|
|
563
|
+
flags: [
|
|
564
|
+
"--clear Remove every tag from the snap.",
|
|
565
|
+
"--json Emit {id, tags} instead of a plain-text summary.",
|
|
566
|
+
],
|
|
567
|
+
examples: [
|
|
568
|
+
"ahood snap tags snap_123",
|
|
569
|
+
"ahood snap tags snap_123 deploy bugfix",
|
|
570
|
+
"ahood snap tags snap_123 deploy,bugfix",
|
|
571
|
+
"ahood snap tags snap_123 --clear",
|
|
572
|
+
],
|
|
356
573
|
},
|
|
357
574
|
];
|
|
358
575
|
// Exported so other consumers of the command list (e.g. shell completion) can
|
|
@@ -366,6 +583,8 @@ export const COMMAND_ALIASES = { show: "view" };
|
|
|
366
583
|
// only has to be added here once, not re-special-cased in both places.
|
|
367
584
|
const ENTITY_COMMANDS_HELP = {
|
|
368
585
|
skill: SKILL_COMMANDS_HELP,
|
|
586
|
+
agent: AGENT_COMMANDS_HELP,
|
|
587
|
+
mcp: MCP_COMMANDS_HELP,
|
|
369
588
|
group: GROUP_COMMANDS_HELP,
|
|
370
589
|
snap: SNAP_COMMANDS_HELP,
|
|
371
590
|
};
|
|
@@ -487,12 +706,58 @@ export function formatSkillHelp() {
|
|
|
487
706
|
return [
|
|
488
707
|
"ahood skill -- manage skills in the ahood registry",
|
|
489
708
|
"",
|
|
709
|
+
"Legacy, cross-kind: in this release `ahood skill <verb>` still acts on any registry kind",
|
|
710
|
+
"(skills, agents, MCP server manifests) -- `skill list`/`skill search` include every kind, and",
|
|
711
|
+
"`skill add`/`skill update` install whatever kind they resolve. Scripts relying on that keep",
|
|
712
|
+
"working. For one kind only, use `ahood agent <verb>` / `ahood mcp <verb>`, or add",
|
|
713
|
+
"`--kind skill|agent|mcp` to any verb below (`--kind all` spells out the legacy behavior).",
|
|
714
|
+
"A future major version will make `ahood skill` skills-only.",
|
|
715
|
+
"",
|
|
490
716
|
"Commands:",
|
|
491
717
|
...lines,
|
|
492
718
|
"",
|
|
493
719
|
"Run `ahood skill <command> --help` for a single command's flags and examples.",
|
|
494
720
|
].join("\n");
|
|
495
721
|
}
|
|
722
|
+
// `ahood agent --help` / `ahood mcp --help` -- kind-scoped registry verbs.
|
|
723
|
+
function formatRegistryKindHelp(header, intro, entries, noun) {
|
|
724
|
+
const lines = formatCommandTable(entries.map((c) => ({ usage: usageWithAliases(c), summary: c.summary })));
|
|
725
|
+
return [
|
|
726
|
+
header,
|
|
727
|
+
"",
|
|
728
|
+
...intro,
|
|
729
|
+
"",
|
|
730
|
+
"Commands:",
|
|
731
|
+
...lines,
|
|
732
|
+
"",
|
|
733
|
+
`Run \`ahood ${noun} <command> --help\` for a single command's flags and examples.`,
|
|
734
|
+
].join("\n");
|
|
735
|
+
}
|
|
736
|
+
export function formatAgentHelp() {
|
|
737
|
+
return formatRegistryKindHelp("ahood agent -- manage agent definitions (AGENT.md) in the ahood registry", [
|
|
738
|
+
"Every verb here acts on agents only: a target of another kind is refused before anything",
|
|
739
|
+
"is downloaded, prompted for, written, or sent. Installs land in .claude/agents/<owner>@<agent>.md.",
|
|
740
|
+
], AGENT_COMMANDS_HELP, "agent");
|
|
741
|
+
}
|
|
742
|
+
export function formatMcpHelp() {
|
|
743
|
+
return formatRegistryKindHelp("ahood mcp -- the local MCP server, and MCP server manifests (server.json) in the ahood registry", [
|
|
744
|
+
"Two meanings, told apart by the first word:",
|
|
745
|
+
" ahood mcp / ahood mcp serve start this CLI's local, read-only MCP server over stdio",
|
|
746
|
+
" (what MCP host configs launch; `serve` is the preferred spelling)",
|
|
747
|
+
" ahood mcp <verb> manage mcp-kind registry entries (installs merge into .mcp.json)",
|
|
748
|
+
"Neither is the hosted registry MCP endpoint, https://ahood.vercel.app/api/mcp.",
|
|
749
|
+
"Registry verbs act on MCP server manifests only; another kind is refused before any side effect.",
|
|
750
|
+
], MCP_COMMANDS_HELP, "mcp");
|
|
751
|
+
}
|
|
752
|
+
// One entry point for every registry noun's group help, so dispatch and
|
|
753
|
+
// `ahood help <noun>` can't disagree about which listing a noun gets.
|
|
754
|
+
export function formatKindHelp(noun) {
|
|
755
|
+
if (noun === "agent")
|
|
756
|
+
return formatAgentHelp();
|
|
757
|
+
if (noun === "mcp")
|
|
758
|
+
return formatMcpHelp();
|
|
759
|
+
return formatSkillHelp();
|
|
760
|
+
}
|
|
496
761
|
// `ahood group --help` -- the group-level listing for every group verb.
|
|
497
762
|
export function formatGroupHelp() {
|
|
498
763
|
const lines = formatCommandTable(GROUP_COMMANDS_HELP.map((c) => ({ usage: usageWithAliases(c), summary: c.summary })));
|
|
@@ -523,7 +788,11 @@ export function formatSnapHelp() {
|
|
|
523
788
|
// entity command lists.
|
|
524
789
|
export function formatHelp() {
|
|
525
790
|
const skillGroupUsage = "ahood skill <command>";
|
|
526
|
-
const skillGroupSummary = "Search, install, and publish skills -- run `ahood skill --help` for the full list.";
|
|
791
|
+
const skillGroupSummary = "Search, install, and publish skills (legacy: also any other kind) -- run `ahood skill --help` for the full list.";
|
|
792
|
+
const agentGroupUsage = "ahood agent <command>";
|
|
793
|
+
const agentGroupSummary = "The same verbs, for agent definitions only -- run `ahood agent --help` for the full list.";
|
|
794
|
+
const mcpGroupUsage = "ahood mcp <command>";
|
|
795
|
+
const mcpGroupSummary = "The same verbs, for MCP server manifests only; bare `ahood mcp` or `ahood mcp serve` starts the local MCP server.";
|
|
527
796
|
const groupGroupUsage = "ahood group <command>";
|
|
528
797
|
const groupGroupSummary = "Create private groups and share skills with them -- run `ahood group --help` for the full list.";
|
|
529
798
|
const snapGroupUsage = "ahood snap <command>";
|
|
@@ -531,6 +800,8 @@ export function formatHelp() {
|
|
|
531
800
|
const lines = formatCommandTable([
|
|
532
801
|
...TOP_LEVEL_COMMANDS_HELP.map((c) => ({ usage: usageWithAliases(c), summary: c.summary })),
|
|
533
802
|
{ usage: skillGroupUsage, summary: skillGroupSummary },
|
|
803
|
+
{ usage: agentGroupUsage, summary: agentGroupSummary },
|
|
804
|
+
{ usage: mcpGroupUsage, summary: mcpGroupSummary },
|
|
534
805
|
{ usage: groupGroupUsage, summary: groupGroupSummary },
|
|
535
806
|
{ usage: snapGroupUsage, summary: snapGroupSummary },
|
|
536
807
|
]);
|
|
@@ -542,11 +813,17 @@ export function formatHelp() {
|
|
|
542
813
|
" ahood skill search <something>",
|
|
543
814
|
" ahood skill add <owner>/<skill>",
|
|
544
815
|
"",
|
|
816
|
+
"Use ahood with an AI agent:",
|
|
817
|
+
" ahood help useme Print a complete SKILL.md teaching an agent how to use ahood.",
|
|
818
|
+
" Raw stdout, offline, no login -- paste it to an agent, or let an",
|
|
819
|
+
" agent with a shell run it.",
|
|
820
|
+
"",
|
|
545
821
|
"Commands:",
|
|
546
822
|
...lines,
|
|
547
823
|
"",
|
|
548
824
|
"Run `ahood <command> --help` (or `ahood help <command>`) for a single command's flags and examples.",
|
|
549
825
|
"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 MCP commands.",
|
|
550
827
|
"Run `ahood group --help` for the full list of group commands.",
|
|
551
828
|
"Run `ahood snap --help` for the full list of snap commands.",
|
|
552
829
|
"Run `ahood --version` to print the installed CLI version.",
|