@savvy-web/mcp 1.7.5 → 1.8.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/@savvy-web%2Fmcp?label=npm&color=cb3837)](https://www.npmjs.com/package/@savvy-web/mcp)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-4caf50.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- The `savvy-mcp` [Model Context Protocol](https://modelcontextprotocol.io/) server. It serves [Silk Suite](https://github.com/savvy-web/systems) tooling to coding agents as structured tools, so an agent can read workspace facts and run Silk checks instead of parsing console output or guessing. It is a tools-only server — eight tools, no resources.
6
+ The `savvy-mcp` [Model Context Protocol](https://modelcontextprotocol.io/) server. It serves [Silk Suite](https://github.com/savvy-web/systems) tooling to coding agents as structured tools, so an agent can read workspace facts and run Silk checks instead of parsing console output or guessing. It is a tools-only server — ten tools, no resources.
7
7
 
8
8
  ## Install
9
9
 
@@ -49,6 +49,8 @@ npx @modelcontextprotocol/inspector savvy-mcp .
49
49
  - `changeset_deps_detect` — read-only preview of the cumulative dependency diff (merge-base to working tree): one entry per affected workspace package with its resolved dependency-table rows, `catalog:`/`workspace:` specifiers resolved to concrete versions. It never writes a changeset. Backed by `silk-effects`' `Changesets.DepsRegen.plan`.
50
50
  - `changeset_deps_regen` — regenerates pure-dependency changesets: deletes stale ones and writes fresh single-package, patch-bump changesets from the cumulative dependency diff. Mutating unless `dryRun` is set, in which case it reports what it would delete and write without touching the filesystem. Backed by `silk-effects`' `Changesets.DepsRegen`.
51
51
  - `biome_check` — run Biome over a path and get structured diagnostics back: `mode=check` (lint, format and organize-imports) or `mode=lint`. Unlike most of the other tools it can mutate — pass `write` for safe fixes or `unsafe` for unsafe ones (both git-reversible) — so it returns the same diagnostics the Biome LSP surfaces for files you have edited.
52
+ - `repos_inspect` — read-only inspection of vendored repositories: `mode=status` reports per-repo presence, the gitlink commit, working-tree dirtiness, and stale note ids; `mode=config` surfaces the validated `.repos/config.json` manifest and its entries. Returns markdown-escaped output since vendored-repo content is untrusted input. Backed by the same `silk-effects` `Repos` services the `savvy` CLI uses.
53
+ - `repos_manage` — manages vendored repositories (mutating counterpart to repos_inspect): `action=sync` initializes any missing submodules (`git submodule update --init --depth 1`), applies sparse-checkout from the manifest, and clears stale git locks; `action=pin` fetches and checks out the new ref, staging the updated gitlink and manifest; `action=add` adds a new repo entry; `action=note` appends a short note to a repo. Pin and add stage git changes for the caller to commit. Backed by the same `silk-effects` `Repos` services the `savvy` CLI uses.
52
54
 
53
55
  ## License
54
56
 
package/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { Changesets, SilkWorkspaceAnalyzer, Turbo } from "@savvy-web/silk-effects";
1
+ import { Changesets, Repos, SilkWorkspaceAnalyzer, Turbo } from "@savvy-web/silk-effects";
2
2
  import { Layer, ManagedRuntime } from "effect";
3
3
  import { WorkspaceDiscoveryError, WorkspaceRoot } from "workspaces-effect";
4
4
  import "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  //#region src/context.d.ts
6
6
  /** The long-lived runtime and the project working directory. */
7
7
  interface McpContext {
8
- readonly runtime: ManagedRuntime.ManagedRuntime<SilkWorkspaceAnalyzer | WorkspaceRoot | Turbo.TurboInspector | Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.ReleasePlanner | Changesets.DepsRegen, WorkspaceDiscoveryError>;
8
+ readonly runtime: ManagedRuntime.ManagedRuntime<SilkWorkspaceAnalyzer | WorkspaceRoot | Turbo.TurboInspector | Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.ReleasePlanner | Changesets.DepsRegen | Repos.ReposManager | Repos.ReposConfigStore, WorkspaceDiscoveryError>;
9
9
  readonly cwd: string;
10
10
  }
11
11
  //#endregion
@@ -13,16 +13,17 @@ interface McpContext {
13
13
  /**
14
14
  * The MCP runtime layer. Provides `SilkWorkspaceAnalyzer`, `WorkspaceRoot`,
15
15
  * `Turbo.TurboInspector`, `Changesets.BranchAnalyzer`,
16
- * `Changesets.ConfigInspector`, `Changesets.ReleasePlanner`, and
17
- * `Changesets.DepsRegen`; requires `CommandExecutor` + `FileSystem` + `Path`
18
- * from the host's platform layer (`NodeContext.layer` in bin.ts).
16
+ * `Changesets.ConfigInspector`, `Changesets.ReleasePlanner`,
17
+ * `Changesets.DepsRegen`, `Repos.ReposManager`, and `Repos.ReposConfigStore`;
18
+ * requires `CommandExecutor` + `FileSystem` + `Path` from the host's platform
19
+ * layer (`NodeContext.layer` in bin.ts).
19
20
  *
20
21
  * `TurboInspectorLive` is fed its own `ToolDiscoveryLive`, whose
21
22
  * `PackageManagerDetector` + `WorkspaceRoot` requirements are satisfied by
22
23
  * {@link DepsLive}; the leftover `CommandExecutor` + `FileSystem` flow up to the
23
24
  * host platform layer.
24
25
  */
25
- declare const SilkRuntimeLive: Layer.Layer<Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.DepsRegen | Changesets.ReleasePlanner | import("@savvy-web/silk-effects").SilkWorkspaceAnalyzer | Turbo.TurboInspector | import("workspaces-effect").WorkspaceRoot, import("workspaces-effect").WorkspaceDiscoveryError, import("@effect/platform/CommandExecutor").CommandExecutor | import("@effect/platform/FileSystem").FileSystem | import("@effect/platform/Path").Path>;
26
+ declare const SilkRuntimeLive: Layer.Layer<Changesets.BranchAnalyzer | Changesets.ConfigInspector | Changesets.DepsRegen | Changesets.ReleasePlanner | Repos.ReposConfigStore | Repos.ReposManager | import("@savvy-web/silk-effects").SilkWorkspaceAnalyzer | Turbo.TurboInspector | import("workspaces-effect").WorkspaceRoot, import("workspaces-effect").WorkspaceDiscoveryError, import("@effect/platform/CommandExecutor").CommandExecutor | import("@effect/platform/FileSystem").FileSystem | import("@effect/platform/Path").Path>;
26
27
  //#endregion
27
28
  //#region src/server.d.ts
28
29
  /** Build the server and connect it over stdio. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savvy-web/mcp",
3
- "version": "1.7.5",
3
+ "version": "1.8.0",
4
4
  "private": false,
5
5
  "description": "The savvy MCP server — Silk Suite tooling and library knowledge for coding agents",
6
6
  "homepage": "https://github.com/savvy-web/systems/tree/main/packages/mcp",
@@ -38,7 +38,7 @@
38
38
  "@effect/sql": "^0.51.1",
39
39
  "@effect/workflow": "^0.18.2",
40
40
  "@modelcontextprotocol/sdk": "^1.29.0",
41
- "@savvy-web/silk-effects": "3.2.4",
41
+ "@savvy-web/silk-effects": "3.3.0",
42
42
  "effect": "^3.21.4",
43
43
  "workspaces-effect": "^2.0.3",
44
44
  "zod": "^4.4.3"
package/runtime.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ChangesetConfigLive, ChangesetConfigReaderLive, Changesets, SilkWorkspaceAnalyzerLive, TagStrategyLive, ToolDiscoveryLive, Turbo, VersioningStrategyLive } from "@savvy-web/silk-effects";
1
+ import { ChangesetConfigLive, ChangesetConfigReaderLive, Changesets, Repos, SilkWorkspaceAnalyzerLive, TagStrategyLive, ToolDiscoveryLive, Turbo, VersioningStrategyLive } from "@savvy-web/silk-effects";
2
2
  import { Layer } from "effect";
3
3
  import { PointInTimeWorkspaceLive, WorkspaceRootLive, WorkspacesLive } from "workspaces-effect";
4
4
 
@@ -43,18 +43,29 @@ const InspectorAndAnalyzerLive = Changesets.BranchAnalyzerLive.pipe(Layer.provid
43
43
  */
44
44
  const DepsRegenGroupLive = Changesets.DepsRegenLive.pipe(Layer.provide(InspectorAndAnalyzerLive), Layer.provide(PointInTimeWorkspaceLive), Layer.provide(ChangesetConfigLive.pipe(Layer.provide(ChangesetConfigReaderLive))));
45
45
  /**
46
+ * `Repos.ReposManager` + `Repos.ReposConfigStore`, both exposed on the
47
+ * runtime. `ReposManagerLive` requires `ReposConfigStore`, so it is given its
48
+ * own `ReposConfigStoreLive` reference here (`Layer.mergeAll` does not
49
+ * cross-feed sibling layers); `ReposConfigStore` is ALSO merged in directly so
50
+ * `repos_inspect`'s config mode can resolve it on its own. The remaining
51
+ * `CommandExecutor` + `FileSystem` + `Path` requirements flow up to the host
52
+ * platform layer (`NodeContext.layer` in bin.ts).
53
+ */
54
+ const ReposGroupLive = Layer.mergeAll(Repos.ReposConfigStoreLive, Repos.ReposManagerLive.pipe(Layer.provide(Repos.ReposConfigStoreLive)));
55
+ /**
46
56
  * The MCP runtime layer. Provides `SilkWorkspaceAnalyzer`, `WorkspaceRoot`,
47
57
  * `Turbo.TurboInspector`, `Changesets.BranchAnalyzer`,
48
- * `Changesets.ConfigInspector`, `Changesets.ReleasePlanner`, and
49
- * `Changesets.DepsRegen`; requires `CommandExecutor` + `FileSystem` + `Path`
50
- * from the host's platform layer (`NodeContext.layer` in bin.ts).
58
+ * `Changesets.ConfigInspector`, `Changesets.ReleasePlanner`,
59
+ * `Changesets.DepsRegen`, `Repos.ReposManager`, and `Repos.ReposConfigStore`;
60
+ * requires `CommandExecutor` + `FileSystem` + `Path` from the host's platform
61
+ * layer (`NodeContext.layer` in bin.ts).
51
62
  *
52
63
  * `TurboInspectorLive` is fed its own `ToolDiscoveryLive`, whose
53
64
  * `PackageManagerDetector` + `WorkspaceRoot` requirements are satisfied by
54
65
  * {@link DepsLive}; the leftover `CommandExecutor` + `FileSystem` flow up to the
55
66
  * host platform layer.
56
67
  */
57
- const SilkRuntimeLive = Layer.mergeAll(SilkWorkspaceAnalyzerLive, WorkspaceRootLive, Turbo.TurboInspectorLive.pipe(Layer.provide(ToolDiscoveryLive)), InspectorAndAnalyzerLive, DepsRegenGroupLive).pipe(Layer.provide(DepsLive));
68
+ const SilkRuntimeLive = Layer.mergeAll(SilkWorkspaceAnalyzerLive, WorkspaceRootLive, Turbo.TurboInspectorLive.pipe(Layer.provide(ToolDiscoveryLive)), InspectorAndAnalyzerLive, DepsRegenGroupLive, ReposGroupLive).pipe(Layer.provide(DepsLive));
58
69
 
59
70
  //#endregion
60
71
  export { SilkRuntimeLive };
package/server.js CHANGED
@@ -5,6 +5,8 @@ import { ChangesetDepsRegenAsMarkdown, ChangesetDepsRegenResult, changesetDepsRe
5
5
  import { ChangesetInspectAsMarkdown, ChangesetInspectResult, changesetInspect } from "./tools/changeset-inspect.js";
6
6
  import { ChangesetPreviewAsMarkdown, ChangesetPreviewResult, changesetPreview } from "./tools/changeset-preview.js";
7
7
  import { ChangesetValidateAsMarkdown, ChangesetValidateResult, changesetValidate } from "./tools/changeset-validate.js";
8
+ import { ReposInspectAsMarkdown, ReposInspectResult, reposInspect } from "./tools/repos-inspect.js";
9
+ import { ReposManageAsMarkdown, ReposManageResult, reposManage } from "./tools/repos-manage.js";
8
10
  import { TurboInspectAsMarkdown, TurboInspectResult, turboInspect } from "./tools/turbo-inspect.js";
9
11
  import { WorkspaceInfoAsMarkdown, WorkspaceInfoResult, workspaceInfo } from "./tools/workspace-info.js";
10
12
  import { CURRENT_MCP_VERSION } from "./version.js";
@@ -141,6 +143,55 @@ function buildServer(ctx) {
141
143
  const text = Schema.decodeSync(ChangesetDepsRegenAsMarkdown)(data);
142
144
  return structuredResult(text, data);
143
145
  });
146
+ server.registerTool("repos_inspect", {
147
+ title: "Inspect vendored repos",
148
+ description: "Read-only: drift report or parsed .repos/config.json manifest with orientation and notes.",
149
+ inputSchema: {
150
+ mode: z.enum(["status", "config"]).describe("status = drift report; config = the full agent brief."),
151
+ cwd: z.optional(z.string()).describe("Directory to resolve the workspace root from.")
152
+ },
153
+ outputSchema: effectToZodSchema(ReposInspectResult),
154
+ annotations: { readOnlyHint: true }
155
+ }, async (args) => {
156
+ const data = await ctx.runtime.runPromise(reposInspect(args, ctx.cwd));
157
+ const text = Schema.decodeSync(ReposInspectAsMarkdown)(data);
158
+ return structuredResult(text, data);
159
+ });
160
+ server.registerTool("repos_manage", {
161
+ title: "Manage vendored repos",
162
+ description: "Mutating: sync (initialize/reconcile submodules per the manifest), pin (re-pin a repo to a new ref), add (vendor a new repo), or note (add/remove/promote an agent note). Pass action plus the fields that action needs: pin needs name+ref; add needs url+ref+purpose (name/sparse optional); note needs name+op, plus note (op=add), id (op=remove), or id+into (op=promote). A decode failure names the missing field. The pin result's markdown surfaces commitMessage and staleNoteIds — review and commit after pinning.",
163
+ inputSchema: {
164
+ action: z.enum([
165
+ "sync",
166
+ "pin",
167
+ "add",
168
+ "note"
169
+ ]).describe("Which mutation to perform."),
170
+ name: z.optional(z.string()).describe("Repo name (pin, note; optional override for add)."),
171
+ ref: z.optional(z.string()).describe("Git ref to pin/vendor to (pin, add)."),
172
+ url: z.optional(z.string()).describe("Repo URL to vendor (add)."),
173
+ purpose: z.optional(z.string()).describe("One-line purpose for the manifest (add)."),
174
+ sparse: z.optional(z.array(z.string())).describe("Sparse-checkout patterns (add)."),
175
+ op: z.optional(z.enum([
176
+ "add",
177
+ "remove",
178
+ "promote"
179
+ ])).describe("Note operation (note)."),
180
+ note: z.optional(z.string()).describe("Note text (note, op=add)."),
181
+ id: z.optional(z.string()).describe("Note id (note, op=remove|promote)."),
182
+ into: z.optional(z.enum(["layout", "startHere"])).describe("Orientation target (note, op=promote)."),
183
+ cwd: z.optional(z.string()).describe("Directory to resolve the workspace root from.")
184
+ },
185
+ outputSchema: effectToZodSchema(ReposManageResult),
186
+ annotations: {
187
+ destructiveHint: true,
188
+ idempotentHint: false
189
+ }
190
+ }, async (args) => {
191
+ const data = await ctx.runtime.runPromise(reposManage(args, ctx.cwd));
192
+ const text = Schema.decodeSync(ReposManageAsMarkdown)(data);
193
+ return structuredResult(text, data);
194
+ });
144
195
  server.registerTool("biome_check", {
145
196
  description: "Run Biome over a path and get structured diagnostics back. mode=check (default; lint + format + organize-imports) or mode=lint. Set write=true to apply safe fixes (--write), unsafe=true for unsafe fixes (--write --unsafe). Severities match the project's Biome config (what `biome check` reports); set strict=true to surface project warnings as errors, each marked with its originalSeverity. Prefer this over shelling out to biome; the LSP already covers files you've edited. Returns markdown in content[] and a typed object in structuredContent. NOTE: with write/unsafe this tool MUTATES files (git-reversible).",
146
197
  inputSchema: {
@@ -0,0 +1,22 @@
1
+ //#region src/tools/md-inline.ts
2
+ /**
3
+ * Render a repo-derived value as an inert markdown code span. Backslash
4
+ * escaping does NOT work inside code spans (CommonMark treats backslashes
5
+ * literally there), so this uses the delimiter-run rule instead: wrap the
6
+ * value in a backtick run strictly longer than any backtick run it contains,
7
+ * padding with spaces when the value starts or ends with a backtick. This
8
+ * keeps vendored-repo content (names, refs, purposes, orientation values,
9
+ * commit messages, note text) — the definition of untrusted input — from
10
+ * injecting markdown structure into the transcript an agent reads.
11
+ *
12
+ * Shared by every tool that interpolates untrusted repo content into a
13
+ * markdown transcript so the escaping rule has exactly one implementation.
14
+ */
15
+ const mdInline = (value) => {
16
+ const runs = value.match(/`+/g) ?? [];
17
+ const delimiter = "`".repeat(Math.max(1, ...runs.map((run) => run.length + 1)));
18
+ return `${delimiter}${value.startsWith("`") || value.endsWith("`") ? ` ${value} ` : value}${delimiter}`;
19
+ };
20
+
21
+ //#endregion
22
+ export { mdInline };
@@ -0,0 +1,104 @@
1
+ import { mdInline } from "./md-inline.js";
2
+ import { Repos } from "@savvy-web/silk-effects";
3
+ import { Effect, ParseResult, Schema } from "effect";
4
+ import { WorkspaceRoot } from "workspaces-effect";
5
+
6
+ //#region src/tools/repos-inspect.ts
7
+ /**
8
+ * The `repos_inspect` MCP tool: a discriminated-union result keyed by `mode`
9
+ * (status | config), each variant embedding the corresponding resolved-output
10
+ * schema from silk-effects' Repos namespace, plus a one-way markdown
11
+ * transform. Read-only.
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+ /** Status-report variant. */
16
+ const ReposStatusResult = Schema.Struct({
17
+ mode: Schema.Literal("status"),
18
+ result: Repos.ReposStatusReport
19
+ }).annotations({ identifier: "ReposStatusResult" });
20
+ /** Manifest-config variant. */
21
+ const ReposConfigResult = Schema.Struct({
22
+ mode: Schema.Literal("config"),
23
+ result: Repos.ReposManifestFile
24
+ }).annotations({ identifier: "ReposConfigResult" });
25
+ /** The `repos_inspect` tool result — a discriminated union keyed by `mode`. */
26
+ const ReposInspectResult = Schema.Union(ReposStatusResult, ReposConfigResult).annotations({
27
+ identifier: "ReposInspectResult",
28
+ title: "repos_inspect result",
29
+ description: "Drift report (status) or the parsed manifest with purposes, orientation, and notes (config)."
30
+ });
31
+ /** Render the structured result as a markdown transcript. */
32
+ const renderMarkdown = (data) => {
33
+ switch (data.mode) {
34
+ case "status": {
35
+ const r = data.result;
36
+ const lines = [
37
+ `# repos status`,
38
+ ``,
39
+ `clean: ${r.clean}`,
40
+ ``,
41
+ `## Repos`
42
+ ];
43
+ for (const entry of r.repos) {
44
+ lines.push(`### ${mdInline(entry.name)}`, `- ref: ${mdInline(entry.ref)}`, `- purpose: ${mdInline(entry.purpose)}`, `- present: ${entry.present}`, `- commit: ${entry.commit ? mdInline(entry.commit) : "(none)"}`, `- dirty: ${entry.dirty}`);
45
+ if (entry.staleNoteIds.length > 0) lines.push(`- staleNoteIds: ${entry.staleNoteIds.map(mdInline).join(", ")}`);
46
+ }
47
+ if (r.repos.length === 0) lines.push("(none)");
48
+ return lines.join("\n");
49
+ }
50
+ case "config": {
51
+ const r = data.result;
52
+ const lines = [
53
+ `# repos config`,
54
+ ``,
55
+ `## Repos`
56
+ ];
57
+ for (const [name, entry] of Object.entries(r.repos)) {
58
+ lines.push(`### ${mdInline(name)}`, `- url: ${mdInline(entry.url)}`, `- ref: ${mdInline(entry.ref)}`, `- purpose: ${mdInline(entry.purpose)}`);
59
+ if (entry.sparse && entry.sparse.length > 0) lines.push(`- sparse: ${entry.sparse.map(mdInline).join(", ")}`);
60
+ if (entry.orientation) {
61
+ const o = entry.orientation;
62
+ if (o.layout) lines.push(`- layout: ${mdInline(o.layout)}`);
63
+ if (o.startHere) lines.push(`- startHere: ${mdInline(o.startHere)}`);
64
+ if (o.keyPaths) {
65
+ lines.push(`- keyPaths:`);
66
+ for (const [key, value] of Object.entries(o.keyPaths)) lines.push(` - ${mdInline(key)}: ${mdInline(value)}`);
67
+ }
68
+ }
69
+ if (entry.notes && entry.notes.length > 0) {
70
+ lines.push(`- notes:`);
71
+ for (const note of entry.notes) lines.push(` - ${mdInline(note.id)} (${mdInline(note.date)}, ref ${mdInline(note.ref)}): ${mdInline(note.note)}`);
72
+ }
73
+ }
74
+ if (Object.keys(r.repos).length === 0) lines.push("(none)");
75
+ return lines.join("\n");
76
+ }
77
+ }
78
+ };
79
+ /** One-way transform: result to markdown. Encoding back is forbidden. */
80
+ const ReposInspectAsMarkdown = Schema.transformOrFail(ReposInspectResult, Schema.String, {
81
+ strict: true,
82
+ decode: (data) => ParseResult.succeed(renderMarkdown(data)),
83
+ encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "ReposInspectAsMarkdown is one-way: markdown cannot be parsed back."))
84
+ });
85
+ /**
86
+ * Effect handler: resolve the workspace root, then dispatch to the matching
87
+ * Repos service keyed by `mode`. Mirrors `changesetInspect`.
88
+ */
89
+ const reposInspect = (args, fallbackCwd) => Effect.gen(function* () {
90
+ const root = yield* (yield* WorkspaceRoot).find(args.cwd ?? fallbackCwd);
91
+ switch (args.mode) {
92
+ case "status": return {
93
+ mode: "status",
94
+ result: yield* (yield* Repos.ReposManager).status(root)
95
+ };
96
+ case "config": return {
97
+ mode: "config",
98
+ result: yield* (yield* Repos.ReposConfigStore).read(root)
99
+ };
100
+ }
101
+ });
102
+
103
+ //#endregion
104
+ export { ReposConfigResult, ReposInspectAsMarkdown, ReposInspectResult, ReposStatusResult, reposInspect };
@@ -0,0 +1,195 @@
1
+ import { mdInline } from "./md-inline.js";
2
+ import { Repos } from "@savvy-web/silk-effects";
3
+ import { Effect, ParseResult, Schema } from "effect";
4
+ import { WorkspaceRoot } from "workspaces-effect";
5
+
6
+ //#region src/tools/repos-manage.ts
7
+ /**
8
+ * The `repos_manage` MCP tool: one action-discriminated mutating tool
9
+ * covering `sync`, `pin`, `add`, and `note` against the vendored `.repos/`
10
+ * submodules. The wire schema is flat (no `oneOf`); the handler maps it into
11
+ * an internal `Schema.TaggedStruct` request union that names the missing
12
+ * field per action on decode failure. Mutating — no `readOnlyHint`.
13
+ *
14
+ * @packageDocumentation
15
+ */
16
+ /** `sync` has no extra fields. */
17
+ const SyncRequest = Schema.TaggedStruct("sync", {});
18
+ /** `pin` requires both `name` and `ref`. */
19
+ const PinRequest = Schema.TaggedStruct("pin", {
20
+ name: Schema.String,
21
+ ref: Schema.String
22
+ });
23
+ /** `add` requires `url`/`ref`/`purpose`; `name` and `sparse` are optional. */
24
+ const AddRequest = Schema.TaggedStruct("add", {
25
+ url: Schema.String,
26
+ ref: Schema.String,
27
+ purpose: Schema.String,
28
+ name: Schema.optional(Schema.String),
29
+ sparse: Schema.optional(Schema.Array(Schema.String))
30
+ });
31
+ /**
32
+ * `note` requires `name` and `op`; the fields required beyond that depend on
33
+ * `op` — enforced by the trailing filter so the decode error names exactly
34
+ * what's missing for the chosen op.
35
+ */
36
+ const NoteRequest = Schema.TaggedStruct("note", {
37
+ name: Schema.String,
38
+ op: Schema.Literal("add", "remove", "promote"),
39
+ note: Schema.optional(Schema.String),
40
+ id: Schema.optional(Schema.String),
41
+ into: Schema.optional(Schema.Literal("layout", "startHere"))
42
+ }).pipe(Schema.filter((request) => {
43
+ if (request.op === "add" && request.note === void 0) return "note op \"add\" requires `note`";
44
+ if (request.op === "remove" && request.id === void 0) return "note op \"remove\" requires `id`";
45
+ if (request.op === "promote" && (request.id === void 0 || request.into === void 0)) return "note op \"promote\" requires both `id` and `into`";
46
+ return true;
47
+ }));
48
+ /** Internal tagged-union request the flat wire args decode into. */
49
+ const ReposManageRequest = Schema.Union(SyncRequest, PinRequest, AddRequest, NoteRequest);
50
+ /** `sync` result variant. */
51
+ const ReposManageSyncResult = Schema.Struct({
52
+ action: Schema.Literal("sync"),
53
+ result: Repos.ReposSyncReport
54
+ }).annotations({ identifier: "ReposManageSyncResult" });
55
+ /** `pin` result variant. */
56
+ const ReposManagePinResult = Schema.Struct({
57
+ action: Schema.Literal("pin"),
58
+ result: Repos.ReposPinResult
59
+ }).annotations({ identifier: "ReposManagePinResult" });
60
+ /** `add` result variant. */
61
+ const ReposManageAddResult = Schema.Struct({
62
+ action: Schema.Literal("add"),
63
+ result: Repos.ReposAddResult
64
+ }).annotations({ identifier: "ReposManageAddResult" });
65
+ /** `note` result variant. */
66
+ const ReposManageNoteResult = Schema.Struct({
67
+ action: Schema.Literal("note"),
68
+ result: Repos.ReposNoteResult
69
+ }).annotations({ identifier: "ReposManageNoteResult" });
70
+ /** The `repos_manage` tool result — a discriminated union keyed by `action`. */
71
+ const ReposManageResult = Schema.Union(ReposManageSyncResult, ReposManagePinResult, ReposManageAddResult, ReposManageNoteResult).annotations({
72
+ identifier: "ReposManageResult",
73
+ title: "repos_manage result",
74
+ description: "Result of a mutating repos action: sync, pin, add, or note."
75
+ });
76
+ /** Render the structured result as a markdown transcript. */
77
+ const renderMarkdown = (data) => {
78
+ switch (data.action) {
79
+ case "sync": {
80
+ const r = data.result;
81
+ const section = (title, names) => [`## ${title}`, ...names.length > 0 ? names.map((name) => `- ${mdInline(name)}`) : ["(none)"]];
82
+ return [
83
+ `# repos sync`,
84
+ ``,
85
+ ...section("Initialized", r.initialized),
86
+ ``,
87
+ ...section("Sparse applied", r.sparseApplied),
88
+ ``,
89
+ ...section("Up to date", r.upToDate),
90
+ ``,
91
+ ...section("Cleared locks", r.clearedLocks)
92
+ ].join("\n");
93
+ }
94
+ case "pin": {
95
+ const r = data.result;
96
+ const lines = [
97
+ `# repos pin — ${mdInline(r.name)}`,
98
+ ``,
99
+ `ref: ${mdInline(r.ref)}`,
100
+ `oldCommit: ${r.oldCommit ? mdInline(r.oldCommit) : "(none)"}`,
101
+ `newCommit: ${mdInline(r.newCommit)}`,
102
+ ``,
103
+ `## Commit message`,
104
+ ``,
105
+ mdInline(r.commitMessage),
106
+ ``,
107
+ `## Stale notes`,
108
+ ``
109
+ ];
110
+ if (r.staleNoteIds.length > 0) lines.push(`These notes reference a ref other than the new pin and should be reviewed before committing:`, ...r.staleNoteIds.map((id) => `- ${mdInline(id)}`));
111
+ else lines.push("(none)");
112
+ lines.push(``, `REVIEW AND COMMIT: stage the updated manifest and submodule gitlink, then commit using the message above.`);
113
+ return lines.join("\n");
114
+ }
115
+ case "add": {
116
+ const r = data.result;
117
+ return [
118
+ `# repos add — ${mdInline(r.name)}`,
119
+ ``,
120
+ `ref: ${mdInline(r.ref)}`,
121
+ `path: ${mdInline(r.path)}`
122
+ ].join("\n");
123
+ }
124
+ case "note": {
125
+ const r = data.result;
126
+ return [
127
+ `# repos note — ${mdInline(r.name)}`,
128
+ ``,
129
+ `op: ${mdInline(r.op)}`,
130
+ `id: ${mdInline(r.id)}`,
131
+ `noteCount: ${r.noteCount}`
132
+ ].join("\n");
133
+ }
134
+ }
135
+ };
136
+ /** One-way transform: result to markdown. Encoding back is forbidden. */
137
+ const ReposManageAsMarkdown = Schema.transformOrFail(ReposManageResult, Schema.String, {
138
+ strict: true,
139
+ decode: (data) => ParseResult.succeed(renderMarkdown(data)),
140
+ encode: (text, _options, ast) => ParseResult.fail(new ParseResult.Forbidden(ast, text, "ReposManageAsMarkdown is one-way: markdown cannot be parsed back."))
141
+ });
142
+ /**
143
+ * Effect handler: resolve the workspace root, decode the flat wire args into
144
+ * the internal per-action request (naming the missing field on failure), then
145
+ * dispatch to the matching `ReposManager` method.
146
+ */
147
+ const reposManage = (args, fallbackCwd) => Effect.gen(function* () {
148
+ const root = yield* (yield* WorkspaceRoot).find(args.cwd ?? fallbackCwd);
149
+ const manager = yield* Repos.ReposManager;
150
+ const { action, cwd: _cwd, ...rest } = args;
151
+ const request = yield* Schema.decodeUnknown(ReposManageRequest)({
152
+ _tag: action,
153
+ ...rest
154
+ });
155
+ switch (request._tag) {
156
+ case "sync": return {
157
+ action: "sync",
158
+ result: yield* manager.sync(root)
159
+ };
160
+ case "pin": return {
161
+ action: "pin",
162
+ result: yield* manager.pin(root, request.name, request.ref)
163
+ };
164
+ case "add": return {
165
+ action: "add",
166
+ result: yield* manager.add(root, {
167
+ url: request.url,
168
+ ref: request.ref,
169
+ purpose: request.purpose,
170
+ ...request.name !== void 0 ? { name: request.name } : {},
171
+ ...request.sparse !== void 0 ? { sparse: request.sparse } : {}
172
+ })
173
+ };
174
+ case "note": {
175
+ const noteOp = request.op === "add" ? {
176
+ op: "add",
177
+ note: request.note
178
+ } : request.op === "remove" ? {
179
+ op: "remove",
180
+ id: request.id
181
+ } : {
182
+ op: "promote",
183
+ id: request.id,
184
+ into: request.into
185
+ };
186
+ return {
187
+ action: "note",
188
+ result: yield* manager.note(root, request.name, noteOp)
189
+ };
190
+ }
191
+ }
192
+ });
193
+
194
+ //#endregion
195
+ export { ReposManageAddResult, ReposManageAsMarkdown, ReposManageNoteResult, ReposManagePinResult, ReposManageResult, ReposManageSyncResult, reposManage };