@apicircle/mcp-server 1.0.9 → 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.
@@ -36,9 +36,10 @@ var init_package = __esm({
36
36
  "package.json"() {
37
37
  package_default = {
38
38
  name: "@apicircle/mcp-server",
39
- version: "1.0.9",
39
+ version: "1.1.0",
40
40
  private: false,
41
41
  type: "module",
42
+ sideEffects: false,
42
43
  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.",
43
44
  keywords: [
44
45
  "apicircle",
@@ -81,7 +82,8 @@ var init_package = __esm({
81
82
  "apicircle-mcp": "./dist/bin/mcp-server.cjs"
82
83
  },
83
84
  exports: {
84
- ".": "./src/index.ts"
85
+ ".": "./src/index.ts",
86
+ "./prompts": "./src/prompts/mcpPrompts.ts"
85
87
  },
86
88
  files: [
87
89
  "dist"
@@ -100,6 +102,16 @@ var init_package = __esm({
100
102
  types: "./dist/index.d.cts",
101
103
  default: "./dist/index.cjs"
102
104
  }
105
+ },
106
+ "./prompts": {
107
+ import: {
108
+ types: "./dist/prompts/mcpPrompts.d.ts",
109
+ default: "./dist/prompts/mcpPrompts.js"
110
+ },
111
+ require: {
112
+ types: "./dist/prompts/mcpPrompts.d.cts",
113
+ default: "./dist/prompts/mcpPrompts.cjs"
114
+ }
103
115
  }
104
116
  }
105
117
  },
@@ -117,6 +129,7 @@ var init_package = __esm({
117
129
  zod: "^3.23.0"
118
130
  },
119
131
  devDependencies: {
132
+ "@apicircle/git": "workspace:*",
120
133
  "@types/node": "^20.0.0",
121
134
  tsup: "^8.3.0",
122
135
  typescript: "^5.4.0",
@@ -619,13 +632,14 @@ var init_workspaceList = __esm({
619
632
  });
620
633
 
621
634
  // src/tools/crud.ts
622
- var import_zod5, import_shared2, import_core2, HTTP_METHOD, requestCreateTool, requestReadTool, requestUpdateTool, requestDeleteTool, folderCreateTool, folderReadTool, folderUpdateTool, folderDeleteTool, VARIABLE, environmentCreateTool, environmentReadTool, environmentUpdateTool, environmentDeleteTool, environmentSetActiveTool, environmentSetPriorityTool, environmentExportTool, environmentImportTool, PLAN_STEP, planCreateTool, planReadTool, planUpdateTool, planDeleteTool, planAddStepTool, planRemoveStepTool, planReorderStepsTool, PLAN_VARIABLE, planSetVariablesTool, planRunTool, ASSERTION, assertionCreateTool, assertionReadTool, assertionUpdateTool, assertionDeleteTool, workspaceReadTool, workspaceWriteTool;
635
+ var import_zod5, import_shared2, import_core2, FULL_REQUEST_AUTH, HTTP_METHOD, requestCreateTool, requestReadTool, requestUpdateTool, requestDeleteTool, folderCreateTool, folderReadTool, folderUpdateTool, folderDeleteTool, VARIABLE, environmentCreateTool, environmentReadTool, environmentUpdateTool, environmentDeleteTool, environmentSetActiveTool, environmentSetPriorityTool, environmentExportTool, environmentImportTool, PLAN_STEP, planCreateTool, planReadTool, planUpdateTool, planDeleteTool, planAddStepTool, planRemoveStepTool, planReorderStepsTool, PLAN_VARIABLE, planSetVariablesTool, planRunTool, ASSERTION, assertionCreateTool, assertionReadTool, assertionUpdateTool, assertionDeleteTool, workspaceReadTool, workspaceWriteTool;
623
636
  var init_crud = __esm({
624
637
  "src/tools/crud.ts"() {
625
638
  "use strict";
626
639
  import_zod5 = require("zod");
627
640
  import_shared2 = require("@apicircle/shared");
628
641
  import_core2 = require("@apicircle/core");
642
+ FULL_REQUEST_AUTH = import_zod5.z.object({ type: import_zod5.z.string() }).passthrough();
629
643
  HTTP_METHOD = import_zod5.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
630
644
  requestCreateTool = {
631
645
  name: "request.create",
@@ -713,16 +727,18 @@ var init_crud = __esm({
713
727
  };
714
728
  folderCreateTool = {
715
729
  name: "folder.create",
716
- description: "Create a folder under an optional parent folder.",
730
+ 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.",
717
731
  inputSchema: import_zod5.z.object({
718
732
  name: import_zod5.z.string().default("New folder"),
719
- parentId: import_zod5.z.string().nullable().optional()
733
+ parentId: import_zod5.z.string().nullable().optional(),
734
+ auth: FULL_REQUEST_AUTH.optional()
720
735
  }),
721
736
  async handler(input, ctx) {
722
737
  const folder = {
723
738
  id: (0, import_shared2.generateId)(),
724
739
  name: input.name,
725
- parentId: input.parentId ?? null
740
+ parentId: input.parentId ?? null,
741
+ ...input.auth ? { auth: input.auth } : {}
726
742
  };
727
743
  const out = await ctx.workspace.apply({ kind: "folder.create", folder });
728
744
  return { id: folder.id, changedIds: out.changedIds };
@@ -746,18 +762,39 @@ var init_crud = __esm({
746
762
  };
747
763
  folderUpdateTool = {
748
764
  name: "folder.update",
749
- description: "Move a folder to a new parent (or to root with parentId: null).",
765
+ 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`).',
750
766
  inputSchema: import_zod5.z.object({
751
767
  id: import_zod5.z.string(),
752
- parentId: import_zod5.z.string().nullable()
768
+ parentId: import_zod5.z.string().nullable().optional(),
769
+ name: import_zod5.z.string().optional(),
770
+ auth: FULL_REQUEST_AUTH.optional(),
771
+ clearAuth: import_zod5.z.boolean().optional()
772
+ }).refine((v) => !(v.auth !== void 0 && v.clearAuth === true), {
773
+ message: "Pass either `auth` or `clearAuth: true`, not both."
753
774
  }),
754
775
  async handler(input, ctx) {
755
- const out = await ctx.workspace.apply({
756
- kind: "folder.move",
757
- id: input.id,
758
- newParentId: input.parentId
759
- });
760
- return { changedIds: out.changedIds };
776
+ const changedIds = [];
777
+ if (input.parentId !== void 0) {
778
+ const out = await ctx.workspace.apply({
779
+ kind: "folder.move",
780
+ id: input.id,
781
+ newParentId: input.parentId
782
+ });
783
+ changedIds.push(...out.changedIds);
784
+ }
785
+ const updatePatch = {};
786
+ if (input.name !== void 0) updatePatch.name = input.name;
787
+ if (input.auth !== void 0) updatePatch.auth = input.auth;
788
+ else if (input.clearAuth === true) updatePatch.auth = void 0;
789
+ if (input.name !== void 0 || input.auth !== void 0 || input.clearAuth === true) {
790
+ const out = await ctx.workspace.apply({
791
+ kind: "folder.update",
792
+ id: input.id,
793
+ patch: updatePatch
794
+ });
795
+ changedIds.push(...out.changedIds);
796
+ }
797
+ return { changedIds: Array.from(new Set(changedIds)) };
761
798
  }
762
799
  };
763
800
  folderDeleteTool = {
@@ -1620,7 +1657,7 @@ function buildEndpoint(input) {
1620
1657
  };
1621
1658
  const validationRules = input.validationRules ?? [];
1622
1659
  const responseRules = input.responseRules ?? [];
1623
- const multipliers = input.multipliers ?? [];
1660
+ const multipliers = (input.multipliers ?? []).slice(0, import_shared3.MAX_RESPONSE_MULTIPLIERS);
1624
1661
  if (multipliers.length > 0) {
1625
1662
  defaultResponse.multipliers = multipliers.map((m) => ({
1626
1663
  id: (0, import_shared3.generateId)(),
@@ -1685,7 +1722,7 @@ function patchEndpoint(mock, endpointId, patcher) {
1685
1722
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1686
1723
  };
1687
1724
  }
1688
- var import_zod9, import_shared3, promptCreateEnvironmentTool, promptCreateAssertionTool, promptCreatePlanTool, HTTP_METHOD2, HEADER_OR_QUERY, REQUEST_BODY, PROMPT_AUTH, PROMPT_ASSERTION, ENDPOINT_RESPONSE, VALIDATION_RULE_NL, CONDITION_CLAUSE_NL, RESPONSE_RULE_NL, MULTIPLIER_NL, ENDPOINT_INPUT, promptCreateRequestTool, promptUpdateRequestTool, FOLDER_TREE_NODE, promptCreateFolderTreeTool, promptAddPlanStepsTool, promptSetPlanVariablesTool, promptCreateMockServerTool, promptAddMockEndpointTool, promptSetEndpointValidationRulesTool, promptSetEndpointResponseRulesTool, promptSetEndpointMultipliersTool;
1725
+ var import_zod9, import_shared3, promptCreateEnvironmentTool, promptCreateAssertionTool, promptCreatePlanTool, HTTP_METHOD2, HEADER_OR_QUERY, REQUEST_BODY, PROMPT_AUTH, PROMPT_ASSERTION, ENDPOINT_RESPONSE, VALIDATION_RULE_NL, CONDITION_CLAUSE_NL, RESPONSE_RULE_NL, MULTIPLIER_NL, ENDPOINT_INPUT, promptCreateRequestTool, promptUpdateRequestTool, FOLDER_TREE_NODE, promptCreateFolderTreeTool, promptAddPlanStepsTool, promptSetPlanVariablesTool, promptCreateMockServerTool, promptAddMockEndpointTool, promptSetEndpointValidationRulesTool, promptSetEndpointResponseRulesTool, promptSetEndpointMultipliersTool, PARAM_NL, promptSetEndpointRequestSchemaTool;
1689
1726
  var init_prompt = __esm({
1690
1727
  "src/tools/prompt.ts"() {
1691
1728
  "use strict";
@@ -1843,7 +1880,9 @@ var init_prompt = __esm({
1843
1880
  RESPONSE_RULE_NL = import_zod9.z.object({
1844
1881
  name: import_zod9.z.string(),
1845
1882
  enabled: import_zod9.z.boolean().default(true),
1846
- when: import_zod9.z.array(CONDITION_CLAUSE_NL).default([]),
1883
+ // At least one clause — a no-condition rule is dead (the runtime engine skips
1884
+ // clause-less rules), so it never fires (mirrors the VS Code parser's reject).
1885
+ when: import_zod9.z.array(CONDITION_CLAUSE_NL).min(1),
1847
1886
  response: import_zod9.z.object({
1848
1887
  status: import_zod9.z.number().int().min(100).max(599).default(200),
1849
1888
  jsonBody: import_zod9.z.string().default("{}")
@@ -2038,10 +2077,13 @@ var init_prompt = __esm({
2038
2077
  };
2039
2078
  promptCreateMockServerTool = {
2040
2079
  name: "prompt.create_mock_server",
2041
- 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.",
2080
+ 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.",
2042
2081
  inputSchema: import_zod9.z.object({
2043
2082
  name: import_zod9.z.string().min(1),
2044
- defaultPort: import_zod9.z.number().int().positive().nullable().optional(),
2083
+ // Mirrors mock.create_manual / mock.start / mock.set_default_port:
2084
+ // reject out-of-range ports at the tool boundary so a prompt that
2085
+ // returns a stray port (1, 80, 999999) never leaks into the synced doc.
2086
+ defaultPort: import_zod9.z.number().int().min(1024).max(65535).nullable().optional(),
2045
2087
  endpoints: import_zod9.z.array(ENDPOINT_INPUT).default([])
2046
2088
  }),
2047
2089
  async handler(input, ctx) {
@@ -2068,7 +2110,7 @@ var init_prompt = __esm({
2068
2110
  };
2069
2111
  promptAddMockEndpointTool = {
2070
2112
  name: "prompt.add_mock_endpoint",
2071
- 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.",
2113
+ 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.",
2072
2114
  inputSchema: import_zod9.z.object({
2073
2115
  mockId: import_zod9.z.string(),
2074
2116
  method: HTTP_METHOD2,
@@ -2171,7 +2213,7 @@ var init_prompt = __esm({
2171
2213
  };
2172
2214
  promptSetEndpointMultipliersTool = {
2173
2215
  name: "prompt.set_endpoint_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 multipliers.",
2216
+ 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.",
2175
2217
  inputSchema: import_zod9.z.object({
2176
2218
  mockId: import_zod9.z.string(),
2177
2219
  endpointId: import_zod9.z.string(),
@@ -2182,6 +2224,9 @@ var init_prompt = __esm({
2182
2224
  const mock = state.synced.mockServers[input.mockId];
2183
2225
  if (!mock) return { ok: false, error: "mock not found" };
2184
2226
  const multipliers = input.multipliers;
2227
+ if (multipliers.length > import_shared3.MAX_RESPONSE_MULTIPLIERS) {
2228
+ return { ok: false, error: "too many multipliers" };
2229
+ }
2185
2230
  const next = patchEndpoint(mock, input.endpointId, (e) => ({
2186
2231
  ...e,
2187
2232
  defaultResponse: {
@@ -2202,6 +2247,53 @@ var init_prompt = __esm({
2202
2247
  return { ok: true, changedIds: out.changedIds };
2203
2248
  }
2204
2249
  };
2250
+ PARAM_NL = import_zod9.z.object({
2251
+ name: import_zod9.z.string(),
2252
+ typeHint: import_zod9.z.string().optional(),
2253
+ required: import_zod9.z.boolean().optional(),
2254
+ description: import_zod9.z.string().optional(),
2255
+ example: import_zod9.z.string().optional()
2256
+ });
2257
+ promptSetEndpointRequestSchemaTool = {
2258
+ name: "prompt.set_endpoint_request_schema",
2259
+ 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).",
2260
+ inputSchema: import_zod9.z.object({
2261
+ mockId: import_zod9.z.string(),
2262
+ endpointId: import_zod9.z.string(),
2263
+ pathParams: import_zod9.z.array(PARAM_NL).default([]),
2264
+ queryParams: import_zod9.z.array(PARAM_NL).default([]),
2265
+ headers: import_zod9.z.array(PARAM_NL).default([]),
2266
+ cookies: import_zod9.z.array(PARAM_NL).default([]),
2267
+ body: import_zod9.z.object({ description: import_zod9.z.string().optional(), example: import_zod9.z.string().optional() }).optional()
2268
+ }),
2269
+ async handler(input, ctx) {
2270
+ const state = await ctx.workspace.read();
2271
+ const mock = state.synced.mockServers[input.mockId];
2272
+ if (!mock) return { ok: false, error: "mock not found" };
2273
+ const toParams = (list) => list.map((p) => ({
2274
+ id: (0, import_shared3.generateId)(),
2275
+ name: p.name,
2276
+ typeHint: p.typeHint,
2277
+ required: p.required,
2278
+ description: p.description,
2279
+ example: p.example
2280
+ }));
2281
+ const body = input.body && (input.body.description || input.body.example) ? input.body : void 0;
2282
+ const next = patchEndpoint(mock, input.endpointId, (e) => ({
2283
+ ...e,
2284
+ requestSchema: {
2285
+ pathParams: toParams(input.pathParams),
2286
+ queryParams: toParams(input.queryParams),
2287
+ headers: toParams(input.headers),
2288
+ cookies: toParams(input.cookies),
2289
+ body
2290
+ }
2291
+ }));
2292
+ if (!next) return { ok: false, error: "endpoint not found" };
2293
+ const out = await ctx.workspace.apply({ kind: "mock.upsert", mock: next });
2294
+ return { ok: true, changedIds: out.changedIds };
2295
+ }
2296
+ };
2205
2297
  }
2206
2298
  });
2207
2299
 
@@ -2401,7 +2493,11 @@ function patchEndpoint2(mock, endpointId, patcher) {
2401
2493
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2402
2494
  };
2403
2495
  }
2404
- var import_zod11, import_shared5, import_mock_server_core2, mockCreateFromOpenApiTool, mockCreateFromPostmanTool, mockCreateFromInsomniaTool, mockImportPostmanMockCollectionTool, mockListTool, mockStartTool, mockStopTool, mockDeleteTool, HTTP_METHOD3, mockCreateManualTool, mockListEndpointsTool, ENDPOINT_RESPONSE2, mockAddEndpointTool, mockUpdateEndpointTool, mockDeleteEndpointTool, VALIDATION_RULE, CONDITION_CLAUSE, RESPONSE_RULE, MULTIPLIER, mockSetValidationRulesTool, mockSetResponseRulesTool, mockSetMultipliersTool;
2496
+ function normalizeSchemaBody(body) {
2497
+ if (!body || !body.description && !body.example) return void 0;
2498
+ return body;
2499
+ }
2500
+ var import_zod11, import_shared5, import_mock_server_core2, mockCreateFromOpenApiTool, mockCreateFromPostmanTool, mockCreateFromInsomniaTool, mockImportPostmanMockCollectionTool, mockListTool, mockStartTool, mockStopTool, mockDeleteTool, mockSetDefaultPortTool, HTTP_METHOD3, mockCreateManualTool, mockListEndpointsTool, ENDPOINT_RESPONSE2, mockAddEndpointTool, mockUpdateEndpointTool, mockDeleteEndpointTool, VALIDATION_RULE, CONDITION_CLAUSE, RESPONSE_RULE, MULTIPLIER, mockSetValidationRulesTool, mockSetResponseRulesTool, PARAM, REQUEST_SCHEMA_BODY, mockSetRequestSchemaTool, mockSetMultipliersTool;
2405
2501
  var init_mocks = __esm({
2406
2502
  "src/tools/mocks.ts"() {
2407
2503
  "use strict";
@@ -2508,10 +2604,14 @@ var init_mocks = __esm({
2508
2604
  };
2509
2605
  mockStartTool = {
2510
2606
  name: "mock.start",
2511
- 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.",
2607
+ 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`.",
2512
2608
  inputSchema: import_zod11.z.object({
2513
2609
  id: import_zod11.z.string(),
2514
- port: import_zod11.z.number().int().positive().optional()
2610
+ // Mirrors the UI 1024-65535 window: <1024 needs OS privileges, and
2611
+ // outside that window the runtime would throw INVALID_PORT anyway —
2612
+ // surface the rejection at the tool boundary so the client sees a
2613
+ // schema error instead of a runtime exception.
2614
+ port: import_zod11.z.number().int().min(1024).max(65535).optional()
2515
2615
  }),
2516
2616
  async handler(input, ctx) {
2517
2617
  const state = await ctx.workspace.read();
@@ -2551,13 +2651,44 @@ var init_mocks = __esm({
2551
2651
  return { ok: true, changedIds: out.changedIds };
2552
2652
  }
2553
2653
  };
2654
+ mockSetDefaultPortTool = {
2655
+ name: "mock.set_default_port",
2656
+ 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.",
2657
+ inputSchema: import_zod11.z.object({
2658
+ id: import_zod11.z.string(),
2659
+ defaultPort: import_zod11.z.number().int().min(1024).max(65535).nullable()
2660
+ }),
2661
+ async handler(input, ctx) {
2662
+ const state = await ctx.workspace.read();
2663
+ const mock = state.synced.mockServers[input.id];
2664
+ if (!mock) return { ok: false, error: "mock not found" };
2665
+ if (mock.defaultPort === input.defaultPort) {
2666
+ return { ok: true, defaultPort: mock.defaultPort, changed: false };
2667
+ }
2668
+ const next = {
2669
+ ...mock,
2670
+ defaultPort: input.defaultPort,
2671
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2672
+ };
2673
+ const out = await ctx.workspace.apply({ kind: "mock.upsert", mock: next });
2674
+ return {
2675
+ ok: true,
2676
+ defaultPort: input.defaultPort,
2677
+ changed: true,
2678
+ changedIds: out.changedIds
2679
+ };
2680
+ }
2681
+ };
2554
2682
  HTTP_METHOD3 = import_zod11.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
2555
2683
  mockCreateManualTool = {
2556
2684
  name: "mock.create_manual",
2557
2685
  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.",
2558
2686
  inputSchema: import_zod11.z.object({
2559
2687
  name: import_zod11.z.string().min(1),
2560
- defaultPort: import_zod11.z.number().int().positive().nullable().optional()
2688
+ // Same 1024-65535 window as mock.start / mock.set_default_port — pin
2689
+ // the validation at the tool boundary so a malformed AI call doesn't
2690
+ // persist a port the runtime will later reject as INVALID_PORT.
2691
+ defaultPort: import_zod11.z.number().int().min(1024).max(65535).nullable().optional()
2561
2692
  }),
2562
2693
  async handler(input, ctx) {
2563
2694
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -2735,7 +2866,10 @@ var init_mocks = __esm({
2735
2866
  id: import_zod11.z.string().optional(),
2736
2867
  name: import_zod11.z.string(),
2737
2868
  enabled: import_zod11.z.boolean().default(true),
2738
- when: import_zod11.z.array(CONDITION_CLAUSE).default([]),
2869
+ // At least one clause — a rule with no `when` is dead (the runtime engine
2870
+ // skips clause-less rules), so it can never fire. The VS Code endpoint parser
2871
+ // rejects the same shape, keeping the surfaces consistent.
2872
+ when: import_zod11.z.array(CONDITION_CLAUSE).min(1),
2739
2873
  response: import_zod11.z.object({
2740
2874
  status: import_zod11.z.number().int().min(100).max(599).default(200),
2741
2875
  jsonBody: import_zod11.z.string().default("{}")
@@ -2817,9 +2951,57 @@ var init_mocks = __esm({
2817
2951
  return { ok: true, changedIds: out.changedIds };
2818
2952
  }
2819
2953
  };
2954
+ PARAM = import_zod11.z.object({
2955
+ id: import_zod11.z.string().optional(),
2956
+ name: import_zod11.z.string(),
2957
+ typeHint: import_zod11.z.string().optional(),
2958
+ required: import_zod11.z.boolean().optional(),
2959
+ description: import_zod11.z.string().optional(),
2960
+ example: import_zod11.z.string().optional()
2961
+ });
2962
+ REQUEST_SCHEMA_BODY = import_zod11.z.object({ description: import_zod11.z.string().optional(), example: import_zod11.z.string().optional() }).optional();
2963
+ mockSetRequestSchemaTool = {
2964
+ name: "mock.set_request_schema",
2965
+ 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.",
2966
+ inputSchema: import_zod11.z.object({
2967
+ mockId: import_zod11.z.string(),
2968
+ endpointId: import_zod11.z.string(),
2969
+ pathParams: import_zod11.z.array(PARAM).default([]),
2970
+ queryParams: import_zod11.z.array(PARAM).default([]),
2971
+ headers: import_zod11.z.array(PARAM).default([]),
2972
+ cookies: import_zod11.z.array(PARAM).default([]),
2973
+ body: REQUEST_SCHEMA_BODY
2974
+ }),
2975
+ async handler(input, ctx) {
2976
+ const state = await ctx.workspace.read();
2977
+ const mock = state.synced.mockServers[input.mockId];
2978
+ if (!mock) return { ok: false, error: "mock not found" };
2979
+ const toParams = (list) => list.map((p) => ({
2980
+ id: p.id ?? (0, import_shared5.generateId)(),
2981
+ name: p.name,
2982
+ typeHint: p.typeHint,
2983
+ required: p.required,
2984
+ description: p.description,
2985
+ example: p.example
2986
+ }));
2987
+ const next = patchEndpoint2(mock, input.endpointId, (e) => ({
2988
+ ...e,
2989
+ requestSchema: {
2990
+ pathParams: toParams(input.pathParams),
2991
+ queryParams: toParams(input.queryParams),
2992
+ headers: toParams(input.headers),
2993
+ cookies: toParams(input.cookies),
2994
+ body: normalizeSchemaBody(input.body)
2995
+ }
2996
+ }));
2997
+ if (!next) return { ok: false, error: "endpoint not found" };
2998
+ const out = await ctx.workspace.apply({ kind: "mock.upsert", mock: next });
2999
+ return { ok: true, changedIds: out.changedIds };
3000
+ }
3001
+ };
2820
3002
  mockSetMultipliersTool = {
2821
3003
  name: "mock.set_multipliers",
2822
- 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.",
3004
+ 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.",
2823
3005
  inputSchema: import_zod11.z.object({
2824
3006
  mockId: import_zod11.z.string(),
2825
3007
  endpointId: import_zod11.z.string(),
@@ -2830,6 +3012,9 @@ var init_mocks = __esm({
2830
3012
  const mock = state.synced.mockServers[input.mockId];
2831
3013
  if (!mock) return { ok: false, error: "mock not found" };
2832
3014
  const multipliers = input.multipliers;
3015
+ if (multipliers.length > import_shared5.MAX_RESPONSE_MULTIPLIERS) {
3016
+ return { ok: false, error: "too many multipliers" };
3017
+ }
2833
3018
  const next = patchEndpoint2(mock, input.endpointId, (e) => ({
2834
3019
  ...e,
2835
3020
  defaultResponse: {
@@ -2853,181 +3038,1525 @@ var init_mocks = __esm({
2853
3038
  }
2854
3039
  });
2855
3040
 
2856
- // src/tools/registry.ts
2857
- function getTool(name) {
2858
- return TOOL_REGISTRY.find((t) => t.name === name);
2859
- }
2860
- var TOOL_REGISTRY;
2861
- var init_registry = __esm({
2862
- "src/tools/registry.ts"() {
2863
- "use strict";
2864
- init_imports();
2865
- init_codegen();
2866
- init_workspaceList();
2867
- init_crud();
2868
- init_folderExchange();
2869
- init_history();
2870
- init_codebase();
2871
- init_prompt();
2872
- init_globalAssets();
2873
- init_mocks();
2874
- TOOL_REGISTRY = [
2875
- importCurlTool,
2876
- importOpenApiTool,
2877
- importPostmanTool,
2878
- importInsomniaTool,
2879
- importHarTool,
2880
- generateCodeTool,
2881
- workspaceListTool,
2882
- workspaceReadTool,
2883
- workspaceWriteTool,
2884
- requestCreateTool,
2885
- requestReadTool,
2886
- requestUpdateTool,
2887
- requestDeleteTool,
2888
- folderCreateTool,
2889
- folderReadTool,
2890
- folderUpdateTool,
2891
- folderDeleteTool,
2892
- folderExportJsonTool,
2893
- folderImportJsonTool,
2894
- environmentCreateTool,
2895
- environmentReadTool,
2896
- environmentUpdateTool,
2897
- environmentDeleteTool,
2898
- environmentSetActiveTool,
2899
- environmentSetPriorityTool,
2900
- environmentExportTool,
2901
- environmentImportTool,
2902
- planCreateTool,
2903
- planRunTool,
2904
- planReadTool,
2905
- planUpdateTool,
2906
- planDeleteTool,
2907
- planAddStepTool,
2908
- planRemoveStepTool,
2909
- planReorderStepsTool,
2910
- planSetVariablesTool,
2911
- assertionCreateTool,
2912
- assertionReadTool,
2913
- assertionUpdateTool,
2914
- assertionDeleteTool,
2915
- historyListRunsTool,
2916
- historyGetRunTool,
2917
- historyDeleteRunTool,
2918
- historyPurgeTool,
2919
- codebaseExtractCollectionTool,
2920
- promptCreateEnvironmentTool,
2921
- promptCreateAssertionTool,
2922
- promptCreatePlanTool,
2923
- promptCreateRequestTool,
2924
- promptUpdateRequestTool,
2925
- promptCreateFolderTreeTool,
2926
- promptAddPlanStepsTool,
2927
- promptSetPlanVariablesTool,
2928
- promptCreateMockServerTool,
2929
- promptAddMockEndpointTool,
2930
- promptSetEndpointValidationRulesTool,
2931
- promptSetEndpointResponseRulesTool,
2932
- promptSetEndpointMultipliersTool,
2933
- globalAssetsFilesListTool,
2934
- globalAssetsFilesCreateTool,
2935
- globalAssetsFilesUpdateTool,
2936
- globalAssetsFilesDeleteTool,
2937
- mockCreateFromOpenApiTool,
2938
- mockCreateFromPostmanTool,
2939
- mockCreateFromInsomniaTool,
2940
- mockCreateManualTool,
2941
- mockListTool,
2942
- mockListEndpointsTool,
2943
- mockStartTool,
2944
- mockStopTool,
2945
- mockDeleteTool,
2946
- mockAddEndpointTool,
2947
- mockUpdateEndpointTool,
2948
- mockDeleteEndpointTool,
2949
- mockSetValidationRulesTool,
2950
- mockSetResponseRulesTool,
2951
- mockSetMultipliersTool,
2952
- mockImportPostmanMockCollectionTool
2953
- ];
2954
- }
2955
- });
2956
-
2957
- // src/providers/Workspaces.ts
2958
- var Workspaces_exports = {};
2959
- __export(Workspaces_exports, {
2960
- SingleWorkspaceAdapter: () => SingleWorkspaceAdapter,
2961
- WorkspaceNotFoundError: () => WorkspaceNotFoundError
2962
- });
2963
- var SingleWorkspaceAdapter, WorkspaceNotFoundError;
2964
- var init_Workspaces = __esm({
2965
- "src/providers/Workspaces.ts"() {
3041
+ // src/tools/releases.ts
3042
+ var import_zod12, import_core4, releaseListTool, releasePublishTool, releaseDeprecateTool, releaseYankTool;
3043
+ var init_releases = __esm({
3044
+ "src/tools/releases.ts"() {
2966
3045
  "use strict";
2967
- SingleWorkspaceAdapter = class {
2968
- constructor(provider, workspaceId, displayName = "Workspace") {
2969
- this.provider = provider;
2970
- this.workspaceId = workspaceId;
2971
- this.displayName = displayName;
2972
- }
2973
- provider;
2974
- workspaceId;
2975
- displayName;
2976
- async list() {
2977
- const state = await this.provider.read();
2978
- const id = state.synced.workspaceId;
2979
- this.workspaceId = id;
2980
- return [
2981
- {
2982
- id,
2983
- name: this.displayName,
2984
- isActive: true,
2985
- createdAt: state.synced.meta.createdAt,
2986
- lastOpenedAt: state.synced.meta.updatedAt,
2987
- counts: {
2988
- requests: Object.keys(state.synced.collections.requests).length,
2989
- folders: Object.keys(state.synced.collections.folders).length,
2990
- environments: Object.keys(state.synced.environments.items).length,
2991
- mockServers: Object.keys(state.synced.mockServers ?? {}).length,
2992
- plans: Object.keys(state.synced.executionPlans ?? {}).length
2993
- }
2994
- }
2995
- ];
2996
- }
2997
- for(workspaceId) {
2998
- if (this.workspaceId && workspaceId !== this.workspaceId) {
2999
- throw new WorkspaceNotFoundError(workspaceId);
3046
+ import_zod12 = require("zod");
3047
+ import_core4 = require("@apicircle/core");
3048
+ releaseListTool = {
3049
+ name: "release.list",
3050
+ description: "List this workspace's published releases (newest first) with their notes, snapshot fingerprint, and deprecated / withdrawn flags. Returns the current version too.",
3051
+ inputSchema: import_zod12.z.object({}),
3052
+ async handler(_input, ctx) {
3053
+ const state = await ctx.workspace.read();
3054
+ const ledger = state.synced.releases.self;
3055
+ if (!ledger) {
3056
+ return { currentVersion: null, count: 0, versions: [] };
3000
3057
  }
3001
- return this.provider;
3058
+ const order = (0, import_core4.sortVersionsDesc)(ledger.versions.map((v) => v.version));
3059
+ const byVersion = new Map(ledger.versions.map((v) => [v.version, v]));
3060
+ const versions = order.map((v) => byVersion.get(v)).filter((v) => v !== void 0).map((v) => ({
3061
+ version: v.version,
3062
+ publishedAt: v.publishedAt,
3063
+ notes: v.notes,
3064
+ workspaceSnapshot: v.workspaceSnapshot,
3065
+ deprecated: v.deprecated,
3066
+ yanked: v.yanked,
3067
+ ...v.sha ? { sha: v.sha } : {},
3068
+ ...v.tagName ? { tagName: v.tagName } : {}
3069
+ }));
3070
+ return { currentVersion: ledger.currentVersion, count: versions.length, versions };
3002
3071
  }
3003
- activeId() {
3004
- return this.workspaceId;
3072
+ };
3073
+ releasePublishTool = {
3074
+ name: "release.publish",
3075
+ 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.",
3076
+ inputSchema: import_zod12.z.object({
3077
+ version: import_zod12.z.string().min(1).describe('Semantic version, e.g. "1.2.0".'),
3078
+ notes: import_zod12.z.string().default("").describe("Markdown release notes."),
3079
+ sha: import_zod12.z.string().optional().describe("Optional source commit SHA for bookkeeping."),
3080
+ tagName: import_zod12.z.string().optional().describe("Optional git tag name for bookkeeping.")
3081
+ }),
3082
+ async handler(input, ctx) {
3083
+ const state = await ctx.workspace.read();
3084
+ let entry;
3085
+ try {
3086
+ entry = await (0, import_core4.buildReleaseEntry)(state.synced, {
3087
+ version: input.version,
3088
+ notes: input.notes,
3089
+ sha: input.sha,
3090
+ tagName: input.tagName
3091
+ });
3092
+ } catch (err) {
3093
+ return { ok: false, error: err instanceof Error ? err.message : "release.publish failed" };
3094
+ }
3095
+ try {
3096
+ const out = await ctx.workspace.apply({ kind: "release.publish", entry });
3097
+ const after = (await ctx.workspace.read()).synced.releases.self;
3098
+ return {
3099
+ ok: true,
3100
+ version: entry.version,
3101
+ currentVersion: after?.currentVersion ?? entry.version,
3102
+ workspaceSnapshot: entry.workspaceSnapshot,
3103
+ changedIds: out.changedIds
3104
+ };
3105
+ } catch (err) {
3106
+ return { ok: false, error: err instanceof Error ? err.message : "release.publish failed" };
3107
+ }
3005
3108
  }
3006
- setActive(workspaceId) {
3007
- if (this.workspaceId && workspaceId !== this.workspaceId) {
3008
- throw new WorkspaceNotFoundError(workspaceId);
3109
+ };
3110
+ releaseDeprecateTool = {
3111
+ name: "release.deprecate",
3112
+ 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.",
3113
+ inputSchema: import_zod12.z.object({ version: import_zod12.z.string().min(1) }),
3114
+ async handler(input, ctx) {
3115
+ try {
3116
+ const out = await ctx.workspace.apply({ kind: "release.deprecate", version: input.version });
3117
+ return { ok: true, version: input.version, changedIds: out.changedIds };
3118
+ } catch (err) {
3119
+ return { ok: false, error: err instanceof Error ? err.message : "release.deprecate failed" };
3009
3120
  }
3010
- return Promise.resolve();
3011
3121
  }
3012
3122
  };
3013
- WorkspaceNotFoundError = class extends Error {
3014
- code = "workspace-not-found";
3015
- workspaceId;
3016
- constructor(workspaceId) {
3017
- super(`No workspace with id "${workspaceId}" is available on this server.`);
3018
- this.name = "WorkspaceNotFoundError";
3019
- this.workspaceId = workspaceId;
3123
+ releaseYankTool = {
3124
+ name: "release.yank",
3125
+ 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.",
3126
+ inputSchema: import_zod12.z.object({ version: import_zod12.z.string().min(1) }),
3127
+ async handler(input, ctx) {
3128
+ try {
3129
+ const out = await ctx.workspace.apply({ kind: "release.yank", version: input.version });
3130
+ return { ok: true, version: input.version, changedIds: out.changedIds };
3131
+ } catch (err) {
3132
+ return { ok: false, error: err instanceof Error ? err.message : "release.yank failed" };
3133
+ }
3020
3134
  }
3021
3135
  };
3022
3136
  }
3023
3137
  });
3024
3138
 
3025
- // src/providers/InMemoryWorkspaceProvider.ts
3026
- var import_core4, InMemoryWorkspaceProvider;
3027
- var init_InMemoryWorkspaceProvider = __esm({
3028
- "src/providers/InMemoryWorkspaceProvider.ts"() {
3029
- "use strict";
3030
- import_core4 = require("@apicircle/core");
3139
+ // src/tools/linkedWorkspaces.ts
3140
+ function summarize(link, currentVersion) {
3141
+ return {
3142
+ id: link.id,
3143
+ name: link.name,
3144
+ kind: link.kind,
3145
+ description: link.description,
3146
+ source: link.source,
3147
+ scope: link.scope,
3148
+ pinnedVersion: link.pinnedVersion,
3149
+ requiredSecretKeyIds: link.requiredSecretKeyIds,
3150
+ marketplace: link.marketplace,
3151
+ cachedCurrentVersion: currentVersion
3152
+ };
3153
+ }
3154
+ var import_zod13, linkedListTool, linkedGetTool, linkedSetConfigTool, linkedUnlinkTool;
3155
+ var init_linkedWorkspaces = __esm({
3156
+ "src/tools/linkedWorkspaces.ts"() {
3157
+ "use strict";
3158
+ import_zod13 = require("zod");
3159
+ linkedListTool = {
3160
+ name: "linked.list",
3161
+ 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.",
3162
+ inputSchema: import_zod13.z.object({}),
3163
+ async handler(_input, ctx) {
3164
+ const state = await ctx.workspace.read();
3165
+ const links = Object.values(state.synced.linkedWorkspaces);
3166
+ return {
3167
+ count: links.length,
3168
+ links: links.map(
3169
+ (l) => summarize(l, state.synced.releases.perLink[l.id]?.currentVersion ?? null)
3170
+ )
3171
+ };
3172
+ }
3173
+ };
3174
+ linkedGetTool = {
3175
+ name: "linked.get",
3176
+ description: "Read one linked workspace by id, including its cached release ledger (the versions available to pin to).",
3177
+ inputSchema: import_zod13.z.object({ id: import_zod13.z.string() }),
3178
+ async handler(input, ctx) {
3179
+ const state = await ctx.workspace.read();
3180
+ const link = state.synced.linkedWorkspaces[input.id];
3181
+ if (!link) return { ok: false, error: `Linked workspace ${input.id} not found` };
3182
+ const ledger = state.synced.releases.perLink[input.id] ?? null;
3183
+ return {
3184
+ ok: true,
3185
+ link: summarize(link, ledger?.currentVersion ?? null),
3186
+ ledger
3187
+ };
3188
+ }
3189
+ };
3190
+ linkedSetConfigTool = {
3191
+ name: "linked.set_config",
3192
+ 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.",
3193
+ inputSchema: import_zod13.z.object({
3194
+ id: import_zod13.z.string(),
3195
+ name: import_zod13.z.string().optional(),
3196
+ description: import_zod13.z.string().optional(),
3197
+ pinnedVersion: import_zod13.z.string().nullable().optional().describe("null = unpin (track source HEAD)."),
3198
+ scope: import_zod13.z.array(import_zod13.z.enum(["collections", "environments"])).optional(),
3199
+ sessionMode: import_zod13.z.enum(["workspace", "dedicated"]).optional(),
3200
+ requiredSecretKeyIds: import_zod13.z.array(import_zod13.z.string()).optional(),
3201
+ marketplace: import_zod13.z.object({
3202
+ listedAs: import_zod13.z.string(),
3203
+ tags: import_zod13.z.array(import_zod13.z.string()),
3204
+ summary: import_zod13.z.string()
3205
+ }).nullable().optional().describe("null = clear marketplace metadata.")
3206
+ }),
3207
+ async handler(input, ctx) {
3208
+ const state = await ctx.workspace.read();
3209
+ const link = state.synced.linkedWorkspaces[input.id];
3210
+ if (!link) return { ok: false, error: `Linked workspace ${input.id} not found` };
3211
+ if (input.pinnedVersion !== void 0 && input.pinnedVersion !== null) {
3212
+ const cached = state.synced.releases.perLink[input.id]?.versions ?? [];
3213
+ if (!cached.some((v) => v.version === input.pinnedVersion)) {
3214
+ return {
3215
+ ok: false,
3216
+ error: `Version ${input.pinnedVersion} is not in the cached ledger \u2014 refresh the link first`
3217
+ };
3218
+ }
3219
+ }
3220
+ const next = {
3221
+ ...link,
3222
+ ...input.name !== void 0 ? { name: input.name } : {},
3223
+ ...input.description !== void 0 ? { description: input.description } : {},
3224
+ ...input.pinnedVersion !== void 0 ? { pinnedVersion: input.pinnedVersion } : {},
3225
+ ...input.scope !== void 0 ? { scope: input.scope } : {},
3226
+ ...input.requiredSecretKeyIds !== void 0 ? { requiredSecretKeyIds: input.requiredSecretKeyIds } : {},
3227
+ ...input.sessionMode !== void 0 ? { source: { ...link.source, sessionMode: input.sessionMode } } : {}
3228
+ };
3229
+ if (input.marketplace !== void 0) {
3230
+ if (input.marketplace === null) {
3231
+ delete next.marketplace;
3232
+ } else {
3233
+ next.marketplace = input.marketplace;
3234
+ }
3235
+ }
3236
+ const out = await ctx.workspace.apply({ kind: "linkedWorkspace.upsert", link: next });
3237
+ return {
3238
+ ok: true,
3239
+ changedIds: out.changedIds,
3240
+ link: summarize(next, state.synced.releases.perLink[input.id]?.currentVersion ?? null)
3241
+ };
3242
+ }
3243
+ };
3244
+ linkedUnlinkTool = {
3245
+ name: "linked.unlink",
3246
+ 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.",
3247
+ inputSchema: import_zod13.z.object({ id: import_zod13.z.string() }),
3248
+ async handler(input, ctx) {
3249
+ const state = await ctx.workspace.read();
3250
+ if (!state.synced.linkedWorkspaces[input.id]) {
3251
+ return { ok: false, error: `Linked workspace ${input.id} not found` };
3252
+ }
3253
+ const out = await ctx.workspace.apply({ kind: "linkedWorkspace.remove", id: input.id });
3254
+ return { ok: true, changedIds: out.changedIds };
3255
+ }
3256
+ };
3257
+ }
3258
+ });
3259
+
3260
+ // ../git/src/github/errors.ts
3261
+ var GitHubError, MissingScopeError, RateLimitedError, UnauthorizedError, TimeoutError;
3262
+ var init_errors = __esm({
3263
+ "../git/src/github/errors.ts"() {
3264
+ "use strict";
3265
+ GitHubError = class extends Error {
3266
+ constructor(message, status, body) {
3267
+ super(message);
3268
+ this.status = status;
3269
+ this.body = body;
3270
+ this.name = "GitHubError";
3271
+ }
3272
+ status;
3273
+ body;
3274
+ };
3275
+ MissingScopeError = class extends GitHubError {
3276
+ /** Scope strings the API said are missing, e.g. ['pull_request']. */
3277
+ missingScopes;
3278
+ /** Scope strings the token currently grants, parsed from x-oauth-scopes. */
3279
+ grantedScopes;
3280
+ constructor(message, status, missingScopes, grantedScopes) {
3281
+ super(message, status);
3282
+ this.name = "MissingScopeError";
3283
+ this.missingScopes = missingScopes;
3284
+ this.grantedScopes = grantedScopes;
3285
+ }
3286
+ };
3287
+ RateLimitedError = class extends GitHubError {
3288
+ /** Unix timestamp (ms) when the rate-limit window resets. */
3289
+ resetAtMs;
3290
+ constructor(message, status, resetAtMs) {
3291
+ super(message, status);
3292
+ this.name = "RateLimitedError";
3293
+ this.resetAtMs = resetAtMs;
3294
+ }
3295
+ };
3296
+ UnauthorizedError = class extends GitHubError {
3297
+ constructor(message, status) {
3298
+ super(message, status);
3299
+ this.name = "UnauthorizedError";
3300
+ }
3301
+ };
3302
+ TimeoutError = class extends GitHubError {
3303
+ /** Timeout that fired, in ms. Useful for the UI message. */
3304
+ timeoutMs;
3305
+ constructor(message, timeoutMs) {
3306
+ super(message, 0);
3307
+ this.name = "TimeoutError";
3308
+ this.timeoutMs = timeoutMs;
3309
+ }
3310
+ };
3311
+ }
3312
+ });
3313
+
3314
+ // ../git/src/github/api.ts
3315
+ function normalizeMarketplaceRepo(raw) {
3316
+ return {
3317
+ fullName: raw.full_name,
3318
+ owner: raw.owner.login,
3319
+ name: raw.name,
3320
+ description: raw.description ?? "",
3321
+ topics: raw.topics ?? [],
3322
+ stargazers: raw.stargazers_count ?? 0,
3323
+ defaultBranch: raw.default_branch ?? "main"
3324
+ };
3325
+ }
3326
+ function decodeBase64Utf8(b64) {
3327
+ return new TextDecoder("utf-8").decode(decodeBase64Bytes(b64));
3328
+ }
3329
+ function decodeBase64Bytes(b64) {
3330
+ const binary = atob(b64);
3331
+ const bytes = new Uint8Array(binary.length);
3332
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
3333
+ return bytes;
3334
+ }
3335
+ function normalizeRepo(raw) {
3336
+ const visibility = raw.visibility ?? (raw.private === true ? "private" : "public");
3337
+ const isPrivate = raw.private ?? visibility !== "public";
3338
+ const pushable = raw.permissions?.push === true || raw.permissions?.admin === true;
3339
+ return {
3340
+ fullName: raw.full_name,
3341
+ owner: raw.owner.login,
3342
+ name: raw.name,
3343
+ defaultBranch: raw.default_branch,
3344
+ visibility,
3345
+ isPrivate,
3346
+ pushable
3347
+ };
3348
+ }
3349
+ function parseScopes(headers) {
3350
+ const raw = headers.get("x-oauth-scopes") ?? "";
3351
+ const granted = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3352
+ const acceptedHeader = headers.get("x-accepted-oauth-scopes") ?? "";
3353
+ const acceptedRequired = acceptedHeader.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3354
+ return acceptedRequired.length > 0 ? { granted, acceptedRequired } : { granted };
3355
+ }
3356
+ function classifyError(response, body, callerRequiredScopes) {
3357
+ const message = extractMessage(body) ?? response.statusText;
3358
+ const status = response.status;
3359
+ if (status === 401) {
3360
+ return new UnauthorizedError(message || "Unauthorized \u2014 token rejected", status);
3361
+ }
3362
+ if (status === 403) {
3363
+ const remaining = response.headers.get("x-ratelimit-remaining");
3364
+ const reset = response.headers.get("x-ratelimit-reset");
3365
+ if (remaining === "0" && reset) {
3366
+ const resetAtMs = Number(reset) * 1e3;
3367
+ const deltaMs = Math.max(0, resetAtMs - Date.now());
3368
+ const totalSeconds = Math.ceil(deltaMs / 1e3);
3369
+ const human = totalSeconds < 60 ? `${totalSeconds}s` : totalSeconds < 3600 ? `${Math.ceil(totalSeconds / 60)} min` : `${Math.ceil(totalSeconds / 3600)} h`;
3370
+ return new RateLimitedError(
3371
+ `GitHub rate limit reached. Resets in ${human} (at ${new Date(resetAtMs).toISOString()}).`,
3372
+ status,
3373
+ resetAtMs
3374
+ );
3375
+ }
3376
+ const accepted = (response.headers.get("x-accepted-oauth-scopes") ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3377
+ const granted = (response.headers.get("x-oauth-scopes") ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
3378
+ const missing = accepted.length > 0 ? accepted.filter((s) => !granted.includes(s)) : callerRequiredScopes.filter((s) => !granted.includes(s));
3379
+ if (missing.length > 0) {
3380
+ return new MissingScopeError(
3381
+ `GitHub denied this action: missing scopes ${missing.join(", ")}.`,
3382
+ status,
3383
+ missing,
3384
+ granted
3385
+ );
3386
+ }
3387
+ }
3388
+ return new GitHubError(message || "GitHub API call failed", status, body);
3389
+ }
3390
+ function extractMessage(body) {
3391
+ if (typeof body === "object" && body !== null && "message" in body) {
3392
+ const m = body.message;
3393
+ if (typeof m === "string") return m;
3394
+ }
3395
+ return null;
3396
+ }
3397
+ async function safeReadJson(response) {
3398
+ try {
3399
+ return await response.json();
3400
+ } catch {
3401
+ return null;
3402
+ }
3403
+ }
3404
+ var API_BASE, LOGIN_BASE, DEFAULT_TIMEOUT_MS, GitHubClient;
3405
+ var init_api = __esm({
3406
+ "../git/src/github/api.ts"() {
3407
+ "use strict";
3408
+ init_errors();
3409
+ API_BASE = "https://api.github.com";
3410
+ LOGIN_BASE = "https://github.com";
3411
+ DEFAULT_TIMEOUT_MS = 15e3;
3412
+ GitHubClient = class {
3413
+ baseUrl;
3414
+ loginBaseUrl;
3415
+ fetchImpl;
3416
+ timeoutMs;
3417
+ constructor(opts = {}) {
3418
+ this.baseUrl = opts.baseUrl ?? API_BASE;
3419
+ this.loginBaseUrl = (opts.loginBaseUrl ?? LOGIN_BASE).replace(/\/$/, "");
3420
+ this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
3421
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
3422
+ }
3423
+ /**
3424
+ * Fetch the authenticated user. Doubles as a "verify token" probe — used
3425
+ * by the Secret Vault Sessions tab to refresh the granted-scopes list.
3426
+ */
3427
+ async getViewer(token, opts = {}) {
3428
+ const { json, response } = await this.call(token, "/user", opts);
3429
+ return {
3430
+ viewer: {
3431
+ login: json.login,
3432
+ id: json.id,
3433
+ name: json.name ?? null,
3434
+ avatarUrl: json.avatar_url ?? null
3435
+ },
3436
+ scopes: parseScopes(response.headers)
3437
+ };
3438
+ }
3439
+ /**
3440
+ * List repositories the authenticated user can access. Used by the repo
3441
+ * picker. Capped at 100 sorted by recent push; users with thousands of
3442
+ * repos can paginate later.
3443
+ */
3444
+ async listAccessibleRepos(token, opts = {}) {
3445
+ const { json } = await this.call(
3446
+ token,
3447
+ "/user/repos?per_page=100&sort=pushed&affiliation=owner,collaborator,organization_member",
3448
+ opts
3449
+ );
3450
+ return json.map(normalizeRepo);
3451
+ }
3452
+ /**
3453
+ * Fetch a specific repo. Validates the user-supplied owner/name pair
3454
+ * exists + is accessible, and exposes the default branch.
3455
+ */
3456
+ async getRepo(token, owner, name, opts = {}) {
3457
+ const { json } = await this.call(
3458
+ token,
3459
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
3460
+ opts
3461
+ );
3462
+ return normalizeRepo(json);
3463
+ }
3464
+ /**
3465
+ * Read the head SHA of a branch. Used to seed a new working branch from
3466
+ * main before any edits land.
3467
+ */
3468
+ async getBranchHead(token, owner, name, branch, opts = {}) {
3469
+ const { json } = await this.call(
3470
+ token,
3471
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/branches/${encodeURIComponent(branch)}`,
3472
+ opts
3473
+ );
3474
+ return { name: json.name, commitSha: json.commit.sha };
3475
+ }
3476
+ /**
3477
+ * List branches on a repo. Used by the Link Workspace repo-browser to
3478
+ * populate the branch dropdown after the user picks a repo. Capped at
3479
+ * 100 (GitHub's max page size); repos with more branches paginate.
3480
+ */
3481
+ async listBranches(token, owner, name, opts = {}) {
3482
+ const { json } = await this.call(
3483
+ token,
3484
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/branches?per_page=100`,
3485
+ opts
3486
+ );
3487
+ return json.map((b) => ({ name: b.name, commitSha: b.commit.sha }));
3488
+ }
3489
+ /**
3490
+ * Create a new branch ref pointing at `sha`. The auto-branch flow calls
3491
+ * this with the head SHA from `getBranchHead(main)`.
3492
+ *
3493
+ * GitHub returns 422 with "Reference already exists" when the branch
3494
+ * already exists; that surfaces as a GitHubError(422) so the UI can
3495
+ * prompt for a different name.
3496
+ */
3497
+ async createBranch(token, owner, name, branchName, sha, opts = {}) {
3498
+ const { json } = await this.call(
3499
+ token,
3500
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs`,
3501
+ {
3502
+ ...opts,
3503
+ method: "POST",
3504
+ body: { ref: `refs/heads/${branchName}`, sha },
3505
+ requiredScopes: ["repo"]
3506
+ }
3507
+ );
3508
+ return { name: branchName, commitSha: json.object.sha };
3509
+ }
3510
+ /**
3511
+ * Read a branch ref's current commit SHA. Used at the start of push-to-
3512
+ * save to find the parent commit before building the new tree.
3513
+ */
3514
+ async getRef(token, owner, name, branch, opts = {}) {
3515
+ const { json } = await this.call(
3516
+ token,
3517
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs/heads/${encodeURIComponent(branch)}`,
3518
+ opts
3519
+ );
3520
+ return { ref: json.ref, sha: json.object.sha };
3521
+ }
3522
+ /**
3523
+ * Read a commit's tree SHA. Used so the new tree can be built `base_tree`
3524
+ * — every path we don't override is inherited from the parent.
3525
+ */
3526
+ async getCommit(token, owner, name, sha, opts = {}) {
3527
+ const { json } = await this.call(
3528
+ token,
3529
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/commits/${encodeURIComponent(sha)}`,
3530
+ opts
3531
+ );
3532
+ return {
3533
+ sha: json.sha,
3534
+ treeSha: json.tree.sha,
3535
+ message: json.message
3536
+ };
3537
+ }
3538
+ /**
3539
+ * Upload a blob to the repo and return its SHA. Used by push-to-save
3540
+ * (P4.3b) for binary attachments — text files go straight into a tree
3541
+ * entry's `content`, but binary bytes have to go through a blob first.
3542
+ *
3543
+ * `content` is base64 when `encoding === 'base64'`. GitHub stores blobs
3544
+ * deduplicated by their git-sha1 (not our sha256), so re-uploading the
3545
+ * same bytes is cheap on their side; we save a roundtrip locally by
3546
+ * tracking lastPushedBlobSha per slot in a future revision.
3547
+ */
3548
+ async createBlob(token, owner, name, args, opts = {}) {
3549
+ const { json } = await this.call(
3550
+ token,
3551
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/blobs`,
3552
+ {
3553
+ ...opts,
3554
+ method: "POST",
3555
+ body: { content: args.content, encoding: args.encoding },
3556
+ requiredScopes: ["repo"]
3557
+ }
3558
+ );
3559
+ return { sha: json.sha, size: json.size ?? 0 };
3560
+ }
3561
+ /**
3562
+ * Build a new tree from `entries`, layered over `baseTreeSha`. Entries
3563
+ * with `content` are inlined (text path); entries with a pre-uploaded
3564
+ * `sha` reference an existing blob (binary path — used by attachments).
3565
+ */
3566
+ async createTree(token, owner, name, args, opts = {}) {
3567
+ const tree = args.entries.map((e) => ({
3568
+ path: e.path,
3569
+ mode: e.mode ?? "100644",
3570
+ type: e.type ?? "blob",
3571
+ ...e.content !== void 0 ? { content: e.content } : {},
3572
+ ...e.sha !== void 0 ? { sha: e.sha } : {}
3573
+ }));
3574
+ const { json } = await this.call(
3575
+ token,
3576
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/trees`,
3577
+ {
3578
+ ...opts,
3579
+ method: "POST",
3580
+ body: { base_tree: args.baseTreeSha, tree },
3581
+ requiredScopes: ["repo"]
3582
+ }
3583
+ );
3584
+ return { sha: json.sha };
3585
+ }
3586
+ /**
3587
+ * Create a new commit object pointing at the given tree, with the given
3588
+ * parents. Returns the new commit's SHA + the tree it points at.
3589
+ */
3590
+ async createCommit(token, owner, name, args, opts = {}) {
3591
+ const { json } = await this.call(
3592
+ token,
3593
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/commits`,
3594
+ {
3595
+ ...opts,
3596
+ method: "POST",
3597
+ body: {
3598
+ message: args.message,
3599
+ tree: args.treeSha,
3600
+ parents: args.parents
3601
+ },
3602
+ requiredScopes: ["repo"]
3603
+ }
3604
+ );
3605
+ return { sha: json.sha, treeSha: json.tree.sha };
3606
+ }
3607
+ /**
3608
+ * Fast-forward a branch ref to a new commit SHA. Pass `force: true` to
3609
+ * skip the FF check (we don't — push-to-save is always FF over the ref
3610
+ * we just read with getRef()).
3611
+ */
3612
+ async updateRef(token, owner, name, args, opts = {}) {
3613
+ const { json } = await this.call(
3614
+ token,
3615
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs/heads/${encodeURIComponent(args.branch)}`,
3616
+ {
3617
+ ...opts,
3618
+ method: "PATCH",
3619
+ body: { sha: args.sha, force: args.force ?? false },
3620
+ requiredScopes: ["repo"]
3621
+ }
3622
+ );
3623
+ return { ref: json.ref, sha: json.object.sha };
3624
+ }
3625
+ /**
3626
+ * Search GitHub for public API Circle workspaces. Appends
3627
+ * `topic:apicircle` to the user-supplied query so only repos carrying
3628
+ * the `apicircle` topic — the topic the Releases & Topics dialog
3629
+ * locks onto every workspace repo — surface in results. GitHub
3630
+ * matches the bare query against repository name, description, and
3631
+ * topics, so category words like `payments` narrow the marketplace by
3632
+ * topic. An empty query lists every public API Circle workspace. Top
3633
+ * 30 results. Token is optional — anonymous browsing is supported
3634
+ * (lower GitHub rate limits apply); pass a PAT when one is available
3635
+ * to lift them. `sort` controls ordering: omit for GitHub's
3636
+ * best-match relevance, or pass `'stars'` / `'updated'`.
3637
+ */
3638
+ async searchMarketplaceRepos(token, query, opts = {}) {
3639
+ const { sort, ...callOpts } = opts;
3640
+ const fullQuery = `${query.trim()} topic:apicircle`.trim();
3641
+ const sortParam = sort ? `&sort=${sort}&order=desc` : "";
3642
+ const path3 = `/search/repositories?q=${encodeURIComponent(fullQuery)}&per_page=30${sortParam}`;
3643
+ const { json } = await this.call(token, path3, callOpts);
3644
+ const items = json.items ?? [];
3645
+ return items.map(normalizeMarketplaceRepo);
3646
+ }
3647
+ /**
3648
+ * Start GitHub's OAuth Device Flow. Returns a user-facing code the
3649
+ * user types into github.com/login/device + a device_code the app
3650
+ * polls with. Pure browser-safe: no client_secret involved (device
3651
+ * flow is the only OAuth path GitHub supports for public clients).
3652
+ *
3653
+ * Requires the OAuth App to have "Enable Device Flow" turned on in
3654
+ * its GitHub settings — surface 400 with `not_supported` to the user
3655
+ * if the App owner hasn't done that yet.
3656
+ */
3657
+ async startDeviceFlow(clientId, scope, opts = {}) {
3658
+ const url = `${this.loginBaseUrl}/login/device/code`;
3659
+ const response = await this.fetchImpl(url, {
3660
+ method: "POST",
3661
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
3662
+ body: JSON.stringify({ client_id: clientId, scope }),
3663
+ signal: opts.signal
3664
+ });
3665
+ if (!response.ok) {
3666
+ throw new GitHubError(
3667
+ `Device-flow start failed: HTTP ${response.status}`,
3668
+ response.status,
3669
+ {}
3670
+ );
3671
+ }
3672
+ const json = await response.json();
3673
+ if (json.error) {
3674
+ throw new GitHubError(json.error_description ?? json.error, 400, json);
3675
+ }
3676
+ return {
3677
+ deviceCode: json.device_code,
3678
+ userCode: json.user_code,
3679
+ verificationUri: json.verification_uri,
3680
+ expiresIn: json.expires_in,
3681
+ interval: json.interval
3682
+ };
3683
+ }
3684
+ /**
3685
+ * Poll for the access token after the user has authorized the device
3686
+ * code. GitHub returns `authorization_pending` until the user
3687
+ * completes the flow, `slow_down` if we polled too fast, then a real
3688
+ * token. Caller wraps this in a polling loop bounded by `expiresIn`.
3689
+ */
3690
+ async pollDeviceToken(clientId, deviceCode, opts = {}) {
3691
+ const url = `${this.loginBaseUrl}/login/oauth/access_token`;
3692
+ const response = await this.fetchImpl(url, {
3693
+ method: "POST",
3694
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
3695
+ body: JSON.stringify({
3696
+ client_id: clientId,
3697
+ device_code: deviceCode,
3698
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
3699
+ }),
3700
+ signal: opts.signal
3701
+ });
3702
+ const json = await response.json();
3703
+ if (json.access_token) {
3704
+ return {
3705
+ kind: "granted",
3706
+ accessToken: json.access_token,
3707
+ tokenType: json.token_type ?? "bearer",
3708
+ scope: json.scope ?? ""
3709
+ };
3710
+ }
3711
+ if (json.error === "authorization_pending") return { kind: "pending", slowDown: false };
3712
+ if (json.error === "slow_down") return { kind: "pending", slowDown: true };
3713
+ if (json.error === "expired_token") return { kind: "expired" };
3714
+ if (json.error === "access_denied")
3715
+ return { kind: "denied", reason: json.error_description ?? "User denied authorization" };
3716
+ throw new GitHubError(
3717
+ json.error_description ?? json.error ?? "Device-token poll failed",
3718
+ response.status,
3719
+ json
3720
+ );
3721
+ }
3722
+ /**
3723
+ * Create a lightweight Git tag (a ref under `refs/tags/<name>`) on the
3724
+ * given commit SHA. Used by the publish-release flow when the user
3725
+ * opts in to "Create Git tag v<x.y.z>". Returns the resolved ref.
3726
+ *
3727
+ * GitHub returns 422 with "Reference already exists" when the tag is
3728
+ * a duplicate; that surfaces as a GitHubError(422) so the UI can warn
3729
+ * the user without ever overwriting an existing tag.
3730
+ */
3731
+ async createTag(token, owner, name, args, opts = {}) {
3732
+ const { json } = await this.call(
3733
+ token,
3734
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs`,
3735
+ {
3736
+ ...opts,
3737
+ method: "POST",
3738
+ body: { ref: `refs/tags/${args.tagName}`, sha: args.sha },
3739
+ requiredScopes: ["repo"]
3740
+ }
3741
+ );
3742
+ return { ref: json.ref, sha: json.object.sha };
3743
+ }
3744
+ /**
3745
+ * Compare two commits. Returns the relationship classification GitHub
3746
+ * gives us: `ahead` (head is descendant of base), `behind` (base is
3747
+ * descendant of head), `identical`, or `diverged` (the two histories
3748
+ * share a base but neither contains the other — typical of a force-push
3749
+ * that rewrote history under us).
3750
+ *
3751
+ * Used by the refresh path so we never silently 3-way-merge across a
3752
+ * history rewrite — divergence steers the user through an explicit
3753
+ * "history rewritten" modal instead of corrupting local state.
3754
+ */
3755
+ async compareCommits(token, owner, name, base, head, opts = {}) {
3756
+ const { json } = await this.call(
3757
+ token,
3758
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/compare/${encodeURIComponent(
3759
+ base
3760
+ )}...${encodeURIComponent(head)}`,
3761
+ { ...opts, requiredScopes: ["repo"] }
3762
+ );
3763
+ return {
3764
+ status: json.status,
3765
+ aheadBy: json.ahead_by,
3766
+ behindBy: json.behind_by
3767
+ };
3768
+ }
3769
+ /**
3770
+ * Is `ancestor` reachable from `descendant`? Thin wrapper around
3771
+ * `compareCommits` — "ahead" or "identical" means yes; "behind" or
3772
+ * "diverged" means the histories don't fit, so the answer is no.
3773
+ */
3774
+ async isAncestor(token, owner, name, ancestor, descendant, opts = {}) {
3775
+ if (ancestor === descendant) return true;
3776
+ const cmp = await this.compareCommits(token, owner, name, ancestor, descendant, opts);
3777
+ return cmp.status === "ahead" || cmp.status === "identical";
3778
+ }
3779
+ /**
3780
+ * Create a GitHub Release pointing at an existing tag. Used by the
3781
+ * publish-release flow when the user opts in to "Create GitHub
3782
+ * Release". Returns the release's HTML URL so the UI can show a
3783
+ * "Released — view on GitHub" link.
3784
+ *
3785
+ * Pass `prerelease: true` for semver pre-release identifiers (e.g.
3786
+ * `1.0.0-rc.1`); GitHub's Releases UI flags those distinctly.
3787
+ */
3788
+ async createRelease(token, owner, name, args, opts = {}) {
3789
+ const { json } = await this.call(token, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/releases`, {
3790
+ ...opts,
3791
+ method: "POST",
3792
+ body: {
3793
+ tag_name: args.tagName,
3794
+ name: args.releaseName ?? args.tagName,
3795
+ body: args.body ?? "",
3796
+ draft: args.draft ?? false,
3797
+ prerelease: args.prerelease ?? false
3798
+ },
3799
+ requiredScopes: ["repo"]
3800
+ });
3801
+ return { id: json.id, htmlUrl: json.html_url, tagName: json.tag_name };
3802
+ }
3803
+ /**
3804
+ * Read a tag ref's current commit SHA. Used by the Release & topics
3805
+ * modal to detect whether a tag with the chosen name already exists
3806
+ * (so the UI can surface an "Override existing tag" toggle instead of
3807
+ * silently 422'ing through createTag).
3808
+ *
3809
+ * Returns `null` when the tag doesn't exist (404). Other failures
3810
+ * surface as typed errors.
3811
+ */
3812
+ async getTagSha(token, owner, name, tagName, opts = {}) {
3813
+ try {
3814
+ const { json } = await this.call(
3815
+ token,
3816
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs/tags/${encodeURIComponent(tagName)}`,
3817
+ opts
3818
+ );
3819
+ return json.object.sha;
3820
+ } catch (err) {
3821
+ if (err instanceof GitHubError && err.status === 404) return null;
3822
+ throw err;
3823
+ }
3824
+ }
3825
+ /**
3826
+ * Delete a ref. Used to support the "Override existing tag" path on
3827
+ * the Release & topics modal — we delete the existing tag ref, then
3828
+ * createTag against the new SHA. (GitHub doesn't have a single
3829
+ * "force-update tag" endpoint via the simple refs API.)
3830
+ *
3831
+ * `ref` is the bare suffix, e.g. `tags/v1.0.0` or `heads/feature-x`.
3832
+ */
3833
+ async deleteRef(token, owner, name, ref, opts = {}) {
3834
+ await this.call(
3835
+ token,
3836
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs/${ref.split("/").map(encodeURIComponent).join("/")}`,
3837
+ {
3838
+ ...opts,
3839
+ method: "DELETE",
3840
+ requiredScopes: ["repo"]
3841
+ }
3842
+ );
3843
+ }
3844
+ /**
3845
+ * Read the repo's current topic list. Topics drive marketplace
3846
+ * discoverability — public API Circle workspaces include `apicircle`
3847
+ * plus user-chosen category topics.
3848
+ *
3849
+ * Note: GitHub's topics API uses a custom Accept header, but we treat
3850
+ * that as transport detail; the `application/vnd.github.mercy-preview+json`
3851
+ * preview is now stable so the default Accept works.
3852
+ */
3853
+ async listRepoTopics(token, owner, name, opts = {}) {
3854
+ const { json } = await this.call(
3855
+ token,
3856
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/topics`,
3857
+ opts
3858
+ );
3859
+ return Array.isArray(json.names) ? json.names : [];
3860
+ }
3861
+ /**
3862
+ * Replace the repo's full topic list. GitHub's `PUT /topics` endpoint
3863
+ * is a full replace (not a merge), so the caller must pass the
3864
+ * complete desired list. Caps at 20 topics; each must match
3865
+ * `^[a-z0-9][a-z0-9-]*$` and be ≤ 50 chars (GitHub enforces this with
3866
+ * a 422). Returns the persisted list.
3867
+ */
3868
+ async setRepoTopics(token, owner, name, topics, opts = {}) {
3869
+ const { json } = await this.call(
3870
+ token,
3871
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/topics`,
3872
+ {
3873
+ ...opts,
3874
+ method: "PUT",
3875
+ body: { names: topics },
3876
+ requiredScopes: ["repo"]
3877
+ }
3878
+ );
3879
+ return Array.isArray(json.names) ? json.names : [];
3880
+ }
3881
+ /**
3882
+ * Fetch a single file's contents from a branch / commit. Returns
3883
+ * `null` when GitHub answers 404 (file simply doesn't exist on that
3884
+ * ref — the common case for the very first pull). Other failures
3885
+ * surface as the usual typed errors.
3886
+ *
3887
+ * Used by the refresh flow to read remote `workspace.json` so the
3888
+ * 3-way diff can compare it against the local doc.
3889
+ */
3890
+ async getContents(token, owner, name, path3, ref, opts = {}) {
3891
+ const query = `?ref=${encodeURIComponent(ref)}`;
3892
+ try {
3893
+ const { json } = await this.call(
3894
+ token,
3895
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/contents/${path3.split("/").map(encodeURIComponent).join("/")}${query}`,
3896
+ opts
3897
+ );
3898
+ if (Array.isArray(json) || json.type !== "file") {
3899
+ throw new GitHubError(`Path ${path3} is not a file`, 422, json);
3900
+ }
3901
+ const cleaned = json.content.replace(/\n/g, "");
3902
+ const decoded = decodeBase64Utf8(cleaned);
3903
+ return { content: decoded, sha: json.sha, path: json.path, size: json.size };
3904
+ } catch (err) {
3905
+ if (err instanceof GitHubError && err.status === 404) return null;
3906
+ throw err;
3907
+ }
3908
+ }
3909
+ /**
3910
+ * Create or update a file via the Contents API. The killer feature here
3911
+ * vs. the git-data flow (createBlob → createTree → createCommit →
3912
+ * updateRef) is that this works on **truly empty repos**: GitHub's git
3913
+ * database isn't initialized until the first commit lands, so all the
3914
+ * `/git/*` endpoints reject with 409 "Git Repository is empty" — but
3915
+ * `PUT /contents/{path}` atomically initializes the database with a
3916
+ * single-file commit on the supplied branch (defaulting to the repo's
3917
+ * default branch).
3918
+ *
3919
+ * Used by the seed-initial-commit flow to bootstrap a freshly-created
3920
+ * empty repo with a scaffold `workspace.json`.
3921
+ *
3922
+ * `contentBase64` must already be base64-encoded — caller chooses the
3923
+ * encoder (TextEncoder for UTF-8 strings, raw bytes for binaries).
3924
+ */
3925
+ async putContents(token, owner, name, path3, args, opts = {}) {
3926
+ const body = {
3927
+ message: args.message,
3928
+ content: args.contentBase64
3929
+ };
3930
+ if (args.branch) body.branch = args.branch;
3931
+ if (args.sha) body.sha = args.sha;
3932
+ const { json } = await this.call(
3933
+ token,
3934
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/contents/${path3.split("/").map(encodeURIComponent).join("/")}`,
3935
+ {
3936
+ ...opts,
3937
+ method: "PUT",
3938
+ body,
3939
+ requiredScopes: ["repo"]
3940
+ }
3941
+ );
3942
+ return { commitSha: json.commit.sha, contentSha: json.content.sha };
3943
+ }
3944
+ /**
3945
+ * Same as `getContents` but returns the raw bytes instead of UTF-8
3946
+ * decoding the file. Used by the refresh flow to pull
3947
+ * `.apicircle/workspace-<id>/attachments/<slotId>` blobs into local IDB without
3948
+ * mangling binary data through TextDecoder.
3949
+ */
3950
+ async getBinaryContents(token, owner, name, path3, ref, opts = {}) {
3951
+ const query = `?ref=${encodeURIComponent(ref)}`;
3952
+ try {
3953
+ const { json } = await this.call(
3954
+ token,
3955
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/contents/${path3.split("/").map(encodeURIComponent).join("/")}${query}`,
3956
+ opts
3957
+ );
3958
+ if (Array.isArray(json) || json.type !== "file") {
3959
+ throw new GitHubError(`Path ${path3} is not a file`, 422, json);
3960
+ }
3961
+ const cleaned = json.content.replace(/\n/g, "");
3962
+ const bytes = decodeBase64Bytes(cleaned);
3963
+ return { bytes, sha: json.sha, path: json.path, size: json.size };
3964
+ } catch (err) {
3965
+ if (err instanceof GitHubError && err.status === 404) return null;
3966
+ throw err;
3967
+ }
3968
+ }
3969
+ /**
3970
+ * Open a pull request from `head` (the working branch) into `base` (the
3971
+ * repo's default branch). PR creation needs the `pull_request` scope on
3972
+ * top of `repo`; missing-scope errors flow through MissingScopeError so
3973
+ * the UI can prompt the user to update the token without losing branch
3974
+ * state (Plan §3.7).
3975
+ *
3976
+ * GitHub returns 422 when:
3977
+ * - head/base are equal (nothing to merge)
3978
+ * - a PR already exists between this head and base
3979
+ * - the head branch doesn't exist
3980
+ * All three surface as a plain GitHubError(422); the UI message is
3981
+ * picked up from response.body.message.
3982
+ */
3983
+ /**
3984
+ * Fetch a single pull request by number. Used by the refresh flow to
3985
+ * detect whether a previously-opened PR has been merged on GitHub —
3986
+ * `merged: true` is what triggers the working-branch retirement path.
3987
+ *
3988
+ * Returns `null` on 404 (PR was deleted or never existed at this number);
3989
+ * other failures surface as the usual typed errors.
3990
+ */
3991
+ async getPullRequest(token, owner, name, number, opts = {}) {
3992
+ try {
3993
+ const { json } = await this.call(
3994
+ token,
3995
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/pulls/${number}`,
3996
+ opts
3997
+ );
3998
+ return {
3999
+ number: json.number,
4000
+ htmlUrl: json.html_url,
4001
+ state: json.state,
4002
+ merged: json.merged === true
4003
+ };
4004
+ } catch (err) {
4005
+ if (err instanceof GitHubError && err.status === 404) return null;
4006
+ throw err;
4007
+ }
4008
+ }
4009
+ /**
4010
+ * List pull requests on a repo. The capability-probe path uses this with
4011
+ * `perPage: 1` to determine whether the token can read PRs (and, by
4012
+ * extension on classic PATs, whether it can also create them).
4013
+ *
4014
+ * Caller declares `requiredScopes` to surface a `MissingScopeError` on
4015
+ * 403, so the capability probe can recognise the missing-scope case
4016
+ * cleanly vs. transient 5xx/network failures.
4017
+ */
4018
+ async listPullRequests(token, owner, name, args = {}, opts = {}) {
4019
+ const params = new URLSearchParams();
4020
+ params.set("per_page", String(args.perPage ?? 30));
4021
+ if (args.state) params.set("state", args.state);
4022
+ const { json } = await this.call(
4023
+ token,
4024
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/pulls?${params.toString()}`,
4025
+ {
4026
+ ...opts,
4027
+ requiredScopes: ["repo", "pull_request"]
4028
+ }
4029
+ );
4030
+ return json.map((pr) => ({
4031
+ number: pr.number,
4032
+ htmlUrl: pr.html_url,
4033
+ state: pr.state,
4034
+ title: pr.title
4035
+ }));
4036
+ }
4037
+ async createPullRequest(token, owner, name, args, opts = {}) {
4038
+ const { json } = await this.call(
4039
+ token,
4040
+ `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/pulls`,
4041
+ {
4042
+ ...opts,
4043
+ method: "POST",
4044
+ body: {
4045
+ title: args.title,
4046
+ body: args.body,
4047
+ head: args.head,
4048
+ base: args.base,
4049
+ draft: args.draft ?? false
4050
+ },
4051
+ requiredScopes: ["repo", "pull_request"]
4052
+ }
4053
+ );
4054
+ return {
4055
+ number: json.number,
4056
+ htmlUrl: json.html_url,
4057
+ state: json.state,
4058
+ title: json.title
4059
+ };
4060
+ }
4061
+ // --- low-level call ----------------------------------------------------
4062
+ async call(token, path3, opts = {}) {
4063
+ const url = path3.startsWith("http") ? path3 : `${this.baseUrl}${path3}`;
4064
+ const controller = new AbortController();
4065
+ const onExternalAbort = () => controller.abort(opts.signal.reason);
4066
+ if (opts.signal) {
4067
+ if (opts.signal.aborted) controller.abort(opts.signal.reason);
4068
+ else opts.signal.addEventListener("abort", onExternalAbort, { once: true });
4069
+ }
4070
+ const timeoutHandle = setTimeout(
4071
+ () => controller.abort(new Error(`GitHub request timed out after ${this.timeoutMs}ms`)),
4072
+ this.timeoutMs
4073
+ );
4074
+ let response;
4075
+ let timedOut = false;
4076
+ try {
4077
+ response = await this.fetchImpl(url, {
4078
+ method: opts.method ?? "GET",
4079
+ headers: {
4080
+ Accept: "application/vnd.github+json",
4081
+ "X-GitHub-Api-Version": "2022-11-28",
4082
+ ...token ? { Authorization: `Bearer ${token}` } : {},
4083
+ ...opts.body !== void 0 ? { "Content-Type": "application/json" } : {}
4084
+ },
4085
+ cache: "no-store",
4086
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
4087
+ signal: controller.signal
4088
+ });
4089
+ } catch (err) {
4090
+ const isAbort = err instanceof DOMException && err.name === "AbortError";
4091
+ const callerAborted = opts.signal?.aborted ?? false;
4092
+ if (isAbort && !callerAborted) {
4093
+ timedOut = true;
4094
+ throw new TimeoutError(
4095
+ `GitHub request timed out after ${this.timeoutMs}ms. The write may have partially landed \u2014 refresh before retrying.`,
4096
+ this.timeoutMs
4097
+ );
4098
+ }
4099
+ throw err;
4100
+ } finally {
4101
+ clearTimeout(timeoutHandle);
4102
+ if (opts.signal) opts.signal.removeEventListener("abort", onExternalAbort);
4103
+ void timedOut;
4104
+ }
4105
+ if (response.ok) {
4106
+ if (response.status === 204 || response.status === 205) {
4107
+ return { json: {}, response };
4108
+ }
4109
+ const json = await response.json();
4110
+ return { json, response };
4111
+ }
4112
+ const errBody = await safeReadJson(response);
4113
+ throw classifyError(response, errBody, opts.requiredScopes ?? []);
4114
+ }
4115
+ };
4116
+ }
4117
+ });
4118
+
4119
+ // ../git/src/index.ts
4120
+ var init_src = __esm({
4121
+ "../git/src/index.ts"() {
4122
+ "use strict";
4123
+ init_api();
4124
+ init_errors();
4125
+ }
4126
+ });
4127
+
4128
+ // src/tools/githubOps.ts
4129
+ function resolveToken(input) {
4130
+ const t = (input ?? process.env.GITHUB_TOKEN ?? "").trim();
4131
+ return t;
4132
+ }
4133
+ var import_zod14, import_core5, import_shared6, TOKEN_HELP, linkedLinkTool, linkedRefreshTool, releaseTagTool, marketplaceSearchTool, TOPIC_RE, repoSetTopicsTool;
4134
+ var init_githubOps = __esm({
4135
+ "src/tools/githubOps.ts"() {
4136
+ "use strict";
4137
+ import_zod14 = require("zod");
4138
+ import_core5 = require("@apicircle/core");
4139
+ import_shared6 = require("@apicircle/shared");
4140
+ init_src();
4141
+ TOKEN_HELP = "Pass `token`, or set the GITHUB_TOKEN env var on the MCP process.";
4142
+ linkedLinkTool = {
4143
+ name: "linked.link",
4144
+ 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,
4145
+ inputSchema: import_zod14.z.object({
4146
+ repoFullName: import_zod14.z.string().describe("owner/name of the source workspace repo."),
4147
+ branch: import_zod14.z.string().default("main"),
4148
+ pinnedVersion: import_zod14.z.string().nullable().optional().describe("null/omitted = source current version."),
4149
+ kind: import_zod14.z.enum(["private", "public"]).default("private"),
4150
+ token: import_zod14.z.string().optional()
4151
+ }),
4152
+ async handler(input, ctx) {
4153
+ const token = resolveToken(input.token);
4154
+ const repoFullName = input.repoFullName.trim();
4155
+ if (!repoFullName.includes("/")) return { ok: false, error: "repoFullName must be owner/name" };
4156
+ if (input.kind === "private" && !token)
4157
+ return { ok: false, error: `A token is required for private repos. ${TOKEN_HELP}` };
4158
+ const [owner, name] = repoFullName.split("/", 2);
4159
+ const state = await ctx.workspace.read();
4160
+ const dup = Object.values(state.synced.linkedWorkspaces).find(
4161
+ (l) => l.source.repoFullName === repoFullName && l.source.branch === input.branch
4162
+ );
4163
+ if (dup)
4164
+ return { ok: false, error: `Already linked to ${repoFullName}@${input.branch} (${dup.id})` };
4165
+ const client = new GitHubClient();
4166
+ let result;
4167
+ try {
4168
+ result = await (0, import_core5.fetchRemoteWorkspaceJson)(async (p) => {
4169
+ const f = await client.getContents(token, owner, name, p, input.branch);
4170
+ return f?.content ?? null;
4171
+ });
4172
+ } catch (e) {
4173
+ return {
4174
+ ok: false,
4175
+ error: e instanceof GitHubError ? e.message : e instanceof Error ? e.message : "fetch failed"
4176
+ };
4177
+ }
4178
+ if ("error" in result)
4179
+ return { ok: false, error: `${repoFullName}@${input.branch}: ${result.error}` };
4180
+ const probe = (0, import_core5.parseLinkedWorkspaceJson)(result.content);
4181
+ const ledger = (0, import_core5.ledgerFromProbe)(probe);
4182
+ const link = {
4183
+ id: (0, import_shared6.generateId)(),
4184
+ kind: input.kind,
4185
+ name: repoFullName,
4186
+ sourceWorkspaceId: result.workspaceId,
4187
+ source: { provider: "github", repoFullName, branch: input.branch, sessionMode: "workspace" },
4188
+ scope: ["collections", "environments"],
4189
+ pinnedVersion: input.pinnedVersion ?? ledger.currentVersion,
4190
+ updatePolicy: "manual",
4191
+ linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
4192
+ requiredSecretKeyIds: probe.secretKeys ? Object.keys(probe.secretKeys) : []
4193
+ };
4194
+ const snapshot = (0, import_core5.buildLinkedSnapshot)(probe, link) ?? void 0;
4195
+ const out = await ctx.workspace.apply({
4196
+ kind: "linkedWorkspace.upsert",
4197
+ link,
4198
+ ledger,
4199
+ ...snapshot ? { snapshot } : {}
4200
+ });
4201
+ return { ok: true, id: link.id, pinnedVersion: link.pinnedVersion, changedIds: out.changedIds };
4202
+ }
4203
+ };
4204
+ linkedRefreshTool = {
4205
+ name: "linked.refresh",
4206
+ description: "Re-pull a linked workspace's cached release ledger (+ bootstrap snapshot if missing) from GitHub. " + TOKEN_HELP,
4207
+ inputSchema: import_zod14.z.object({ id: import_zod14.z.string(), token: import_zod14.z.string().optional() }),
4208
+ async handler(input, ctx) {
4209
+ const state = await ctx.workspace.read();
4210
+ const link = state.synced.linkedWorkspaces[input.id];
4211
+ if (!link) return { ok: false, error: `Linked workspace ${input.id} not found` };
4212
+ const token = resolveToken(input.token);
4213
+ if (link.kind === "private" && !token)
4214
+ return { ok: false, error: `A token is required for private links. ${TOKEN_HELP}` };
4215
+ const [owner, name] = link.source.repoFullName.split("/", 2);
4216
+ const client = new GitHubClient();
4217
+ let result;
4218
+ try {
4219
+ result = await (0, import_core5.fetchRemoteWorkspaceJson)(async (p) => {
4220
+ const f = await client.getContents(token, owner, name, p, link.source.branch);
4221
+ return f?.content ?? null;
4222
+ });
4223
+ } catch (e) {
4224
+ return { ok: false, error: e instanceof Error ? e.message : "fetch failed" };
4225
+ }
4226
+ if ("error" in result)
4227
+ return {
4228
+ ok: false,
4229
+ error: `${link.source.repoFullName}@${link.source.branch}: ${result.error}`
4230
+ };
4231
+ const probe = (0, import_core5.parseLinkedWorkspaceJson)(result.content);
4232
+ const ledger = (0, import_core5.ledgerFromProbe)(probe);
4233
+ const needsSnapshot = !state.local.linkedCollections[input.id];
4234
+ const snapshot = needsSnapshot ? (0, import_core5.buildLinkedSnapshot)(probe, link) ?? void 0 : void 0;
4235
+ await ctx.workspace.apply({
4236
+ kind: "linkedWorkspace.upsert",
4237
+ link,
4238
+ ledger,
4239
+ ...snapshot ? { snapshot } : {}
4240
+ });
4241
+ return {
4242
+ ok: true,
4243
+ currentVersion: ledger.currentVersion,
4244
+ versionCount: ledger.versions.length
4245
+ };
4246
+ }
4247
+ };
4248
+ releaseTagTool = {
4249
+ name: "release.tag",
4250
+ 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,
4251
+ inputSchema: import_zod14.z.object({
4252
+ owner: import_zod14.z.string(),
4253
+ name: import_zod14.z.string(),
4254
+ version: import_zod14.z.string(),
4255
+ createGitHubRelease: import_zod14.z.boolean().default(false),
4256
+ notes: import_zod14.z.string().optional(),
4257
+ overrideExisting: import_zod14.z.boolean().default(false),
4258
+ token: import_zod14.z.string().optional()
4259
+ }),
4260
+ async handler(input, _ctx) {
4261
+ const token = resolveToken(input.token);
4262
+ if (!token) return { ok: false, error: `A token is required to tag. ${TOKEN_HELP}` };
4263
+ const client = new GitHubClient();
4264
+ const tagName = `v${input.version.replace(/^v/, "")}`;
4265
+ try {
4266
+ const repo = await client.getRepo(token, input.owner, input.name);
4267
+ const ref = await client.getRef(token, input.owner, input.name, repo.defaultBranch);
4268
+ const existing = await client.getTagSha(token, input.owner, input.name, tagName);
4269
+ if (existing !== null) {
4270
+ if (!input.overrideExisting) {
4271
+ return {
4272
+ ok: false,
4273
+ error: `Tag ${tagName} already exists at ${existing.slice(0, 7)}. Pass overrideExisting:true to replace.`
4274
+ };
4275
+ }
4276
+ await client.deleteRef(token, input.owner, input.name, `tags/${tagName}`);
4277
+ }
4278
+ await client.createTag(token, input.owner, input.name, { tagName, sha: ref.sha });
4279
+ let releaseUrl;
4280
+ if (input.createGitHubRelease) {
4281
+ const release = await client.createRelease(token, input.owner, input.name, {
4282
+ tagName,
4283
+ releaseName: tagName,
4284
+ body: input.notes ?? ""
4285
+ });
4286
+ releaseUrl = release.htmlUrl;
4287
+ }
4288
+ return {
4289
+ ok: true,
4290
+ tagName,
4291
+ sha: ref.sha,
4292
+ branch: repo.defaultBranch,
4293
+ ...releaseUrl ? { releaseUrl } : {}
4294
+ };
4295
+ } catch (e) {
4296
+ return { ok: false, error: e instanceof Error ? e.message : "tag failed" };
4297
+ }
4298
+ }
4299
+ };
4300
+ marketplaceSearchTool = {
4301
+ name: "marketplace.search",
4302
+ 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,
4303
+ inputSchema: import_zod14.z.object({
4304
+ query: import_zod14.z.string().default("").describe("Search query \u2014 matches repo name, description, and topics. Empty = browse all."),
4305
+ sort: import_zod14.z.enum(["best-match", "stars", "updated"]).default("best-match").describe(
4306
+ "Sort order: best-match (default relevance), stars (most starred first), updated (recently pushed first)."
4307
+ ),
4308
+ token: import_zod14.z.string().optional()
4309
+ }),
4310
+ async handler(input, _ctx) {
4311
+ const token = resolveToken(input.token) || null;
4312
+ const client = new GitHubClient();
4313
+ try {
4314
+ const repos = await client.searchMarketplaceRepos(token, input.query, {
4315
+ sort: input.sort === "best-match" ? void 0 : input.sort
4316
+ });
4317
+ return { ok: true, count: repos.length, results: repos };
4318
+ } catch (e) {
4319
+ return {
4320
+ ok: false,
4321
+ error: e instanceof GitHubError ? e.message : e instanceof Error ? e.message : "search failed"
4322
+ };
4323
+ }
4324
+ }
4325
+ };
4326
+ TOPIC_RE = /^[a-z0-9][a-z0-9-]*$/;
4327
+ repoSetTopicsTool = {
4328
+ name: "repo.set_topics",
4329
+ 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,
4330
+ inputSchema: import_zod14.z.object({
4331
+ owner: import_zod14.z.string(),
4332
+ name: import_zod14.z.string(),
4333
+ topics: import_zod14.z.array(import_zod14.z.string()),
4334
+ token: import_zod14.z.string().optional()
4335
+ }),
4336
+ async handler(input, _ctx) {
4337
+ const token = resolveToken(input.token);
4338
+ if (!token) return { ok: false, error: `A token is required to set topics. ${TOKEN_HELP}` };
4339
+ const requested = input.topics;
4340
+ const normalized = Array.from(
4341
+ /* @__PURE__ */ new Set(["apicircle", ...requested.map((t) => t.trim().toLowerCase()).filter(Boolean)])
4342
+ );
4343
+ for (const t of normalized) {
4344
+ if (!TOPIC_RE.test(t))
4345
+ return {
4346
+ ok: false,
4347
+ error: `Invalid topic "${t}" \u2014 lowercase letters/digits/"-", starting with a letter or digit.`
4348
+ };
4349
+ if (t.length > 50) return { ok: false, error: `Topic "${t}" exceeds 50 characters.` };
4350
+ }
4351
+ if (normalized.length > 20) return { ok: false, error: "GitHub allows at most 20 topics." };
4352
+ const client = new GitHubClient();
4353
+ try {
4354
+ const saved = await client.setRepoTopics(token, input.owner, input.name, normalized);
4355
+ return { ok: true, topics: saved };
4356
+ } catch (e) {
4357
+ return { ok: false, error: e instanceof Error ? e.message : "set topics failed" };
4358
+ }
4359
+ }
4360
+ };
4361
+ }
4362
+ });
4363
+
4364
+ // src/tools/registry.ts
4365
+ function getTool(name) {
4366
+ return TOOL_REGISTRY.find((t) => t.name === name);
4367
+ }
4368
+ var TOOL_REGISTRY;
4369
+ var init_registry = __esm({
4370
+ "src/tools/registry.ts"() {
4371
+ "use strict";
4372
+ init_imports();
4373
+ init_codegen();
4374
+ init_workspaceList();
4375
+ init_crud();
4376
+ init_folderExchange();
4377
+ init_history();
4378
+ init_codebase();
4379
+ init_prompt();
4380
+ init_globalAssets();
4381
+ init_mocks();
4382
+ init_releases();
4383
+ init_linkedWorkspaces();
4384
+ init_githubOps();
4385
+ TOOL_REGISTRY = [
4386
+ importCurlTool,
4387
+ importOpenApiTool,
4388
+ importPostmanTool,
4389
+ importInsomniaTool,
4390
+ importHarTool,
4391
+ generateCodeTool,
4392
+ workspaceListTool,
4393
+ workspaceReadTool,
4394
+ workspaceWriteTool,
4395
+ requestCreateTool,
4396
+ requestReadTool,
4397
+ requestUpdateTool,
4398
+ requestDeleteTool,
4399
+ folderCreateTool,
4400
+ folderReadTool,
4401
+ folderUpdateTool,
4402
+ folderDeleteTool,
4403
+ folderExportJsonTool,
4404
+ folderImportJsonTool,
4405
+ environmentCreateTool,
4406
+ environmentReadTool,
4407
+ environmentUpdateTool,
4408
+ environmentDeleteTool,
4409
+ environmentSetActiveTool,
4410
+ environmentSetPriorityTool,
4411
+ environmentExportTool,
4412
+ environmentImportTool,
4413
+ planCreateTool,
4414
+ planRunTool,
4415
+ planReadTool,
4416
+ planUpdateTool,
4417
+ planDeleteTool,
4418
+ planAddStepTool,
4419
+ planRemoveStepTool,
4420
+ planReorderStepsTool,
4421
+ planSetVariablesTool,
4422
+ assertionCreateTool,
4423
+ assertionReadTool,
4424
+ assertionUpdateTool,
4425
+ assertionDeleteTool,
4426
+ historyListRunsTool,
4427
+ historyGetRunTool,
4428
+ historyDeleteRunTool,
4429
+ historyPurgeTool,
4430
+ codebaseExtractCollectionTool,
4431
+ promptCreateEnvironmentTool,
4432
+ promptCreateAssertionTool,
4433
+ promptCreatePlanTool,
4434
+ promptCreateRequestTool,
4435
+ promptUpdateRequestTool,
4436
+ promptCreateFolderTreeTool,
4437
+ promptAddPlanStepsTool,
4438
+ promptSetPlanVariablesTool,
4439
+ promptCreateMockServerTool,
4440
+ promptAddMockEndpointTool,
4441
+ promptSetEndpointValidationRulesTool,
4442
+ promptSetEndpointResponseRulesTool,
4443
+ promptSetEndpointMultipliersTool,
4444
+ promptSetEndpointRequestSchemaTool,
4445
+ globalAssetsFilesListTool,
4446
+ globalAssetsFilesCreateTool,
4447
+ globalAssetsFilesUpdateTool,
4448
+ globalAssetsFilesDeleteTool,
4449
+ mockCreateFromOpenApiTool,
4450
+ mockCreateFromPostmanTool,
4451
+ mockCreateFromInsomniaTool,
4452
+ mockCreateManualTool,
4453
+ mockListTool,
4454
+ mockListEndpointsTool,
4455
+ mockStartTool,
4456
+ mockStopTool,
4457
+ mockDeleteTool,
4458
+ mockAddEndpointTool,
4459
+ mockUpdateEndpointTool,
4460
+ mockDeleteEndpointTool,
4461
+ mockSetValidationRulesTool,
4462
+ mockSetResponseRulesTool,
4463
+ mockSetMultipliersTool,
4464
+ mockSetRequestSchemaTool,
4465
+ mockSetDefaultPortTool,
4466
+ mockImportPostmanMockCollectionTool,
4467
+ releaseListTool,
4468
+ releasePublishTool,
4469
+ releaseDeprecateTool,
4470
+ releaseYankTool,
4471
+ linkedListTool,
4472
+ linkedGetTool,
4473
+ linkedSetConfigTool,
4474
+ linkedUnlinkTool,
4475
+ linkedLinkTool,
4476
+ linkedRefreshTool,
4477
+ releaseTagTool,
4478
+ repoSetTopicsTool,
4479
+ marketplaceSearchTool
4480
+ ];
4481
+ }
4482
+ });
4483
+
4484
+ // src/providers/Workspaces.ts
4485
+ var Workspaces_exports = {};
4486
+ __export(Workspaces_exports, {
4487
+ SingleWorkspaceAdapter: () => SingleWorkspaceAdapter,
4488
+ WorkspaceNotFoundError: () => WorkspaceNotFoundError
4489
+ });
4490
+ var SingleWorkspaceAdapter, WorkspaceNotFoundError;
4491
+ var init_Workspaces = __esm({
4492
+ "src/providers/Workspaces.ts"() {
4493
+ "use strict";
4494
+ SingleWorkspaceAdapter = class {
4495
+ constructor(provider, workspaceId, displayName = "Workspace") {
4496
+ this.provider = provider;
4497
+ this.workspaceId = workspaceId;
4498
+ this.displayName = displayName;
4499
+ }
4500
+ provider;
4501
+ workspaceId;
4502
+ displayName;
4503
+ async list() {
4504
+ const state = await this.provider.read();
4505
+ const s = state.synced;
4506
+ const id = s.workspaceId ?? this.workspaceId ?? "unknown";
4507
+ this.workspaceId = id;
4508
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4509
+ return [
4510
+ {
4511
+ id,
4512
+ name: this.displayName,
4513
+ isActive: true,
4514
+ createdAt: s.meta?.createdAt ?? now,
4515
+ lastOpenedAt: s.meta?.updatedAt ?? now,
4516
+ counts: s.collections ? {
4517
+ requests: Object.keys(s.collections.requests ?? {}).length,
4518
+ folders: Object.keys(s.collections.folders ?? {}).length,
4519
+ environments: Object.keys(s.environments?.items ?? {}).length,
4520
+ mockServers: Object.keys(s.mockServers ?? {}).length,
4521
+ plans: Object.keys(s.executionPlans ?? {}).length
4522
+ } : null
4523
+ }
4524
+ ];
4525
+ }
4526
+ for(workspaceId) {
4527
+ if (this.workspaceId && workspaceId !== this.workspaceId) {
4528
+ throw new WorkspaceNotFoundError(workspaceId);
4529
+ }
4530
+ return this.provider;
4531
+ }
4532
+ activeId() {
4533
+ return this.workspaceId;
4534
+ }
4535
+ setActive(workspaceId) {
4536
+ if (this.workspaceId && workspaceId !== this.workspaceId) {
4537
+ throw new WorkspaceNotFoundError(workspaceId);
4538
+ }
4539
+ return Promise.resolve();
4540
+ }
4541
+ };
4542
+ WorkspaceNotFoundError = class extends Error {
4543
+ code = "workspace-not-found";
4544
+ workspaceId;
4545
+ constructor(workspaceId) {
4546
+ super(`No workspace with id "${workspaceId}" is available on this server.`);
4547
+ this.name = "WorkspaceNotFoundError";
4548
+ this.workspaceId = workspaceId;
4549
+ }
4550
+ };
4551
+ }
4552
+ });
4553
+
4554
+ // src/providers/InMemoryWorkspaceProvider.ts
4555
+ var import_core6, InMemoryWorkspaceProvider;
4556
+ var init_InMemoryWorkspaceProvider = __esm({
4557
+ "src/providers/InMemoryWorkspaceProvider.ts"() {
4558
+ "use strict";
4559
+ import_core6 = require("@apicircle/core");
3031
4560
  InMemoryWorkspaceProvider = class {
3032
4561
  state;
3033
4562
  constructor(initial) {
@@ -3037,7 +4566,7 @@ var init_InMemoryWorkspaceProvider = __esm({
3037
4566
  return this.state;
3038
4567
  }
3039
4568
  async apply(patch) {
3040
- const out = (0, import_core4.applyMutation)(this.state, patch);
4569
+ const out = (0, import_core6.applyMutation)(this.state, patch);
3041
4570
  this.state = out.next;
3042
4571
  return { state: this.state, changedIds: out.changedIds };
3043
4572
  }
@@ -3057,11 +4586,11 @@ var FileBackedWorkspaceProvider_exports = {};
3057
4586
  __export(FileBackedWorkspaceProvider_exports, {
3058
4587
  FileBackedWorkspaceProvider: () => FileBackedWorkspaceProvider
3059
4588
  });
3060
- var import_core5, import_file_backed, FileBackedWorkspaceProvider;
4589
+ var import_core7, import_file_backed, FileBackedWorkspaceProvider;
3061
4590
  var init_FileBackedWorkspaceProvider = __esm({
3062
4591
  "src/providers/FileBackedWorkspaceProvider.ts"() {
3063
4592
  "use strict";
3064
- import_core5 = require("@apicircle/core");
4593
+ import_core7 = require("@apicircle/core");
3065
4594
  import_file_backed = require("@apicircle/core/workspace/file-backed");
3066
4595
  FileBackedWorkspaceProvider = class {
3067
4596
  constructor(dir) {
@@ -3078,7 +4607,7 @@ var init_FileBackedWorkspaceProvider = __esm({
3078
4607
  async apply(patch) {
3079
4608
  let captured = null;
3080
4609
  await (0, import_file_backed.withWorkspace)(this.dir, async (state) => {
3081
- const result = (0, import_core5.applyMutation)(state, patch);
4610
+ const result = (0, import_core7.applyMutation)(state, patch);
3082
4611
  captured = { state: result.next, changedIds: result.changedIds };
3083
4612
  return { next: result.next };
3084
4613
  });
@@ -3098,6 +4627,58 @@ var init_FileBackedWorkspaceProvider = __esm({
3098
4627
  }
3099
4628
  });
3100
4629
 
4630
+ // src/providers/GitBackedWorkspaceProvider.ts
4631
+ var import_core8, import_file_backed2, GIT_SYNCED_FILENAME, GitBackedWorkspaceProvider;
4632
+ var init_GitBackedWorkspaceProvider = __esm({
4633
+ "src/providers/GitBackedWorkspaceProvider.ts"() {
4634
+ "use strict";
4635
+ import_core8 = require("@apicircle/core");
4636
+ import_file_backed2 = require("@apicircle/core/workspace/file-backed");
4637
+ GIT_SYNCED_FILENAME = "workspace.json";
4638
+ GitBackedWorkspaceProvider = class {
4639
+ constructor(dir) {
4640
+ this.dir = dir;
4641
+ }
4642
+ dir;
4643
+ async read() {
4644
+ const out = await (0, import_file_backed2.loadFromFile)(this.dir, {
4645
+ syncedFilename: GIT_SYNCED_FILENAME,
4646
+ allowMissing: true
4647
+ });
4648
+ if (!out) {
4649
+ throw new Error(
4650
+ `No workspace found at ${this.dir}. Expected .apicircle/registry.json and .apicircle/workspace-<id>/workspace.json in the repo.`
4651
+ );
4652
+ }
4653
+ return out;
4654
+ }
4655
+ async apply(patch) {
4656
+ let captured = null;
4657
+ await (0, import_file_backed2.withWorkspace)(
4658
+ this.dir,
4659
+ async (state) => {
4660
+ const result = (0, import_core8.applyMutation)(state, patch);
4661
+ captured = { state: result.next, changedIds: result.changedIds };
4662
+ return { next: result.next };
4663
+ },
4664
+ { syncedFilename: GIT_SYNCED_FILENAME }
4665
+ );
4666
+ if (!captured) throw new Error("apply did not run");
4667
+ return captured;
4668
+ }
4669
+ async write(next) {
4670
+ const current = await this.read();
4671
+ const merged = {
4672
+ synced: next.synced ?? current.synced,
4673
+ local: next.local ?? current.local
4674
+ };
4675
+ await (0, import_file_backed2.saveToFile)(this.dir, merged, { syncedFilename: GIT_SYNCED_FILENAME });
4676
+ return merged;
4677
+ }
4678
+ };
4679
+ }
4680
+ });
4681
+
3101
4682
  // src/providers/MultiWorkspaceProvider.ts
3102
4683
  var MultiWorkspaceProvider_exports = {};
3103
4684
  __export(MultiWorkspaceProvider_exports, {
@@ -3194,13 +4775,14 @@ var init_MultiWorkspaceProvider = __esm({
3194
4775
  let counts = null;
3195
4776
  try {
3196
4777
  const state = await (0, import_registry.loadWorkspaceById)(this.registryRoot, entry.id);
3197
- if (state) {
4778
+ if (state?.synced?.collections) {
4779
+ const s = state.synced;
3198
4780
  counts = {
3199
- requests: Object.keys(state.synced.collections.requests).length,
3200
- folders: Object.keys(state.synced.collections.folders).length,
3201
- environments: Object.keys(state.synced.environments.items).length,
3202
- mockServers: Object.keys(state.synced.mockServers ?? {}).length,
3203
- plans: Object.keys(state.synced.executionPlans ?? {}).length
4781
+ requests: Object.keys(s.collections.requests ?? {}).length,
4782
+ folders: Object.keys(s.collections.folders ?? {}).length,
4783
+ environments: Object.keys(s.environments?.items ?? {}).length,
4784
+ mockServers: Object.keys(s.mockServers ?? {}).length,
4785
+ plans: Object.keys(s.executionPlans ?? {}).length
3204
4786
  };
3205
4787
  }
3206
4788
  } catch {
@@ -3292,19 +4874,275 @@ var init_InProcessMockController = __esm({
3292
4874
  }
3293
4875
  });
3294
4876
 
4877
+ // src/config/snippets.ts
4878
+ function buildSnippetVariants(client, binary, workspace) {
4879
+ const forwardWorkspace = workspace.replace(/\\/g, "/");
4880
+ const render = client === "codex" ? renderTomlSnippet : renderJsonSnippet;
4881
+ const escaped = render(binary, workspace);
4882
+ const forwardSlash = render(binary, forwardWorkspace);
4883
+ return {
4884
+ forwardSlash,
4885
+ escaped,
4886
+ identical: forwardSlash === escaped
4887
+ };
4888
+ }
4889
+ function renderJsonSnippet(binary, workspace) {
4890
+ const entry = {
4891
+ command: binary,
4892
+ args: ["--workspace", workspace],
4893
+ env: { APICIRCLE_WORKSPACE: workspace }
4894
+ };
4895
+ return JSON.stringify({ mcpServers: { apicircle: entry } }, null, 2);
4896
+ }
4897
+ function renderTomlSnippet(binary, workspace) {
4898
+ const esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
4899
+ return [
4900
+ `[mcp_servers.apicircle]`,
4901
+ `command = "${esc(binary)}"`,
4902
+ `args = ["--workspace", "${esc(workspace)}"]`,
4903
+ ``,
4904
+ `[mcp_servers.apicircle.env]`,
4905
+ `APICIRCLE_WORKSPACE = "${esc(workspace)}"`
4906
+ ].join("\n");
4907
+ }
4908
+ function resolveAiClientConfigPath(client, env) {
4909
+ const { homedir, platform, appdata } = env;
4910
+ switch (client) {
4911
+ case "claude-desktop":
4912
+ if (platform === "darwin") {
4913
+ return path.join(homedir, "Library/Application Support/Claude/claude_desktop_config.json");
4914
+ }
4915
+ if (platform === "win32") {
4916
+ return path.join(
4917
+ appdata ?? path.join(homedir, "AppData/Roaming"),
4918
+ "Claude/claude_desktop_config.json"
4919
+ );
4920
+ }
4921
+ return path.join(homedir, ".config/Claude/claude_desktop_config.json");
4922
+ case "claude-code":
4923
+ return path.join(homedir, ".claude/mcp.json");
4924
+ case "cursor":
4925
+ return path.join(homedir, ".cursor/mcp.json");
4926
+ case "continue":
4927
+ return path.join(homedir, ".continue/config.yaml");
4928
+ case "zed":
4929
+ return path.join(homedir, ".config/zed/settings.json");
4930
+ case "windsurf":
4931
+ return path.join(homedir, ".codeium/windsurf/mcp_config.json");
4932
+ case "codex":
4933
+ return path.join(homedir, ".codex/config.toml");
4934
+ default:
4935
+ return null;
4936
+ }
4937
+ }
4938
+ var path, AI_CLIENTS;
4939
+ var init_snippets = __esm({
4940
+ "src/config/snippets.ts"() {
4941
+ "use strict";
4942
+ path = __toESM(require("path"), 1);
4943
+ AI_CLIENTS = [
4944
+ "claude-desktop",
4945
+ "claude-code",
4946
+ "codex",
4947
+ "cursor",
4948
+ "continue",
4949
+ "cline",
4950
+ "zed",
4951
+ "windsurf",
4952
+ "github-copilot",
4953
+ "chatgpt",
4954
+ "generic"
4955
+ ];
4956
+ }
4957
+ });
4958
+
4959
+ // src/prompts/mcpPrompts.ts
4960
+ var MCP_PROMPT_CATEGORIES, MCP_PROMPTS;
4961
+ var init_mcpPrompts = __esm({
4962
+ "src/prompts/mcpPrompts.ts"() {
4963
+ "use strict";
4964
+ MCP_PROMPT_CATEGORIES = [
4965
+ { id: "workspaces", label: "Workspaces" },
4966
+ { id: "collections", label: "Collections" },
4967
+ { id: "environments", label: "Environments" },
4968
+ { id: "execution", label: "Execution" },
4969
+ { id: "mocks", label: "Mocks" },
4970
+ { id: "auth", label: "Auth" },
4971
+ { id: "imports", label: "Imports" }
4972
+ ];
4973
+ MCP_PROMPTS = [
4974
+ // ── Workspaces (multi-workspace discovery) ───────────────────────
4975
+ {
4976
+ id: "list-workspaces",
4977
+ text: "List every API Circle workspace I have and tell me which is active.",
4978
+ description: "Multi-workspace discovery \u2014 call this first when you are not sure which workspace to drive.",
4979
+ category: "workspaces",
4980
+ tools: ["workspace.list"]
4981
+ },
4982
+ {
4983
+ id: "scope-to-workspace",
4984
+ text: 'Read the requests in the "Petstore" workspace.',
4985
+ description: "Drill into a specific workspace by name; the AI passes `workspaceId` to scope reads.",
4986
+ category: "workspaces",
4987
+ tools: ["workspace.list", "workspace.read"]
4988
+ },
4989
+ {
4990
+ id: "multi-workspace-summary",
4991
+ text: "Across every workspace, count requests, folders, environments, and mocks. Give me one row per workspace.",
4992
+ description: "High-level summary across every registered workspace \u2014 pairs well with the multi-workspace envelope.",
4993
+ category: "workspaces",
4994
+ tools: ["workspace.list"]
4995
+ },
4996
+ // ── Collections (requests + folders in the active workspace) ─────
4997
+ {
4998
+ id: "list-requests",
4999
+ text: "List every request in my API Circle workspace grouped by folder.",
5000
+ description: "Quick overview of the request catalog so you know what is already wired up.",
5001
+ category: "collections",
5002
+ tools: ["workspace.read", "request.read", "folder.read"]
5003
+ },
5004
+ {
5005
+ id: "create-request",
5006
+ text: 'Add a new GET request named "Health check" pointing at https://example.com/healthz with an Accept: application/json header.',
5007
+ description: "Have the AI author a request and persist it to the workspace.",
5008
+ category: "collections",
5009
+ tools: ["request.create"]
5010
+ },
5011
+ {
5012
+ id: "update-request",
5013
+ text: 'Find the "Create user" request and change its method to POST and body to {"name": "Ada"}.',
5014
+ description: "Targeted edit by name \u2014 the AI looks it up, then updates.",
5015
+ category: "collections",
5016
+ tools: ["request.read", "request.update"]
5017
+ },
5018
+ {
5019
+ id: "organize-folders",
5020
+ text: 'Move every request whose URL contains /users into a folder named "User API".',
5021
+ description: "Bulk reorganisation via natural language.",
5022
+ category: "collections",
5023
+ tools: ["workspace.read", "folder.create", "request.update"]
5024
+ },
5025
+ // ── Environments ──────────────────────────────────────────────────
5026
+ {
5027
+ id: "env-list",
5028
+ text: "Show me all environments and which one is active.",
5029
+ description: "Inventory of envs + which is layered onto requests right now.",
5030
+ category: "environments",
5031
+ tools: ["environment.read"]
5032
+ },
5033
+ {
5034
+ id: "env-create",
5035
+ text: 'Create a "staging" environment with BASE_URL=https://staging.example.com and API_KEY={{SECRET:staging-key}}.',
5036
+ description: "Spin up a new env with both a plain variable and a secret reference.",
5037
+ category: "environments",
5038
+ tools: ["environment.create"]
5039
+ },
5040
+ {
5041
+ id: "env-switch",
5042
+ text: 'Switch the active environment to "production" and confirm by previewing the effective URL of the "Get user" request.',
5043
+ description: "Activate an env then verify variable interpolation.",
5044
+ category: "environments",
5045
+ tools: ["environment.update", "request.read"]
5046
+ },
5047
+ // ── Execution ─────────────────────────────────────────────────────
5048
+ {
5049
+ id: "run-request",
5050
+ text: 'Run the "Get user" request with userId=42 and show me the JSON response.',
5051
+ description: "One-shot execution with overridden context vars.",
5052
+ category: "execution",
5053
+ tools: ["request.execute"]
5054
+ },
5055
+ {
5056
+ id: "run-plan",
5057
+ text: 'Execute the "Regression smoke" plan and summarise which assertions failed.',
5058
+ description: "Drive a saved execution plan end-to-end.",
5059
+ category: "execution",
5060
+ tools: ["plan.read", "plan.execute"]
5061
+ },
5062
+ {
5063
+ id: "inspect-history",
5064
+ text: "Show me the last 5 requests I ran and their status codes.",
5065
+ description: "Quick triage when something just broke.",
5066
+ category: "execution",
5067
+ tools: ["history.read"]
5068
+ },
5069
+ // ── Mocks ─────────────────────────────────────────────────────────
5070
+ {
5071
+ id: "mock-start",
5072
+ text: 'Start the "Petstore" mock on port 4010 and tell me its base URL.',
5073
+ description: "Spin up a local mock so requests can hit it.",
5074
+ category: "mocks",
5075
+ tools: ["mock.list", "mock.start"]
5076
+ },
5077
+ {
5078
+ id: "mock-list",
5079
+ text: "List every running mock with its port, served spec, and request count.",
5080
+ description: "Status snapshot of every active mock runtime.",
5081
+ category: "mocks",
5082
+ tools: ["mock.list"]
5083
+ },
5084
+ {
5085
+ id: "mock-stop",
5086
+ text: "Stop every running mock.",
5087
+ description: "Clean shutdown of all mock servers in one go.",
5088
+ category: "mocks",
5089
+ tools: ["mock.stopAll"]
5090
+ },
5091
+ // ── Auth ──────────────────────────────────────────────────────────
5092
+ {
5093
+ id: "auth-set-bearer",
5094
+ text: 'Set the "Get user" request to use bearer auth with token={{ACCESS_TOKEN}}.',
5095
+ description: "Wire bearer auth onto a single request via env-var reference.",
5096
+ category: "auth",
5097
+ tools: ["request.update"]
5098
+ },
5099
+ {
5100
+ id: "auth-oauth2",
5101
+ text: 'Configure the "Create order" request to use OAuth2 client-credentials against https://auth.example.com/token with the "orders.write" scope.',
5102
+ description: "Full OAuth2 client-credentials wiring without leaving the chat.",
5103
+ category: "auth",
5104
+ tools: ["request.update"]
5105
+ },
5106
+ // ── Imports ───────────────────────────────────────────────────────
5107
+ {
5108
+ id: "import-openapi",
5109
+ text: "Import the OpenAPI spec at ./openapi.yaml and create one request per operation.",
5110
+ description: "Bulk import of a spec file from the workspace.",
5111
+ category: "imports",
5112
+ tools: ["import.openapi"]
5113
+ },
5114
+ {
5115
+ id: "import-curl",
5116
+ text: 'I am going to paste a cURL command \u2014 turn it into a saved request named "Webhook test".',
5117
+ description: "cURL \u2192 saved request with a name you control.",
5118
+ category: "imports",
5119
+ tools: ["import.curl"]
5120
+ }
5121
+ ];
5122
+ }
5123
+ });
5124
+
3295
5125
  // src/index.ts
3296
5126
  var src_exports = {};
3297
5127
  __export(src_exports, {
5128
+ AI_CLIENTS: () => AI_CLIENTS,
3298
5129
  FileBackedWorkspaceProvider: () => FileBackedWorkspaceProvider,
5130
+ GitBackedWorkspaceProvider: () => GitBackedWorkspaceProvider,
3299
5131
  InMemoryWorkspaceProvider: () => InMemoryWorkspaceProvider,
3300
5132
  InProcessMockController: () => InProcessMockController,
5133
+ MCP_PROMPTS: () => MCP_PROMPTS,
5134
+ MCP_PROMPT_CATEGORIES: () => MCP_PROMPT_CATEGORIES,
3301
5135
  McpHost: () => McpHost,
3302
5136
  MultiWorkspaceProvider: () => MultiWorkspaceProvider,
3303
5137
  SingleWorkspaceAdapter: () => SingleWorkspaceAdapter,
5138
+ StdioServerTransport: () => import_stdio2.StdioServerTransport,
5139
+ StreamableHTTPServerTransport: () => import_streamableHttp.StreamableHTTPServerTransport,
3304
5140
  TOOL_REGISTRY: () => TOOL_REGISTRY,
3305
5141
  WorkspaceNotFoundError: () => WorkspaceNotFoundError,
5142
+ buildSnippetVariants: () => buildSnippetVariants,
3306
5143
  createMcpServer: () => createMcpServer,
3307
- getTool: () => getTool
5144
+ getTool: () => getTool,
5145
+ resolveAiClientConfigPath: () => resolveAiClientConfigPath
3308
5146
  });
3309
5147
  function createMcpServer(options) {
3310
5148
  const workspaces = options.workspaces ?? new SingleWorkspaceAdapter(
@@ -3322,7 +5160,8 @@ function createMcpServer(options) {
3322
5160
  }
3323
5161
  });
3324
5162
  }
3325
- var init_src = __esm({
5163
+ var import_streamableHttp, import_stdio2;
5164
+ var init_src2 = __esm({
3326
5165
  "src/index.ts"() {
3327
5166
  "use strict";
3328
5167
  init_McpHost();
@@ -3333,13 +5172,18 @@ var init_src = __esm({
3333
5172
  init_Workspaces();
3334
5173
  init_InMemoryWorkspaceProvider();
3335
5174
  init_FileBackedWorkspaceProvider();
5175
+ init_GitBackedWorkspaceProvider();
3336
5176
  init_MultiWorkspaceProvider();
3337
5177
  init_InProcessMockController();
5178
+ init_snippets();
5179
+ init_mcpPrompts();
5180
+ import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
5181
+ import_stdio2 = require("@modelcontextprotocol/sdk/server/stdio.js");
3338
5182
  }
3339
5183
  });
3340
5184
 
3341
5185
  // src/bin/mcp-server.ts
3342
- var path = __toESM(require("path"), 1);
5186
+ var path2 = __toESM(require("path"), 1);
3343
5187
  var import_node_fs = require("fs");
3344
5188
 
3345
5189
  // src/bin/args.ts
@@ -3355,14 +5199,17 @@ function formatHelp() {
3355
5199
  Starts the API Circle MCP stdio server for AI clients.
3356
5200
 
3357
5201
  Options:
3358
- --workspace <dir> Registry root or single-workspace directory.
3359
- Defaults to APICIRCLE_WORKSPACE, then the current directory.
5202
+ --workspace <dir> Workspace directory. Auto-detected layout:
5203
+ \u2022 registry.json \u2192 multi-workspace registry root
5204
+ \u2022 workspace.json \u2192 single workspace / Git-backed dir
5205
+ Defaults to APICIRCLE_WORKSPACE env var, then cwd.
3360
5206
  -v, -V, --version Print the version number.
3361
5207
  -h, --help Show help.
3362
5208
 
3363
5209
  Examples:
3364
5210
  apicircle-mcp
3365
- apicircle-mcp --workspace ./api-circle-workspaces
5211
+ apicircle-mcp --workspace ~/.apicircle
5212
+ apicircle-mcp --workspace ./my-repo/.apicircle
3366
5213
  `;
3367
5214
  }
3368
5215
 
@@ -3371,11 +5218,11 @@ init_packageVersion();
3371
5218
  function getWorkspaceDir() {
3372
5219
  const argIdx = process.argv.indexOf("--workspace");
3373
5220
  if (argIdx !== -1 && process.argv[argIdx + 1]) {
3374
- return path.resolve(process.argv[argIdx + 1]);
5221
+ return path2.resolve(process.argv[argIdx + 1]);
3375
5222
  }
3376
5223
  const env = process.env.APICIRCLE_WORKSPACE;
3377
- if (env) return path.resolve(env);
3378
- return path.resolve(process.cwd());
5224
+ if (env) return path2.resolve(env);
5225
+ return path2.resolve(process.cwd());
3379
5226
  }
3380
5227
  async function fileExists(p) {
3381
5228
  try {
@@ -3403,19 +5250,21 @@ async function main() {
3403
5250
  { InProcessMockController: InProcessMockController2 },
3404
5251
  { SingleWorkspaceAdapter: SingleWorkspaceAdapter2 }
3405
5252
  ] = await Promise.all([
3406
- Promise.resolve().then(() => (init_src(), src_exports)),
5253
+ Promise.resolve().then(() => (init_src2(), src_exports)),
3407
5254
  Promise.resolve().then(() => (init_FileBackedWorkspaceProvider(), FileBackedWorkspaceProvider_exports)),
3408
5255
  Promise.resolve().then(() => (init_MultiWorkspaceProvider(), MultiWorkspaceProvider_exports)),
3409
5256
  Promise.resolve().then(() => (init_InProcessMockController(), InProcessMockController_exports)),
3410
5257
  Promise.resolve().then(() => (init_Workspaces(), Workspaces_exports))
3411
5258
  ]);
3412
5259
  const dir = getWorkspaceDir();
3413
- const registryPath = path.join(dir, "registry.json");
3414
- const isRegistryRoot = await fileExists(registryPath);
5260
+ const [hasRegistry, hasWorkspaceJson] = await Promise.all([
5261
+ fileExists(path2.join(dir, "registry.json")),
5262
+ fileExists(path2.join(dir, "workspace.json"))
5263
+ ]);
3415
5264
  const mock = new InProcessMockController2();
3416
- if (isRegistryRoot) {
3417
- const workspaces2 = new MultiWorkspaceProvider2(dir);
3418
- const registry = await workspaces2.init();
5265
+ if (hasRegistry) {
5266
+ const workspaces = new MultiWorkspaceProvider2(dir);
5267
+ const registry = await workspaces.init();
3419
5268
  if (!registry.activeWorkspaceId) {
3420
5269
  process.stderr.write(
3421
5270
  `apicircle-mcp: registry at ${dir} has no active workspace. Open the desktop app once, or run \`apicircle workspaces create <name>\`.
@@ -3423,21 +5272,33 @@ async function main() {
3423
5272
  );
3424
5273
  process.exit(1);
3425
5274
  }
3426
- const workspace2 = workspaces2.activeProvider();
3427
- const host2 = createMcpServer2({ workspace: workspace2, workspaces: workspaces2, mock });
5275
+ const workspace = workspaces.activeProvider();
5276
+ const host = createMcpServer2({ workspace, workspaces, mock });
3428
5277
  process.stderr.write(
3429
5278
  `apicircle-mcp: multi-workspace mode \xB7 ${registry.workspaces.length} workspace(s) \xB7 active=${registry.activeWorkspaceId}
3430
5279
  `
3431
5280
  );
3432
- await host2.connect();
5281
+ await host.connect();
3433
5282
  return;
3434
5283
  }
3435
- const workspace = new FileBackedWorkspaceProvider2(dir);
3436
- const workspaces = new SingleWorkspaceAdapter2(workspace, null);
3437
- const host = createMcpServer2({ workspace, workspaces, mock });
3438
- process.stderr.write(`apicircle-mcp: single-workspace mode \xB7 ${dir}
5284
+ if (hasWorkspaceJson) {
5285
+ const workspace = new FileBackedWorkspaceProvider2(dir);
5286
+ const workspaces = new SingleWorkspaceAdapter2(workspace, null);
5287
+ const host = createMcpServer2({ workspace, workspaces, mock });
5288
+ process.stderr.write(`apicircle-mcp: single-workspace mode \xB7 ${dir}
3439
5289
  `);
3440
- await host.connect();
5290
+ await host.connect();
5291
+ return;
5292
+ }
5293
+ process.stderr.write(
5294
+ `apicircle-mcp: no workspace found at ${dir}.
5295
+ Expected one of:
5296
+ \u2022 registry.json (multi-workspace registry root, e.g. ~/.apicircle/)
5297
+ \u2022 workspace.json (single workspace or Git-backed .apicircle/ directory)
5298
+ Point --workspace at the correct directory, or set APICIRCLE_WORKSPACE.
5299
+ `
5300
+ );
5301
+ process.exit(1);
3441
5302
  }
3442
5303
  main().catch((err) => {
3443
5304
  process.stderr.write(