@apicircle/mcp-server 1.0.8 → 1.1.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/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,21 +17,37 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
33
+ AI_CLIENTS: () => AI_CLIENTS,
23
34
  FileBackedWorkspaceProvider: () => FileBackedWorkspaceProvider,
35
+ GitBackedWorkspaceProvider: () => GitBackedWorkspaceProvider,
24
36
  InMemoryWorkspaceProvider: () => InMemoryWorkspaceProvider,
25
37
  InProcessMockController: () => InProcessMockController,
38
+ MCP_PROMPTS: () => MCP_PROMPTS,
39
+ MCP_PROMPT_CATEGORIES: () => MCP_PROMPT_CATEGORIES,
26
40
  McpHost: () => McpHost,
27
41
  MultiWorkspaceProvider: () => MultiWorkspaceProvider,
28
42
  SingleWorkspaceAdapter: () => SingleWorkspaceAdapter,
43
+ StdioServerTransport: () => import_stdio2.StdioServerTransport,
44
+ StreamableHTTPServerTransport: () => import_streamableHttp.StreamableHTTPServerTransport,
29
45
  TOOL_REGISTRY: () => TOOL_REGISTRY,
30
46
  WorkspaceNotFoundError: () => WorkspaceNotFoundError,
47
+ buildSnippetVariants: () => buildSnippetVariants,
31
48
  createMcpServer: () => createMcpServer,
32
- getTool: () => getTool
49
+ getTool: () => getTool,
50
+ resolveAiClientConfigPath: () => resolveAiClientConfigPath
33
51
  });
34
52
  module.exports = __toCommonJS(index_exports);
35
53
 
@@ -41,9 +59,10 @@ var import_zod = require("zod");
41
59
  // package.json
42
60
  var package_default = {
43
61
  name: "@apicircle/mcp-server",
44
- version: "1.0.8",
62
+ version: "1.1.0",
45
63
  private: false,
46
64
  type: "module",
65
+ sideEffects: false,
47
66
  description: "Model Context Protocol server exposing API Circle Studio's workspace as a tool catalog. Used by Claude Desktop, ChatGPT, Cursor, GitHub Copilot, and any other MCP-compatible AI client.",
48
67
  keywords: [
49
68
  "apicircle",
@@ -86,7 +105,8 @@ var package_default = {
86
105
  "apicircle-mcp": "./dist/bin/mcp-server.cjs"
87
106
  },
88
107
  exports: {
89
- ".": "./src/index.ts"
108
+ ".": "./src/index.ts",
109
+ "./prompts": "./src/prompts/mcpPrompts.ts"
90
110
  },
91
111
  files: [
92
112
  "dist"
@@ -105,6 +125,16 @@ var package_default = {
105
125
  types: "./dist/index.d.cts",
106
126
  default: "./dist/index.cjs"
107
127
  }
128
+ },
129
+ "./prompts": {
130
+ import: {
131
+ types: "./dist/prompts/mcpPrompts.d.ts",
132
+ default: "./dist/prompts/mcpPrompts.js"
133
+ },
134
+ require: {
135
+ types: "./dist/prompts/mcpPrompts.d.cts",
136
+ default: "./dist/prompts/mcpPrompts.cjs"
137
+ }
108
138
  }
109
139
  }
110
140
  },
@@ -122,6 +152,7 @@ var package_default = {
122
152
  zod: "^3.23.0"
123
153
  },
124
154
  devDependencies: {
155
+ "@apicircle/git": "workspace:*",
125
156
  "@types/node": "^20.0.0",
126
157
  tsup: "^8.3.0",
127
158
  typescript: "^5.4.0",
@@ -590,6 +621,7 @@ var workspaceListTool = {
590
621
  var import_zod5 = require("zod");
591
622
  var import_shared2 = require("@apicircle/shared");
592
623
  var import_core2 = require("@apicircle/core");
624
+ var FULL_REQUEST_AUTH = import_zod5.z.object({ type: import_zod5.z.string() }).passthrough();
593
625
  var HTTP_METHOD = import_zod5.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
594
626
  var requestCreateTool = {
595
627
  name: "request.create",
@@ -677,16 +709,18 @@ var requestDeleteTool = {
677
709
  };
678
710
  var folderCreateTool = {
679
711
  name: "folder.create",
680
- description: "Create a folder under an optional parent folder.",
712
+ description: "Create a folder under an optional parent folder. Optionally seed folder-level `auth` so descendants with `auth: inherit` resolve to it immediately \u2014 saves a follow-up `folder.update` round-trip.",
681
713
  inputSchema: import_zod5.z.object({
682
714
  name: import_zod5.z.string().default("New folder"),
683
- parentId: import_zod5.z.string().nullable().optional()
715
+ parentId: import_zod5.z.string().nullable().optional(),
716
+ auth: FULL_REQUEST_AUTH.optional()
684
717
  }),
685
718
  async handler(input, ctx) {
686
719
  const folder = {
687
720
  id: (0, import_shared2.generateId)(),
688
721
  name: input.name,
689
- parentId: input.parentId ?? null
722
+ parentId: input.parentId ?? null,
723
+ ...input.auth ? { auth: input.auth } : {}
690
724
  };
691
725
  const out = await ctx.workspace.apply({ kind: "folder.create", folder });
692
726
  return { id: folder.id, changedIds: out.changedIds };
@@ -710,18 +744,39 @@ var folderReadTool = {
710
744
  };
711
745
  var folderUpdateTool = {
712
746
  name: "folder.update",
713
- description: "Move a folder to a new parent (or to root with parentId: null).",
747
+ description: 'Update a folder. Supply `parentId` to move it (use `null` for root), `name` to rename, and/or `auth` to set the folder-level auth that descendants inherit when their `auth.type === "inherit"`. Set `clearAuth: true` to remove the folder-level auth entirely (descendants then fall through to the next ancestor). The `auth` field accepts any of the 17 RequestAuth types \u2014 same surface as `request.update`. The simple ones (bearer / basic / api-key / custom-header / none / inherit) are easy to author by hand; OAuth2 / AWS SigV4 / Digest / NTLM / Hawk / JWT Bearer require the full canonical shape (see `RequestAuth` in `@apicircle/shared/types.ts`).',
714
748
  inputSchema: import_zod5.z.object({
715
749
  id: import_zod5.z.string(),
716
- parentId: import_zod5.z.string().nullable()
750
+ parentId: import_zod5.z.string().nullable().optional(),
751
+ name: import_zod5.z.string().optional(),
752
+ auth: FULL_REQUEST_AUTH.optional(),
753
+ clearAuth: import_zod5.z.boolean().optional()
754
+ }).refine((v) => !(v.auth !== void 0 && v.clearAuth === true), {
755
+ message: "Pass either `auth` or `clearAuth: true`, not both."
717
756
  }),
718
757
  async handler(input, ctx) {
719
- const out = await ctx.workspace.apply({
720
- kind: "folder.move",
721
- id: input.id,
722
- newParentId: input.parentId
723
- });
724
- return { changedIds: out.changedIds };
758
+ const changedIds = [];
759
+ if (input.parentId !== void 0) {
760
+ const out = await ctx.workspace.apply({
761
+ kind: "folder.move",
762
+ id: input.id,
763
+ newParentId: input.parentId
764
+ });
765
+ changedIds.push(...out.changedIds);
766
+ }
767
+ const updatePatch = {};
768
+ if (input.name !== void 0) updatePatch.name = input.name;
769
+ if (input.auth !== void 0) updatePatch.auth = input.auth;
770
+ else if (input.clearAuth === true) updatePatch.auth = void 0;
771
+ if (input.name !== void 0 || input.auth !== void 0 || input.clearAuth === true) {
772
+ const out = await ctx.workspace.apply({
773
+ kind: "folder.update",
774
+ id: input.id,
775
+ patch: updatePatch
776
+ });
777
+ changedIds.push(...out.changedIds);
778
+ }
779
+ return { changedIds: Array.from(new Set(changedIds)) };
725
780
  }
726
781
  };
727
782
  var folderDeleteTool = {
@@ -1695,7 +1750,9 @@ var CONDITION_CLAUSE_NL = import_zod9.z.object({
1695
1750
  var RESPONSE_RULE_NL = import_zod9.z.object({
1696
1751
  name: import_zod9.z.string(),
1697
1752
  enabled: import_zod9.z.boolean().default(true),
1698
- when: import_zod9.z.array(CONDITION_CLAUSE_NL).default([]),
1753
+ // At least one clause — a no-condition rule is dead (the runtime engine skips
1754
+ // clause-less rules), so it never fires (mirrors the VS Code parser's reject).
1755
+ when: import_zod9.z.array(CONDITION_CLAUSE_NL).min(1),
1699
1756
  response: import_zod9.z.object({
1700
1757
  status: import_zod9.z.number().int().min(100).max(599).default(200),
1701
1758
  jsonBody: import_zod9.z.string().default("{}")
@@ -1745,7 +1802,7 @@ function buildEndpoint(input) {
1745
1802
  };
1746
1803
  const validationRules = input.validationRules ?? [];
1747
1804
  const responseRules = input.responseRules ?? [];
1748
- const multipliers = input.multipliers ?? [];
1805
+ const multipliers = (input.multipliers ?? []).slice(0, import_shared3.MAX_RESPONSE_MULTIPLIERS);
1749
1806
  if (multipliers.length > 0) {
1750
1807
  defaultResponse.multipliers = multipliers.map((m) => ({
1751
1808
  id: (0, import_shared3.generateId)(),
@@ -1978,10 +2035,13 @@ var promptSetPlanVariablesTool = {
1978
2035
  };
1979
2036
  var promptCreateMockServerTool = {
1980
2037
  name: "prompt.create_mock_server",
1981
- description: "Create a manual-mode mock server with optional inline endpoints from an LLM-shaped JSON envelope. The model produces `{ name, defaultPort?, endpoints: [{ method, pathPattern, name?, response?, validationRules?, responseRules?, multipliers? }] }`; this tool generates ids for the server and every endpoint / rule, then persists in one shot.",
2038
+ description: "Create a manual-mode mock server with optional inline endpoints from an LLM-shaped JSON envelope. The model produces `{ name, defaultPort?, endpoints: [{ method, pathPattern, name?, response?, validationRules?, responseRules?, multiplier? }] }`; this tool generates ids for the server and every endpoint / rule, then persists in one shot.",
1982
2039
  inputSchema: import_zod9.z.object({
1983
2040
  name: import_zod9.z.string().min(1),
1984
- defaultPort: import_zod9.z.number().int().positive().nullable().optional(),
2041
+ // Mirrors mock.create_manual / mock.start / mock.set_default_port:
2042
+ // reject out-of-range ports at the tool boundary so a prompt that
2043
+ // returns a stray port (1, 80, 999999) never leaks into the synced doc.
2044
+ defaultPort: import_zod9.z.number().int().min(1024).max(65535).nullable().optional(),
1985
2045
  endpoints: import_zod9.z.array(ENDPOINT_INPUT).default([])
1986
2046
  }),
1987
2047
  async handler(input, ctx) {
@@ -2008,7 +2068,7 @@ var promptCreateMockServerTool = {
2008
2068
  };
2009
2069
  var promptAddMockEndpointTool = {
2010
2070
  name: "prompt.add_mock_endpoint",
2011
- description: "Append a new endpoint (with optional inline validation rules, response rules, and multipliers) to an existing mock server from an LLM-shaped JSON envelope. All ids are auto-generated; the existing endpoints stay in place.",
2071
+ description: "Append a new endpoint (with optional inline validation rules, response rules, and a single response multiplier) to an existing mock server from an LLM-shaped JSON envelope. All ids are auto-generated; the existing endpoints stay in place.",
2012
2072
  inputSchema: import_zod9.z.object({
2013
2073
  mockId: import_zod9.z.string(),
2014
2074
  method: HTTP_METHOD2,
@@ -2111,7 +2171,7 @@ var promptSetEndpointResponseRulesTool = {
2111
2171
  };
2112
2172
  var promptSetEndpointMultipliersTool = {
2113
2173
  name: "prompt.set_endpoint_multipliers",
2114
- description: "Replace the response multipliers on an endpoint's defaultResponse with an LLM-shaped list. Multipliers expand an array at `targetJsonPath` to a count derived from a request value. Every multiplier gets a fresh id. Empty array clears all multipliers.",
2174
+ description: "Replace the response multipliers on an endpoint's defaultResponse with an LLM-shaped list. Multipliers expand an array at `targetJsonPath` to a count derived from a request value. Every multiplier gets a fresh id. Empty array clears all. Capped at MAX_RESPONSE_MULTIPLIERS (1) \u2014 extra entries are rejected.",
2115
2175
  inputSchema: import_zod9.z.object({
2116
2176
  mockId: import_zod9.z.string(),
2117
2177
  endpointId: import_zod9.z.string(),
@@ -2122,6 +2182,9 @@ var promptSetEndpointMultipliersTool = {
2122
2182
  const mock = state.synced.mockServers[input.mockId];
2123
2183
  if (!mock) return { ok: false, error: "mock not found" };
2124
2184
  const multipliers = input.multipliers;
2185
+ if (multipliers.length > import_shared3.MAX_RESPONSE_MULTIPLIERS) {
2186
+ return { ok: false, error: "too many multipliers" };
2187
+ }
2125
2188
  const next = patchEndpoint(mock, input.endpointId, (e) => ({
2126
2189
  ...e,
2127
2190
  defaultResponse: {
@@ -2142,16 +2205,191 @@ var promptSetEndpointMultipliersTool = {
2142
2205
  return { ok: true, changedIds: out.changedIds };
2143
2206
  }
2144
2207
  };
2208
+ var PARAM_NL = import_zod9.z.object({
2209
+ name: import_zod9.z.string(),
2210
+ typeHint: import_zod9.z.string().optional(),
2211
+ required: import_zod9.z.boolean().optional(),
2212
+ description: import_zod9.z.string().optional(),
2213
+ example: import_zod9.z.string().optional()
2214
+ });
2215
+ var promptSetEndpointRequestSchemaTool = {
2216
+ name: "prompt.set_endpoint_request_schema",
2217
+ description: "Declare an endpoint's expected inputs with an LLM-shaped list: path / query / header / cookie params (name + optional typeHint / required / description / example) plus an optional body-shape doc. Every param gets a fresh id; omitted lists are cleared. Documentation-only \u2014 it drives the editor UI + OpenAPI export, not runtime gating (use validation rules for that).",
2218
+ inputSchema: import_zod9.z.object({
2219
+ mockId: import_zod9.z.string(),
2220
+ endpointId: import_zod9.z.string(),
2221
+ pathParams: import_zod9.z.array(PARAM_NL).default([]),
2222
+ queryParams: import_zod9.z.array(PARAM_NL).default([]),
2223
+ headers: import_zod9.z.array(PARAM_NL).default([]),
2224
+ cookies: import_zod9.z.array(PARAM_NL).default([]),
2225
+ body: import_zod9.z.object({ description: import_zod9.z.string().optional(), example: import_zod9.z.string().optional() }).optional()
2226
+ }),
2227
+ async handler(input, ctx) {
2228
+ const state = await ctx.workspace.read();
2229
+ const mock = state.synced.mockServers[input.mockId];
2230
+ if (!mock) return { ok: false, error: "mock not found" };
2231
+ const toParams = (list) => list.map((p) => ({
2232
+ id: (0, import_shared3.generateId)(),
2233
+ name: p.name,
2234
+ typeHint: p.typeHint,
2235
+ required: p.required,
2236
+ description: p.description,
2237
+ example: p.example
2238
+ }));
2239
+ const body = input.body && (input.body.description || input.body.example) ? input.body : void 0;
2240
+ const next = patchEndpoint(mock, input.endpointId, (e) => ({
2241
+ ...e,
2242
+ requestSchema: {
2243
+ pathParams: toParams(input.pathParams),
2244
+ queryParams: toParams(input.queryParams),
2245
+ headers: toParams(input.headers),
2246
+ cookies: toParams(input.cookies),
2247
+ body
2248
+ }
2249
+ }));
2250
+ if (!next) return { ok: false, error: "endpoint not found" };
2251
+ const out = await ctx.workspace.apply({ kind: "mock.upsert", mock: next });
2252
+ return { ok: true, changedIds: out.changedIds };
2253
+ }
2254
+ };
2145
2255
 
2146
- // src/tools/mocks.ts
2256
+ // src/tools/globalAssets.ts
2147
2257
  var import_zod10 = require("zod");
2148
2258
  var import_shared4 = require("@apicircle/shared");
2259
+ function deriveState(asset, hasPendingUpload) {
2260
+ const w = asset.workingBranchRef ?? null;
2261
+ const b = asset.baseBranchRef ?? null;
2262
+ if (w && b) {
2263
+ if (w.blobSha && b.blobSha && w.blobSha !== b.blobSha) return "diverged";
2264
+ return "merged";
2265
+ }
2266
+ if (w && !b) return "workingOnly";
2267
+ if (!w && b) return "baseOnly";
2268
+ if (hasPendingUpload) return "uploading";
2269
+ return "missing";
2270
+ }
2271
+ var globalAssetsFilesListTool = {
2272
+ name: "assets.list_files",
2273
+ description: "List every Global File Asset with its provenance state and reference count. Each entry includes id, name, filename, size, mimeType, sha256, state (uploading | workingOnly | merged | baseOnly | missing | diverged), workingBranchRef, baseBranchRef, and usage { requests, mockEndpoints, total }.",
2274
+ inputSchema: import_zod10.z.object({}).strict(),
2275
+ async handler(_input, ctx) {
2276
+ const state = await ctx.workspace.read();
2277
+ const files = state.synced.globalAssets.files ?? {};
2278
+ const pending = state.local.pendingFileUploads ?? {};
2279
+ const usage = state.local.assetUsageIndex ?? {};
2280
+ const items = Object.values(files).map((asset) => {
2281
+ const hasPending = Boolean(pending[asset.id]);
2282
+ const u = usage[asset.id] ?? { requests: [], mockEndpoints: [], total: 0 };
2283
+ return {
2284
+ id: asset.id,
2285
+ name: asset.name,
2286
+ description: asset.description ?? null,
2287
+ filename: asset.filename,
2288
+ size: asset.size,
2289
+ mimeType: asset.mimeType,
2290
+ sha256: asset.sha256 ?? null,
2291
+ state: deriveState(asset, hasPending),
2292
+ workingBranchRef: asset.workingBranchRef ?? null,
2293
+ baseBranchRef: asset.baseBranchRef ?? null,
2294
+ usage: { ...u }
2295
+ };
2296
+ });
2297
+ return { count: items.length, files: items };
2298
+ }
2299
+ };
2300
+ var globalAssetsFilesCreateTool = {
2301
+ name: "assets.create_file",
2302
+ description: 'Register a Global File Asset entry. Bytes are NOT carried \u2014 MCP returns the new asset id and the asset enters the "missing" state. The user fills the bytes from the Global Assets panel (a "Fill bytes" button surfaces on missing-state assets) which preserves the slot id and queues the bytes for the next push. Use this when an AI client wants to claim an asset slot for a file the user will provide later.',
2303
+ inputSchema: import_zod10.z.object({
2304
+ name: import_zod10.z.string().min(1, "name is required"),
2305
+ description: import_zod10.z.string().optional(),
2306
+ filename: import_zod10.z.string().min(1, "filename is required"),
2307
+ size: import_zod10.z.number().int().nonnegative(),
2308
+ mimeType: import_zod10.z.string().default("application/octet-stream"),
2309
+ sha256: import_zod10.z.string().optional()
2310
+ }).strict(),
2311
+ async handler(input, ctx) {
2312
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2313
+ const file = {
2314
+ id: (0, import_shared4.generateId)(),
2315
+ name: input.name,
2316
+ description: input.description,
2317
+ slotId: (0, import_shared4.generateId)(),
2318
+ filename: input.filename,
2319
+ size: input.size,
2320
+ mimeType: input.mimeType,
2321
+ sha256: input.sha256,
2322
+ createdAt: now,
2323
+ updatedAt: now
2324
+ };
2325
+ const out = await ctx.workspace.apply({ kind: "globalAsset.upsertFile", file });
2326
+ return { id: file.id, slotId: file.slotId, changedIds: out.changedIds };
2327
+ }
2328
+ };
2329
+ var globalAssetsFilesUpdateTool = {
2330
+ name: "assets.update_file",
2331
+ description: "Rename or re-describe a Global File Asset. Provenance refs (workingBranchRef, baseBranchRef) and binary metadata (slotId, sha256, size, mimeType) are preserved verbatim.",
2332
+ inputSchema: import_zod10.z.object({
2333
+ id: import_zod10.z.string().min(1),
2334
+ patch: import_zod10.z.object({
2335
+ name: import_zod10.z.string().optional(),
2336
+ description: import_zod10.z.string().nullable().optional()
2337
+ }).strict()
2338
+ }).strict(),
2339
+ async handler(input, ctx) {
2340
+ const state = await ctx.workspace.read();
2341
+ const existing = state.synced.globalAssets.files?.[input.id];
2342
+ if (!existing) {
2343
+ return { found: false, changedIds: [] };
2344
+ }
2345
+ const next = {
2346
+ ...existing,
2347
+ name: input.patch.name ?? existing.name,
2348
+ description: input.patch.description === null ? void 0 : input.patch.description ?? existing.description
2349
+ };
2350
+ const out = await ctx.workspace.apply({ kind: "globalAsset.upsertFile", file: next });
2351
+ return { found: true, id: next.id, changedIds: out.changedIds };
2352
+ }
2353
+ };
2354
+ var globalAssetsFilesDeleteTool = {
2355
+ name: "assets.delete_file",
2356
+ description: "Delete a Global File Asset. Cascades \u2014 every request body and mock-response body that referenced the asset is unbound in the same mutation. The result envelope includes the consumer list that was cleared so the caller can report what changed.",
2357
+ inputSchema: import_zod10.z.object({ id: import_zod10.z.string().min(1) }).strict(),
2358
+ async handler(input, ctx) {
2359
+ const before = await ctx.workspace.read();
2360
+ const usage = before.local.assetUsageIndex?.[input.id] ?? {
2361
+ requests: [],
2362
+ mockEndpoints: [],
2363
+ total: 0
2364
+ };
2365
+ const existing = before.synced.globalAssets.files?.[input.id];
2366
+ if (!existing) {
2367
+ return { found: false, changedIds: [] };
2368
+ }
2369
+ const out = await ctx.workspace.apply({ kind: "globalAsset.removeFile", id: input.id });
2370
+ return {
2371
+ found: true,
2372
+ id: input.id,
2373
+ filename: existing.filename,
2374
+ unbound: {
2375
+ requests: usage.requests,
2376
+ mockEndpoints: usage.mockEndpoints,
2377
+ total: usage.total
2378
+ },
2379
+ changedIds: out.changedIds
2380
+ };
2381
+ }
2382
+ };
2383
+
2384
+ // src/tools/mocks.ts
2385
+ var import_zod11 = require("zod");
2386
+ var import_shared5 = require("@apicircle/shared");
2149
2387
  var import_mock_server_core2 = require("@apicircle/mock-server-core");
2150
2388
  async function ingestSource(source, name) {
2151
2389
  const { endpoints, warnings } = await (0, import_mock_server_core2.parseSourceToEndpoints)(source);
2152
2390
  const now = (/* @__PURE__ */ new Date()).toISOString();
2153
2391
  const mock = {
2154
- id: (0, import_shared4.generateId)(),
2392
+ id: (0, import_shared5.generateId)(),
2155
2393
  name,
2156
2394
  source,
2157
2395
  endpoints,
@@ -2167,10 +2405,10 @@ async function ingestSource(source, name) {
2167
2405
  var mockCreateFromOpenApiTool = {
2168
2406
  name: "mock.create_from_openapi",
2169
2407
  description: "Create a mock server from an OpenAPI / Swagger spec (YAML or JSON).",
2170
- inputSchema: import_zod10.z.object({
2171
- name: import_zod10.z.string(),
2172
- spec: import_zod10.z.string().min(1),
2173
- format: import_zod10.z.enum(["json", "yaml"]).default("json")
2408
+ inputSchema: import_zod11.z.object({
2409
+ name: import_zod11.z.string(),
2410
+ spec: import_zod11.z.string().min(1),
2411
+ format: import_zod11.z.enum(["json", "yaml"]).default("json")
2174
2412
  }),
2175
2413
  async handler(input, ctx) {
2176
2414
  const { mock, warnings } = await ingestSource(
@@ -2189,7 +2427,7 @@ var mockCreateFromOpenApiTool = {
2189
2427
  var mockCreateFromPostmanTool = {
2190
2428
  name: "mock.create_from_postman",
2191
2429
  description: "Create a mock server from a Postman v2/v2.1 collection.",
2192
- inputSchema: import_zod10.z.object({ name: import_zod10.z.string(), collection: import_zod10.z.string().min(1) }),
2430
+ inputSchema: import_zod11.z.object({ name: import_zod11.z.string(), collection: import_zod11.z.string().min(1) }),
2193
2431
  async handler(input, ctx) {
2194
2432
  const { mock, warnings } = await ingestSource(
2195
2433
  { kind: "postman", collection: input.collection },
@@ -2207,7 +2445,7 @@ var mockCreateFromPostmanTool = {
2207
2445
  var mockCreateFromInsomniaTool = {
2208
2446
  name: "mock.create_from_insomnia",
2209
2447
  description: "Create a mock server from an Insomnia v4 export.",
2210
- inputSchema: import_zod10.z.object({ name: import_zod10.z.string(), export: import_zod10.z.string().min(1) }),
2448
+ inputSchema: import_zod11.z.object({ name: import_zod11.z.string(), export: import_zod11.z.string().min(1) }),
2211
2449
  async handler(input, ctx) {
2212
2450
  const { mock, warnings } = await ingestSource(
2213
2451
  { kind: "insomnia", export: input.export },
@@ -2225,7 +2463,7 @@ var mockCreateFromInsomniaTool = {
2225
2463
  var mockImportPostmanMockCollectionTool = {
2226
2464
  name: "mock.import_postman_mock_collection",
2227
2465
  description: "Import a Postman Mock Collection (collections previously hosted on Postman's mock service). Same parser as a regular Postman collection but marked as a mock import.",
2228
- inputSchema: import_zod10.z.object({ name: import_zod10.z.string(), collection: import_zod10.z.string().min(1) }),
2466
+ inputSchema: import_zod11.z.object({ name: import_zod11.z.string(), collection: import_zod11.z.string().min(1) }),
2229
2467
  async handler(input, ctx) {
2230
2468
  const { mock, warnings } = await ingestSource(
2231
2469
  { kind: "postman", collection: input.collection },
@@ -2243,7 +2481,7 @@ var mockImportPostmanMockCollectionTool = {
2243
2481
  var mockListTool = {
2244
2482
  name: "mock.list",
2245
2483
  description: "List all mock servers in the workspace plus their runtime status (running / stopped, port).",
2246
- inputSchema: import_zod10.z.object({}),
2484
+ inputSchema: import_zod11.z.object({}),
2247
2485
  async handler(_input, ctx) {
2248
2486
  const state = await ctx.workspace.read();
2249
2487
  const running = await ctx.mock.list();
@@ -2264,10 +2502,14 @@ var mockListTool = {
2264
2502
  };
2265
2503
  var mockStartTool = {
2266
2504
  name: "mock.start",
2267
- description: "Start a mock server by id. Returns the bound port. Errors if the mock is already running or the requested port is in use.",
2268
- inputSchema: import_zod10.z.object({
2269
- id: import_zod10.z.string(),
2270
- port: import_zod10.z.number().int().positive().optional()
2505
+ description: "Start a mock server by id. Returns the bound port. Errors if the mock is already running or the requested port is in use. Optional `port` (1024-65535) overrides the saved `defaultPort` for this run only \u2014 to persist a new default port, use `mock.set_default_port`.",
2506
+ inputSchema: import_zod11.z.object({
2507
+ id: import_zod11.z.string(),
2508
+ // Mirrors the UI 1024-65535 window: <1024 needs OS privileges, and
2509
+ // outside that window the runtime would throw INVALID_PORT anyway —
2510
+ // surface the rejection at the tool boundary so the client sees a
2511
+ // schema error instead of a runtime exception.
2512
+ port: import_zod11.z.number().int().min(1024).max(65535).optional()
2271
2513
  }),
2272
2514
  async handler(input, ctx) {
2273
2515
  const state = await ctx.workspace.read();
@@ -2284,7 +2526,7 @@ var mockStartTool = {
2284
2526
  var mockStopTool = {
2285
2527
  name: "mock.stop",
2286
2528
  description: "Stop a running mock server by id (no-op if not running).",
2287
- inputSchema: import_zod10.z.object({ id: import_zod10.z.string() }),
2529
+ inputSchema: import_zod11.z.object({ id: import_zod11.z.string() }),
2288
2530
  async handler(input, ctx) {
2289
2531
  try {
2290
2532
  await ctx.mock.stop(input.id);
@@ -2297,7 +2539,7 @@ var mockStopTool = {
2297
2539
  var mockDeleteTool = {
2298
2540
  name: "mock.delete",
2299
2541
  description: "Delete a mock server definition. Stops it first if it's running.",
2300
- inputSchema: import_zod10.z.object({ id: import_zod10.z.string() }),
2542
+ inputSchema: import_zod11.z.object({ id: import_zod11.z.string() }),
2301
2543
  async handler(input, ctx) {
2302
2544
  try {
2303
2545
  await ctx.mock.stop(input.id);
@@ -2307,18 +2549,49 @@ var mockDeleteTool = {
2307
2549
  return { ok: true, changedIds: out.changedIds };
2308
2550
  }
2309
2551
  };
2310
- var HTTP_METHOD3 = import_zod10.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
2552
+ var mockSetDefaultPortTool = {
2553
+ name: "mock.set_default_port",
2554
+ description: "Persist a new `defaultPort` on an existing mock server. Pass an integer 1024-65535 to pin the port, or `null` for 'pick a free port at next start'. Does NOT restart a running mock \u2014 the change takes effect on the next mock.start.",
2555
+ inputSchema: import_zod11.z.object({
2556
+ id: import_zod11.z.string(),
2557
+ defaultPort: import_zod11.z.number().int().min(1024).max(65535).nullable()
2558
+ }),
2559
+ async handler(input, ctx) {
2560
+ const state = await ctx.workspace.read();
2561
+ const mock = state.synced.mockServers[input.id];
2562
+ if (!mock) return { ok: false, error: "mock not found" };
2563
+ if (mock.defaultPort === input.defaultPort) {
2564
+ return { ok: true, defaultPort: mock.defaultPort, changed: false };
2565
+ }
2566
+ const next = {
2567
+ ...mock,
2568
+ defaultPort: input.defaultPort,
2569
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2570
+ };
2571
+ const out = await ctx.workspace.apply({ kind: "mock.upsert", mock: next });
2572
+ return {
2573
+ ok: true,
2574
+ defaultPort: input.defaultPort,
2575
+ changed: true,
2576
+ changedIds: out.changedIds
2577
+ };
2578
+ }
2579
+ };
2580
+ var HTTP_METHOD3 = import_zod11.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
2311
2581
  var mockCreateManualTool = {
2312
2582
  name: "mock.create_manual",
2313
2583
  description: "Create an empty manual-mode mock server. Use `mock.add_endpoint` afterward to populate it. CORS defaults to off (same-origin only); enable + list explicit origins via `mock.update_cors` if cross-origin access is needed.",
2314
- inputSchema: import_zod10.z.object({
2315
- name: import_zod10.z.string().min(1),
2316
- defaultPort: import_zod10.z.number().int().positive().nullable().optional()
2584
+ inputSchema: import_zod11.z.object({
2585
+ name: import_zod11.z.string().min(1),
2586
+ // Same 1024-65535 window as mock.start / mock.set_default_port — pin
2587
+ // the validation at the tool boundary so a malformed AI call doesn't
2588
+ // persist a port the runtime will later reject as INVALID_PORT.
2589
+ defaultPort: import_zod11.z.number().int().min(1024).max(65535).nullable().optional()
2317
2590
  }),
2318
2591
  async handler(input, ctx) {
2319
2592
  const now = (/* @__PURE__ */ new Date()).toISOString();
2320
2593
  const mock = {
2321
- id: (0, import_shared4.generateId)(),
2594
+ id: (0, import_shared5.generateId)(),
2322
2595
  name: input.name,
2323
2596
  source: { kind: "manual", endpoints: [] },
2324
2597
  endpoints: [],
@@ -2336,7 +2609,7 @@ var mockCreateManualTool = {
2336
2609
  var mockListEndpointsTool = {
2337
2610
  name: "mock.list_endpoints",
2338
2611
  description: "List endpoints for a mock server (id, method, path, name).",
2339
- inputSchema: import_zod10.z.object({ mockId: import_zod10.z.string() }),
2612
+ inputSchema: import_zod11.z.object({ mockId: import_zod11.z.string() }),
2340
2613
  async handler(input, ctx) {
2341
2614
  const state = await ctx.workspace.read();
2342
2615
  const mock = state.synced.mockServers[input.mockId];
@@ -2355,10 +2628,10 @@ var mockListEndpointsTool = {
2355
2628
  };
2356
2629
  }
2357
2630
  };
2358
- var ENDPOINT_RESPONSE2 = import_zod10.z.object({
2359
- status: import_zod10.z.number().int().min(100).max(599).default(200),
2360
- jsonBody: import_zod10.z.string().default("{}"),
2361
- contentType: import_zod10.z.string().default("application/json")
2631
+ var ENDPOINT_RESPONSE2 = import_zod11.z.object({
2632
+ status: import_zod11.z.number().int().min(100).max(599).default(200),
2633
+ jsonBody: import_zod11.z.string().default("{}"),
2634
+ contentType: import_zod11.z.string().default("application/json")
2362
2635
  });
2363
2636
  function buildDefaultEndpoint(args) {
2364
2637
  const response = args.response ?? {
@@ -2368,16 +2641,16 @@ function buildDefaultEndpoint(args) {
2368
2641
  };
2369
2642
  const headers = [{ key: "Content-Type", value: response.contentType, enabled: true }];
2370
2643
  return {
2371
- id: (0, import_shared4.generateId)(),
2644
+ id: (0, import_shared5.generateId)(),
2372
2645
  name: args.name ?? `${args.method} ${args.pathPattern}`,
2373
2646
  method: args.method,
2374
2647
  pathPattern: args.pathPattern,
2375
2648
  description: args.description,
2376
- requestSchema: (0, import_shared4.makeDefaultRequestSchema)(),
2649
+ requestSchema: (0, import_shared5.makeDefaultRequestSchema)(),
2377
2650
  requestValidation: [],
2378
2651
  responseRules: [],
2379
2652
  defaultResponse: {
2380
- ...(0, import_shared4.makeDefaultMockResponse)(),
2653
+ ...(0, import_shared5.makeDefaultMockResponse)(),
2381
2654
  status: response.status,
2382
2655
  headers,
2383
2656
  body: { type: "json", content: response.jsonBody }
@@ -2387,12 +2660,12 @@ function buildDefaultEndpoint(args) {
2387
2660
  var mockAddEndpointTool = {
2388
2661
  name: "mock.add_endpoint",
2389
2662
  description: "Append a new endpoint to a mock server. Defaults to a 200 JSON response of `{}`. Returns the new endpoint id.",
2390
- inputSchema: import_zod10.z.object({
2391
- mockId: import_zod10.z.string(),
2663
+ inputSchema: import_zod11.z.object({
2664
+ mockId: import_zod11.z.string(),
2392
2665
  method: HTTP_METHOD3,
2393
- pathPattern: import_zod10.z.string().min(1),
2394
- name: import_zod10.z.string().optional(),
2395
- description: import_zod10.z.string().optional(),
2666
+ pathPattern: import_zod11.z.string().min(1),
2667
+ name: import_zod11.z.string().optional(),
2668
+ description: import_zod11.z.string().optional(),
2396
2669
  response: ENDPOINT_RESPONSE2.optional()
2397
2670
  }),
2398
2671
  async handler(input, ctx) {
@@ -2415,13 +2688,13 @@ var mockAddEndpointTool = {
2415
2688
  var mockUpdateEndpointTool = {
2416
2689
  name: "mock.update_endpoint",
2417
2690
  description: "Patch fields on a single mock endpoint (method, pathPattern, name, description, defaultResponse status / contentType / json body). Pass only the fields you want to change.",
2418
- inputSchema: import_zod10.z.object({
2419
- mockId: import_zod10.z.string(),
2420
- endpointId: import_zod10.z.string(),
2691
+ inputSchema: import_zod11.z.object({
2692
+ mockId: import_zod11.z.string(),
2693
+ endpointId: import_zod11.z.string(),
2421
2694
  method: HTTP_METHOD3.optional(),
2422
- pathPattern: import_zod10.z.string().optional(),
2423
- name: import_zod10.z.string().optional(),
2424
- description: import_zod10.z.string().optional(),
2695
+ pathPattern: import_zod11.z.string().optional(),
2696
+ name: import_zod11.z.string().optional(),
2697
+ description: import_zod11.z.string().optional(),
2425
2698
  response: ENDPOINT_RESPONSE2.partial().optional()
2426
2699
  }),
2427
2700
  async handler(input, ctx) {
@@ -2462,7 +2735,7 @@ var mockUpdateEndpointTool = {
2462
2735
  var mockDeleteEndpointTool = {
2463
2736
  name: "mock.delete_endpoint",
2464
2737
  description: "Remove an endpoint from a mock server.",
2465
- inputSchema: import_zod10.z.object({ mockId: import_zod10.z.string(), endpointId: import_zod10.z.string() }),
2738
+ inputSchema: import_zod11.z.object({ mockId: import_zod11.z.string(), endpointId: import_zod11.z.string() }),
2466
2739
  async handler(input, ctx) {
2467
2740
  const state = await ctx.workspace.read();
2468
2741
  const mock = state.synced.mockServers[input.mockId];
@@ -2482,9 +2755,9 @@ var mockDeleteEndpointTool = {
2482
2755
  return { ok: true, changedIds: out.changedIds };
2483
2756
  }
2484
2757
  };
2485
- var VALIDATION_RULE = import_zod10.z.object({
2486
- id: import_zod10.z.string().optional(),
2487
- kind: import_zod10.z.enum([
2758
+ var VALIDATION_RULE = import_zod11.z.object({
2759
+ id: import_zod11.z.string().optional(),
2760
+ kind: import_zod11.z.enum([
2488
2761
  "header-required",
2489
2762
  "header-equals",
2490
2763
  "header-matches",
@@ -2495,43 +2768,46 @@ var VALIDATION_RULE = import_zod10.z.object({
2495
2768
  "body-required",
2496
2769
  "content-type-equals"
2497
2770
  ]),
2498
- target: import_zod10.z.string().default(""),
2499
- expected: import_zod10.z.string().optional(),
2500
- message: import_zod10.z.string().optional(),
2501
- enabled: import_zod10.z.boolean().default(true),
2502
- failResponse: import_zod10.z.object({
2503
- status: import_zod10.z.number().int().min(100).max(599).default(400),
2504
- jsonBody: import_zod10.z.string().default('{"error":"validation failed"}')
2771
+ target: import_zod11.z.string().default(""),
2772
+ expected: import_zod11.z.string().optional(),
2773
+ message: import_zod11.z.string().optional(),
2774
+ enabled: import_zod11.z.boolean().default(true),
2775
+ failResponse: import_zod11.z.object({
2776
+ status: import_zod11.z.number().int().min(100).max(599).default(400),
2777
+ jsonBody: import_zod11.z.string().default('{"error":"validation failed"}')
2505
2778
  }).default({})
2506
2779
  });
2507
- var CONDITION_CLAUSE = import_zod10.z.object({
2508
- id: import_zod10.z.string().optional(),
2509
- scope: import_zod10.z.enum(["query", "pathParam", "header", "cookie", "body-json-path"]),
2510
- target: import_zod10.z.string(),
2511
- op: import_zod10.z.enum(["equals", "not-equals", "matches", "gt", "lt", "gte", "lte", "present", "absent"]),
2512
- value: import_zod10.z.string().optional()
2780
+ var CONDITION_CLAUSE = import_zod11.z.object({
2781
+ id: import_zod11.z.string().optional(),
2782
+ scope: import_zod11.z.enum(["query", "pathParam", "header", "cookie", "body-json-path"]),
2783
+ target: import_zod11.z.string(),
2784
+ op: import_zod11.z.enum(["equals", "not-equals", "matches", "gt", "lt", "gte", "lte", "present", "absent"]),
2785
+ value: import_zod11.z.string().optional()
2513
2786
  });
2514
- var RESPONSE_RULE = import_zod10.z.object({
2515
- id: import_zod10.z.string().optional(),
2516
- name: import_zod10.z.string(),
2517
- enabled: import_zod10.z.boolean().default(true),
2518
- when: import_zod10.z.array(CONDITION_CLAUSE).default([]),
2519
- response: import_zod10.z.object({
2520
- status: import_zod10.z.number().int().min(100).max(599).default(200),
2521
- jsonBody: import_zod10.z.string().default("{}")
2787
+ var RESPONSE_RULE = import_zod11.z.object({
2788
+ id: import_zod11.z.string().optional(),
2789
+ name: import_zod11.z.string(),
2790
+ enabled: import_zod11.z.boolean().default(true),
2791
+ // At least one clause — a rule with no `when` is dead (the runtime engine
2792
+ // skips clause-less rules), so it can never fire. The VS Code endpoint parser
2793
+ // rejects the same shape, keeping the surfaces consistent.
2794
+ when: import_zod11.z.array(CONDITION_CLAUSE).min(1),
2795
+ response: import_zod11.z.object({
2796
+ status: import_zod11.z.number().int().min(100).max(599).default(200),
2797
+ jsonBody: import_zod11.z.string().default("{}")
2522
2798
  }).default({})
2523
2799
  });
2524
- var MULTIPLIER = import_zod10.z.object({
2525
- id: import_zod10.z.string().optional(),
2526
- name: import_zod10.z.string().optional(),
2527
- source: import_zod10.z.object({
2528
- kind: import_zod10.z.enum(["query", "pathParam", "header", "body-json-path"]),
2529
- key: import_zod10.z.string()
2530
- }),
2531
- targetJsonPath: import_zod10.z.string(),
2532
- defaultCount: import_zod10.z.number().int().nonnegative().default(0),
2533
- min: import_zod10.z.number().int().nonnegative().optional(),
2534
- max: import_zod10.z.number().int().nonnegative().optional()
2800
+ var MULTIPLIER = import_zod11.z.object({
2801
+ id: import_zod11.z.string().optional(),
2802
+ name: import_zod11.z.string().optional(),
2803
+ source: import_zod11.z.object({
2804
+ kind: import_zod11.z.enum(["query", "pathParam", "header", "body-json-path"]),
2805
+ key: import_zod11.z.string()
2806
+ }),
2807
+ targetJsonPath: import_zod11.z.string(),
2808
+ defaultCount: import_zod11.z.number().int().nonnegative().default(0),
2809
+ min: import_zod11.z.number().int().nonnegative().optional(),
2810
+ max: import_zod11.z.number().int().nonnegative().optional()
2535
2811
  });
2536
2812
  function defaultJsonResponseConfig(args) {
2537
2813
  return {
@@ -2556,10 +2832,10 @@ function patchEndpoint2(mock, endpointId, patcher) {
2556
2832
  var mockSetValidationRulesTool = {
2557
2833
  name: "mock.set_validation_rules",
2558
2834
  description: "Replace an endpoint's validation rules. Rules without an `id` get a fresh one; existing rules can keep theirs to preserve client-side selection state. Empty array clears all rules.",
2559
- inputSchema: import_zod10.z.object({
2560
- mockId: import_zod10.z.string(),
2561
- endpointId: import_zod10.z.string(),
2562
- rules: import_zod10.z.array(VALIDATION_RULE)
2835
+ inputSchema: import_zod11.z.object({
2836
+ mockId: import_zod11.z.string(),
2837
+ endpointId: import_zod11.z.string(),
2838
+ rules: import_zod11.z.array(VALIDATION_RULE)
2563
2839
  }),
2564
2840
  async handler(input, ctx) {
2565
2841
  const state = await ctx.workspace.read();
@@ -2569,7 +2845,7 @@ var mockSetValidationRulesTool = {
2569
2845
  const next = patchEndpoint2(mock, input.endpointId, (e) => ({
2570
2846
  ...e,
2571
2847
  requestValidation: rules.map((r) => ({
2572
- id: r.id ?? (0, import_shared4.generateId)(),
2848
+ id: r.id ?? (0, import_shared5.generateId)(),
2573
2849
  kind: r.kind,
2574
2850
  target: r.target,
2575
2851
  expected: r.expected,
@@ -2586,10 +2862,10 @@ var mockSetValidationRulesTool = {
2586
2862
  var mockSetResponseRulesTool = {
2587
2863
  name: "mock.set_response_rules",
2588
2864
  description: "Replace an endpoint's conditional response rules. Rules fire in order; the first whose every clause matches wins. Disabled rules are skipped. Empty array falls back to defaultResponse.",
2589
- inputSchema: import_zod10.z.object({
2590
- mockId: import_zod10.z.string(),
2591
- endpointId: import_zod10.z.string(),
2592
- rules: import_zod10.z.array(RESPONSE_RULE)
2865
+ inputSchema: import_zod11.z.object({
2866
+ mockId: import_zod11.z.string(),
2867
+ endpointId: import_zod11.z.string(),
2868
+ rules: import_zod11.z.array(RESPONSE_RULE)
2593
2869
  }),
2594
2870
  async handler(input, ctx) {
2595
2871
  const state = await ctx.workspace.read();
@@ -2599,11 +2875,11 @@ var mockSetResponseRulesTool = {
2599
2875
  const next = patchEndpoint2(mock, input.endpointId, (e) => ({
2600
2876
  ...e,
2601
2877
  responseRules: rules.map((r) => ({
2602
- id: r.id ?? (0, import_shared4.generateId)(),
2878
+ id: r.id ?? (0, import_shared5.generateId)(),
2603
2879
  name: r.name,
2604
2880
  enabled: r.enabled,
2605
2881
  when: r.when.map((c) => ({
2606
- id: c.id ?? (0, import_shared4.generateId)(),
2882
+ id: c.id ?? (0, import_shared5.generateId)(),
2607
2883
  scope: c.scope,
2608
2884
  target: c.target,
2609
2885
  op: c.op,
@@ -2617,25 +2893,80 @@ var mockSetResponseRulesTool = {
2617
2893
  return { ok: true, changedIds: out.changedIds };
2618
2894
  }
2619
2895
  };
2896
+ var PARAM = import_zod11.z.object({
2897
+ id: import_zod11.z.string().optional(),
2898
+ name: import_zod11.z.string(),
2899
+ typeHint: import_zod11.z.string().optional(),
2900
+ required: import_zod11.z.boolean().optional(),
2901
+ description: import_zod11.z.string().optional(),
2902
+ example: import_zod11.z.string().optional()
2903
+ });
2904
+ var REQUEST_SCHEMA_BODY = import_zod11.z.object({ description: import_zod11.z.string().optional(), example: import_zod11.z.string().optional() }).optional();
2905
+ function normalizeSchemaBody(body) {
2906
+ if (!body || !body.description && !body.example) return void 0;
2907
+ return body;
2908
+ }
2909
+ var mockSetRequestSchemaTool = {
2910
+ name: "mock.set_request_schema",
2911
+ description: "Replace an endpoint's requestSchema \u2014 the declared path / query / header / cookie params plus an optional body-shape doc. Params without an `id` get a fresh one; omitted lists are cleared. Documentation-only (drives the editor UI + OpenAPI export); runtime gating lives in the validation rules.",
2912
+ inputSchema: import_zod11.z.object({
2913
+ mockId: import_zod11.z.string(),
2914
+ endpointId: import_zod11.z.string(),
2915
+ pathParams: import_zod11.z.array(PARAM).default([]),
2916
+ queryParams: import_zod11.z.array(PARAM).default([]),
2917
+ headers: import_zod11.z.array(PARAM).default([]),
2918
+ cookies: import_zod11.z.array(PARAM).default([]),
2919
+ body: REQUEST_SCHEMA_BODY
2920
+ }),
2921
+ async handler(input, ctx) {
2922
+ const state = await ctx.workspace.read();
2923
+ const mock = state.synced.mockServers[input.mockId];
2924
+ if (!mock) return { ok: false, error: "mock not found" };
2925
+ const toParams = (list) => list.map((p) => ({
2926
+ id: p.id ?? (0, import_shared5.generateId)(),
2927
+ name: p.name,
2928
+ typeHint: p.typeHint,
2929
+ required: p.required,
2930
+ description: p.description,
2931
+ example: p.example
2932
+ }));
2933
+ const next = patchEndpoint2(mock, input.endpointId, (e) => ({
2934
+ ...e,
2935
+ requestSchema: {
2936
+ pathParams: toParams(input.pathParams),
2937
+ queryParams: toParams(input.queryParams),
2938
+ headers: toParams(input.headers),
2939
+ cookies: toParams(input.cookies),
2940
+ body: normalizeSchemaBody(input.body)
2941
+ }
2942
+ }));
2943
+ if (!next) return { ok: false, error: "endpoint not found" };
2944
+ const out = await ctx.workspace.apply({ kind: "mock.upsert", mock: next });
2945
+ return { ok: true, changedIds: out.changedIds };
2946
+ }
2947
+ };
2620
2948
  var mockSetMultipliersTool = {
2621
2949
  name: "mock.set_multipliers",
2622
- description: "Replace the response multipliers on an endpoint's defaultResponse. Multipliers expand an array at `targetJsonPath` to a count derived from a request value. Empty array clears all multipliers.",
2623
- inputSchema: import_zod10.z.object({
2624
- mockId: import_zod10.z.string(),
2625
- endpointId: import_zod10.z.string(),
2626
- multipliers: import_zod10.z.array(MULTIPLIER)
2950
+ description: "Replace the response multipliers on an endpoint's defaultResponse. Multipliers expand an array at `targetJsonPath` to a count derived from a request value. Empty array clears all. The list is currently capped at MAX_RESPONSE_MULTIPLIERS (1) \u2014 passing more is rejected.",
2951
+ inputSchema: import_zod11.z.object({
2952
+ mockId: import_zod11.z.string(),
2953
+ endpointId: import_zod11.z.string(),
2954
+ multipliers: import_zod11.z.array(MULTIPLIER)
2627
2955
  }),
2628
2956
  async handler(input, ctx) {
2629
2957
  const state = await ctx.workspace.read();
2630
2958
  const mock = state.synced.mockServers[input.mockId];
2631
2959
  if (!mock) return { ok: false, error: "mock not found" };
2632
2960
  const multipliers = input.multipliers;
2961
+ if (multipliers.length > import_shared5.MAX_RESPONSE_MULTIPLIERS) {
2962
+ return { ok: false, error: "too many multipliers" };
2963
+ }
2633
2964
  const next = patchEndpoint2(mock, input.endpointId, (e) => ({
2634
2965
  ...e,
2635
2966
  defaultResponse: {
2636
2967
  ...e.defaultResponse,
2637
2968
  multipliers: multipliers.length === 0 ? void 0 : multipliers.map((m) => ({
2638
- id: m.id ?? (0, import_shared4.generateId)(),
2969
+ id: m.id ?? (0, import_shared5.generateId)(),
2639
2970
  name: m.name,
2640
2971
  source: { kind: m.source.kind, key: m.source.key },
2641
2972
  targetJsonPath: m.targetJsonPath,
@@ -2651,132 +2982,1438 @@ var mockSetMultipliersTool = {
2651
2982
  }
2652
2983
  };
2653
2984
 
2654
- // src/tools/registry.ts
2655
- var TOOL_REGISTRY = [
2656
- importCurlTool,
2657
- importOpenApiTool,
2658
- importPostmanTool,
2659
- importInsomniaTool,
2660
- importHarTool,
2661
- generateCodeTool,
2662
- workspaceListTool,
2663
- workspaceReadTool,
2664
- workspaceWriteTool,
2665
- requestCreateTool,
2666
- requestReadTool,
2667
- requestUpdateTool,
2668
- requestDeleteTool,
2669
- folderCreateTool,
2670
- folderReadTool,
2671
- folderUpdateTool,
2672
- folderDeleteTool,
2673
- folderExportJsonTool,
2674
- folderImportJsonTool,
2675
- environmentCreateTool,
2676
- environmentReadTool,
2677
- environmentUpdateTool,
2678
- environmentDeleteTool,
2679
- environmentSetActiveTool,
2680
- environmentSetPriorityTool,
2681
- environmentExportTool,
2682
- environmentImportTool,
2683
- planCreateTool,
2684
- planRunTool,
2685
- planReadTool,
2686
- planUpdateTool,
2687
- planDeleteTool,
2688
- planAddStepTool,
2689
- planRemoveStepTool,
2690
- planReorderStepsTool,
2691
- planSetVariablesTool,
2692
- assertionCreateTool,
2693
- assertionReadTool,
2694
- assertionUpdateTool,
2695
- assertionDeleteTool,
2696
- historyListRunsTool,
2697
- historyGetRunTool,
2698
- historyDeleteRunTool,
2699
- historyPurgeTool,
2700
- codebaseExtractCollectionTool,
2701
- promptCreateEnvironmentTool,
2702
- promptCreateAssertionTool,
2703
- promptCreatePlanTool,
2704
- promptCreateRequestTool,
2705
- promptUpdateRequestTool,
2706
- promptCreateFolderTreeTool,
2707
- promptAddPlanStepsTool,
2708
- promptSetPlanVariablesTool,
2709
- promptCreateMockServerTool,
2710
- promptAddMockEndpointTool,
2711
- promptSetEndpointValidationRulesTool,
2712
- promptSetEndpointResponseRulesTool,
2713
- promptSetEndpointMultipliersTool,
2714
- mockCreateFromOpenApiTool,
2715
- mockCreateFromPostmanTool,
2716
- mockCreateFromInsomniaTool,
2717
- mockCreateManualTool,
2718
- mockListTool,
2719
- mockListEndpointsTool,
2720
- mockStartTool,
2721
- mockStopTool,
2722
- mockDeleteTool,
2723
- mockAddEndpointTool,
2724
- mockUpdateEndpointTool,
2725
- mockDeleteEndpointTool,
2726
- mockSetValidationRulesTool,
2727
- mockSetResponseRulesTool,
2728
- mockSetMultipliersTool,
2729
- mockImportPostmanMockCollectionTool
2730
- ];
2731
- function getTool(name) {
2732
- return TOOL_REGISTRY.find((t) => t.name === name);
2733
- }
2734
-
2735
- // src/providers/Workspaces.ts
2736
- var SingleWorkspaceAdapter = class {
2737
- constructor(provider, workspaceId, displayName = "Workspace") {
2738
- this.provider = provider;
2739
- this.workspaceId = workspaceId;
2740
- this.displayName = displayName;
2741
- }
2742
- provider;
2743
- workspaceId;
2744
- displayName;
2745
- async list() {
2746
- const state = await this.provider.read();
2747
- const id = state.synced.workspaceId;
2748
- this.workspaceId = id;
2749
- return [
2750
- {
2751
- id,
2752
- name: this.displayName,
2753
- isActive: true,
2754
- createdAt: state.synced.meta.createdAt,
2755
- lastOpenedAt: state.synced.meta.updatedAt,
2756
- counts: {
2757
- requests: Object.keys(state.synced.collections.requests).length,
2758
- folders: Object.keys(state.synced.collections.folders).length,
2759
- environments: Object.keys(state.synced.environments.items).length,
2760
- mockServers: Object.keys(state.synced.mockServers ?? {}).length,
2761
- plans: Object.keys(state.synced.executionPlans ?? {}).length
2762
- }
2763
- }
2764
- ];
2765
- }
2766
- for(workspaceId) {
2767
- if (this.workspaceId && workspaceId !== this.workspaceId) {
2768
- throw new WorkspaceNotFoundError(workspaceId);
2985
+ // src/tools/releases.ts
2986
+ var import_zod12 = require("zod");
2987
+ var import_core4 = require("@apicircle/core");
2988
+ var releaseListTool = {
2989
+ name: "release.list",
2990
+ description: "List this workspace's published releases (newest first) with their notes, snapshot fingerprint, and deprecated / withdrawn flags. Returns the current version too.",
2991
+ inputSchema: import_zod12.z.object({}),
2992
+ async handler(_input, ctx) {
2993
+ const state = await ctx.workspace.read();
2994
+ const ledger = state.synced.releases.self;
2995
+ if (!ledger) {
2996
+ return { currentVersion: null, count: 0, versions: [] };
2769
2997
  }
2770
- return this.provider;
2771
- }
2772
- activeId() {
2773
- return this.workspaceId;
2998
+ const order = (0, import_core4.sortVersionsDesc)(ledger.versions.map((v) => v.version));
2999
+ const byVersion = new Map(ledger.versions.map((v) => [v.version, v]));
3000
+ const versions = order.map((v) => byVersion.get(v)).filter((v) => v !== void 0).map((v) => ({
3001
+ version: v.version,
3002
+ publishedAt: v.publishedAt,
3003
+ notes: v.notes,
3004
+ workspaceSnapshot: v.workspaceSnapshot,
3005
+ deprecated: v.deprecated,
3006
+ yanked: v.yanked,
3007
+ ...v.sha ? { sha: v.sha } : {},
3008
+ ...v.tagName ? { tagName: v.tagName } : {}
3009
+ }));
3010
+ return { currentVersion: ledger.currentVersion, count: versions.length, versions };
2774
3011
  }
2775
- setActive(workspaceId) {
2776
- if (this.workspaceId && workspaceId !== this.workspaceId) {
2777
- throw new WorkspaceNotFoundError(workspaceId);
3012
+ };
3013
+ var releasePublishTool = {
3014
+ name: "release.publish",
3015
+ description: "Publish a new release of this workspace. Appends a semver version + markdown notes to the ledger and bumps currentVersion. The release is fingerprinted with a SHA-256 of the workspace at publish time. Rejects invalid semver or a duplicate version. Does NOT create a Git tag or GitHub Release.",
3016
+ inputSchema: import_zod12.z.object({
3017
+ version: import_zod12.z.string().min(1).describe('Semantic version, e.g. "1.2.0".'),
3018
+ notes: import_zod12.z.string().default("").describe("Markdown release notes."),
3019
+ sha: import_zod12.z.string().optional().describe("Optional source commit SHA for bookkeeping."),
3020
+ tagName: import_zod12.z.string().optional().describe("Optional git tag name for bookkeeping.")
3021
+ }),
3022
+ async handler(input, ctx) {
3023
+ const state = await ctx.workspace.read();
3024
+ let entry;
3025
+ try {
3026
+ entry = await (0, import_core4.buildReleaseEntry)(state.synced, {
3027
+ version: input.version,
3028
+ notes: input.notes,
3029
+ sha: input.sha,
3030
+ tagName: input.tagName
3031
+ });
3032
+ } catch (err) {
3033
+ return { ok: false, error: err instanceof Error ? err.message : "release.publish failed" };
3034
+ }
3035
+ try {
3036
+ const out = await ctx.workspace.apply({ kind: "release.publish", entry });
3037
+ const after = (await ctx.workspace.read()).synced.releases.self;
3038
+ return {
3039
+ ok: true,
3040
+ version: entry.version,
3041
+ currentVersion: after?.currentVersion ?? entry.version,
3042
+ workspaceSnapshot: entry.workspaceSnapshot,
3043
+ changedIds: out.changedIds
3044
+ };
3045
+ } catch (err) {
3046
+ return { ok: false, error: err instanceof Error ? err.message : "release.publish failed" };
2778
3047
  }
2779
- return Promise.resolve();
3048
+ }
3049
+ };
3050
+ var releaseDeprecateTool = {
3051
+ name: "release.deprecate",
3052
+ description: "Mark a published version as deprecated (soft signal). Consumers see a warning but the version stays installable. Errors if the version is unknown or no ledger exists.",
3053
+ inputSchema: import_zod12.z.object({ version: import_zod12.z.string().min(1) }),
3054
+ async handler(input, ctx) {
3055
+ try {
3056
+ const out = await ctx.workspace.apply({ kind: "release.deprecate", version: input.version });
3057
+ return { ok: true, version: input.version, changedIds: out.changedIds };
3058
+ } catch (err) {
3059
+ return { ok: false, error: err instanceof Error ? err.message : "release.deprecate failed" };
3060
+ }
3061
+ }
3062
+ };
3063
+ var releaseYankTool = {
3064
+ name: "release.yank",
3065
+ description: "Withdraw (yank) a published version (hard signal). Consumers are warned the version is broken / unsafe and told to move to a different one. The entry stays in the ledger. Errors if the version is unknown or no ledger exists.",
3066
+ inputSchema: import_zod12.z.object({ version: import_zod12.z.string().min(1) }),
3067
+ async handler(input, ctx) {
3068
+ try {
3069
+ const out = await ctx.workspace.apply({ kind: "release.yank", version: input.version });
3070
+ return { ok: true, version: input.version, changedIds: out.changedIds };
3071
+ } catch (err) {
3072
+ return { ok: false, error: err instanceof Error ? err.message : "release.yank failed" };
3073
+ }
3074
+ }
3075
+ };
3076
+
3077
+ // src/tools/linkedWorkspaces.ts
3078
+ var import_zod13 = require("zod");
3079
+ function summarize(link, currentVersion) {
3080
+ return {
3081
+ id: link.id,
3082
+ name: link.name,
3083
+ kind: link.kind,
3084
+ description: link.description,
3085
+ source: link.source,
3086
+ scope: link.scope,
3087
+ pinnedVersion: link.pinnedVersion,
3088
+ requiredSecretKeyIds: link.requiredSecretKeyIds,
3089
+ marketplace: link.marketplace,
3090
+ cachedCurrentVersion: currentVersion
3091
+ };
3092
+ }
3093
+ var linkedListTool = {
3094
+ name: "linked.list",
3095
+ description: "List the workspaces this workspace links to (consumes). Each entry includes its source repo/branch, scope, pinned version, required secret-key ids, and the current version of its cached release ledger.",
3096
+ inputSchema: import_zod13.z.object({}),
3097
+ async handler(_input, ctx) {
3098
+ const state = await ctx.workspace.read();
3099
+ const links = Object.values(state.synced.linkedWorkspaces);
3100
+ return {
3101
+ count: links.length,
3102
+ links: links.map(
3103
+ (l) => summarize(l, state.synced.releases.perLink[l.id]?.currentVersion ?? null)
3104
+ )
3105
+ };
3106
+ }
3107
+ };
3108
+ var linkedGetTool = {
3109
+ name: "linked.get",
3110
+ description: "Read one linked workspace by id, including its cached release ledger (the versions available to pin to).",
3111
+ inputSchema: import_zod13.z.object({ id: import_zod13.z.string() }),
3112
+ async handler(input, ctx) {
3113
+ const state = await ctx.workspace.read();
3114
+ const link = state.synced.linkedWorkspaces[input.id];
3115
+ if (!link) return { ok: false, error: `Linked workspace ${input.id} not found` };
3116
+ const ledger = state.synced.releases.perLink[input.id] ?? null;
3117
+ return {
3118
+ ok: true,
3119
+ link: summarize(link, ledger?.currentVersion ?? null),
3120
+ ledger
3121
+ };
3122
+ }
3123
+ };
3124
+ var linkedSetConfigTool = {
3125
+ name: "linked.set_config",
3126
+ description: "Update an existing linked workspace's config: rename, pin/unpin a version (must exist in the cached ledger), set scope, session mode, required secret-key ids, or marketplace metadata. Only supplied fields change. Does NOT fetch from the network.",
3127
+ inputSchema: import_zod13.z.object({
3128
+ id: import_zod13.z.string(),
3129
+ name: import_zod13.z.string().optional(),
3130
+ description: import_zod13.z.string().optional(),
3131
+ pinnedVersion: import_zod13.z.string().nullable().optional().describe("null = unpin (track source HEAD)."),
3132
+ scope: import_zod13.z.array(import_zod13.z.enum(["collections", "environments"])).optional(),
3133
+ sessionMode: import_zod13.z.enum(["workspace", "dedicated"]).optional(),
3134
+ requiredSecretKeyIds: import_zod13.z.array(import_zod13.z.string()).optional(),
3135
+ marketplace: import_zod13.z.object({
3136
+ listedAs: import_zod13.z.string(),
3137
+ tags: import_zod13.z.array(import_zod13.z.string()),
3138
+ summary: import_zod13.z.string()
3139
+ }).nullable().optional().describe("null = clear marketplace metadata.")
3140
+ }),
3141
+ async handler(input, ctx) {
3142
+ const state = await ctx.workspace.read();
3143
+ const link = state.synced.linkedWorkspaces[input.id];
3144
+ if (!link) return { ok: false, error: `Linked workspace ${input.id} not found` };
3145
+ if (input.pinnedVersion !== void 0 && input.pinnedVersion !== null) {
3146
+ const cached = state.synced.releases.perLink[input.id]?.versions ?? [];
3147
+ if (!cached.some((v) => v.version === input.pinnedVersion)) {
3148
+ return {
3149
+ ok: false,
3150
+ error: `Version ${input.pinnedVersion} is not in the cached ledger \u2014 refresh the link first`
3151
+ };
3152
+ }
3153
+ }
3154
+ const next = {
3155
+ ...link,
3156
+ ...input.name !== void 0 ? { name: input.name } : {},
3157
+ ...input.description !== void 0 ? { description: input.description } : {},
3158
+ ...input.pinnedVersion !== void 0 ? { pinnedVersion: input.pinnedVersion } : {},
3159
+ ...input.scope !== void 0 ? { scope: input.scope } : {},
3160
+ ...input.requiredSecretKeyIds !== void 0 ? { requiredSecretKeyIds: input.requiredSecretKeyIds } : {},
3161
+ ...input.sessionMode !== void 0 ? { source: { ...link.source, sessionMode: input.sessionMode } } : {}
3162
+ };
3163
+ if (input.marketplace !== void 0) {
3164
+ if (input.marketplace === null) {
3165
+ delete next.marketplace;
3166
+ } else {
3167
+ next.marketplace = input.marketplace;
3168
+ }
3169
+ }
3170
+ const out = await ctx.workspace.apply({ kind: "linkedWorkspace.upsert", link: next });
3171
+ return {
3172
+ ok: true,
3173
+ changedIds: out.changedIds,
3174
+ link: summarize(next, state.synced.releases.perLink[input.id]?.currentVersion ?? null)
3175
+ };
3176
+ }
3177
+ };
3178
+ var linkedUnlinkTool = {
3179
+ name: "linked.unlink",
3180
+ description: "Unlink a workspace by id. Removes the link, its cached release ledger, every local override for it, the cached collections/environments snapshot, and any per-link session entry. The source repo is untouched.",
3181
+ inputSchema: import_zod13.z.object({ id: import_zod13.z.string() }),
3182
+ async handler(input, ctx) {
3183
+ const state = await ctx.workspace.read();
3184
+ if (!state.synced.linkedWorkspaces[input.id]) {
3185
+ return { ok: false, error: `Linked workspace ${input.id} not found` };
3186
+ }
3187
+ const out = await ctx.workspace.apply({ kind: "linkedWorkspace.remove", id: input.id });
3188
+ return { ok: true, changedIds: out.changedIds };
3189
+ }
3190
+ };
3191
+
3192
+ // src/tools/githubOps.ts
3193
+ var import_zod14 = require("zod");
3194
+ var import_core5 = require("@apicircle/core");
3195
+ var import_shared6 = require("@apicircle/shared");
3196
+
3197
+ // ../git/src/github/errors.ts
3198
+ var GitHubError = class extends Error {
3199
+ constructor(message, status, body) {
3200
+ super(message);
3201
+ this.status = status;
3202
+ this.body = body;
3203
+ this.name = "GitHubError";
3204
+ }
3205
+ status;
3206
+ body;
3207
+ };
3208
+ var MissingScopeError = class extends GitHubError {
3209
+ /** Scope strings the API said are missing, e.g. ['pull_request']. */
3210
+ missingScopes;
3211
+ /** Scope strings the token currently grants, parsed from x-oauth-scopes. */
3212
+ grantedScopes;
3213
+ constructor(message, status, missingScopes, grantedScopes) {
3214
+ super(message, status);
3215
+ this.name = "MissingScopeError";
3216
+ this.missingScopes = missingScopes;
3217
+ this.grantedScopes = grantedScopes;
3218
+ }
3219
+ };
3220
+ var RateLimitedError = class extends GitHubError {
3221
+ /** Unix timestamp (ms) when the rate-limit window resets. */
3222
+ resetAtMs;
3223
+ constructor(message, status, resetAtMs) {
3224
+ super(message, status);
3225
+ this.name = "RateLimitedError";
3226
+ this.resetAtMs = resetAtMs;
3227
+ }
3228
+ };
3229
+ var UnauthorizedError = class extends GitHubError {
3230
+ constructor(message, status) {
3231
+ super(message, status);
3232
+ this.name = "UnauthorizedError";
3233
+ }
3234
+ };
3235
+ var TimeoutError = class extends GitHubError {
3236
+ /** Timeout that fired, in ms. Useful for the UI message. */
3237
+ timeoutMs;
3238
+ constructor(message, timeoutMs) {
3239
+ super(message, 0);
3240
+ this.name = "TimeoutError";
3241
+ this.timeoutMs = timeoutMs;
3242
+ }
3243
+ };
3244
+
3245
+ // ../git/src/github/api.ts
3246
+ var API_BASE = "https://api.github.com";
3247
+ var LOGIN_BASE = "https://github.com";
3248
+ var DEFAULT_TIMEOUT_MS = 15e3;
3249
+ var GitHubClient = class {
3250
+ baseUrl;
3251
+ loginBaseUrl;
3252
+ fetchImpl;
3253
+ timeoutMs;
3254
+ constructor(opts = {}) {
3255
+ this.baseUrl = opts.baseUrl ?? API_BASE;
3256
+ this.loginBaseUrl = (opts.loginBaseUrl ?? LOGIN_BASE).replace(/\/$/, "");
3257
+ this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
3258
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
3259
+ }
3260
+ /**
3261
+ * Fetch the authenticated user. Doubles as a "verify token" probe — used
3262
+ * by the Secret Vault Sessions tab to refresh the granted-scopes list.
3263
+ */
3264
+ async getViewer(token, opts = {}) {
3265
+ const { json, response } = await this.call(token, "/user", opts);
3266
+ return {
3267
+ viewer: {
3268
+ login: json.login,
3269
+ id: json.id,
3270
+ name: json.name ?? null,
3271
+ avatarUrl: json.avatar_url ?? null
3272
+ },
3273
+ scopes: parseScopes(response.headers)
3274
+ };
3275
+ }
3276
+ /**
3277
+ * List repositories the authenticated user can access. Used by the repo
3278
+ * picker. Capped at 100 sorted by recent push; users with thousands of
3279
+ * repos can paginate later.
3280
+ */
3281
+ async listAccessibleRepos(token, opts = {}) {
3282
+ const { json } = await this.call(
3283
+ token,
3284
+ "/user/repos?per_page=100&sort=pushed&affiliation=owner,collaborator,organization_member",
3285
+ opts
3286
+ );
3287
+ return json.map(normalizeRepo);
3288
+ }
3289
+ /**
3290
+ * Fetch a specific repo. Validates the user-supplied owner/name pair
3291
+ * exists + is accessible, and exposes the default branch.
3292
+ */
3293
+ async getRepo(token, owner, name, opts = {}) {
3294
+ const { json } = await this.call(
3295
+ token,
3296
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
3297
+ opts
3298
+ );
3299
+ return normalizeRepo(json);
3300
+ }
3301
+ /**
3302
+ * Read the head SHA of a branch. Used to seed a new working branch from
3303
+ * main before any edits land.
3304
+ */
3305
+ async getBranchHead(token, owner, name, branch, opts = {}) {
3306
+ const { json } = await this.call(
3307
+ token,
3308
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/branches/${encodeURIComponent(branch)}`,
3309
+ opts
3310
+ );
3311
+ return { name: json.name, commitSha: json.commit.sha };
3312
+ }
3313
+ /**
3314
+ * List branches on a repo. Used by the Link Workspace repo-browser to
3315
+ * populate the branch dropdown after the user picks a repo. Capped at
3316
+ * 100 (GitHub's max page size); repos with more branches paginate.
3317
+ */
3318
+ async listBranches(token, owner, name, opts = {}) {
3319
+ const { json } = await this.call(
3320
+ token,
3321
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/branches?per_page=100`,
3322
+ opts
3323
+ );
3324
+ return json.map((b) => ({ name: b.name, commitSha: b.commit.sha }));
3325
+ }
3326
+ /**
3327
+ * Create a new branch ref pointing at `sha`. The auto-branch flow calls
3328
+ * this with the head SHA from `getBranchHead(main)`.
3329
+ *
3330
+ * GitHub returns 422 with "Reference already exists" when the branch
3331
+ * already exists; that surfaces as a GitHubError(422) so the UI can
3332
+ * prompt for a different name.
3333
+ */
3334
+ async createBranch(token, owner, name, branchName, sha, opts = {}) {
3335
+ const { json } = await this.call(
3336
+ token,
3337
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs`,
3338
+ {
3339
+ ...opts,
3340
+ method: "POST",
3341
+ body: { ref: `refs/heads/${branchName}`, sha },
3342
+ requiredScopes: ["repo"]
3343
+ }
3344
+ );
3345
+ return { name: branchName, commitSha: json.object.sha };
3346
+ }
3347
+ /**
3348
+ * Read a branch ref's current commit SHA. Used at the start of push-to-
3349
+ * save to find the parent commit before building the new tree.
3350
+ */
3351
+ async getRef(token, owner, name, branch, opts = {}) {
3352
+ const { json } = await this.call(
3353
+ token,
3354
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs/heads/${encodeURIComponent(branch)}`,
3355
+ opts
3356
+ );
3357
+ return { ref: json.ref, sha: json.object.sha };
3358
+ }
3359
+ /**
3360
+ * Read a commit's tree SHA. Used so the new tree can be built `base_tree`
3361
+ * — every path we don't override is inherited from the parent.
3362
+ */
3363
+ async getCommit(token, owner, name, sha, opts = {}) {
3364
+ const { json } = await this.call(
3365
+ token,
3366
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/commits/${encodeURIComponent(sha)}`,
3367
+ opts
3368
+ );
3369
+ return {
3370
+ sha: json.sha,
3371
+ treeSha: json.tree.sha,
3372
+ message: json.message
3373
+ };
3374
+ }
3375
+ /**
3376
+ * Upload a blob to the repo and return its SHA. Used by push-to-save
3377
+ * (P4.3b) for binary attachments — text files go straight into a tree
3378
+ * entry's `content`, but binary bytes have to go through a blob first.
3379
+ *
3380
+ * `content` is base64 when `encoding === 'base64'`. GitHub stores blobs
3381
+ * deduplicated by their git-sha1 (not our sha256), so re-uploading the
3382
+ * same bytes is cheap on their side; we save a roundtrip locally by
3383
+ * tracking lastPushedBlobSha per slot in a future revision.
3384
+ */
3385
+ async createBlob(token, owner, name, args, opts = {}) {
3386
+ const { json } = await this.call(
3387
+ token,
3388
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/blobs`,
3389
+ {
3390
+ ...opts,
3391
+ method: "POST",
3392
+ body: { content: args.content, encoding: args.encoding },
3393
+ requiredScopes: ["repo"]
3394
+ }
3395
+ );
3396
+ return { sha: json.sha, size: json.size ?? 0 };
3397
+ }
3398
+ /**
3399
+ * Build a new tree from `entries`, layered over `baseTreeSha`. Entries
3400
+ * with `content` are inlined (text path); entries with a pre-uploaded
3401
+ * `sha` reference an existing blob (binary path — used by attachments).
3402
+ */
3403
+ async createTree(token, owner, name, args, opts = {}) {
3404
+ const tree = args.entries.map((e) => ({
3405
+ path: e.path,
3406
+ mode: e.mode ?? "100644",
3407
+ type: e.type ?? "blob",
3408
+ ...e.content !== void 0 ? { content: e.content } : {},
3409
+ ...e.sha !== void 0 ? { sha: e.sha } : {}
3410
+ }));
3411
+ const { json } = await this.call(
3412
+ token,
3413
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/trees`,
3414
+ {
3415
+ ...opts,
3416
+ method: "POST",
3417
+ body: { base_tree: args.baseTreeSha, tree },
3418
+ requiredScopes: ["repo"]
3419
+ }
3420
+ );
3421
+ return { sha: json.sha };
3422
+ }
3423
+ /**
3424
+ * Create a new commit object pointing at the given tree, with the given
3425
+ * parents. Returns the new commit's SHA + the tree it points at.
3426
+ */
3427
+ async createCommit(token, owner, name, args, opts = {}) {
3428
+ const { json } = await this.call(
3429
+ token,
3430
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/commits`,
3431
+ {
3432
+ ...opts,
3433
+ method: "POST",
3434
+ body: {
3435
+ message: args.message,
3436
+ tree: args.treeSha,
3437
+ parents: args.parents
3438
+ },
3439
+ requiredScopes: ["repo"]
3440
+ }
3441
+ );
3442
+ return { sha: json.sha, treeSha: json.tree.sha };
3443
+ }
3444
+ /**
3445
+ * Fast-forward a branch ref to a new commit SHA. Pass `force: true` to
3446
+ * skip the FF check (we don't — push-to-save is always FF over the ref
3447
+ * we just read with getRef()).
3448
+ */
3449
+ async updateRef(token, owner, name, args, opts = {}) {
3450
+ const { json } = await this.call(
3451
+ token,
3452
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs/heads/${encodeURIComponent(args.branch)}`,
3453
+ {
3454
+ ...opts,
3455
+ method: "PATCH",
3456
+ body: { sha: args.sha, force: args.force ?? false },
3457
+ requiredScopes: ["repo"]
3458
+ }
3459
+ );
3460
+ return { ref: json.ref, sha: json.object.sha };
3461
+ }
3462
+ /**
3463
+ * Search GitHub for public API Circle workspaces. Appends
3464
+ * `topic:apicircle` to the user-supplied query so only repos carrying
3465
+ * the `apicircle` topic — the topic the Releases & Topics dialog
3466
+ * locks onto every workspace repo — surface in results. GitHub
3467
+ * matches the bare query against repository name, description, and
3468
+ * topics, so category words like `payments` narrow the marketplace by
3469
+ * topic. An empty query lists every public API Circle workspace. Top
3470
+ * 30 results. Token is optional — anonymous browsing is supported
3471
+ * (lower GitHub rate limits apply); pass a PAT when one is available
3472
+ * to lift them. `sort` controls ordering: omit for GitHub's
3473
+ * best-match relevance, or pass `'stars'` / `'updated'`.
3474
+ */
3475
+ async searchMarketplaceRepos(token, query, opts = {}) {
3476
+ const { sort, ...callOpts } = opts;
3477
+ const fullQuery = `${query.trim()} topic:apicircle`.trim();
3478
+ const sortParam = sort ? `&sort=${sort}&order=desc` : "";
3479
+ const path2 = `/search/repositories?q=${encodeURIComponent(fullQuery)}&per_page=30${sortParam}`;
3480
+ const { json } = await this.call(token, path2, callOpts);
3481
+ const items = json.items ?? [];
3482
+ return items.map(normalizeMarketplaceRepo);
3483
+ }
3484
+ /**
3485
+ * Start GitHub's OAuth Device Flow. Returns a user-facing code the
3486
+ * user types into github.com/login/device + a device_code the app
3487
+ * polls with. Pure browser-safe: no client_secret involved (device
3488
+ * flow is the only OAuth path GitHub supports for public clients).
3489
+ *
3490
+ * Requires the OAuth App to have "Enable Device Flow" turned on in
3491
+ * its GitHub settings — surface 400 with `not_supported` to the user
3492
+ * if the App owner hasn't done that yet.
3493
+ */
3494
+ async startDeviceFlow(clientId, scope, opts = {}) {
3495
+ const url = `${this.loginBaseUrl}/login/device/code`;
3496
+ const response = await this.fetchImpl(url, {
3497
+ method: "POST",
3498
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
3499
+ body: JSON.stringify({ client_id: clientId, scope }),
3500
+ signal: opts.signal
3501
+ });
3502
+ if (!response.ok) {
3503
+ throw new GitHubError(
3504
+ `Device-flow start failed: HTTP ${response.status}`,
3505
+ response.status,
3506
+ {}
3507
+ );
3508
+ }
3509
+ const json = await response.json();
3510
+ if (json.error) {
3511
+ throw new GitHubError(json.error_description ?? json.error, 400, json);
3512
+ }
3513
+ return {
3514
+ deviceCode: json.device_code,
3515
+ userCode: json.user_code,
3516
+ verificationUri: json.verification_uri,
3517
+ expiresIn: json.expires_in,
3518
+ interval: json.interval
3519
+ };
3520
+ }
3521
+ /**
3522
+ * Poll for the access token after the user has authorized the device
3523
+ * code. GitHub returns `authorization_pending` until the user
3524
+ * completes the flow, `slow_down` if we polled too fast, then a real
3525
+ * token. Caller wraps this in a polling loop bounded by `expiresIn`.
3526
+ */
3527
+ async pollDeviceToken(clientId, deviceCode, opts = {}) {
3528
+ const url = `${this.loginBaseUrl}/login/oauth/access_token`;
3529
+ const response = await this.fetchImpl(url, {
3530
+ method: "POST",
3531
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
3532
+ body: JSON.stringify({
3533
+ client_id: clientId,
3534
+ device_code: deviceCode,
3535
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
3536
+ }),
3537
+ signal: opts.signal
3538
+ });
3539
+ const json = await response.json();
3540
+ if (json.access_token) {
3541
+ return {
3542
+ kind: "granted",
3543
+ accessToken: json.access_token,
3544
+ tokenType: json.token_type ?? "bearer",
3545
+ scope: json.scope ?? ""
3546
+ };
3547
+ }
3548
+ if (json.error === "authorization_pending") return { kind: "pending", slowDown: false };
3549
+ if (json.error === "slow_down") return { kind: "pending", slowDown: true };
3550
+ if (json.error === "expired_token") return { kind: "expired" };
3551
+ if (json.error === "access_denied")
3552
+ return { kind: "denied", reason: json.error_description ?? "User denied authorization" };
3553
+ throw new GitHubError(
3554
+ json.error_description ?? json.error ?? "Device-token poll failed",
3555
+ response.status,
3556
+ json
3557
+ );
3558
+ }
3559
+ /**
3560
+ * Create a lightweight Git tag (a ref under `refs/tags/<name>`) on the
3561
+ * given commit SHA. Used by the publish-release flow when the user
3562
+ * opts in to "Create Git tag v<x.y.z>". Returns the resolved ref.
3563
+ *
3564
+ * GitHub returns 422 with "Reference already exists" when the tag is
3565
+ * a duplicate; that surfaces as a GitHubError(422) so the UI can warn
3566
+ * the user without ever overwriting an existing tag.
3567
+ */
3568
+ async createTag(token, owner, name, args, opts = {}) {
3569
+ const { json } = await this.call(
3570
+ token,
3571
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs`,
3572
+ {
3573
+ ...opts,
3574
+ method: "POST",
3575
+ body: { ref: `refs/tags/${args.tagName}`, sha: args.sha },
3576
+ requiredScopes: ["repo"]
3577
+ }
3578
+ );
3579
+ return { ref: json.ref, sha: json.object.sha };
3580
+ }
3581
+ /**
3582
+ * Compare two commits. Returns the relationship classification GitHub
3583
+ * gives us: `ahead` (head is descendant of base), `behind` (base is
3584
+ * descendant of head), `identical`, or `diverged` (the two histories
3585
+ * share a base but neither contains the other — typical of a force-push
3586
+ * that rewrote history under us).
3587
+ *
3588
+ * Used by the refresh path so we never silently 3-way-merge across a
3589
+ * history rewrite — divergence steers the user through an explicit
3590
+ * "history rewritten" modal instead of corrupting local state.
3591
+ */
3592
+ async compareCommits(token, owner, name, base, head, opts = {}) {
3593
+ const { json } = await this.call(
3594
+ token,
3595
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/compare/${encodeURIComponent(
3596
+ base
3597
+ )}...${encodeURIComponent(head)}`,
3598
+ { ...opts, requiredScopes: ["repo"] }
3599
+ );
3600
+ return {
3601
+ status: json.status,
3602
+ aheadBy: json.ahead_by,
3603
+ behindBy: json.behind_by
3604
+ };
3605
+ }
3606
+ /**
3607
+ * Is `ancestor` reachable from `descendant`? Thin wrapper around
3608
+ * `compareCommits` — "ahead" or "identical" means yes; "behind" or
3609
+ * "diverged" means the histories don't fit, so the answer is no.
3610
+ */
3611
+ async isAncestor(token, owner, name, ancestor, descendant, opts = {}) {
3612
+ if (ancestor === descendant) return true;
3613
+ const cmp = await this.compareCommits(token, owner, name, ancestor, descendant, opts);
3614
+ return cmp.status === "ahead" || cmp.status === "identical";
3615
+ }
3616
+ /**
3617
+ * Create a GitHub Release pointing at an existing tag. Used by the
3618
+ * publish-release flow when the user opts in to "Create GitHub
3619
+ * Release". Returns the release's HTML URL so the UI can show a
3620
+ * "Released — view on GitHub" link.
3621
+ *
3622
+ * Pass `prerelease: true` for semver pre-release identifiers (e.g.
3623
+ * `1.0.0-rc.1`); GitHub's Releases UI flags those distinctly.
3624
+ */
3625
+ async createRelease(token, owner, name, args, opts = {}) {
3626
+ const { json } = await this.call(token, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/releases`, {
3627
+ ...opts,
3628
+ method: "POST",
3629
+ body: {
3630
+ tag_name: args.tagName,
3631
+ name: args.releaseName ?? args.tagName,
3632
+ body: args.body ?? "",
3633
+ draft: args.draft ?? false,
3634
+ prerelease: args.prerelease ?? false
3635
+ },
3636
+ requiredScopes: ["repo"]
3637
+ });
3638
+ return { id: json.id, htmlUrl: json.html_url, tagName: json.tag_name };
3639
+ }
3640
+ /**
3641
+ * Read a tag ref's current commit SHA. Used by the Release & topics
3642
+ * modal to detect whether a tag with the chosen name already exists
3643
+ * (so the UI can surface an "Override existing tag" toggle instead of
3644
+ * silently 422'ing through createTag).
3645
+ *
3646
+ * Returns `null` when the tag doesn't exist (404). Other failures
3647
+ * surface as typed errors.
3648
+ */
3649
+ async getTagSha(token, owner, name, tagName, opts = {}) {
3650
+ try {
3651
+ const { json } = await this.call(
3652
+ token,
3653
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs/tags/${encodeURIComponent(tagName)}`,
3654
+ opts
3655
+ );
3656
+ return json.object.sha;
3657
+ } catch (err) {
3658
+ if (err instanceof GitHubError && err.status === 404) return null;
3659
+ throw err;
3660
+ }
3661
+ }
3662
+ /**
3663
+ * Delete a ref. Used to support the "Override existing tag" path on
3664
+ * the Release & topics modal — we delete the existing tag ref, then
3665
+ * createTag against the new SHA. (GitHub doesn't have a single
3666
+ * "force-update tag" endpoint via the simple refs API.)
3667
+ *
3668
+ * `ref` is the bare suffix, e.g. `tags/v1.0.0` or `heads/feature-x`.
3669
+ */
3670
+ async deleteRef(token, owner, name, ref, opts = {}) {
3671
+ await this.call(
3672
+ token,
3673
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs/${ref.split("/").map(encodeURIComponent).join("/")}`,
3674
+ {
3675
+ ...opts,
3676
+ method: "DELETE",
3677
+ requiredScopes: ["repo"]
3678
+ }
3679
+ );
3680
+ }
3681
+ /**
3682
+ * Read the repo's current topic list. Topics drive marketplace
3683
+ * discoverability — public API Circle workspaces include `apicircle`
3684
+ * plus user-chosen category topics.
3685
+ *
3686
+ * Note: GitHub's topics API uses a custom Accept header, but we treat
3687
+ * that as transport detail; the `application/vnd.github.mercy-preview+json`
3688
+ * preview is now stable so the default Accept works.
3689
+ */
3690
+ async listRepoTopics(token, owner, name, opts = {}) {
3691
+ const { json } = await this.call(
3692
+ token,
3693
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/topics`,
3694
+ opts
3695
+ );
3696
+ return Array.isArray(json.names) ? json.names : [];
3697
+ }
3698
+ /**
3699
+ * Replace the repo's full topic list. GitHub's `PUT /topics` endpoint
3700
+ * is a full replace (not a merge), so the caller must pass the
3701
+ * complete desired list. Caps at 20 topics; each must match
3702
+ * `^[a-z0-9][a-z0-9-]*$` and be ≤ 50 chars (GitHub enforces this with
3703
+ * a 422). Returns the persisted list.
3704
+ */
3705
+ async setRepoTopics(token, owner, name, topics, opts = {}) {
3706
+ const { json } = await this.call(
3707
+ token,
3708
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/topics`,
3709
+ {
3710
+ ...opts,
3711
+ method: "PUT",
3712
+ body: { names: topics },
3713
+ requiredScopes: ["repo"]
3714
+ }
3715
+ );
3716
+ return Array.isArray(json.names) ? json.names : [];
3717
+ }
3718
+ /**
3719
+ * Fetch a single file's contents from a branch / commit. Returns
3720
+ * `null` when GitHub answers 404 (file simply doesn't exist on that
3721
+ * ref — the common case for the very first pull). Other failures
3722
+ * surface as the usual typed errors.
3723
+ *
3724
+ * Used by the refresh flow to read remote `workspace.json` so the
3725
+ * 3-way diff can compare it against the local doc.
3726
+ */
3727
+ async getContents(token, owner, name, path2, ref, opts = {}) {
3728
+ const query = `?ref=${encodeURIComponent(ref)}`;
3729
+ try {
3730
+ const { json } = await this.call(
3731
+ token,
3732
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/contents/${path2.split("/").map(encodeURIComponent).join("/")}${query}`,
3733
+ opts
3734
+ );
3735
+ if (Array.isArray(json) || json.type !== "file") {
3736
+ throw new GitHubError(`Path ${path2} is not a file`, 422, json);
3737
+ }
3738
+ const cleaned = json.content.replace(/\n/g, "");
3739
+ const decoded = decodeBase64Utf8(cleaned);
3740
+ return { content: decoded, sha: json.sha, path: json.path, size: json.size };
3741
+ } catch (err) {
3742
+ if (err instanceof GitHubError && err.status === 404) return null;
3743
+ throw err;
3744
+ }
3745
+ }
3746
+ /**
3747
+ * Create or update a file via the Contents API. The killer feature here
3748
+ * vs. the git-data flow (createBlob → createTree → createCommit →
3749
+ * updateRef) is that this works on **truly empty repos**: GitHub's git
3750
+ * database isn't initialized until the first commit lands, so all the
3751
+ * `/git/*` endpoints reject with 409 "Git Repository is empty" — but
3752
+ * `PUT /contents/{path}` atomically initializes the database with a
3753
+ * single-file commit on the supplied branch (defaulting to the repo's
3754
+ * default branch).
3755
+ *
3756
+ * Used by the seed-initial-commit flow to bootstrap a freshly-created
3757
+ * empty repo with a scaffold `workspace.json`.
3758
+ *
3759
+ * `contentBase64` must already be base64-encoded — caller chooses the
3760
+ * encoder (TextEncoder for UTF-8 strings, raw bytes for binaries).
3761
+ */
3762
+ async putContents(token, owner, name, path2, args, opts = {}) {
3763
+ const body = {
3764
+ message: args.message,
3765
+ content: args.contentBase64
3766
+ };
3767
+ if (args.branch) body.branch = args.branch;
3768
+ if (args.sha) body.sha = args.sha;
3769
+ const { json } = await this.call(
3770
+ token,
3771
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/contents/${path2.split("/").map(encodeURIComponent).join("/")}`,
3772
+ {
3773
+ ...opts,
3774
+ method: "PUT",
3775
+ body,
3776
+ requiredScopes: ["repo"]
3777
+ }
3778
+ );
3779
+ return { commitSha: json.commit.sha, contentSha: json.content.sha };
3780
+ }
3781
+ /**
3782
+ * Same as `getContents` but returns the raw bytes instead of UTF-8
3783
+ * decoding the file. Used by the refresh flow to pull
3784
+ * `.apicircle/workspace-<id>/attachments/<slotId>` blobs into local IDB without
3785
+ * mangling binary data through TextDecoder.
3786
+ */
3787
+ async getBinaryContents(token, owner, name, path2, ref, opts = {}) {
3788
+ const query = `?ref=${encodeURIComponent(ref)}`;
3789
+ try {
3790
+ const { json } = await this.call(
3791
+ token,
3792
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/contents/${path2.split("/").map(encodeURIComponent).join("/")}${query}`,
3793
+ opts
3794
+ );
3795
+ if (Array.isArray(json) || json.type !== "file") {
3796
+ throw new GitHubError(`Path ${path2} is not a file`, 422, json);
3797
+ }
3798
+ const cleaned = json.content.replace(/\n/g, "");
3799
+ const bytes = decodeBase64Bytes(cleaned);
3800
+ return { bytes, sha: json.sha, path: json.path, size: json.size };
3801
+ } catch (err) {
3802
+ if (err instanceof GitHubError && err.status === 404) return null;
3803
+ throw err;
3804
+ }
3805
+ }
3806
+ /**
3807
+ * Open a pull request from `head` (the working branch) into `base` (the
3808
+ * repo's default branch). PR creation needs the `pull_request` scope on
3809
+ * top of `repo`; missing-scope errors flow through MissingScopeError so
3810
+ * the UI can prompt the user to update the token without losing branch
3811
+ * state (Plan §3.7).
3812
+ *
3813
+ * GitHub returns 422 when:
3814
+ * - head/base are equal (nothing to merge)
3815
+ * - a PR already exists between this head and base
3816
+ * - the head branch doesn't exist
3817
+ * All three surface as a plain GitHubError(422); the UI message is
3818
+ * picked up from response.body.message.
3819
+ */
3820
+ /**
3821
+ * Fetch a single pull request by number. Used by the refresh flow to
3822
+ * detect whether a previously-opened PR has been merged on GitHub —
3823
+ * `merged: true` is what triggers the working-branch retirement path.
3824
+ *
3825
+ * Returns `null` on 404 (PR was deleted or never existed at this number);
3826
+ * other failures surface as the usual typed errors.
3827
+ */
3828
+ async getPullRequest(token, owner, name, number, opts = {}) {
3829
+ try {
3830
+ const { json } = await this.call(
3831
+ token,
3832
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/pulls/${number}`,
3833
+ opts
3834
+ );
3835
+ return {
3836
+ number: json.number,
3837
+ htmlUrl: json.html_url,
3838
+ state: json.state,
3839
+ merged: json.merged === true
3840
+ };
3841
+ } catch (err) {
3842
+ if (err instanceof GitHubError && err.status === 404) return null;
3843
+ throw err;
3844
+ }
3845
+ }
3846
+ /**
3847
+ * List pull requests on a repo. The capability-probe path uses this with
3848
+ * `perPage: 1` to determine whether the token can read PRs (and, by
3849
+ * extension on classic PATs, whether it can also create them).
3850
+ *
3851
+ * Caller declares `requiredScopes` to surface a `MissingScopeError` on
3852
+ * 403, so the capability probe can recognise the missing-scope case
3853
+ * cleanly vs. transient 5xx/network failures.
3854
+ */
3855
+ async listPullRequests(token, owner, name, args = {}, opts = {}) {
3856
+ const params = new URLSearchParams();
3857
+ params.set("per_page", String(args.perPage ?? 30));
3858
+ if (args.state) params.set("state", args.state);
3859
+ const { json } = await this.call(
3860
+ token,
3861
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/pulls?${params.toString()}`,
3862
+ {
3863
+ ...opts,
3864
+ requiredScopes: ["repo", "pull_request"]
3865
+ }
3866
+ );
3867
+ return json.map((pr) => ({
3868
+ number: pr.number,
3869
+ htmlUrl: pr.html_url,
3870
+ state: pr.state,
3871
+ title: pr.title
3872
+ }));
3873
+ }
3874
+ async createPullRequest(token, owner, name, args, opts = {}) {
3875
+ const { json } = await this.call(
3876
+ token,
3877
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/pulls`,
3878
+ {
3879
+ ...opts,
3880
+ method: "POST",
3881
+ body: {
3882
+ title: args.title,
3883
+ body: args.body,
3884
+ head: args.head,
3885
+ base: args.base,
3886
+ draft: args.draft ?? false
3887
+ },
3888
+ requiredScopes: ["repo", "pull_request"]
3889
+ }
3890
+ );
3891
+ return {
3892
+ number: json.number,
3893
+ htmlUrl: json.html_url,
3894
+ state: json.state,
3895
+ title: json.title
3896
+ };
3897
+ }
3898
+ // --- low-level call ----------------------------------------------------
3899
+ async call(token, path2, opts = {}) {
3900
+ const url = path2.startsWith("http") ? path2 : `${this.baseUrl}${path2}`;
3901
+ const controller = new AbortController();
3902
+ const onExternalAbort = () => controller.abort(opts.signal.reason);
3903
+ if (opts.signal) {
3904
+ if (opts.signal.aborted) controller.abort(opts.signal.reason);
3905
+ else opts.signal.addEventListener("abort", onExternalAbort, { once: true });
3906
+ }
3907
+ const timeoutHandle = setTimeout(
3908
+ () => controller.abort(new Error(`GitHub request timed out after ${this.timeoutMs}ms`)),
3909
+ this.timeoutMs
3910
+ );
3911
+ let response;
3912
+ let timedOut = false;
3913
+ try {
3914
+ response = await this.fetchImpl(url, {
3915
+ method: opts.method ?? "GET",
3916
+ headers: {
3917
+ Accept: "application/vnd.github+json",
3918
+ "X-GitHub-Api-Version": "2022-11-28",
3919
+ ...token ? { Authorization: `Bearer ${token}` } : {},
3920
+ ...opts.body !== void 0 ? { "Content-Type": "application/json" } : {}
3921
+ },
3922
+ cache: "no-store",
3923
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
3924
+ signal: controller.signal
3925
+ });
3926
+ } catch (err) {
3927
+ const isAbort = err instanceof DOMException && err.name === "AbortError";
3928
+ const callerAborted = opts.signal?.aborted ?? false;
3929
+ if (isAbort && !callerAborted) {
3930
+ timedOut = true;
3931
+ throw new TimeoutError(
3932
+ `GitHub request timed out after ${this.timeoutMs}ms. The write may have partially landed \u2014 refresh before retrying.`,
3933
+ this.timeoutMs
3934
+ );
3935
+ }
3936
+ throw err;
3937
+ } finally {
3938
+ clearTimeout(timeoutHandle);
3939
+ if (opts.signal) opts.signal.removeEventListener("abort", onExternalAbort);
3940
+ void timedOut;
3941
+ }
3942
+ if (response.ok) {
3943
+ if (response.status === 204 || response.status === 205) {
3944
+ return { json: {}, response };
3945
+ }
3946
+ const json = await response.json();
3947
+ return { json, response };
3948
+ }
3949
+ const errBody = await safeReadJson(response);
3950
+ throw classifyError(response, errBody, opts.requiredScopes ?? []);
3951
+ }
3952
+ };
3953
+ function normalizeMarketplaceRepo(raw) {
3954
+ return {
3955
+ fullName: raw.full_name,
3956
+ owner: raw.owner.login,
3957
+ name: raw.name,
3958
+ description: raw.description ?? "",
3959
+ topics: raw.topics ?? [],
3960
+ stargazers: raw.stargazers_count ?? 0,
3961
+ defaultBranch: raw.default_branch ?? "main"
3962
+ };
3963
+ }
3964
+ function decodeBase64Utf8(b64) {
3965
+ return new TextDecoder("utf-8").decode(decodeBase64Bytes(b64));
3966
+ }
3967
+ function decodeBase64Bytes(b64) {
3968
+ const binary = atob(b64);
3969
+ const bytes = new Uint8Array(binary.length);
3970
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
3971
+ return bytes;
3972
+ }
3973
+ function normalizeRepo(raw) {
3974
+ const visibility = raw.visibility ?? (raw.private === true ? "private" : "public");
3975
+ const isPrivate = raw.private ?? visibility !== "public";
3976
+ const pushable = raw.permissions?.push === true || raw.permissions?.admin === true;
3977
+ return {
3978
+ fullName: raw.full_name,
3979
+ owner: raw.owner.login,
3980
+ name: raw.name,
3981
+ defaultBranch: raw.default_branch,
3982
+ visibility,
3983
+ isPrivate,
3984
+ pushable
3985
+ };
3986
+ }
3987
+ function parseScopes(headers) {
3988
+ const raw = headers.get("x-oauth-scopes") ?? "";
3989
+ const granted = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3990
+ const acceptedHeader = headers.get("x-accepted-oauth-scopes") ?? "";
3991
+ const acceptedRequired = acceptedHeader.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3992
+ return acceptedRequired.length > 0 ? { granted, acceptedRequired } : { granted };
3993
+ }
3994
+ function classifyError(response, body, callerRequiredScopes) {
3995
+ const message = extractMessage(body) ?? response.statusText;
3996
+ const status = response.status;
3997
+ if (status === 401) {
3998
+ return new UnauthorizedError(message || "Unauthorized \u2014 token rejected", status);
3999
+ }
4000
+ if (status === 403) {
4001
+ const remaining = response.headers.get("x-ratelimit-remaining");
4002
+ const reset = response.headers.get("x-ratelimit-reset");
4003
+ if (remaining === "0" && reset) {
4004
+ const resetAtMs = Number(reset) * 1e3;
4005
+ const deltaMs = Math.max(0, resetAtMs - Date.now());
4006
+ const totalSeconds = Math.ceil(deltaMs / 1e3);
4007
+ const human = totalSeconds < 60 ? `${totalSeconds}s` : totalSeconds < 3600 ? `${Math.ceil(totalSeconds / 60)} min` : `${Math.ceil(totalSeconds / 3600)} h`;
4008
+ return new RateLimitedError(
4009
+ `GitHub rate limit reached. Resets in ${human} (at ${new Date(resetAtMs).toISOString()}).`,
4010
+ status,
4011
+ resetAtMs
4012
+ );
4013
+ }
4014
+ const accepted = (response.headers.get("x-accepted-oauth-scopes") ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4015
+ const granted = (response.headers.get("x-oauth-scopes") ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4016
+ const missing = accepted.length > 0 ? accepted.filter((s) => !granted.includes(s)) : callerRequiredScopes.filter((s) => !granted.includes(s));
4017
+ if (missing.length > 0) {
4018
+ return new MissingScopeError(
4019
+ `GitHub denied this action: missing scopes ${missing.join(", ")}.`,
4020
+ status,
4021
+ missing,
4022
+ granted
4023
+ );
4024
+ }
4025
+ }
4026
+ return new GitHubError(message || "GitHub API call failed", status, body);
4027
+ }
4028
+ function extractMessage(body) {
4029
+ if (typeof body === "object" && body !== null && "message" in body) {
4030
+ const m = body.message;
4031
+ if (typeof m === "string") return m;
4032
+ }
4033
+ return null;
4034
+ }
4035
+ async function safeReadJson(response) {
4036
+ try {
4037
+ return await response.json();
4038
+ } catch {
4039
+ return null;
4040
+ }
4041
+ }
4042
+
4043
+ // src/tools/githubOps.ts
4044
+ function resolveToken(input) {
4045
+ const t = (input ?? process.env.GITHUB_TOKEN ?? "").trim();
4046
+ return t;
4047
+ }
4048
+ var TOKEN_HELP = "Pass `token`, or set the GITHUB_TOKEN env var on the MCP process.";
4049
+ var linkedLinkTool = {
4050
+ name: "linked.link",
4051
+ description: "Link a workspace by fetching its `.apicircle/` workspace from GitHub (registry.json \u2192 workspace-<id>/workspace.json). Caches the release ledger + a collections/environments snapshot. Needs a token for private repos. " + TOKEN_HELP,
4052
+ inputSchema: import_zod14.z.object({
4053
+ repoFullName: import_zod14.z.string().describe("owner/name of the source workspace repo."),
4054
+ branch: import_zod14.z.string().default("main"),
4055
+ pinnedVersion: import_zod14.z.string().nullable().optional().describe("null/omitted = source current version."),
4056
+ kind: import_zod14.z.enum(["private", "public"]).default("private"),
4057
+ token: import_zod14.z.string().optional()
4058
+ }),
4059
+ async handler(input, ctx) {
4060
+ const token = resolveToken(input.token);
4061
+ const repoFullName = input.repoFullName.trim();
4062
+ if (!repoFullName.includes("/")) return { ok: false, error: "repoFullName must be owner/name" };
4063
+ if (input.kind === "private" && !token)
4064
+ return { ok: false, error: `A token is required for private repos. ${TOKEN_HELP}` };
4065
+ const [owner, name] = repoFullName.split("/", 2);
4066
+ const state = await ctx.workspace.read();
4067
+ const dup = Object.values(state.synced.linkedWorkspaces).find(
4068
+ (l) => l.source.repoFullName === repoFullName && l.source.branch === input.branch
4069
+ );
4070
+ if (dup)
4071
+ return { ok: false, error: `Already linked to ${repoFullName}@${input.branch} (${dup.id})` };
4072
+ const client = new GitHubClient();
4073
+ let result;
4074
+ try {
4075
+ result = await (0, import_core5.fetchRemoteWorkspaceJson)(async (p) => {
4076
+ const f = await client.getContents(token, owner, name, p, input.branch);
4077
+ return f?.content ?? null;
4078
+ });
4079
+ } catch (e) {
4080
+ return {
4081
+ ok: false,
4082
+ error: e instanceof GitHubError ? e.message : e instanceof Error ? e.message : "fetch failed"
4083
+ };
4084
+ }
4085
+ if ("error" in result)
4086
+ return { ok: false, error: `${repoFullName}@${input.branch}: ${result.error}` };
4087
+ const probe = (0, import_core5.parseLinkedWorkspaceJson)(result.content);
4088
+ const ledger = (0, import_core5.ledgerFromProbe)(probe);
4089
+ const link = {
4090
+ id: (0, import_shared6.generateId)(),
4091
+ kind: input.kind,
4092
+ name: repoFullName,
4093
+ sourceWorkspaceId: result.workspaceId,
4094
+ source: { provider: "github", repoFullName, branch: input.branch, sessionMode: "workspace" },
4095
+ scope: ["collections", "environments"],
4096
+ pinnedVersion: input.pinnedVersion ?? ledger.currentVersion,
4097
+ updatePolicy: "manual",
4098
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
4099
+ requiredSecretKeyIds: probe.secretKeys ? Object.keys(probe.secretKeys) : []
4100
+ };
4101
+ const snapshot = (0, import_core5.buildLinkedSnapshot)(probe, link) ?? void 0;
4102
+ const out = await ctx.workspace.apply({
4103
+ kind: "linkedWorkspace.upsert",
4104
+ link,
4105
+ ledger,
4106
+ ...snapshot ? { snapshot } : {}
4107
+ });
4108
+ return { ok: true, id: link.id, pinnedVersion: link.pinnedVersion, changedIds: out.changedIds };
4109
+ }
4110
+ };
4111
+ var linkedRefreshTool = {
4112
+ name: "linked.refresh",
4113
+ description: "Re-pull a linked workspace's cached release ledger (+ bootstrap snapshot if missing) from GitHub. " + TOKEN_HELP,
4114
+ inputSchema: import_zod14.z.object({ id: import_zod14.z.string(), token: import_zod14.z.string().optional() }),
4115
+ async handler(input, ctx) {
4116
+ const state = await ctx.workspace.read();
4117
+ const link = state.synced.linkedWorkspaces[input.id];
4118
+ if (!link) return { ok: false, error: `Linked workspace ${input.id} not found` };
4119
+ const token = resolveToken(input.token);
4120
+ if (link.kind === "private" && !token)
4121
+ return { ok: false, error: `A token is required for private links. ${TOKEN_HELP}` };
4122
+ const [owner, name] = link.source.repoFullName.split("/", 2);
4123
+ const client = new GitHubClient();
4124
+ let result;
4125
+ try {
4126
+ result = await (0, import_core5.fetchRemoteWorkspaceJson)(async (p) => {
4127
+ const f = await client.getContents(token, owner, name, p, link.source.branch);
4128
+ return f?.content ?? null;
4129
+ });
4130
+ } catch (e) {
4131
+ return { ok: false, error: e instanceof Error ? e.message : "fetch failed" };
4132
+ }
4133
+ if ("error" in result)
4134
+ return {
4135
+ ok: false,
4136
+ error: `${link.source.repoFullName}@${link.source.branch}: ${result.error}`
4137
+ };
4138
+ const probe = (0, import_core5.parseLinkedWorkspaceJson)(result.content);
4139
+ const ledger = (0, import_core5.ledgerFromProbe)(probe);
4140
+ const needsSnapshot = !state.local.linkedCollections[input.id];
4141
+ const snapshot = needsSnapshot ? (0, import_core5.buildLinkedSnapshot)(probe, link) ?? void 0 : void 0;
4142
+ await ctx.workspace.apply({
4143
+ kind: "linkedWorkspace.upsert",
4144
+ link,
4145
+ ledger,
4146
+ ...snapshot ? { snapshot } : {}
4147
+ });
4148
+ return {
4149
+ ok: true,
4150
+ currentVersion: ledger.currentVersion,
4151
+ versionCount: ledger.versions.length
4152
+ };
4153
+ }
4154
+ };
4155
+ var releaseTagTool = {
4156
+ name: "release.tag",
4157
+ description: "Create a `v<version>` Git tag (optionally a GitHub Release) on the workspace's own repo. The version should already exist in the release ledger. " + TOKEN_HELP,
4158
+ inputSchema: import_zod14.z.object({
4159
+ owner: import_zod14.z.string(),
4160
+ name: import_zod14.z.string(),
4161
+ version: import_zod14.z.string(),
4162
+ createGitHubRelease: import_zod14.z.boolean().default(false),
4163
+ notes: import_zod14.z.string().optional(),
4164
+ overrideExisting: import_zod14.z.boolean().default(false),
4165
+ token: import_zod14.z.string().optional()
4166
+ }),
4167
+ async handler(input, _ctx) {
4168
+ const token = resolveToken(input.token);
4169
+ if (!token) return { ok: false, error: `A token is required to tag. ${TOKEN_HELP}` };
4170
+ const client = new GitHubClient();
4171
+ const tagName = `v${input.version.replace(/^v/, "")}`;
4172
+ try {
4173
+ const repo = await client.getRepo(token, input.owner, input.name);
4174
+ const ref = await client.getRef(token, input.owner, input.name, repo.defaultBranch);
4175
+ const existing = await client.getTagSha(token, input.owner, input.name, tagName);
4176
+ if (existing !== null) {
4177
+ if (!input.overrideExisting) {
4178
+ return {
4179
+ ok: false,
4180
+ error: `Tag ${tagName} already exists at ${existing.slice(0, 7)}. Pass overrideExisting:true to replace.`
4181
+ };
4182
+ }
4183
+ await client.deleteRef(token, input.owner, input.name, `tags/${tagName}`);
4184
+ }
4185
+ await client.createTag(token, input.owner, input.name, { tagName, sha: ref.sha });
4186
+ let releaseUrl;
4187
+ if (input.createGitHubRelease) {
4188
+ const release = await client.createRelease(token, input.owner, input.name, {
4189
+ tagName,
4190
+ releaseName: tagName,
4191
+ body: input.notes ?? ""
4192
+ });
4193
+ releaseUrl = release.htmlUrl;
4194
+ }
4195
+ return {
4196
+ ok: true,
4197
+ tagName,
4198
+ sha: ref.sha,
4199
+ branch: repo.defaultBranch,
4200
+ ...releaseUrl ? { releaseUrl } : {}
4201
+ };
4202
+ } catch (e) {
4203
+ return { ok: false, error: e instanceof Error ? e.message : "tag failed" };
4204
+ }
4205
+ }
4206
+ };
4207
+ var marketplaceSearchTool = {
4208
+ name: "marketplace.search",
4209
+ description: "Search the API Circle marketplace for public workspaces tagged with `apicircle` on GitHub. Returns up to 30 results sorted by relevance (default), stars, or recent updates. Token is optional \u2014 anonymous browsing is supported (lower rate limits); pass a token to lift them. " + TOKEN_HELP,
4210
+ inputSchema: import_zod14.z.object({
4211
+ query: import_zod14.z.string().default("").describe("Search query \u2014 matches repo name, description, and topics. Empty = browse all."),
4212
+ sort: import_zod14.z.enum(["best-match", "stars", "updated"]).default("best-match").describe(
4213
+ "Sort order: best-match (default relevance), stars (most starred first), updated (recently pushed first)."
4214
+ ),
4215
+ token: import_zod14.z.string().optional()
4216
+ }),
4217
+ async handler(input, _ctx) {
4218
+ const token = resolveToken(input.token) || null;
4219
+ const client = new GitHubClient();
4220
+ try {
4221
+ const repos = await client.searchMarketplaceRepos(token, input.query, {
4222
+ sort: input.sort === "best-match" ? void 0 : input.sort
4223
+ });
4224
+ return { ok: true, count: repos.length, results: repos };
4225
+ } catch (e) {
4226
+ return {
4227
+ ok: false,
4228
+ error: e instanceof GitHubError ? e.message : e instanceof Error ? e.message : "search failed"
4229
+ };
4230
+ }
4231
+ }
4232
+ };
4233
+ var TOPIC_RE = /^[a-z0-9][a-z0-9-]*$/;
4234
+ var repoSetTopicsTool = {
4235
+ name: "repo.set_topics",
4236
+ description: "Replace a repo's topics (the `apicircle` topic is always kept \u2014 it drives marketplace discovery). Topics must be lowercase, start with a letter/digit, \u226450 chars, \u226420 total. " + TOKEN_HELP,
4237
+ inputSchema: import_zod14.z.object({
4238
+ owner: import_zod14.z.string(),
4239
+ name: import_zod14.z.string(),
4240
+ topics: import_zod14.z.array(import_zod14.z.string()),
4241
+ token: import_zod14.z.string().optional()
4242
+ }),
4243
+ async handler(input, _ctx) {
4244
+ const token = resolveToken(input.token);
4245
+ if (!token) return { ok: false, error: `A token is required to set topics. ${TOKEN_HELP}` };
4246
+ const requested = input.topics;
4247
+ const normalized = Array.from(
4248
+ /* @__PURE__ */ new Set(["apicircle", ...requested.map((t) => t.trim().toLowerCase()).filter(Boolean)])
4249
+ );
4250
+ for (const t of normalized) {
4251
+ if (!TOPIC_RE.test(t))
4252
+ return {
4253
+ ok: false,
4254
+ error: `Invalid topic "${t}" \u2014 lowercase letters/digits/"-", starting with a letter or digit.`
4255
+ };
4256
+ if (t.length > 50) return { ok: false, error: `Topic "${t}" exceeds 50 characters.` };
4257
+ }
4258
+ if (normalized.length > 20) return { ok: false, error: "GitHub allows at most 20 topics." };
4259
+ const client = new GitHubClient();
4260
+ try {
4261
+ const saved = await client.setRepoTopics(token, input.owner, input.name, normalized);
4262
+ return { ok: true, topics: saved };
4263
+ } catch (e) {
4264
+ return { ok: false, error: e instanceof Error ? e.message : "set topics failed" };
4265
+ }
4266
+ }
4267
+ };
4268
+
4269
+ // src/tools/registry.ts
4270
+ var TOOL_REGISTRY = [
4271
+ importCurlTool,
4272
+ importOpenApiTool,
4273
+ importPostmanTool,
4274
+ importInsomniaTool,
4275
+ importHarTool,
4276
+ generateCodeTool,
4277
+ workspaceListTool,
4278
+ workspaceReadTool,
4279
+ workspaceWriteTool,
4280
+ requestCreateTool,
4281
+ requestReadTool,
4282
+ requestUpdateTool,
4283
+ requestDeleteTool,
4284
+ folderCreateTool,
4285
+ folderReadTool,
4286
+ folderUpdateTool,
4287
+ folderDeleteTool,
4288
+ folderExportJsonTool,
4289
+ folderImportJsonTool,
4290
+ environmentCreateTool,
4291
+ environmentReadTool,
4292
+ environmentUpdateTool,
4293
+ environmentDeleteTool,
4294
+ environmentSetActiveTool,
4295
+ environmentSetPriorityTool,
4296
+ environmentExportTool,
4297
+ environmentImportTool,
4298
+ planCreateTool,
4299
+ planRunTool,
4300
+ planReadTool,
4301
+ planUpdateTool,
4302
+ planDeleteTool,
4303
+ planAddStepTool,
4304
+ planRemoveStepTool,
4305
+ planReorderStepsTool,
4306
+ planSetVariablesTool,
4307
+ assertionCreateTool,
4308
+ assertionReadTool,
4309
+ assertionUpdateTool,
4310
+ assertionDeleteTool,
4311
+ historyListRunsTool,
4312
+ historyGetRunTool,
4313
+ historyDeleteRunTool,
4314
+ historyPurgeTool,
4315
+ codebaseExtractCollectionTool,
4316
+ promptCreateEnvironmentTool,
4317
+ promptCreateAssertionTool,
4318
+ promptCreatePlanTool,
4319
+ promptCreateRequestTool,
4320
+ promptUpdateRequestTool,
4321
+ promptCreateFolderTreeTool,
4322
+ promptAddPlanStepsTool,
4323
+ promptSetPlanVariablesTool,
4324
+ promptCreateMockServerTool,
4325
+ promptAddMockEndpointTool,
4326
+ promptSetEndpointValidationRulesTool,
4327
+ promptSetEndpointResponseRulesTool,
4328
+ promptSetEndpointMultipliersTool,
4329
+ promptSetEndpointRequestSchemaTool,
4330
+ globalAssetsFilesListTool,
4331
+ globalAssetsFilesCreateTool,
4332
+ globalAssetsFilesUpdateTool,
4333
+ globalAssetsFilesDeleteTool,
4334
+ mockCreateFromOpenApiTool,
4335
+ mockCreateFromPostmanTool,
4336
+ mockCreateFromInsomniaTool,
4337
+ mockCreateManualTool,
4338
+ mockListTool,
4339
+ mockListEndpointsTool,
4340
+ mockStartTool,
4341
+ mockStopTool,
4342
+ mockDeleteTool,
4343
+ mockAddEndpointTool,
4344
+ mockUpdateEndpointTool,
4345
+ mockDeleteEndpointTool,
4346
+ mockSetValidationRulesTool,
4347
+ mockSetResponseRulesTool,
4348
+ mockSetMultipliersTool,
4349
+ mockSetRequestSchemaTool,
4350
+ mockSetDefaultPortTool,
4351
+ mockImportPostmanMockCollectionTool,
4352
+ releaseListTool,
4353
+ releasePublishTool,
4354
+ releaseDeprecateTool,
4355
+ releaseYankTool,
4356
+ linkedListTool,
4357
+ linkedGetTool,
4358
+ linkedSetConfigTool,
4359
+ linkedUnlinkTool,
4360
+ linkedLinkTool,
4361
+ linkedRefreshTool,
4362
+ releaseTagTool,
4363
+ repoSetTopicsTool,
4364
+ marketplaceSearchTool
4365
+ ];
4366
+ function getTool(name) {
4367
+ return TOOL_REGISTRY.find((t) => t.name === name);
4368
+ }
4369
+
4370
+ // src/providers/Workspaces.ts
4371
+ var SingleWorkspaceAdapter = class {
4372
+ constructor(provider, workspaceId, displayName = "Workspace") {
4373
+ this.provider = provider;
4374
+ this.workspaceId = workspaceId;
4375
+ this.displayName = displayName;
4376
+ }
4377
+ provider;
4378
+ workspaceId;
4379
+ displayName;
4380
+ async list() {
4381
+ const state = await this.provider.read();
4382
+ const s = state.synced;
4383
+ const id = s.workspaceId ?? this.workspaceId ?? "unknown";
4384
+ this.workspaceId = id;
4385
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4386
+ return [
4387
+ {
4388
+ id,
4389
+ name: this.displayName,
4390
+ isActive: true,
4391
+ createdAt: s.meta?.createdAt ?? now,
4392
+ lastOpenedAt: s.meta?.updatedAt ?? now,
4393
+ counts: s.collections ? {
4394
+ requests: Object.keys(s.collections.requests ?? {}).length,
4395
+ folders: Object.keys(s.collections.folders ?? {}).length,
4396
+ environments: Object.keys(s.environments?.items ?? {}).length,
4397
+ mockServers: Object.keys(s.mockServers ?? {}).length,
4398
+ plans: Object.keys(s.executionPlans ?? {}).length
4399
+ } : null
4400
+ }
4401
+ ];
4402
+ }
4403
+ for(workspaceId) {
4404
+ if (this.workspaceId && workspaceId !== this.workspaceId) {
4405
+ throw new WorkspaceNotFoundError(workspaceId);
4406
+ }
4407
+ return this.provider;
4408
+ }
4409
+ activeId() {
4410
+ return this.workspaceId;
4411
+ }
4412
+ setActive(workspaceId) {
4413
+ if (this.workspaceId && workspaceId !== this.workspaceId) {
4414
+ throw new WorkspaceNotFoundError(workspaceId);
4415
+ }
4416
+ return Promise.resolve();
2780
4417
  }
2781
4418
  };
2782
4419
  var WorkspaceNotFoundError = class extends Error {
@@ -2790,7 +4427,7 @@ var WorkspaceNotFoundError = class extends Error {
2790
4427
  };
2791
4428
 
2792
4429
  // src/providers/InMemoryWorkspaceProvider.ts
2793
- var import_core4 = require("@apicircle/core");
4430
+ var import_core6 = require("@apicircle/core");
2794
4431
  var InMemoryWorkspaceProvider = class {
2795
4432
  state;
2796
4433
  constructor(initial) {
@@ -2800,7 +4437,7 @@ var InMemoryWorkspaceProvider = class {
2800
4437
  return this.state;
2801
4438
  }
2802
4439
  async apply(patch) {
2803
- const out = (0, import_core4.applyMutation)(this.state, patch);
4440
+ const out = (0, import_core6.applyMutation)(this.state, patch);
2804
4441
  this.state = out.next;
2805
4442
  return { state: this.state, changedIds: out.changedIds };
2806
4443
  }
@@ -2814,7 +4451,7 @@ var InMemoryWorkspaceProvider = class {
2814
4451
  };
2815
4452
 
2816
4453
  // src/providers/FileBackedWorkspaceProvider.ts
2817
- var import_core5 = require("@apicircle/core");
4454
+ var import_core7 = require("@apicircle/core");
2818
4455
  var import_file_backed = require("@apicircle/core/workspace/file-backed");
2819
4456
  var FileBackedWorkspaceProvider = class {
2820
4457
  constructor(dir) {
@@ -2831,7 +4468,7 @@ var FileBackedWorkspaceProvider = class {
2831
4468
  async apply(patch) {
2832
4469
  let captured = null;
2833
4470
  await (0, import_file_backed.withWorkspace)(this.dir, async (state) => {
2834
- const result = (0, import_core5.applyMutation)(state, patch);
4471
+ const result = (0, import_core7.applyMutation)(state, patch);
2835
4472
  captured = { state: result.next, changedIds: result.changedIds };
2836
4473
  return { next: result.next };
2837
4474
  });
@@ -2849,6 +4486,52 @@ var FileBackedWorkspaceProvider = class {
2849
4486
  }
2850
4487
  };
2851
4488
 
4489
+ // src/providers/GitBackedWorkspaceProvider.ts
4490
+ var import_core8 = require("@apicircle/core");
4491
+ var import_file_backed2 = require("@apicircle/core/workspace/file-backed");
4492
+ var GIT_SYNCED_FILENAME = "workspace.json";
4493
+ var GitBackedWorkspaceProvider = class {
4494
+ constructor(dir) {
4495
+ this.dir = dir;
4496
+ }
4497
+ dir;
4498
+ async read() {
4499
+ const out = await (0, import_file_backed2.loadFromFile)(this.dir, {
4500
+ syncedFilename: GIT_SYNCED_FILENAME,
4501
+ allowMissing: true
4502
+ });
4503
+ if (!out) {
4504
+ throw new Error(
4505
+ `No workspace found at ${this.dir}. Expected .apicircle/registry.json and .apicircle/workspace-<id>/workspace.json in the repo.`
4506
+ );
4507
+ }
4508
+ return out;
4509
+ }
4510
+ async apply(patch) {
4511
+ let captured = null;
4512
+ await (0, import_file_backed2.withWorkspace)(
4513
+ this.dir,
4514
+ async (state) => {
4515
+ const result = (0, import_core8.applyMutation)(state, patch);
4516
+ captured = { state: result.next, changedIds: result.changedIds };
4517
+ return { next: result.next };
4518
+ },
4519
+ { syncedFilename: GIT_SYNCED_FILENAME }
4520
+ );
4521
+ if (!captured) throw new Error("apply did not run");
4522
+ return captured;
4523
+ }
4524
+ async write(next) {
4525
+ const current = await this.read();
4526
+ const merged = {
4527
+ synced: next.synced ?? current.synced,
4528
+ local: next.local ?? current.local
4529
+ };
4530
+ await (0, import_file_backed2.saveToFile)(this.dir, merged, { syncedFilename: GIT_SYNCED_FILENAME });
4531
+ return merged;
4532
+ }
4533
+ };
4534
+
2852
4535
  // src/providers/MultiWorkspaceProvider.ts
2853
4536
  var import_registry = require("@apicircle/core/workspace/registry");
2854
4537
  var LazyActiveWorkspaceProvider = class {
@@ -2935,13 +4618,14 @@ var MultiWorkspaceProvider = class {
2935
4618
  let counts = null;
2936
4619
  try {
2937
4620
  const state = await (0, import_registry.loadWorkspaceById)(this.registryRoot, entry.id);
2938
- if (state) {
4621
+ if (state?.synced?.collections) {
4622
+ const s = state.synced;
2939
4623
  counts = {
2940
- requests: Object.keys(state.synced.collections.requests).length,
2941
- folders: Object.keys(state.synced.collections.folders).length,
2942
- environments: Object.keys(state.synced.environments.items).length,
2943
- mockServers: Object.keys(state.synced.mockServers ?? {}).length,
2944
- plans: Object.keys(state.synced.executionPlans ?? {}).length
4624
+ requests: Object.keys(s.collections.requests ?? {}).length,
4625
+ folders: Object.keys(s.collections.folders ?? {}).length,
4626
+ environments: Object.keys(s.environments?.items ?? {}).length,
4627
+ mockServers: Object.keys(s.mockServers ?? {}).length,
4628
+ plans: Object.keys(s.executionPlans ?? {}).length
2945
4629
  };
2946
4630
  }
2947
4631
  } catch {
@@ -3021,7 +4705,245 @@ var InProcessMockController = class {
3021
4705
  }
3022
4706
  };
3023
4707
 
4708
+ // src/config/snippets.ts
4709
+ var path = __toESM(require("path"), 1);
4710
+ var AI_CLIENTS = [
4711
+ "claude-desktop",
4712
+ "claude-code",
4713
+ "codex",
4714
+ "cursor",
4715
+ "continue",
4716
+ "cline",
4717
+ "zed",
4718
+ "windsurf",
4719
+ "github-copilot",
4720
+ "chatgpt",
4721
+ "generic"
4722
+ ];
4723
+ function buildSnippetVariants(client, binary, workspace) {
4724
+ const forwardWorkspace = workspace.replace(/\\/g, "/");
4725
+ const render = client === "codex" ? renderTomlSnippet : renderJsonSnippet;
4726
+ const escaped = render(binary, workspace);
4727
+ const forwardSlash = render(binary, forwardWorkspace);
4728
+ return {
4729
+ forwardSlash,
4730
+ escaped,
4731
+ identical: forwardSlash === escaped
4732
+ };
4733
+ }
4734
+ function renderJsonSnippet(binary, workspace) {
4735
+ const entry = {
4736
+ command: binary,
4737
+ args: ["--workspace", workspace],
4738
+ env: { APICIRCLE_WORKSPACE: workspace }
4739
+ };
4740
+ return JSON.stringify({ mcpServers: { apicircle: entry } }, null, 2);
4741
+ }
4742
+ function renderTomlSnippet(binary, workspace) {
4743
+ const esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
4744
+ return [
4745
+ `[mcp_servers.apicircle]`,
4746
+ `command = "${esc(binary)}"`,
4747
+ `args = ["--workspace", "${esc(workspace)}"]`,
4748
+ ``,
4749
+ `[mcp_servers.apicircle.env]`,
4750
+ `APICIRCLE_WORKSPACE = "${esc(workspace)}"`
4751
+ ].join("\n");
4752
+ }
4753
+ function resolveAiClientConfigPath(client, env) {
4754
+ const { homedir, platform, appdata } = env;
4755
+ switch (client) {
4756
+ case "claude-desktop":
4757
+ if (platform === "darwin") {
4758
+ return path.join(homedir, "Library/Application Support/Claude/claude_desktop_config.json");
4759
+ }
4760
+ if (platform === "win32") {
4761
+ return path.join(
4762
+ appdata ?? path.join(homedir, "AppData/Roaming"),
4763
+ "Claude/claude_desktop_config.json"
4764
+ );
4765
+ }
4766
+ return path.join(homedir, ".config/Claude/claude_desktop_config.json");
4767
+ case "claude-code":
4768
+ return path.join(homedir, ".claude/mcp.json");
4769
+ case "cursor":
4770
+ return path.join(homedir, ".cursor/mcp.json");
4771
+ case "continue":
4772
+ return path.join(homedir, ".continue/config.yaml");
4773
+ case "zed":
4774
+ return path.join(homedir, ".config/zed/settings.json");
4775
+ case "windsurf":
4776
+ return path.join(homedir, ".codeium/windsurf/mcp_config.json");
4777
+ case "codex":
4778
+ return path.join(homedir, ".codex/config.toml");
4779
+ default:
4780
+ return null;
4781
+ }
4782
+ }
4783
+
4784
+ // src/prompts/mcpPrompts.ts
4785
+ var MCP_PROMPT_CATEGORIES = [
4786
+ { id: "workspaces", label: "Workspaces" },
4787
+ { id: "collections", label: "Collections" },
4788
+ { id: "environments", label: "Environments" },
4789
+ { id: "execution", label: "Execution" },
4790
+ { id: "mocks", label: "Mocks" },
4791
+ { id: "auth", label: "Auth" },
4792
+ { id: "imports", label: "Imports" }
4793
+ ];
4794
+ var MCP_PROMPTS = [
4795
+ // ── Workspaces (multi-workspace discovery) ───────────────────────
4796
+ {
4797
+ id: "list-workspaces",
4798
+ text: "List every API Circle workspace I have and tell me which is active.",
4799
+ description: "Multi-workspace discovery \u2014 call this first when you are not sure which workspace to drive.",
4800
+ category: "workspaces",
4801
+ tools: ["workspace.list"]
4802
+ },
4803
+ {
4804
+ id: "scope-to-workspace",
4805
+ text: 'Read the requests in the "Petstore" workspace.',
4806
+ description: "Drill into a specific workspace by name; the AI passes `workspaceId` to scope reads.",
4807
+ category: "workspaces",
4808
+ tools: ["workspace.list", "workspace.read"]
4809
+ },
4810
+ {
4811
+ id: "multi-workspace-summary",
4812
+ text: "Across every workspace, count requests, folders, environments, and mocks. Give me one row per workspace.",
4813
+ description: "High-level summary across every registered workspace \u2014 pairs well with the multi-workspace envelope.",
4814
+ category: "workspaces",
4815
+ tools: ["workspace.list"]
4816
+ },
4817
+ // ── Collections (requests + folders in the active workspace) ─────
4818
+ {
4819
+ id: "list-requests",
4820
+ text: "List every request in my API Circle workspace grouped by folder.",
4821
+ description: "Quick overview of the request catalog so you know what is already wired up.",
4822
+ category: "collections",
4823
+ tools: ["workspace.read", "request.read", "folder.read"]
4824
+ },
4825
+ {
4826
+ id: "create-request",
4827
+ text: 'Add a new GET request named "Health check" pointing at https://example.com/healthz with an Accept: application/json header.',
4828
+ description: "Have the AI author a request and persist it to the workspace.",
4829
+ category: "collections",
4830
+ tools: ["request.create"]
4831
+ },
4832
+ {
4833
+ id: "update-request",
4834
+ text: 'Find the "Create user" request and change its method to POST and body to {"name": "Ada"}.',
4835
+ description: "Targeted edit by name \u2014 the AI looks it up, then updates.",
4836
+ category: "collections",
4837
+ tools: ["request.read", "request.update"]
4838
+ },
4839
+ {
4840
+ id: "organize-folders",
4841
+ text: 'Move every request whose URL contains /users into a folder named "User API".',
4842
+ description: "Bulk reorganisation via natural language.",
4843
+ category: "collections",
4844
+ tools: ["workspace.read", "folder.create", "request.update"]
4845
+ },
4846
+ // ── Environments ──────────────────────────────────────────────────
4847
+ {
4848
+ id: "env-list",
4849
+ text: "Show me all environments and which one is active.",
4850
+ description: "Inventory of envs + which is layered onto requests right now.",
4851
+ category: "environments",
4852
+ tools: ["environment.read"]
4853
+ },
4854
+ {
4855
+ id: "env-create",
4856
+ text: 'Create a "staging" environment with BASE_URL=https://staging.example.com and API_KEY={{SECRET:staging-key}}.',
4857
+ description: "Spin up a new env with both a plain variable and a secret reference.",
4858
+ category: "environments",
4859
+ tools: ["environment.create"]
4860
+ },
4861
+ {
4862
+ id: "env-switch",
4863
+ text: 'Switch the active environment to "production" and confirm by previewing the effective URL of the "Get user" request.',
4864
+ description: "Activate an env then verify variable interpolation.",
4865
+ category: "environments",
4866
+ tools: ["environment.update", "request.read"]
4867
+ },
4868
+ // ── Execution ─────────────────────────────────────────────────────
4869
+ {
4870
+ id: "run-request",
4871
+ text: 'Run the "Get user" request with userId=42 and show me the JSON response.',
4872
+ description: "One-shot execution with overridden context vars.",
4873
+ category: "execution",
4874
+ tools: ["request.execute"]
4875
+ },
4876
+ {
4877
+ id: "run-plan",
4878
+ text: 'Execute the "Regression smoke" plan and summarise which assertions failed.',
4879
+ description: "Drive a saved execution plan end-to-end.",
4880
+ category: "execution",
4881
+ tools: ["plan.read", "plan.execute"]
4882
+ },
4883
+ {
4884
+ id: "inspect-history",
4885
+ text: "Show me the last 5 requests I ran and their status codes.",
4886
+ description: "Quick triage when something just broke.",
4887
+ category: "execution",
4888
+ tools: ["history.read"]
4889
+ },
4890
+ // ── Mocks ─────────────────────────────────────────────────────────
4891
+ {
4892
+ id: "mock-start",
4893
+ text: 'Start the "Petstore" mock on port 4010 and tell me its base URL.',
4894
+ description: "Spin up a local mock so requests can hit it.",
4895
+ category: "mocks",
4896
+ tools: ["mock.list", "mock.start"]
4897
+ },
4898
+ {
4899
+ id: "mock-list",
4900
+ text: "List every running mock with its port, served spec, and request count.",
4901
+ description: "Status snapshot of every active mock runtime.",
4902
+ category: "mocks",
4903
+ tools: ["mock.list"]
4904
+ },
4905
+ {
4906
+ id: "mock-stop",
4907
+ text: "Stop every running mock.",
4908
+ description: "Clean shutdown of all mock servers in one go.",
4909
+ category: "mocks",
4910
+ tools: ["mock.stopAll"]
4911
+ },
4912
+ // ── Auth ──────────────────────────────────────────────────────────
4913
+ {
4914
+ id: "auth-set-bearer",
4915
+ text: 'Set the "Get user" request to use bearer auth with token={{ACCESS_TOKEN}}.',
4916
+ description: "Wire bearer auth onto a single request via env-var reference.",
4917
+ category: "auth",
4918
+ tools: ["request.update"]
4919
+ },
4920
+ {
4921
+ id: "auth-oauth2",
4922
+ text: 'Configure the "Create order" request to use OAuth2 client-credentials against https://auth.example.com/token with the "orders.write" scope.',
4923
+ description: "Full OAuth2 client-credentials wiring without leaving the chat.",
4924
+ category: "auth",
4925
+ tools: ["request.update"]
4926
+ },
4927
+ // ── Imports ───────────────────────────────────────────────────────
4928
+ {
4929
+ id: "import-openapi",
4930
+ text: "Import the OpenAPI spec at ./openapi.yaml and create one request per operation.",
4931
+ description: "Bulk import of a spec file from the workspace.",
4932
+ category: "imports",
4933
+ tools: ["import.openapi"]
4934
+ },
4935
+ {
4936
+ id: "import-curl",
4937
+ text: 'I am going to paste a cURL command \u2014 turn it into a saved request named "Webhook test".',
4938
+ description: "cURL \u2192 saved request with a name you control.",
4939
+ category: "imports",
4940
+ tools: ["import.curl"]
4941
+ }
4942
+ ];
4943
+
3024
4944
  // src/index.ts
4945
+ var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
4946
+ var import_stdio2 = require("@modelcontextprotocol/sdk/server/stdio.js");
3025
4947
  function createMcpServer(options) {
3026
4948
  const workspaces = options.workspaces ?? new SingleWorkspaceAdapter(
3027
4949
  options.workspace,
@@ -3040,15 +4962,23 @@ function createMcpServer(options) {
3040
4962
  }
3041
4963
  // Annotate the CommonJS export names for ESM import in node:
3042
4964
  0 && (module.exports = {
4965
+ AI_CLIENTS,
3043
4966
  FileBackedWorkspaceProvider,
4967
+ GitBackedWorkspaceProvider,
3044
4968
  InMemoryWorkspaceProvider,
3045
4969
  InProcessMockController,
4970
+ MCP_PROMPTS,
4971
+ MCP_PROMPT_CATEGORIES,
3046
4972
  McpHost,
3047
4973
  MultiWorkspaceProvider,
3048
4974
  SingleWorkspaceAdapter,
4975
+ StdioServerTransport,
4976
+ StreamableHTTPServerTransport,
3049
4977
  TOOL_REGISTRY,
3050
4978
  WorkspaceNotFoundError,
4979
+ buildSnippetVariants,
3051
4980
  createMcpServer,
3052
- getTool
4981
+ getTool,
4982
+ resolveAiClientConfigPath
3053
4983
  });
3054
4984
  //# sourceMappingURL=index.cjs.map