specguard-mcp 0.1.3 → 0.1.5

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.
Files changed (41) hide show
  1. package/README.md +255 -6
  2. package/dist/src/support/specguard-api.d.ts +36 -0
  3. package/dist/src/support/specguard-api.js +59 -4
  4. package/dist/src/support/specguard-api.js.map +1 -1
  5. package/dist/src/tools/add-repository-member.d.ts +31 -0
  6. package/dist/src/tools/add-repository-member.js +105 -0
  7. package/dist/src/tools/add-repository-member.js.map +1 -0
  8. package/dist/src/tools/args.d.ts +26 -0
  9. package/dist/src/tools/args.js +52 -0
  10. package/dist/src/tools/args.js.map +1 -1
  11. package/dist/src/tools/create-repository-api-key.d.ts +26 -0
  12. package/dist/src/tools/create-repository-api-key.js +83 -0
  13. package/dist/src/tools/create-repository-api-key.js.map +1 -0
  14. package/dist/src/tools/index.d.ts +96 -11
  15. package/dist/src/tools/index.js +116 -11
  16. package/dist/src/tools/index.js.map +1 -1
  17. package/dist/src/tools/list-repository-members.d.ts +32 -0
  18. package/dist/src/tools/list-repository-members.js +76 -0
  19. package/dist/src/tools/list-repository-members.js.map +1 -0
  20. package/dist/src/tools/near-duplicate-clusters.d.ts +58 -0
  21. package/dist/src/tools/near-duplicate-clusters.js +116 -0
  22. package/dist/src/tools/near-duplicate-clusters.js.map +1 -0
  23. package/dist/src/tools/registrable-repositories.d.ts +51 -0
  24. package/dist/src/tools/registrable-repositories.js +92 -0
  25. package/dist/src/tools/registrable-repositories.js.map +1 -0
  26. package/dist/src/tools/remove-repository-member.d.ts +30 -0
  27. package/dist/src/tools/remove-repository-member.js +93 -0
  28. package/dist/src/tools/remove-repository-member.js.map +1 -0
  29. package/dist/src/tools/remove-repository.d.ts +33 -0
  30. package/dist/src/tools/remove-repository.js +81 -0
  31. package/dist/src/tools/remove-repository.js.map +1 -0
  32. package/dist/src/tools/rename-repository.d.ts +44 -0
  33. package/dist/src/tools/rename-repository.js +99 -0
  34. package/dist/src/tools/rename-repository.js.map +1 -0
  35. package/dist/src/tools/revoke-repository-api-key.d.ts +29 -0
  36. package/dist/src/tools/revoke-repository-api-key.js +85 -0
  37. package/dist/src/tools/revoke-repository-api-key.js.map +1 -0
  38. package/dist/src/tools/update-repository-member-permissions.d.ts +30 -0
  39. package/dist/src/tools/update-repository-member-permissions.js +97 -0
  40. package/dist/src/tools/update-repository-member-permissions.js.map +1 -0
  41. package/package.json +2 -1
@@ -93,4 +93,56 @@ export function optionalBoolean(value, field) {
93
93
  throw new ArgumentError(`\`${field}\` must be a boolean.`);
94
94
  return value;
95
95
  }
96
+ /**
97
+ * An optional array of non-blank strings, or nothing.
98
+ *
99
+ * The SHAPE-ONLY sibling of the linter's `optionalStringArray` (which stays in
100
+ * `lint-intent-annotations.ts` because its refusals — empty list, leading dash
101
+ * — are about the linter's own argument grammar and throw `CommandError`). This
102
+ * one answers only "is it an array of strings" and always throws
103
+ * `ArgumentError`, which is what a pass-through array of permission names
104
+ * needs: the platform is the authority on which strings are legal, and a
105
+ * client-side copy of that rule is a rule with no owner, free to drift from the
106
+ * one that actually decides.
107
+ */
108
+ export function optionalStringArray(value, field) {
109
+ if (value === undefined || value === null)
110
+ return undefined;
111
+ if (!Array.isArray(value))
112
+ throw new ArgumentError(`\`${field}\` must be an array of strings.`);
113
+ const entries = value.map((entry) => {
114
+ if (typeof entry !== "string")
115
+ throw new ArgumentError(`\`${field}\` must contain only strings.`);
116
+ // Trimmed like its scalar siblings: a value an agent produced by
117
+ // concatenating strings arrives with whitespace that is not part of what it
118
+ // meant to send.
119
+ return entry.trim();
120
+ });
121
+ return entries;
122
+ }
123
+ /**
124
+ * A mandatory array of non-blank strings, and nothing else will do.
125
+ *
126
+ * The mandatory counterpart of `optionalStringArray`, added with
127
+ * `update_repository_member_permissions` (SPGD-885): the server column behind
128
+ * that call is `text[]`, and a scalar on the wire is silently dropped
129
+ * server-side — so the array shape is checked HERE, before a write, rather
130
+ * than persisted as a member holding nothing. Blank entries are dropped rather
131
+ * than refused: the server's own normalisation does the same, so refusing here
132
+ * would be a second vocabulary the caller must satisfy before the one that
133
+ * decides even sees the request.
134
+ */
135
+ export function requireStringArray(value, field) {
136
+ if (value === undefined || value === null) {
137
+ throw new ArgumentError(`\`${field}\` is required.`);
138
+ }
139
+ if (!Array.isArray(value))
140
+ throw new ArgumentError(`\`${field}\` must be an array of strings.`);
141
+ const entries = value.map((entry) => {
142
+ if (typeof entry !== "string")
143
+ throw new ArgumentError(`\`${field}\` must contain only strings.`);
144
+ return entry.trim();
145
+ });
146
+ return entries.filter((entry) => entry !== "");
147
+ }
96
148
  //# sourceMappingURL=args.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"args.js","sourceRoot":"","sources":["../../../src/tools/args.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AAEH;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc,EAAE,KAAa;IAC1D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,sBAAsB,CAAC,CAAC;IACzF,8EAA8E;IAC9E,gFAAgF;IAChF,2EAA2E;IAC3E,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc,EAAE,KAAa;IACzD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QAC1C,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,iBAAiB,CAAC,CAAC;IACvD,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,sBAAsB,CAAC,CAAC;IAEzF,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,OAAO,KAAK,EAAE;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,uBAAuB,CAAC,CAAC;IAE/E,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAc,EAAE,KAAa;IAC3D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,MAAM,IAAI,aAAa,CAAC,KAAK,KAAK,uBAAuB,CAAC,CAAC;IAC3F,OAAO,KAAK,CAAC;AACf,CAAC"}
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../../../src/tools/args.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ /**
3
+ * `POST /api/v1/repositories/:repository_id/api_keys` as a tool — shipped in
4
+ * the platform (`specguard/config/routes.rb:158`,
5
+ * `user_repository_api_keys_controller#create`, SPGD-754).
6
+ *
7
+ * == Reveal-once, again
8
+ *
9
+ * The 201 body carries `api_key.token` — the raw key, the only time it exists
10
+ * anywhere — exactly as `add_repository`'s does. The body is therefore passed
11
+ * through UNRESHAPED in both `text` and `structured` for the same reason that
12
+ * tool states: any reshaping on this hop is a value that cannot be recovered
13
+ * rather than a field that can be re-fetched. Recovery for a dropped token is
14
+ * minting another key — this same tool — because the platform ships no
15
+ * `#regenerate` and no re-serve.
16
+ *
17
+ * == The name is top-level and optional
18
+ *
19
+ * `params[:name]` defaults to `ApiKey::DEFAULT_NAME` server-side; `undefined`
20
+ * here means "let the server name it" and simply omits the key from the POST
21
+ * body. Not re-validated here for the reason `add_repository` states: a second
22
+ * format rule on this side is a rule with no owner, free to drift from the one
23
+ * that actually decides.
24
+ */
25
+ declare const createRepositoryApiKey: ToolDefinition;
26
+ export default createRepositoryApiKey;
@@ -0,0 +1,83 @@
1
+ import { postJsonObject, requireUserApiConfig } from "../support/specguard-api.js";
2
+ import { optionalString, requireString } from "./args.js";
3
+ /**
4
+ * `POST /api/v1/repositories/:repository_id/api_keys` as a tool — shipped in
5
+ * the platform (`specguard/config/routes.rb:158`,
6
+ * `user_repository_api_keys_controller#create`, SPGD-754).
7
+ *
8
+ * == Reveal-once, again
9
+ *
10
+ * The 201 body carries `api_key.token` — the raw key, the only time it exists
11
+ * anywhere — exactly as `add_repository`'s does. The body is therefore passed
12
+ * through UNRESHAPED in both `text` and `structured` for the same reason that
13
+ * tool states: any reshaping on this hop is a value that cannot be recovered
14
+ * rather than a field that can be re-fetched. Recovery for a dropped token is
15
+ * minting another key — this same tool — because the platform ships no
16
+ * `#regenerate` and no re-serve.
17
+ *
18
+ * == The name is top-level and optional
19
+ *
20
+ * `params[:name]` defaults to `ApiKey::DEFAULT_NAME` server-side; `undefined`
21
+ * here means "let the server name it" and simply omits the key from the POST
22
+ * body. Not re-validated here for the reason `add_repository` states: a second
23
+ * format rule on this side is a rule with no owner, free to drift from the one
24
+ * that actually decides.
25
+ */
26
+ const createRepositoryApiKey = {
27
+ name: "create_repository_api_key",
28
+ title: "Create repository API key",
29
+ description: "Mints a new CI API key (an sgk_… key) for a SpecGuard repository, and returns it " +
30
+ "alongside the repository's existing keys. " +
31
+ "⚠️ `api_key.token` is shown THIS ONCE AND NEVER AGAIN — nothing stores it and no " +
32
+ "endpoint can re-serve it, so hand it to the user in your reply rather than assuming " +
33
+ "it can be fetched later. If it is dropped, the recovery is minting another key with " +
34
+ "this same tool (the platform has no regenerate), then revoking the orphaned one. " +
35
+ "On success the response carries an `api_key` block (`name`, `token`, `hint`, " +
36
+ "`created_at`) — the same reveal-once shape `add_repository` serves. " +
37
+ "Minting does not disturb existing keys: each key on a repository authenticates " +
38
+ "independently until revoked. " +
39
+ "Takes `repository_id` (the numeric id `list_repositories` reports) and an optional " +
40
+ "`name`, which the server defaults when omitted. " +
41
+ "Authorization is the `keys_manage` capability — a member without it is refused 403 " +
42
+ "in SpecGuard's own words. " +
43
+ "Needs SPECGUARD_USER_API_KEY (an sgu_… key), the same credential `add_repository` " +
44
+ "writes with and a DIFFERENT one from the sgk_… repository key " +
45
+ "`get_repository_overview` uses.",
46
+ inputSchema: {
47
+ type: "object",
48
+ properties: {
49
+ repository_id: {
50
+ type: "string",
51
+ description: "The repository to mint the key for — its numeric id, as `add_repository` " +
52
+ "returns and `list_repositories` reports, not the `org/repo` handle.",
53
+ },
54
+ name: {
55
+ type: "string",
56
+ description: "An optional label for the key. Omit it to let SpecGuard use its default name. " +
57
+ "The server validates the name and refuses an unusable one in its own words.",
58
+ },
59
+ },
60
+ required: ["repository_id"],
61
+ // Closed for the reason every tool here states — and on a WRITE, a silently
62
+ // dropped misspelled argument still mints something, just possibly mislabeled.
63
+ additionalProperties: false,
64
+ },
65
+ async run(args, context) {
66
+ const repositoryId = requireString(args["repository_id"], "repository_id");
67
+ const name = optionalString(args["name"], "name");
68
+ const api = requireUserApiConfig(context.config);
69
+ // `name` omitted when absent rather than sent as null, so the server's own
70
+ // `ApiKey::DEFAULT_NAME` default applies — this bridge expresses "no
71
+ // preference", it does not choose on the server's behalf.
72
+ const created = await postJsonObject(api, `/api/v1/repositories/${encodeURIComponent(repositoryId)}/api_keys`, name === undefined ? {} : { name }, context.fetch);
73
+ // Unreshaped, for the reason `add_repository` states at its own return:
74
+ // `api_key.token` exists nowhere else, so any reshaping on this hop is a
75
+ // value that cannot be recovered.
76
+ return {
77
+ text: JSON.stringify(created, null, 2),
78
+ structured: created,
79
+ };
80
+ },
81
+ };
82
+ export default createRepositoryApiKey;
83
+ //# sourceMappingURL=create-repository-api-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-repository-api-key.js","sourceRoot":"","sources":["../../../src/tools/create-repository-api-key.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,sBAAsB,GAAmB;IAC7C,IAAI,EAAE,2BAA2B;IACjC,KAAK,EAAE,2BAA2B;IAClC,WAAW,EACT,mFAAmF;QACnF,4CAA4C;QAC5C,mFAAmF;QACnF,sFAAsF;QACtF,sFAAsF;QACtF,mFAAmF;QACnF,+EAA+E;QAC/E,sEAAsE;QACtE,iFAAiF;QACjF,+BAA+B;QAC/B,qFAAqF;QACrF,kDAAkD;QAClD,qFAAqF;QACrF,4BAA4B;QAC5B,oFAAoF;QACpF,gEAAgE;QAChE,iCAAiC;IACnC,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,aAAa,EAAE;gBACb,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,2EAA2E;oBAC3E,qEAAqE;aACxE;YACD,IAAI,EAAE;gBACJ,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,gFAAgF;oBAChF,6EAA6E;aAChF;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,CAAC;QAC3B,4EAA4E;QAC5E,+EAA+E;QAC/E,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC,CAAC;QAC3E,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC;QAElD,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,2EAA2E;QAC3E,qEAAqE;QACrE,0DAA0D;QAC1D,MAAM,OAAO,GAAG,MAAM,cAAc,CAClC,GAAG,EACH,wBAAwB,kBAAkB,CAAC,YAAY,CAAC,WAAW,EACnE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAClC,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,wEAAwE;QACxE,yEAAyE;QACzE,kCAAkC;QAClC,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACtC,UAAU,EAAE,OAAO;SACpB,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
@@ -34,12 +34,21 @@ import type { ToolDefinition } from "./types.js";
34
34
  *
35
35
  * == What is deliberately absent
36
36
  *
37
- * `/check-intent` and duplicate clustering are NOT here and must not be added
38
- * until their backing engine and data exist (SPGD-114 / SPGD-115). A tool
39
- * advertised in `tools/list` is a promise an agent will act on: wrapping an
40
- * endpoint that does not exist would produce a server that discovers cleanly
41
- * and fails on use, which is worse than not offering the tool, because the
42
- * agent has already committed to a plan by the time it finds out.
37
+ * `/check-intent` is NOT here and must not be added until its backing endpoint
38
+ * exists — `specguard/config/routes.rb:113` carries it as a comment only, which
39
+ * is the evidence this forbid rests on. A tool advertised in `tools/list` is a
40
+ * promise an agent will act on: wrapping an endpoint that does not exist would
41
+ * produce a server that discovers cleanly and fails on use, which is worse than
42
+ * not offering the tool, because the agent has already committed to a plan by
43
+ * the time it finds out.
44
+ *
45
+ * Duplicate clustering was once under this same forbid, on the same standing
46
+ * rule — no tool may wrap what has not shipped. That half retired when the
47
+ * platform moved: SPGD-703 (`specguard` `c43dc19`, 2026-08-28) shipped
48
+ * `GET /api/v1/repository?near_duplicates=`, serving
49
+ * `RepositoryOverview#serialized_near_duplicates` behind an opt-in ask, and
50
+ * `near_duplicate_clusters` below wraps it. What moved was the platform, not
51
+ * the bar.
43
52
  *
44
53
  * == The fourth: the first tool that WRITES
45
54
  *
@@ -56,11 +65,87 @@ import type { ToolDefinition } from "./types.js";
56
65
  * beside it, and `describeFailure` grew the `400` branch that surfaces
57
66
  * SpecGuard's own refusal sentence — the modal answer this endpoint gives.
58
67
  *
59
- * The standing rule is unchanged and still binding, which is what keeps the rest
60
- * of the user-scoped write surface out: `DELETE /api/v1/repositories/:id` and
61
- * the API-key endpoints (SPGD-754) are NOT on `origin/main`, so they may not be
62
- * wrapped here however useful a tool for them would be. What moved was the
63
- * platform, not the bar.
68
+ * The standing rule is unchanged and still binding. What once kept `DELETE
69
+ * /api/v1/repositories/:id` and the API-key endpoints out under it — "not on
70
+ * `origin/main`, so they may not be wrapped" — stopped being true when SPGD-754
71
+ * shipped them, and the sixth-through-eighth section below records their
72
+ * wrapping. What moved was the platform, not the bar.
73
+ *
74
+ * == The fifth: the read half of the registration gate
75
+ *
76
+ * - `registrable_repositories` wraps `GET /api/v1/repositories/registrable`
77
+ * (shipped: `specguard/config/routes.rb:117`,
78
+ * `Api::V1::UserRepositoriesController#registrable`).
79
+ *
80
+ * `list_repositories` says what IS registered; this says what COULD be — the
81
+ * set the gate would consult, read out in advance, so an agent can pick a
82
+ * `full_name` for `add_repository` from a real answer. Landing it also
83
+ * generalised the 400 branch in `describeFailure` into a status-parameterised
84
+ * extractor, because this endpoint's modal first answer is a 403 (`not_granted`)
85
+ * carrying the same `{error, message}` contract — the identical defect, given
86
+ * the identical remedy.
87
+ *
88
+ * The user-scoped surface as it now stands is therefore three tools: read the
89
+ * list, read the gate's answer, write a registration. The standing rule is
90
+ * unchanged and still binding, which is what keeps the rest out:
91
+ * `DELETE /api/v1/repositories/:id` and the API-key endpoints (SPGD-754) are
92
+ * NOT on `origin/main`, so they may not be wrapped here however useful a tool
93
+ * for them would be. What moved was the platform, not the bar.
94
+ *
95
+ * == The sixth through eighth: removal and the key lifecycle
96
+ *
97
+ * - `remove_repository` wraps `DELETE /api/v1/repositories/:id`, and
98
+ * `create_repository_api_key` / `revoke_repository_api_key` wrap the two
99
+ * `api_keys` endpoints (all shipped: SPGD-754, `specguard@origin/main`).
100
+ *
101
+ * The closing fence the two paragraphs above share — "the DELETE and API-key
102
+ * endpoints are NOT on `origin/main`, so they may not be wrapped" — stopped
103
+ * being true when SPGD-754 landed, and these three entries are what became
104
+ * wrappable the moment it did. They also forced the transport's third verb:
105
+ * both DELETE endpoints answer `204` with NO body, the one response in the
106
+ * `sgu_` surface that is deliberately not JSON, which is why `deleteJson`
107
+ * returns the raw body text instead of routing an empty 204 through
108
+ * `requestJson`'s JSON parse.
109
+ *
110
+ * The standing rule itself is unchanged and still binding — which still keeps
111
+ * out `/check-intent`: it has no backing endpoint (`routes.rb:113` mounts it
112
+ * as a comment only). A tool advertised in `tools/list` remains a promise an
113
+ * agent will act on.
114
+ *
115
+ * == The ninth through twelfth: member management
116
+ *
117
+ * - `list_repository_members`, `add_repository_member`,
118
+ * `update_repository_member_permissions` and `remove_repository_member`
119
+ * wrap the four `user_repository_members` endpoints (all shipped: SPGD-875,
120
+ * specguard@origin/main).
121
+ *
122
+ * They also forced the transport's fourth verb: `update` is a PATCH, which
123
+ * arrived with `patchJson` in this same slice — 200 with a JSON body, so it
124
+ * routes through `requestJson` like `postJson` rather than `deleteJson`'s
125
+ * raw-body handling.
126
+ *
127
+ * A known limitation rides with them and is stated in the edit/revoke tool
128
+ * descriptions rather than papered over: PATCH and DELETE name a membership by
129
+ * id, but no endpoint serves that id (the platform's `#serialize` deliberately
130
+ * omits it), so the id comes from the platform's web members page today. That
131
+ * is a platform-side follow-up, not a client-side workaround — there is no
132
+ * name-based lookup.
133
+ *
134
+ * == The thirteenth: rename
135
+ *
136
+ * - `rename_repository` wraps `PATCH /api/v1/repositories/:id` (shipped:
137
+ * SPGD-878, `specguard@origin/main` e026793, PR #266).
138
+ *
139
+ * This is the entry the forbid above once named as "the natural next slice" —
140
+ * the platform moved (SPGD-878 shipped the endpoint), so the forbid retired
141
+ * exactly as the duplicate-clustering one did. It reuses `patchJson` landed
142
+ * with the member-management slice rather than minting a transport of its own.
143
+ * The verb's one asymmetry is carried in the tool description rather than
144
+ * papered over: rename authorizes OWNER-ONLY, while removal authorizes the
145
+ * BROADER `repo.delete` capability — a member who may destroy the repository
146
+ * may not rename it. Before this tool the bridge's only rename path was
147
+ * remove-and-re-register, which destroys every key, run and intent; this one
148
+ * keeps them.
64
149
  */
65
150
  export declare const TOOLS: readonly ToolDefinition[];
66
151
  export type { ToolContext, ToolDefinition, ToolResult } from "./types.js";
@@ -1,7 +1,17 @@
1
1
  import addRepository from "./add-repository.js";
2
+ import addRepositoryMember from "./add-repository-member.js";
3
+ import createRepositoryApiKey from "./create-repository-api-key.js";
2
4
  import lintIntentAnnotations from "./lint-intent-annotations.js";
3
5
  import listRepositories from "./list-repositories.js";
6
+ import listRepositoryMembers from "./list-repository-members.js";
7
+ import nearDuplicateClusters from "./near-duplicate-clusters.js";
4
8
  import getRepositoryOverview from "./repository-overview.js";
9
+ import registrableRepositories from "./registrable-repositories.js";
10
+ import removeRepository from "./remove-repository.js";
11
+ import removeRepositoryMember from "./remove-repository-member.js";
12
+ import renameRepository from "./rename-repository.js";
13
+ import revokeRepositoryApiKey from "./revoke-repository-api-key.js";
14
+ import updateRepositoryMemberPermissions from "./update-repository-member-permissions.js";
5
15
  /**
6
16
  * THE REGISTRY — the one file that changes when the toolset grows.
7
17
  *
@@ -37,12 +47,21 @@ import getRepositoryOverview from "./repository-overview.js";
37
47
  *
38
48
  * == What is deliberately absent
39
49
  *
40
- * `/check-intent` and duplicate clustering are NOT here and must not be added
41
- * until their backing engine and data exist (SPGD-114 / SPGD-115). A tool
42
- * advertised in `tools/list` is a promise an agent will act on: wrapping an
43
- * endpoint that does not exist would produce a server that discovers cleanly
44
- * and fails on use, which is worse than not offering the tool, because the
45
- * agent has already committed to a plan by the time it finds out.
50
+ * `/check-intent` is NOT here and must not be added until its backing endpoint
51
+ * exists — `specguard/config/routes.rb:113` carries it as a comment only, which
52
+ * is the evidence this forbid rests on. A tool advertised in `tools/list` is a
53
+ * promise an agent will act on: wrapping an endpoint that does not exist would
54
+ * produce a server that discovers cleanly and fails on use, which is worse than
55
+ * not offering the tool, because the agent has already committed to a plan by
56
+ * the time it finds out.
57
+ *
58
+ * Duplicate clustering was once under this same forbid, on the same standing
59
+ * rule — no tool may wrap what has not shipped. That half retired when the
60
+ * platform moved: SPGD-703 (`specguard` `c43dc19`, 2026-08-28) shipped
61
+ * `GET /api/v1/repository?near_duplicates=`, serving
62
+ * `RepositoryOverview#serialized_near_duplicates` behind an opt-in ask, and
63
+ * `near_duplicate_clusters` below wraps it. What moved was the platform, not
64
+ * the bar.
46
65
  *
47
66
  * == The fourth: the first tool that WRITES
48
67
  *
@@ -59,16 +78,102 @@ import getRepositoryOverview from "./repository-overview.js";
59
78
  * beside it, and `describeFailure` grew the `400` branch that surfaces
60
79
  * SpecGuard's own refusal sentence — the modal answer this endpoint gives.
61
80
  *
62
- * The standing rule is unchanged and still binding, which is what keeps the rest
63
- * of the user-scoped write surface out: `DELETE /api/v1/repositories/:id` and
64
- * the API-key endpoints (SPGD-754) are NOT on `origin/main`, so they may not be
65
- * wrapped here however useful a tool for them would be. What moved was the
66
- * platform, not the bar.
81
+ * The standing rule is unchanged and still binding. What once kept `DELETE
82
+ * /api/v1/repositories/:id` and the API-key endpoints out under it — "not on
83
+ * `origin/main`, so they may not be wrapped" — stopped being true when SPGD-754
84
+ * shipped them, and the sixth-through-eighth section below records their
85
+ * wrapping. What moved was the platform, not the bar.
86
+ *
87
+ * == The fifth: the read half of the registration gate
88
+ *
89
+ * - `registrable_repositories` wraps `GET /api/v1/repositories/registrable`
90
+ * (shipped: `specguard/config/routes.rb:117`,
91
+ * `Api::V1::UserRepositoriesController#registrable`).
92
+ *
93
+ * `list_repositories` says what IS registered; this says what COULD be — the
94
+ * set the gate would consult, read out in advance, so an agent can pick a
95
+ * `full_name` for `add_repository` from a real answer. Landing it also
96
+ * generalised the 400 branch in `describeFailure` into a status-parameterised
97
+ * extractor, because this endpoint's modal first answer is a 403 (`not_granted`)
98
+ * carrying the same `{error, message}` contract — the identical defect, given
99
+ * the identical remedy.
100
+ *
101
+ * The user-scoped surface as it now stands is therefore three tools: read the
102
+ * list, read the gate's answer, write a registration. The standing rule is
103
+ * unchanged and still binding, which is what keeps the rest out:
104
+ * `DELETE /api/v1/repositories/:id` and the API-key endpoints (SPGD-754) are
105
+ * NOT on `origin/main`, so they may not be wrapped here however useful a tool
106
+ * for them would be. What moved was the platform, not the bar.
107
+ *
108
+ * == The sixth through eighth: removal and the key lifecycle
109
+ *
110
+ * - `remove_repository` wraps `DELETE /api/v1/repositories/:id`, and
111
+ * `create_repository_api_key` / `revoke_repository_api_key` wrap the two
112
+ * `api_keys` endpoints (all shipped: SPGD-754, `specguard@origin/main`).
113
+ *
114
+ * The closing fence the two paragraphs above share — "the DELETE and API-key
115
+ * endpoints are NOT on `origin/main`, so they may not be wrapped" — stopped
116
+ * being true when SPGD-754 landed, and these three entries are what became
117
+ * wrappable the moment it did. They also forced the transport's third verb:
118
+ * both DELETE endpoints answer `204` with NO body, the one response in the
119
+ * `sgu_` surface that is deliberately not JSON, which is why `deleteJson`
120
+ * returns the raw body text instead of routing an empty 204 through
121
+ * `requestJson`'s JSON parse.
122
+ *
123
+ * The standing rule itself is unchanged and still binding — which still keeps
124
+ * out `/check-intent`: it has no backing endpoint (`routes.rb:113` mounts it
125
+ * as a comment only). A tool advertised in `tools/list` remains a promise an
126
+ * agent will act on.
127
+ *
128
+ * == The ninth through twelfth: member management
129
+ *
130
+ * - `list_repository_members`, `add_repository_member`,
131
+ * `update_repository_member_permissions` and `remove_repository_member`
132
+ * wrap the four `user_repository_members` endpoints (all shipped: SPGD-875,
133
+ * specguard@origin/main).
134
+ *
135
+ * They also forced the transport's fourth verb: `update` is a PATCH, which
136
+ * arrived with `patchJson` in this same slice — 200 with a JSON body, so it
137
+ * routes through `requestJson` like `postJson` rather than `deleteJson`'s
138
+ * raw-body handling.
139
+ *
140
+ * A known limitation rides with them and is stated in the edit/revoke tool
141
+ * descriptions rather than papered over: PATCH and DELETE name a membership by
142
+ * id, but no endpoint serves that id (the platform's `#serialize` deliberately
143
+ * omits it), so the id comes from the platform's web members page today. That
144
+ * is a platform-side follow-up, not a client-side workaround — there is no
145
+ * name-based lookup.
146
+ *
147
+ * == The thirteenth: rename
148
+ *
149
+ * - `rename_repository` wraps `PATCH /api/v1/repositories/:id` (shipped:
150
+ * SPGD-878, `specguard@origin/main` e026793, PR #266).
151
+ *
152
+ * This is the entry the forbid above once named as "the natural next slice" —
153
+ * the platform moved (SPGD-878 shipped the endpoint), so the forbid retired
154
+ * exactly as the duplicate-clustering one did. It reuses `patchJson` landed
155
+ * with the member-management slice rather than minting a transport of its own.
156
+ * The verb's one asymmetry is carried in the tool description rather than
157
+ * papered over: rename authorizes OWNER-ONLY, while removal authorizes the
158
+ * BROADER `repo.delete` capability — a member who may destroy the repository
159
+ * may not rename it. Before this tool the bridge's only rename path was
160
+ * remove-and-re-register, which destroys every key, run and intent; this one
161
+ * keeps them.
67
162
  */
68
163
  export const TOOLS = [
69
164
  lintIntentAnnotations,
70
165
  getRepositoryOverview,
71
166
  listRepositories,
72
167
  addRepository,
168
+ registrableRepositories,
169
+ removeRepository,
170
+ createRepositoryApiKey,
171
+ revokeRepositoryApiKey,
172
+ nearDuplicateClusters,
173
+ listRepositoryMembers,
174
+ addRepositoryMember,
175
+ updateRepositoryMemberPermissions,
176
+ removeRepositoryMember,
177
+ renameRepository,
73
178
  ];
74
179
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAG7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8DG;AACH,MAAM,CAAC,MAAM,KAAK,GAA8B;IAC9C,qBAAqB;IACrB,qBAAqB;IACrB,gBAAgB;IAChB,aAAa;CACd,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,qBAAqB,CAAC;AAChD,OAAO,mBAAmB,MAAM,4BAA4B,CAAC;AAC7D,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AACpE,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,qBAAqB,MAAM,8BAA8B,CAAC;AACjE,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAC7D,OAAO,uBAAuB,MAAM,+BAA+B,CAAC;AACpE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,sBAAsB,MAAM,+BAA+B,CAAC;AACnE,OAAO,gBAAgB,MAAM,wBAAwB,CAAC;AACtD,OAAO,sBAAsB,MAAM,gCAAgC,CAAC;AACpE,OAAO,iCAAiC,MAAM,2CAA2C,CAAC;AAG1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmJG;AACH,MAAM,CAAC,MAAM,KAAK,GAA8B;IAC9C,qBAAqB;IACrB,qBAAqB;IACrB,gBAAgB;IAChB,aAAa;IACb,uBAAuB;IACvB,gBAAgB;IAChB,sBAAsB;IACtB,sBAAsB;IACtB,qBAAqB;IACrB,qBAAqB;IACrB,mBAAmB;IACnB,iCAAiC;IACjC,sBAAsB;IACtB,gBAAgB;CACjB,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ /**
3
+ * `GET /api/v1/repositories/:repository_id/members` as a tool — shipped in the
4
+ * platform (`specguard/config/routes.rb`, `Api::V1::UserRepositoryMembersController#index`,
5
+ * SPGD-875).
6
+ *
7
+ * == Memberships only, never `keys_minted`
8
+ *
9
+ * The endpoint answers the same rows the web members page renders — `handle`,
10
+ * `permissions`, `granted_by`, `created_at`, ordered by handle — and
11
+ * deliberately NOTHING else. `keys_minted` is a `keys.manage` disclosure that
12
+ * page gates separately; this read answers memberships to a `members.manage`
13
+ * holder and no more. A tool that promised counts here would be promising
14
+ * something the endpoint refuses to say.
15
+ *
16
+ * == No membership id, by design
17
+ *
18
+ * The body carries no membership id: ids are not portable between
19
+ * repositories, and serving one would invite treating it as portable. PATCH
20
+ * and DELETE still name rows by that id — see
21
+ * `update_repository_member_permissions` and `remove_repository_member` for
22
+ * where the id comes from today.
23
+ *
24
+ * == No client-side capability probing
25
+ *
26
+ * The server gates at `current_repository(:members_manage)`: a non-member gets
27
+ * 404 (the repository's existence stays hidden), a member without the
28
+ * capability gets 403 with SpecGuard's own sentence. This tool predicts
29
+ * neither; it reports what came back.
30
+ */
31
+ declare const listRepositoryMembers: ToolDefinition;
32
+ export default listRepositoryMembers;
@@ -0,0 +1,76 @@
1
+ import { getJsonObject, requireUserApiConfig } from "../support/specguard-api.js";
2
+ import { requireString } from "./args.js";
3
+ /**
4
+ * `GET /api/v1/repositories/:repository_id/members` as a tool — shipped in the
5
+ * platform (`specguard/config/routes.rb`, `Api::V1::UserRepositoryMembersController#index`,
6
+ * SPGD-875).
7
+ *
8
+ * == Memberships only, never `keys_minted`
9
+ *
10
+ * The endpoint answers the same rows the web members page renders — `handle`,
11
+ * `permissions`, `granted_by`, `created_at`, ordered by handle — and
12
+ * deliberately NOTHING else. `keys_minted` is a `keys.manage` disclosure that
13
+ * page gates separately; this read answers memberships to a `members.manage`
14
+ * holder and no more. A tool that promised counts here would be promising
15
+ * something the endpoint refuses to say.
16
+ *
17
+ * == No membership id, by design
18
+ *
19
+ * The body carries no membership id: ids are not portable between
20
+ * repositories, and serving one would invite treating it as portable. PATCH
21
+ * and DELETE still name rows by that id — see
22
+ * `update_repository_member_permissions` and `remove_repository_member` for
23
+ * where the id comes from today.
24
+ *
25
+ * == No client-side capability probing
26
+ *
27
+ * The server gates at `current_repository(:members_manage)`: a non-member gets
28
+ * 404 (the repository's existence stays hidden), a member without the
29
+ * capability gets 403 with SpecGuard's own sentence. This tool predicts
30
+ * neither; it reports what came back.
31
+ */
32
+ const listRepositoryMembers = {
33
+ name: "list_repository_members",
34
+ title: "List repository members",
35
+ description: "Lists who has access to a SpecGuard repository: one row per member with their `handle`, " +
36
+ "`permissions`, `granted_by` (who last set them) and `created_at`, ordered by handle. " +
37
+ "The list answers MEMBERSHIPS only and never reports how many CI keys a member has minted " +
38
+ "(`keys_minted`) — that is a separate `keys.manage` disclosure this endpoint deliberately " +
39
+ "withholds, and the API-keys tools are the surface for it. " +
40
+ "Authorization is the `members.manage` capability: a caller who is not a member of the " +
41
+ "repository is refused 404 (the repository's existence stays hidden), and a member without " +
42
+ "`members.manage` is refused 403 in SpecGuard's own words. " +
43
+ "Takes `repository_id` — the numeric id `list_repositories` reports, not the `org/repo` " +
44
+ "handle. " +
45
+ "Needs SPECGUARD_USER_API_KEY (an sgu_… key), the same credential `list_repositories` " +
46
+ "reads and a DIFFERENT one from the sgk_… repository key `get_repository_overview` uses.",
47
+ inputSchema: {
48
+ type: "object",
49
+ properties: {
50
+ repository_id: {
51
+ type: "string",
52
+ description: "The repository whose members to list — its numeric id, as `add_repository` " +
53
+ "returns and `list_repositories` reports, not the `org/repo` handle.",
54
+ },
55
+ },
56
+ required: ["repository_id"],
57
+ // Closed for the reason every tool here states: `server.ts` forwards
58
+ // `arguments` unvalidated, so an open schema would let a misspelled argument
59
+ // be dropped silently.
60
+ additionalProperties: false,
61
+ },
62
+ async run(args, context) {
63
+ const repositoryId = requireString(args["repository_id"], "repository_id");
64
+ const api = requireUserApiConfig(context.config);
65
+ const members = await getJsonObject(api, `/api/v1/repositories/${encodeURIComponent(repositoryId)}/members`, {}, context.fetch);
66
+ // Passed through unreshaped, the standing rule (`types.ts`: "A thin client
67
+ // that reshapes its upstream is not thin") — the endpoint serves the same
68
+ // four fields the web members page renders, under the same names.
69
+ return {
70
+ text: JSON.stringify(members, null, 2),
71
+ structured: members,
72
+ };
73
+ },
74
+ };
75
+ export default listRepositoryMembers;
76
+ //# sourceMappingURL=list-repository-members.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-repository-members.js","sourceRoot":"","sources":["../../../src/tools/list-repository-members.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAClF,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAG1C;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,qBAAqB,GAAmB;IAC5C,IAAI,EAAE,yBAAyB;IAC/B,KAAK,EAAE,yBAAyB;IAChC,WAAW,EACT,0FAA0F;QAC1F,uFAAuF;QACvF,2FAA2F;QAC3F,2FAA2F;QAC3F,4DAA4D;QAC5D,wFAAwF;QACxF,4FAA4F;QAC5F,4DAA4D;QAC5D,yFAAyF;QACzF,UAAU;QACV,uFAAuF;QACvF,yFAAyF;IAC3F,WAAW,EAAE;QACX,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,aAAa,EAAE;gBACb,IAAI,EAAE,QAAQ;gBACd,WAAW,EACT,6EAA6E;oBAC7E,qEAAqE;aACxE;SACF;QACD,QAAQ,EAAE,CAAC,eAAe,CAAC;QAC3B,qEAAqE;QACrE,6EAA6E;QAC7E,uBAAuB;QACvB,oBAAoB,EAAE,KAAK;KAC5B;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO;QACrB,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,eAAe,CAAC,CAAC;QAE3E,MAAM,GAAG,GAAG,oBAAoB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjD,MAAM,OAAO,GAAG,MAAM,aAAa,CACjC,GAAG,EACH,wBAAwB,kBAAkB,CAAC,YAAY,CAAC,UAAU,EAClE,EAAE,EACF,OAAO,CAAC,KAAK,CACd,CAAC;QAEF,2EAA2E;QAC3E,0EAA0E;QAC1E,kEAAkE;QAClE,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;YACtC,UAAU,EAAE,OAAO;SACpB,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
@@ -0,0 +1,58 @@
1
+ import type { ToolDefinition } from "./types.js";
2
+ /**
3
+ * `GET /api/v1/repository?near_duplicates=` as a tool — shipped in the
4
+ * platform by SPGD-703 (`specguard` `c43dc19`, 2026-08-28), which added the
5
+ * `near_duplicates` block to `RepositoryOverview` behind an opt-in ask.
6
+ *
7
+ * == What the block is, and why it is behind an ask at all
8
+ *
9
+ * It is the suite-wide near-duplicate census: which tests READ alike — same
10
+ * body text, not same file — clustered by the engine SPGD-369 shipped
11
+ * (`specguard` `f7d5352`). It is the first block on that endpoint whose grain
12
+ * is the REPOSITORY rather than a run or a window of runs, and the first one
13
+ * gated on COST rather than rows: `NearDuplicateClusters` is linear but
14
+ * measured in seconds (seven queries at every size; seconds at a few thousand
15
+ * identities, extrapolating to tens of seconds at the 20,000-identity design
16
+ * point). `?near_duplicates=` confines that cost to the client that named it —
17
+ * no ask, key present and `null`, not one query — which is why this block is a
18
+ * SEPARATE tool rather than a forwarded parameter on
19
+ * `get_repository_overview`: an agent calling the overview should never pay
20
+ * the census by accident, and an agent calling this tool has asked for nothing
21
+ * else. Splitting the tools splits the cost along exactly the line the server
22
+ * drew.
23
+ *
24
+ * == The ask is always sent, and always spelled `"true"`
25
+ *
26
+ * The server reads only whether the parameter is PRESENT
27
+ * (`RequestedNearDuplicatesParam`): `?near_duplicates=false` opens the block
28
+ * exactly as `=true` does, and a non-String shape is read as no ask at all.
29
+ * There is no "off" value for a client to send, so this tool has no arguments
30
+ * — nothing about the census is choosable from here, which is also why the
31
+ * schema is CLOSED rather than merely empty: `server.ts` forwards `arguments`
32
+ * unvalidated, and an open schema would let an invented argument ride through
33
+ * and be silently dropped (see `registrable-repositories.ts` for the same
34
+ * call). `near_duplicates: "true"` is built rather than stringified for the
35
+ * same reason `repository-overview.ts` builds its `unannotated_examples` key:
36
+ * `getJson` omits only `undefined`, so a conditional send is the only honest
37
+ * way to spell "always" here.
38
+ *
39
+ * == The response is passed through, not re-modelled
40
+ *
41
+ * Same rule as `repository-overview.ts`: every figure in the block is
42
+ * annotated in `RepositoryOverview#serialized_near_duplicates` with the reason
43
+ * for its shape, and several of those reasons are about honesty rather than
44
+ * convenience — `similarity_floor`/`similarity_basis` served FIRST because a
45
+ * count without the statement of what the similarity means is a vacuous
46
+ * figure; `member_count` (texts in the REPOSITORY, across every run) and
47
+ * `example_count` (examples in the ONE run `weighed_run_id` names) served side
48
+ * by side because a three-example table-driven loop is one member and three
49
+ * examples, and flattening them would erase the figure the ranking is built
50
+ * on; `unobserved_members` disclosing that the member list holds an identity
51
+ * the weighed run did not observe; `similarity_range` as the `[strongest,
52
+ * weakest]` pair because membership is transitive while similarity is not.
53
+ * Reshaping here would discard distinctions the serializer spent that care
54
+ * preserving, so the body goes back as it arrived — the whole body, which
55
+ * carries the repository/run context the clusters sit inside.
56
+ */
57
+ declare const nearDuplicateClusters: ToolDefinition;
58
+ export default nearDuplicateClusters;