@apicircle/mcp-server 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/bin/mcp-server.cjs +2253 -253
- package/dist/bin/mcp-server.cjs.map +1 -1
- package/dist/chunk-N7LZVN3U.js +165 -0
- package/dist/chunk-N7LZVN3U.js.map +1 -0
- package/dist/index.cjs +2177 -247
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -1
- package/dist/index.d.ts +65 -1
- package/dist/index.js +2005 -233
- package/dist/index.js.map +1 -1
- package/dist/prompts/mcpPrompts.cjs +190 -0
- package/dist/prompts/mcpPrompts.cjs.map +1 -0
- package/dist/prompts/mcpPrompts.d.cts +19 -0
- package/dist/prompts/mcpPrompts.d.ts +19 -0
- package/dist/prompts/mcpPrompts.js +9 -0
- package/dist/prompts/mcpPrompts.js.map +1 -0
- package/package.json +17 -5
package/dist/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MCP_PROMPTS,
|
|
3
|
+
MCP_PROMPT_CATEGORIES
|
|
4
|
+
} from "./chunk-N7LZVN3U.js";
|
|
5
|
+
|
|
1
6
|
// src/host/McpHost.ts
|
|
2
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
8
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -6,9 +11,10 @@ import { z } from "zod";
|
|
|
6
11
|
// package.json
|
|
7
12
|
var package_default = {
|
|
8
13
|
name: "@apicircle/mcp-server",
|
|
9
|
-
version: "1.0
|
|
14
|
+
version: "1.1.0",
|
|
10
15
|
private: false,
|
|
11
16
|
type: "module",
|
|
17
|
+
sideEffects: false,
|
|
12
18
|
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.",
|
|
13
19
|
keywords: [
|
|
14
20
|
"apicircle",
|
|
@@ -51,7 +57,8 @@ var package_default = {
|
|
|
51
57
|
"apicircle-mcp": "./dist/bin/mcp-server.cjs"
|
|
52
58
|
},
|
|
53
59
|
exports: {
|
|
54
|
-
".": "./src/index.ts"
|
|
60
|
+
".": "./src/index.ts",
|
|
61
|
+
"./prompts": "./src/prompts/mcpPrompts.ts"
|
|
55
62
|
},
|
|
56
63
|
files: [
|
|
57
64
|
"dist"
|
|
@@ -70,6 +77,16 @@ var package_default = {
|
|
|
70
77
|
types: "./dist/index.d.cts",
|
|
71
78
|
default: "./dist/index.cjs"
|
|
72
79
|
}
|
|
80
|
+
},
|
|
81
|
+
"./prompts": {
|
|
82
|
+
import: {
|
|
83
|
+
types: "./dist/prompts/mcpPrompts.d.ts",
|
|
84
|
+
default: "./dist/prompts/mcpPrompts.js"
|
|
85
|
+
},
|
|
86
|
+
require: {
|
|
87
|
+
types: "./dist/prompts/mcpPrompts.d.cts",
|
|
88
|
+
default: "./dist/prompts/mcpPrompts.cjs"
|
|
89
|
+
}
|
|
73
90
|
}
|
|
74
91
|
}
|
|
75
92
|
},
|
|
@@ -87,6 +104,7 @@ var package_default = {
|
|
|
87
104
|
zod: "^3.23.0"
|
|
88
105
|
},
|
|
89
106
|
devDependencies: {
|
|
107
|
+
"@apicircle/git": "workspace:*",
|
|
90
108
|
"@types/node": "^20.0.0",
|
|
91
109
|
tsup: "^8.3.0",
|
|
92
110
|
typescript: "^5.4.0",
|
|
@@ -559,6 +577,7 @@ var workspaceListTool = {
|
|
|
559
577
|
import { z as z5 } from "zod";
|
|
560
578
|
import { generateId as generateId2 } from "@apicircle/shared";
|
|
561
579
|
import { parseApicircleEnvironmentDoc } from "@apicircle/core";
|
|
580
|
+
var FULL_REQUEST_AUTH = z5.object({ type: z5.string() }).passthrough();
|
|
562
581
|
var HTTP_METHOD = z5.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
563
582
|
var requestCreateTool = {
|
|
564
583
|
name: "request.create",
|
|
@@ -646,16 +665,18 @@ var requestDeleteTool = {
|
|
|
646
665
|
};
|
|
647
666
|
var folderCreateTool = {
|
|
648
667
|
name: "folder.create",
|
|
649
|
-
description: "Create a folder under an optional parent folder.",
|
|
668
|
+
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.",
|
|
650
669
|
inputSchema: z5.object({
|
|
651
670
|
name: z5.string().default("New folder"),
|
|
652
|
-
parentId: z5.string().nullable().optional()
|
|
671
|
+
parentId: z5.string().nullable().optional(),
|
|
672
|
+
auth: FULL_REQUEST_AUTH.optional()
|
|
653
673
|
}),
|
|
654
674
|
async handler(input, ctx) {
|
|
655
675
|
const folder = {
|
|
656
676
|
id: generateId2(),
|
|
657
677
|
name: input.name,
|
|
658
|
-
parentId: input.parentId ?? null
|
|
678
|
+
parentId: input.parentId ?? null,
|
|
679
|
+
...input.auth ? { auth: input.auth } : {}
|
|
659
680
|
};
|
|
660
681
|
const out = await ctx.workspace.apply({ kind: "folder.create", folder });
|
|
661
682
|
return { id: folder.id, changedIds: out.changedIds };
|
|
@@ -679,18 +700,39 @@ var folderReadTool = {
|
|
|
679
700
|
};
|
|
680
701
|
var folderUpdateTool = {
|
|
681
702
|
name: "folder.update",
|
|
682
|
-
description:
|
|
703
|
+
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`).',
|
|
683
704
|
inputSchema: z5.object({
|
|
684
705
|
id: z5.string(),
|
|
685
|
-
parentId: z5.string().nullable()
|
|
706
|
+
parentId: z5.string().nullable().optional(),
|
|
707
|
+
name: z5.string().optional(),
|
|
708
|
+
auth: FULL_REQUEST_AUTH.optional(),
|
|
709
|
+
clearAuth: z5.boolean().optional()
|
|
710
|
+
}).refine((v) => !(v.auth !== void 0 && v.clearAuth === true), {
|
|
711
|
+
message: "Pass either `auth` or `clearAuth: true`, not both."
|
|
686
712
|
}),
|
|
687
713
|
async handler(input, ctx) {
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
714
|
+
const changedIds = [];
|
|
715
|
+
if (input.parentId !== void 0) {
|
|
716
|
+
const out = await ctx.workspace.apply({
|
|
717
|
+
kind: "folder.move",
|
|
718
|
+
id: input.id,
|
|
719
|
+
newParentId: input.parentId
|
|
720
|
+
});
|
|
721
|
+
changedIds.push(...out.changedIds);
|
|
722
|
+
}
|
|
723
|
+
const updatePatch = {};
|
|
724
|
+
if (input.name !== void 0) updatePatch.name = input.name;
|
|
725
|
+
if (input.auth !== void 0) updatePatch.auth = input.auth;
|
|
726
|
+
else if (input.clearAuth === true) updatePatch.auth = void 0;
|
|
727
|
+
if (input.name !== void 0 || input.auth !== void 0 || input.clearAuth === true) {
|
|
728
|
+
const out = await ctx.workspace.apply({
|
|
729
|
+
kind: "folder.update",
|
|
730
|
+
id: input.id,
|
|
731
|
+
patch: updatePatch
|
|
732
|
+
});
|
|
733
|
+
changedIds.push(...out.changedIds);
|
|
734
|
+
}
|
|
735
|
+
return { changedIds: Array.from(new Set(changedIds)) };
|
|
694
736
|
}
|
|
695
737
|
};
|
|
696
738
|
var folderDeleteTool = {
|
|
@@ -1517,7 +1559,12 @@ var codebaseExtractCollectionTool = {
|
|
|
1517
1559
|
|
|
1518
1560
|
// src/tools/prompt.ts
|
|
1519
1561
|
import { z as z9 } from "zod";
|
|
1520
|
-
import {
|
|
1562
|
+
import {
|
|
1563
|
+
generateId as generateId3,
|
|
1564
|
+
makeDefaultMockResponse,
|
|
1565
|
+
makeDefaultRequestSchema,
|
|
1566
|
+
MAX_RESPONSE_MULTIPLIERS
|
|
1567
|
+
} from "@apicircle/shared";
|
|
1521
1568
|
var promptCreateEnvironmentTool = {
|
|
1522
1569
|
name: "prompt.create_environment",
|
|
1523
1570
|
description: "Create a new environment from an LLM-shaped JSON envelope. The model produces { name, variables: [{ key, value, encrypted }] }; this tool validates and persists it.",
|
|
@@ -1670,7 +1717,9 @@ var CONDITION_CLAUSE_NL = z9.object({
|
|
|
1670
1717
|
var RESPONSE_RULE_NL = z9.object({
|
|
1671
1718
|
name: z9.string(),
|
|
1672
1719
|
enabled: z9.boolean().default(true),
|
|
1673
|
-
|
|
1720
|
+
// At least one clause — a no-condition rule is dead (the runtime engine skips
|
|
1721
|
+
// clause-less rules), so it never fires (mirrors the VS Code parser's reject).
|
|
1722
|
+
when: z9.array(CONDITION_CLAUSE_NL).min(1),
|
|
1674
1723
|
response: z9.object({
|
|
1675
1724
|
status: z9.number().int().min(100).max(599).default(200),
|
|
1676
1725
|
jsonBody: z9.string().default("{}")
|
|
@@ -1720,7 +1769,7 @@ function buildEndpoint(input) {
|
|
|
1720
1769
|
};
|
|
1721
1770
|
const validationRules = input.validationRules ?? [];
|
|
1722
1771
|
const responseRules = input.responseRules ?? [];
|
|
1723
|
-
const multipliers = input.multipliers ?? [];
|
|
1772
|
+
const multipliers = (input.multipliers ?? []).slice(0, MAX_RESPONSE_MULTIPLIERS);
|
|
1724
1773
|
if (multipliers.length > 0) {
|
|
1725
1774
|
defaultResponse.multipliers = multipliers.map((m) => ({
|
|
1726
1775
|
id: generateId3(),
|
|
@@ -1953,10 +2002,13 @@ var promptSetPlanVariablesTool = {
|
|
|
1953
2002
|
};
|
|
1954
2003
|
var promptCreateMockServerTool = {
|
|
1955
2004
|
name: "prompt.create_mock_server",
|
|
1956
|
-
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?,
|
|
2005
|
+
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.",
|
|
1957
2006
|
inputSchema: z9.object({
|
|
1958
2007
|
name: z9.string().min(1),
|
|
1959
|
-
|
|
2008
|
+
// Mirrors mock.create_manual / mock.start / mock.set_default_port:
|
|
2009
|
+
// reject out-of-range ports at the tool boundary so a prompt that
|
|
2010
|
+
// returns a stray port (1, 80, 999999) never leaks into the synced doc.
|
|
2011
|
+
defaultPort: z9.number().int().min(1024).max(65535).nullable().optional(),
|
|
1960
2012
|
endpoints: z9.array(ENDPOINT_INPUT).default([])
|
|
1961
2013
|
}),
|
|
1962
2014
|
async handler(input, ctx) {
|
|
@@ -1983,7 +2035,7 @@ var promptCreateMockServerTool = {
|
|
|
1983
2035
|
};
|
|
1984
2036
|
var promptAddMockEndpointTool = {
|
|
1985
2037
|
name: "prompt.add_mock_endpoint",
|
|
1986
|
-
description: "Append a new endpoint (with optional inline validation rules, response rules, and
|
|
2038
|
+
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.",
|
|
1987
2039
|
inputSchema: z9.object({
|
|
1988
2040
|
mockId: z9.string(),
|
|
1989
2041
|
method: HTTP_METHOD2,
|
|
@@ -2086,7 +2138,7 @@ var promptSetEndpointResponseRulesTool = {
|
|
|
2086
2138
|
};
|
|
2087
2139
|
var promptSetEndpointMultipliersTool = {
|
|
2088
2140
|
name: "prompt.set_endpoint_multipliers",
|
|
2089
|
-
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
|
|
2141
|
+
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.",
|
|
2090
2142
|
inputSchema: z9.object({
|
|
2091
2143
|
mockId: z9.string(),
|
|
2092
2144
|
endpointId: z9.string(),
|
|
@@ -2097,6 +2149,9 @@ var promptSetEndpointMultipliersTool = {
|
|
|
2097
2149
|
const mock = state.synced.mockServers[input.mockId];
|
|
2098
2150
|
if (!mock) return { ok: false, error: "mock not found" };
|
|
2099
2151
|
const multipliers = input.multipliers;
|
|
2152
|
+
if (multipliers.length > MAX_RESPONSE_MULTIPLIERS) {
|
|
2153
|
+
return { ok: false, error: "too many multipliers" };
|
|
2154
|
+
}
|
|
2100
2155
|
const next = patchEndpoint(mock, input.endpointId, (e) => ({
|
|
2101
2156
|
...e,
|
|
2102
2157
|
defaultResponse: {
|
|
@@ -2117,16 +2172,196 @@ var promptSetEndpointMultipliersTool = {
|
|
|
2117
2172
|
return { ok: true, changedIds: out.changedIds };
|
|
2118
2173
|
}
|
|
2119
2174
|
};
|
|
2175
|
+
var PARAM_NL = z9.object({
|
|
2176
|
+
name: z9.string(),
|
|
2177
|
+
typeHint: z9.string().optional(),
|
|
2178
|
+
required: z9.boolean().optional(),
|
|
2179
|
+
description: z9.string().optional(),
|
|
2180
|
+
example: z9.string().optional()
|
|
2181
|
+
});
|
|
2182
|
+
var promptSetEndpointRequestSchemaTool = {
|
|
2183
|
+
name: "prompt.set_endpoint_request_schema",
|
|
2184
|
+
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).",
|
|
2185
|
+
inputSchema: z9.object({
|
|
2186
|
+
mockId: z9.string(),
|
|
2187
|
+
endpointId: z9.string(),
|
|
2188
|
+
pathParams: z9.array(PARAM_NL).default([]),
|
|
2189
|
+
queryParams: z9.array(PARAM_NL).default([]),
|
|
2190
|
+
headers: z9.array(PARAM_NL).default([]),
|
|
2191
|
+
cookies: z9.array(PARAM_NL).default([]),
|
|
2192
|
+
body: z9.object({ description: z9.string().optional(), example: z9.string().optional() }).optional()
|
|
2193
|
+
}),
|
|
2194
|
+
async handler(input, ctx) {
|
|
2195
|
+
const state = await ctx.workspace.read();
|
|
2196
|
+
const mock = state.synced.mockServers[input.mockId];
|
|
2197
|
+
if (!mock) return { ok: false, error: "mock not found" };
|
|
2198
|
+
const toParams = (list) => list.map((p) => ({
|
|
2199
|
+
id: generateId3(),
|
|
2200
|
+
name: p.name,
|
|
2201
|
+
typeHint: p.typeHint,
|
|
2202
|
+
required: p.required,
|
|
2203
|
+
description: p.description,
|
|
2204
|
+
example: p.example
|
|
2205
|
+
}));
|
|
2206
|
+
const body = input.body && (input.body.description || input.body.example) ? input.body : void 0;
|
|
2207
|
+
const next = patchEndpoint(mock, input.endpointId, (e) => ({
|
|
2208
|
+
...e,
|
|
2209
|
+
requestSchema: {
|
|
2210
|
+
pathParams: toParams(input.pathParams),
|
|
2211
|
+
queryParams: toParams(input.queryParams),
|
|
2212
|
+
headers: toParams(input.headers),
|
|
2213
|
+
cookies: toParams(input.cookies),
|
|
2214
|
+
body
|
|
2215
|
+
}
|
|
2216
|
+
}));
|
|
2217
|
+
if (!next) return { ok: false, error: "endpoint not found" };
|
|
2218
|
+
const out = await ctx.workspace.apply({ kind: "mock.upsert", mock: next });
|
|
2219
|
+
return { ok: true, changedIds: out.changedIds };
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2120
2222
|
|
|
2121
|
-
// src/tools/
|
|
2223
|
+
// src/tools/globalAssets.ts
|
|
2122
2224
|
import { z as z10 } from "zod";
|
|
2123
|
-
import { generateId as generateId4
|
|
2225
|
+
import { generateId as generateId4 } from "@apicircle/shared";
|
|
2226
|
+
function deriveState(asset, hasPendingUpload) {
|
|
2227
|
+
const w = asset.workingBranchRef ?? null;
|
|
2228
|
+
const b = asset.baseBranchRef ?? null;
|
|
2229
|
+
if (w && b) {
|
|
2230
|
+
if (w.blobSha && b.blobSha && w.blobSha !== b.blobSha) return "diverged";
|
|
2231
|
+
return "merged";
|
|
2232
|
+
}
|
|
2233
|
+
if (w && !b) return "workingOnly";
|
|
2234
|
+
if (!w && b) return "baseOnly";
|
|
2235
|
+
if (hasPendingUpload) return "uploading";
|
|
2236
|
+
return "missing";
|
|
2237
|
+
}
|
|
2238
|
+
var globalAssetsFilesListTool = {
|
|
2239
|
+
name: "assets.list_files",
|
|
2240
|
+
description: "List every Global File Asset with its provenance state and reference count. Each entry includes id, name, filename, size, mimeType, sha256, state (uploading | workingOnly | merged | baseOnly | missing | diverged), workingBranchRef, baseBranchRef, and usage { requests, mockEndpoints, total }.",
|
|
2241
|
+
inputSchema: z10.object({}).strict(),
|
|
2242
|
+
async handler(_input, ctx) {
|
|
2243
|
+
const state = await ctx.workspace.read();
|
|
2244
|
+
const files = state.synced.globalAssets.files ?? {};
|
|
2245
|
+
const pending = state.local.pendingFileUploads ?? {};
|
|
2246
|
+
const usage = state.local.assetUsageIndex ?? {};
|
|
2247
|
+
const items = Object.values(files).map((asset) => {
|
|
2248
|
+
const hasPending = Boolean(pending[asset.id]);
|
|
2249
|
+
const u = usage[asset.id] ?? { requests: [], mockEndpoints: [], total: 0 };
|
|
2250
|
+
return {
|
|
2251
|
+
id: asset.id,
|
|
2252
|
+
name: asset.name,
|
|
2253
|
+
description: asset.description ?? null,
|
|
2254
|
+
filename: asset.filename,
|
|
2255
|
+
size: asset.size,
|
|
2256
|
+
mimeType: asset.mimeType,
|
|
2257
|
+
sha256: asset.sha256 ?? null,
|
|
2258
|
+
state: deriveState(asset, hasPending),
|
|
2259
|
+
workingBranchRef: asset.workingBranchRef ?? null,
|
|
2260
|
+
baseBranchRef: asset.baseBranchRef ?? null,
|
|
2261
|
+
usage: { ...u }
|
|
2262
|
+
};
|
|
2263
|
+
});
|
|
2264
|
+
return { count: items.length, files: items };
|
|
2265
|
+
}
|
|
2266
|
+
};
|
|
2267
|
+
var globalAssetsFilesCreateTool = {
|
|
2268
|
+
name: "assets.create_file",
|
|
2269
|
+
description: 'Register a Global File Asset entry. Bytes are NOT carried \u2014 MCP returns the new asset id and the asset enters the "missing" state. The user fills the bytes from the Global Assets panel (a "Fill bytes" button surfaces on missing-state assets) which preserves the slot id and queues the bytes for the next push. Use this when an AI client wants to claim an asset slot for a file the user will provide later.',
|
|
2270
|
+
inputSchema: z10.object({
|
|
2271
|
+
name: z10.string().min(1, "name is required"),
|
|
2272
|
+
description: z10.string().optional(),
|
|
2273
|
+
filename: z10.string().min(1, "filename is required"),
|
|
2274
|
+
size: z10.number().int().nonnegative(),
|
|
2275
|
+
mimeType: z10.string().default("application/octet-stream"),
|
|
2276
|
+
sha256: z10.string().optional()
|
|
2277
|
+
}).strict(),
|
|
2278
|
+
async handler(input, ctx) {
|
|
2279
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2280
|
+
const file = {
|
|
2281
|
+
id: generateId4(),
|
|
2282
|
+
name: input.name,
|
|
2283
|
+
description: input.description,
|
|
2284
|
+
slotId: generateId4(),
|
|
2285
|
+
filename: input.filename,
|
|
2286
|
+
size: input.size,
|
|
2287
|
+
mimeType: input.mimeType,
|
|
2288
|
+
sha256: input.sha256,
|
|
2289
|
+
createdAt: now,
|
|
2290
|
+
updatedAt: now
|
|
2291
|
+
};
|
|
2292
|
+
const out = await ctx.workspace.apply({ kind: "globalAsset.upsertFile", file });
|
|
2293
|
+
return { id: file.id, slotId: file.slotId, changedIds: out.changedIds };
|
|
2294
|
+
}
|
|
2295
|
+
};
|
|
2296
|
+
var globalAssetsFilesUpdateTool = {
|
|
2297
|
+
name: "assets.update_file",
|
|
2298
|
+
description: "Rename or re-describe a Global File Asset. Provenance refs (workingBranchRef, baseBranchRef) and binary metadata (slotId, sha256, size, mimeType) are preserved verbatim.",
|
|
2299
|
+
inputSchema: z10.object({
|
|
2300
|
+
id: z10.string().min(1),
|
|
2301
|
+
patch: z10.object({
|
|
2302
|
+
name: z10.string().optional(),
|
|
2303
|
+
description: z10.string().nullable().optional()
|
|
2304
|
+
}).strict()
|
|
2305
|
+
}).strict(),
|
|
2306
|
+
async handler(input, ctx) {
|
|
2307
|
+
const state = await ctx.workspace.read();
|
|
2308
|
+
const existing = state.synced.globalAssets.files?.[input.id];
|
|
2309
|
+
if (!existing) {
|
|
2310
|
+
return { found: false, changedIds: [] };
|
|
2311
|
+
}
|
|
2312
|
+
const next = {
|
|
2313
|
+
...existing,
|
|
2314
|
+
name: input.patch.name ?? existing.name,
|
|
2315
|
+
description: input.patch.description === null ? void 0 : input.patch.description ?? existing.description
|
|
2316
|
+
};
|
|
2317
|
+
const out = await ctx.workspace.apply({ kind: "globalAsset.upsertFile", file: next });
|
|
2318
|
+
return { found: true, id: next.id, changedIds: out.changedIds };
|
|
2319
|
+
}
|
|
2320
|
+
};
|
|
2321
|
+
var globalAssetsFilesDeleteTool = {
|
|
2322
|
+
name: "assets.delete_file",
|
|
2323
|
+
description: "Delete a Global File Asset. Cascades \u2014 every request body and mock-response body that referenced the asset is unbound in the same mutation. The result envelope includes the consumer list that was cleared so the caller can report what changed.",
|
|
2324
|
+
inputSchema: z10.object({ id: z10.string().min(1) }).strict(),
|
|
2325
|
+
async handler(input, ctx) {
|
|
2326
|
+
const before = await ctx.workspace.read();
|
|
2327
|
+
const usage = before.local.assetUsageIndex?.[input.id] ?? {
|
|
2328
|
+
requests: [],
|
|
2329
|
+
mockEndpoints: [],
|
|
2330
|
+
total: 0
|
|
2331
|
+
};
|
|
2332
|
+
const existing = before.synced.globalAssets.files?.[input.id];
|
|
2333
|
+
if (!existing) {
|
|
2334
|
+
return { found: false, changedIds: [] };
|
|
2335
|
+
}
|
|
2336
|
+
const out = await ctx.workspace.apply({ kind: "globalAsset.removeFile", id: input.id });
|
|
2337
|
+
return {
|
|
2338
|
+
found: true,
|
|
2339
|
+
id: input.id,
|
|
2340
|
+
filename: existing.filename,
|
|
2341
|
+
unbound: {
|
|
2342
|
+
requests: usage.requests,
|
|
2343
|
+
mockEndpoints: usage.mockEndpoints,
|
|
2344
|
+
total: usage.total
|
|
2345
|
+
},
|
|
2346
|
+
changedIds: out.changedIds
|
|
2347
|
+
};
|
|
2348
|
+
}
|
|
2349
|
+
};
|
|
2350
|
+
|
|
2351
|
+
// src/tools/mocks.ts
|
|
2352
|
+
import { z as z11 } from "zod";
|
|
2353
|
+
import {
|
|
2354
|
+
generateId as generateId5,
|
|
2355
|
+
makeDefaultMockResponse as makeDefaultMockResponse2,
|
|
2356
|
+
makeDefaultRequestSchema as makeDefaultRequestSchema2,
|
|
2357
|
+
MAX_RESPONSE_MULTIPLIERS as MAX_RESPONSE_MULTIPLIERS2
|
|
2358
|
+
} from "@apicircle/shared";
|
|
2124
2359
|
import { parseSourceToEndpoints } from "@apicircle/mock-server-core";
|
|
2125
2360
|
async function ingestSource(source, name) {
|
|
2126
2361
|
const { endpoints, warnings } = await parseSourceToEndpoints(source);
|
|
2127
2362
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2128
2363
|
const mock = {
|
|
2129
|
-
id:
|
|
2364
|
+
id: generateId5(),
|
|
2130
2365
|
name,
|
|
2131
2366
|
source,
|
|
2132
2367
|
endpoints,
|
|
@@ -2142,10 +2377,10 @@ async function ingestSource(source, name) {
|
|
|
2142
2377
|
var mockCreateFromOpenApiTool = {
|
|
2143
2378
|
name: "mock.create_from_openapi",
|
|
2144
2379
|
description: "Create a mock server from an OpenAPI / Swagger spec (YAML or JSON).",
|
|
2145
|
-
inputSchema:
|
|
2146
|
-
name:
|
|
2147
|
-
spec:
|
|
2148
|
-
format:
|
|
2380
|
+
inputSchema: z11.object({
|
|
2381
|
+
name: z11.string(),
|
|
2382
|
+
spec: z11.string().min(1),
|
|
2383
|
+
format: z11.enum(["json", "yaml"]).default("json")
|
|
2149
2384
|
}),
|
|
2150
2385
|
async handler(input, ctx) {
|
|
2151
2386
|
const { mock, warnings } = await ingestSource(
|
|
@@ -2164,7 +2399,7 @@ var mockCreateFromOpenApiTool = {
|
|
|
2164
2399
|
var mockCreateFromPostmanTool = {
|
|
2165
2400
|
name: "mock.create_from_postman",
|
|
2166
2401
|
description: "Create a mock server from a Postman v2/v2.1 collection.",
|
|
2167
|
-
inputSchema:
|
|
2402
|
+
inputSchema: z11.object({ name: z11.string(), collection: z11.string().min(1) }),
|
|
2168
2403
|
async handler(input, ctx) {
|
|
2169
2404
|
const { mock, warnings } = await ingestSource(
|
|
2170
2405
|
{ kind: "postman", collection: input.collection },
|
|
@@ -2182,7 +2417,7 @@ var mockCreateFromPostmanTool = {
|
|
|
2182
2417
|
var mockCreateFromInsomniaTool = {
|
|
2183
2418
|
name: "mock.create_from_insomnia",
|
|
2184
2419
|
description: "Create a mock server from an Insomnia v4 export.",
|
|
2185
|
-
inputSchema:
|
|
2420
|
+
inputSchema: z11.object({ name: z11.string(), export: z11.string().min(1) }),
|
|
2186
2421
|
async handler(input, ctx) {
|
|
2187
2422
|
const { mock, warnings } = await ingestSource(
|
|
2188
2423
|
{ kind: "insomnia", export: input.export },
|
|
@@ -2200,7 +2435,7 @@ var mockCreateFromInsomniaTool = {
|
|
|
2200
2435
|
var mockImportPostmanMockCollectionTool = {
|
|
2201
2436
|
name: "mock.import_postman_mock_collection",
|
|
2202
2437
|
description: "Import a Postman Mock Collection (collections previously hosted on Postman's mock service). Same parser as a regular Postman collection but marked as a mock import.",
|
|
2203
|
-
inputSchema:
|
|
2438
|
+
inputSchema: z11.object({ name: z11.string(), collection: z11.string().min(1) }),
|
|
2204
2439
|
async handler(input, ctx) {
|
|
2205
2440
|
const { mock, warnings } = await ingestSource(
|
|
2206
2441
|
{ kind: "postman", collection: input.collection },
|
|
@@ -2218,7 +2453,7 @@ var mockImportPostmanMockCollectionTool = {
|
|
|
2218
2453
|
var mockListTool = {
|
|
2219
2454
|
name: "mock.list",
|
|
2220
2455
|
description: "List all mock servers in the workspace plus their runtime status (running / stopped, port).",
|
|
2221
|
-
inputSchema:
|
|
2456
|
+
inputSchema: z11.object({}),
|
|
2222
2457
|
async handler(_input, ctx) {
|
|
2223
2458
|
const state = await ctx.workspace.read();
|
|
2224
2459
|
const running = await ctx.mock.list();
|
|
@@ -2239,10 +2474,14 @@ var mockListTool = {
|
|
|
2239
2474
|
};
|
|
2240
2475
|
var mockStartTool = {
|
|
2241
2476
|
name: "mock.start",
|
|
2242
|
-
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.",
|
|
2243
|
-
inputSchema:
|
|
2244
|
-
id:
|
|
2245
|
-
|
|
2477
|
+
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`.",
|
|
2478
|
+
inputSchema: z11.object({
|
|
2479
|
+
id: z11.string(),
|
|
2480
|
+
// Mirrors the UI 1024-65535 window: <1024 needs OS privileges, and
|
|
2481
|
+
// outside that window the runtime would throw INVALID_PORT anyway —
|
|
2482
|
+
// surface the rejection at the tool boundary so the client sees a
|
|
2483
|
+
// schema error instead of a runtime exception.
|
|
2484
|
+
port: z11.number().int().min(1024).max(65535).optional()
|
|
2246
2485
|
}),
|
|
2247
2486
|
async handler(input, ctx) {
|
|
2248
2487
|
const state = await ctx.workspace.read();
|
|
@@ -2259,7 +2498,7 @@ var mockStartTool = {
|
|
|
2259
2498
|
var mockStopTool = {
|
|
2260
2499
|
name: "mock.stop",
|
|
2261
2500
|
description: "Stop a running mock server by id (no-op if not running).",
|
|
2262
|
-
inputSchema:
|
|
2501
|
+
inputSchema: z11.object({ id: z11.string() }),
|
|
2263
2502
|
async handler(input, ctx) {
|
|
2264
2503
|
try {
|
|
2265
2504
|
await ctx.mock.stop(input.id);
|
|
@@ -2272,7 +2511,7 @@ var mockStopTool = {
|
|
|
2272
2511
|
var mockDeleteTool = {
|
|
2273
2512
|
name: "mock.delete",
|
|
2274
2513
|
description: "Delete a mock server definition. Stops it first if it's running.",
|
|
2275
|
-
inputSchema:
|
|
2514
|
+
inputSchema: z11.object({ id: z11.string() }),
|
|
2276
2515
|
async handler(input, ctx) {
|
|
2277
2516
|
try {
|
|
2278
2517
|
await ctx.mock.stop(input.id);
|
|
@@ -2282,18 +2521,49 @@ var mockDeleteTool = {
|
|
|
2282
2521
|
return { ok: true, changedIds: out.changedIds };
|
|
2283
2522
|
}
|
|
2284
2523
|
};
|
|
2285
|
-
var
|
|
2524
|
+
var mockSetDefaultPortTool = {
|
|
2525
|
+
name: "mock.set_default_port",
|
|
2526
|
+
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.",
|
|
2527
|
+
inputSchema: z11.object({
|
|
2528
|
+
id: z11.string(),
|
|
2529
|
+
defaultPort: z11.number().int().min(1024).max(65535).nullable()
|
|
2530
|
+
}),
|
|
2531
|
+
async handler(input, ctx) {
|
|
2532
|
+
const state = await ctx.workspace.read();
|
|
2533
|
+
const mock = state.synced.mockServers[input.id];
|
|
2534
|
+
if (!mock) return { ok: false, error: "mock not found" };
|
|
2535
|
+
if (mock.defaultPort === input.defaultPort) {
|
|
2536
|
+
return { ok: true, defaultPort: mock.defaultPort, changed: false };
|
|
2537
|
+
}
|
|
2538
|
+
const next = {
|
|
2539
|
+
...mock,
|
|
2540
|
+
defaultPort: input.defaultPort,
|
|
2541
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2542
|
+
};
|
|
2543
|
+
const out = await ctx.workspace.apply({ kind: "mock.upsert", mock: next });
|
|
2544
|
+
return {
|
|
2545
|
+
ok: true,
|
|
2546
|
+
defaultPort: input.defaultPort,
|
|
2547
|
+
changed: true,
|
|
2548
|
+
changedIds: out.changedIds
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
};
|
|
2552
|
+
var HTTP_METHOD3 = z11.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
2286
2553
|
var mockCreateManualTool = {
|
|
2287
2554
|
name: "mock.create_manual",
|
|
2288
2555
|
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.",
|
|
2289
|
-
inputSchema:
|
|
2290
|
-
name:
|
|
2291
|
-
|
|
2556
|
+
inputSchema: z11.object({
|
|
2557
|
+
name: z11.string().min(1),
|
|
2558
|
+
// Same 1024-65535 window as mock.start / mock.set_default_port — pin
|
|
2559
|
+
// the validation at the tool boundary so a malformed AI call doesn't
|
|
2560
|
+
// persist a port the runtime will later reject as INVALID_PORT.
|
|
2561
|
+
defaultPort: z11.number().int().min(1024).max(65535).nullable().optional()
|
|
2292
2562
|
}),
|
|
2293
2563
|
async handler(input, ctx) {
|
|
2294
2564
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2295
2565
|
const mock = {
|
|
2296
|
-
id:
|
|
2566
|
+
id: generateId5(),
|
|
2297
2567
|
name: input.name,
|
|
2298
2568
|
source: { kind: "manual", endpoints: [] },
|
|
2299
2569
|
endpoints: [],
|
|
@@ -2311,7 +2581,7 @@ var mockCreateManualTool = {
|
|
|
2311
2581
|
var mockListEndpointsTool = {
|
|
2312
2582
|
name: "mock.list_endpoints",
|
|
2313
2583
|
description: "List endpoints for a mock server (id, method, path, name).",
|
|
2314
|
-
inputSchema:
|
|
2584
|
+
inputSchema: z11.object({ mockId: z11.string() }),
|
|
2315
2585
|
async handler(input, ctx) {
|
|
2316
2586
|
const state = await ctx.workspace.read();
|
|
2317
2587
|
const mock = state.synced.mockServers[input.mockId];
|
|
@@ -2330,10 +2600,10 @@ var mockListEndpointsTool = {
|
|
|
2330
2600
|
};
|
|
2331
2601
|
}
|
|
2332
2602
|
};
|
|
2333
|
-
var ENDPOINT_RESPONSE2 =
|
|
2334
|
-
status:
|
|
2335
|
-
jsonBody:
|
|
2336
|
-
contentType:
|
|
2603
|
+
var ENDPOINT_RESPONSE2 = z11.object({
|
|
2604
|
+
status: z11.number().int().min(100).max(599).default(200),
|
|
2605
|
+
jsonBody: z11.string().default("{}"),
|
|
2606
|
+
contentType: z11.string().default("application/json")
|
|
2337
2607
|
});
|
|
2338
2608
|
function buildDefaultEndpoint(args) {
|
|
2339
2609
|
const response = args.response ?? {
|
|
@@ -2343,7 +2613,7 @@ function buildDefaultEndpoint(args) {
|
|
|
2343
2613
|
};
|
|
2344
2614
|
const headers = [{ key: "Content-Type", value: response.contentType, enabled: true }];
|
|
2345
2615
|
return {
|
|
2346
|
-
id:
|
|
2616
|
+
id: generateId5(),
|
|
2347
2617
|
name: args.name ?? `${args.method} ${args.pathPattern}`,
|
|
2348
2618
|
method: args.method,
|
|
2349
2619
|
pathPattern: args.pathPattern,
|
|
@@ -2362,12 +2632,12 @@ function buildDefaultEndpoint(args) {
|
|
|
2362
2632
|
var mockAddEndpointTool = {
|
|
2363
2633
|
name: "mock.add_endpoint",
|
|
2364
2634
|
description: "Append a new endpoint to a mock server. Defaults to a 200 JSON response of `{}`. Returns the new endpoint id.",
|
|
2365
|
-
inputSchema:
|
|
2366
|
-
mockId:
|
|
2635
|
+
inputSchema: z11.object({
|
|
2636
|
+
mockId: z11.string(),
|
|
2367
2637
|
method: HTTP_METHOD3,
|
|
2368
|
-
pathPattern:
|
|
2369
|
-
name:
|
|
2370
|
-
description:
|
|
2638
|
+
pathPattern: z11.string().min(1),
|
|
2639
|
+
name: z11.string().optional(),
|
|
2640
|
+
description: z11.string().optional(),
|
|
2371
2641
|
response: ENDPOINT_RESPONSE2.optional()
|
|
2372
2642
|
}),
|
|
2373
2643
|
async handler(input, ctx) {
|
|
@@ -2390,13 +2660,13 @@ var mockAddEndpointTool = {
|
|
|
2390
2660
|
var mockUpdateEndpointTool = {
|
|
2391
2661
|
name: "mock.update_endpoint",
|
|
2392
2662
|
description: "Patch fields on a single mock endpoint (method, pathPattern, name, description, defaultResponse status / contentType / json body). Pass only the fields you want to change.",
|
|
2393
|
-
inputSchema:
|
|
2394
|
-
mockId:
|
|
2395
|
-
endpointId:
|
|
2663
|
+
inputSchema: z11.object({
|
|
2664
|
+
mockId: z11.string(),
|
|
2665
|
+
endpointId: z11.string(),
|
|
2396
2666
|
method: HTTP_METHOD3.optional(),
|
|
2397
|
-
pathPattern:
|
|
2398
|
-
name:
|
|
2399
|
-
description:
|
|
2667
|
+
pathPattern: z11.string().optional(),
|
|
2668
|
+
name: z11.string().optional(),
|
|
2669
|
+
description: z11.string().optional(),
|
|
2400
2670
|
response: ENDPOINT_RESPONSE2.partial().optional()
|
|
2401
2671
|
}),
|
|
2402
2672
|
async handler(input, ctx) {
|
|
@@ -2437,7 +2707,7 @@ var mockUpdateEndpointTool = {
|
|
|
2437
2707
|
var mockDeleteEndpointTool = {
|
|
2438
2708
|
name: "mock.delete_endpoint",
|
|
2439
2709
|
description: "Remove an endpoint from a mock server.",
|
|
2440
|
-
inputSchema:
|
|
2710
|
+
inputSchema: z11.object({ mockId: z11.string(), endpointId: z11.string() }),
|
|
2441
2711
|
async handler(input, ctx) {
|
|
2442
2712
|
const state = await ctx.workspace.read();
|
|
2443
2713
|
const mock = state.synced.mockServers[input.mockId];
|
|
@@ -2457,9 +2727,9 @@ var mockDeleteEndpointTool = {
|
|
|
2457
2727
|
return { ok: true, changedIds: out.changedIds };
|
|
2458
2728
|
}
|
|
2459
2729
|
};
|
|
2460
|
-
var VALIDATION_RULE =
|
|
2461
|
-
id:
|
|
2462
|
-
kind:
|
|
2730
|
+
var VALIDATION_RULE = z11.object({
|
|
2731
|
+
id: z11.string().optional(),
|
|
2732
|
+
kind: z11.enum([
|
|
2463
2733
|
"header-required",
|
|
2464
2734
|
"header-equals",
|
|
2465
2735
|
"header-matches",
|
|
@@ -2470,43 +2740,46 @@ var VALIDATION_RULE = z10.object({
|
|
|
2470
2740
|
"body-required",
|
|
2471
2741
|
"content-type-equals"
|
|
2472
2742
|
]),
|
|
2473
|
-
target:
|
|
2474
|
-
expected:
|
|
2475
|
-
message:
|
|
2476
|
-
enabled:
|
|
2477
|
-
failResponse:
|
|
2478
|
-
status:
|
|
2479
|
-
jsonBody:
|
|
2743
|
+
target: z11.string().default(""),
|
|
2744
|
+
expected: z11.string().optional(),
|
|
2745
|
+
message: z11.string().optional(),
|
|
2746
|
+
enabled: z11.boolean().default(true),
|
|
2747
|
+
failResponse: z11.object({
|
|
2748
|
+
status: z11.number().int().min(100).max(599).default(400),
|
|
2749
|
+
jsonBody: z11.string().default('{"error":"validation failed"}')
|
|
2480
2750
|
}).default({})
|
|
2481
2751
|
});
|
|
2482
|
-
var CONDITION_CLAUSE =
|
|
2483
|
-
id:
|
|
2484
|
-
scope:
|
|
2485
|
-
target:
|
|
2486
|
-
op:
|
|
2487
|
-
value:
|
|
2752
|
+
var CONDITION_CLAUSE = z11.object({
|
|
2753
|
+
id: z11.string().optional(),
|
|
2754
|
+
scope: z11.enum(["query", "pathParam", "header", "cookie", "body-json-path"]),
|
|
2755
|
+
target: z11.string(),
|
|
2756
|
+
op: z11.enum(["equals", "not-equals", "matches", "gt", "lt", "gte", "lte", "present", "absent"]),
|
|
2757
|
+
value: z11.string().optional()
|
|
2488
2758
|
});
|
|
2489
|
-
var RESPONSE_RULE =
|
|
2490
|
-
id:
|
|
2491
|
-
name:
|
|
2492
|
-
enabled:
|
|
2493
|
-
when
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2759
|
+
var RESPONSE_RULE = z11.object({
|
|
2760
|
+
id: z11.string().optional(),
|
|
2761
|
+
name: z11.string(),
|
|
2762
|
+
enabled: z11.boolean().default(true),
|
|
2763
|
+
// At least one clause — a rule with no `when` is dead (the runtime engine
|
|
2764
|
+
// skips clause-less rules), so it can never fire. The VS Code endpoint parser
|
|
2765
|
+
// rejects the same shape, keeping the surfaces consistent.
|
|
2766
|
+
when: z11.array(CONDITION_CLAUSE).min(1),
|
|
2767
|
+
response: z11.object({
|
|
2768
|
+
status: z11.number().int().min(100).max(599).default(200),
|
|
2769
|
+
jsonBody: z11.string().default("{}")
|
|
2497
2770
|
}).default({})
|
|
2498
2771
|
});
|
|
2499
|
-
var MULTIPLIER =
|
|
2500
|
-
id:
|
|
2501
|
-
name:
|
|
2502
|
-
source:
|
|
2503
|
-
kind:
|
|
2504
|
-
key:
|
|
2505
|
-
}),
|
|
2506
|
-
targetJsonPath:
|
|
2507
|
-
defaultCount:
|
|
2508
|
-
min:
|
|
2509
|
-
max:
|
|
2772
|
+
var MULTIPLIER = z11.object({
|
|
2773
|
+
id: z11.string().optional(),
|
|
2774
|
+
name: z11.string().optional(),
|
|
2775
|
+
source: z11.object({
|
|
2776
|
+
kind: z11.enum(["query", "pathParam", "header", "body-json-path"]),
|
|
2777
|
+
key: z11.string()
|
|
2778
|
+
}),
|
|
2779
|
+
targetJsonPath: z11.string(),
|
|
2780
|
+
defaultCount: z11.number().int().nonnegative().default(0),
|
|
2781
|
+
min: z11.number().int().nonnegative().optional(),
|
|
2782
|
+
max: z11.number().int().nonnegative().optional()
|
|
2510
2783
|
});
|
|
2511
2784
|
function defaultJsonResponseConfig(args) {
|
|
2512
2785
|
return {
|
|
@@ -2531,10 +2804,10 @@ function patchEndpoint2(mock, endpointId, patcher) {
|
|
|
2531
2804
|
var mockSetValidationRulesTool = {
|
|
2532
2805
|
name: "mock.set_validation_rules",
|
|
2533
2806
|
description: "Replace an endpoint's validation rules. Rules without an `id` get a fresh one; existing rules can keep theirs to preserve client-side selection state. Empty array clears all rules.",
|
|
2534
|
-
inputSchema:
|
|
2535
|
-
mockId:
|
|
2536
|
-
endpointId:
|
|
2537
|
-
rules:
|
|
2807
|
+
inputSchema: z11.object({
|
|
2808
|
+
mockId: z11.string(),
|
|
2809
|
+
endpointId: z11.string(),
|
|
2810
|
+
rules: z11.array(VALIDATION_RULE)
|
|
2538
2811
|
}),
|
|
2539
2812
|
async handler(input, ctx) {
|
|
2540
2813
|
const state = await ctx.workspace.read();
|
|
@@ -2544,7 +2817,7 @@ var mockSetValidationRulesTool = {
|
|
|
2544
2817
|
const next = patchEndpoint2(mock, input.endpointId, (e) => ({
|
|
2545
2818
|
...e,
|
|
2546
2819
|
requestValidation: rules.map((r) => ({
|
|
2547
|
-
id: r.id ??
|
|
2820
|
+
id: r.id ?? generateId5(),
|
|
2548
2821
|
kind: r.kind,
|
|
2549
2822
|
target: r.target,
|
|
2550
2823
|
expected: r.expected,
|
|
@@ -2561,10 +2834,10 @@ var mockSetValidationRulesTool = {
|
|
|
2561
2834
|
var mockSetResponseRulesTool = {
|
|
2562
2835
|
name: "mock.set_response_rules",
|
|
2563
2836
|
description: "Replace an endpoint's conditional response rules. Rules fire in order; the first whose every clause matches wins. Disabled rules are skipped. Empty array falls back to defaultResponse.",
|
|
2564
|
-
inputSchema:
|
|
2565
|
-
mockId:
|
|
2566
|
-
endpointId:
|
|
2567
|
-
rules:
|
|
2837
|
+
inputSchema: z11.object({
|
|
2838
|
+
mockId: z11.string(),
|
|
2839
|
+
endpointId: z11.string(),
|
|
2840
|
+
rules: z11.array(RESPONSE_RULE)
|
|
2568
2841
|
}),
|
|
2569
2842
|
async handler(input, ctx) {
|
|
2570
2843
|
const state = await ctx.workspace.read();
|
|
@@ -2574,11 +2847,11 @@ var mockSetResponseRulesTool = {
|
|
|
2574
2847
|
const next = patchEndpoint2(mock, input.endpointId, (e) => ({
|
|
2575
2848
|
...e,
|
|
2576
2849
|
responseRules: rules.map((r) => ({
|
|
2577
|
-
id: r.id ??
|
|
2850
|
+
id: r.id ?? generateId5(),
|
|
2578
2851
|
name: r.name,
|
|
2579
2852
|
enabled: r.enabled,
|
|
2580
2853
|
when: r.when.map((c) => ({
|
|
2581
|
-
id: c.id ??
|
|
2854
|
+
id: c.id ?? generateId5(),
|
|
2582
2855
|
scope: c.scope,
|
|
2583
2856
|
target: c.target,
|
|
2584
2857
|
op: c.op,
|
|
@@ -2592,25 +2865,80 @@ var mockSetResponseRulesTool = {
|
|
|
2592
2865
|
return { ok: true, changedIds: out.changedIds };
|
|
2593
2866
|
}
|
|
2594
2867
|
};
|
|
2868
|
+
var PARAM = z11.object({
|
|
2869
|
+
id: z11.string().optional(),
|
|
2870
|
+
name: z11.string(),
|
|
2871
|
+
typeHint: z11.string().optional(),
|
|
2872
|
+
required: z11.boolean().optional(),
|
|
2873
|
+
description: z11.string().optional(),
|
|
2874
|
+
example: z11.string().optional()
|
|
2875
|
+
});
|
|
2876
|
+
var REQUEST_SCHEMA_BODY = z11.object({ description: z11.string().optional(), example: z11.string().optional() }).optional();
|
|
2877
|
+
function normalizeSchemaBody(body) {
|
|
2878
|
+
if (!body || !body.description && !body.example) return void 0;
|
|
2879
|
+
return body;
|
|
2880
|
+
}
|
|
2881
|
+
var mockSetRequestSchemaTool = {
|
|
2882
|
+
name: "mock.set_request_schema",
|
|
2883
|
+
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.",
|
|
2884
|
+
inputSchema: z11.object({
|
|
2885
|
+
mockId: z11.string(),
|
|
2886
|
+
endpointId: z11.string(),
|
|
2887
|
+
pathParams: z11.array(PARAM).default([]),
|
|
2888
|
+
queryParams: z11.array(PARAM).default([]),
|
|
2889
|
+
headers: z11.array(PARAM).default([]),
|
|
2890
|
+
cookies: z11.array(PARAM).default([]),
|
|
2891
|
+
body: REQUEST_SCHEMA_BODY
|
|
2892
|
+
}),
|
|
2893
|
+
async handler(input, ctx) {
|
|
2894
|
+
const state = await ctx.workspace.read();
|
|
2895
|
+
const mock = state.synced.mockServers[input.mockId];
|
|
2896
|
+
if (!mock) return { ok: false, error: "mock not found" };
|
|
2897
|
+
const toParams = (list) => list.map((p) => ({
|
|
2898
|
+
id: p.id ?? generateId5(),
|
|
2899
|
+
name: p.name,
|
|
2900
|
+
typeHint: p.typeHint,
|
|
2901
|
+
required: p.required,
|
|
2902
|
+
description: p.description,
|
|
2903
|
+
example: p.example
|
|
2904
|
+
}));
|
|
2905
|
+
const next = patchEndpoint2(mock, input.endpointId, (e) => ({
|
|
2906
|
+
...e,
|
|
2907
|
+
requestSchema: {
|
|
2908
|
+
pathParams: toParams(input.pathParams),
|
|
2909
|
+
queryParams: toParams(input.queryParams),
|
|
2910
|
+
headers: toParams(input.headers),
|
|
2911
|
+
cookies: toParams(input.cookies),
|
|
2912
|
+
body: normalizeSchemaBody(input.body)
|
|
2913
|
+
}
|
|
2914
|
+
}));
|
|
2915
|
+
if (!next) return { ok: false, error: "endpoint not found" };
|
|
2916
|
+
const out = await ctx.workspace.apply({ kind: "mock.upsert", mock: next });
|
|
2917
|
+
return { ok: true, changedIds: out.changedIds };
|
|
2918
|
+
}
|
|
2919
|
+
};
|
|
2595
2920
|
var mockSetMultipliersTool = {
|
|
2596
2921
|
name: "mock.set_multipliers",
|
|
2597
|
-
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
|
|
2598
|
-
inputSchema:
|
|
2599
|
-
mockId:
|
|
2600
|
-
endpointId:
|
|
2601
|
-
multipliers:
|
|
2922
|
+
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.",
|
|
2923
|
+
inputSchema: z11.object({
|
|
2924
|
+
mockId: z11.string(),
|
|
2925
|
+
endpointId: z11.string(),
|
|
2926
|
+
multipliers: z11.array(MULTIPLIER)
|
|
2602
2927
|
}),
|
|
2603
2928
|
async handler(input, ctx) {
|
|
2604
2929
|
const state = await ctx.workspace.read();
|
|
2605
2930
|
const mock = state.synced.mockServers[input.mockId];
|
|
2606
2931
|
if (!mock) return { ok: false, error: "mock not found" };
|
|
2607
2932
|
const multipliers = input.multipliers;
|
|
2933
|
+
if (multipliers.length > MAX_RESPONSE_MULTIPLIERS2) {
|
|
2934
|
+
return { ok: false, error: "too many multipliers" };
|
|
2935
|
+
}
|
|
2608
2936
|
const next = patchEndpoint2(mock, input.endpointId, (e) => ({
|
|
2609
2937
|
...e,
|
|
2610
2938
|
defaultResponse: {
|
|
2611
2939
|
...e.defaultResponse,
|
|
2612
2940
|
multipliers: multipliers.length === 0 ? void 0 : multipliers.map((m) => ({
|
|
2613
|
-
id: m.id ??
|
|
2941
|
+
id: m.id ?? generateId5(),
|
|
2614
2942
|
name: m.name,
|
|
2615
2943
|
source: { kind: m.source.kind, key: m.source.key },
|
|
2616
2944
|
targetJsonPath: m.targetJsonPath,
|
|
@@ -2626,123 +2954,1434 @@ var mockSetMultipliersTool = {
|
|
|
2626
2954
|
}
|
|
2627
2955
|
};
|
|
2628
2956
|
|
|
2629
|
-
// src/tools/
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
environmentSetPriorityTool,
|
|
2656
|
-
environmentExportTool,
|
|
2657
|
-
environmentImportTool,
|
|
2658
|
-
planCreateTool,
|
|
2659
|
-
planRunTool,
|
|
2660
|
-
planReadTool,
|
|
2661
|
-
planUpdateTool,
|
|
2662
|
-
planDeleteTool,
|
|
2663
|
-
planAddStepTool,
|
|
2664
|
-
planRemoveStepTool,
|
|
2665
|
-
planReorderStepsTool,
|
|
2666
|
-
planSetVariablesTool,
|
|
2667
|
-
assertionCreateTool,
|
|
2668
|
-
assertionReadTool,
|
|
2669
|
-
assertionUpdateTool,
|
|
2670
|
-
assertionDeleteTool,
|
|
2671
|
-
historyListRunsTool,
|
|
2672
|
-
historyGetRunTool,
|
|
2673
|
-
historyDeleteRunTool,
|
|
2674
|
-
historyPurgeTool,
|
|
2675
|
-
codebaseExtractCollectionTool,
|
|
2676
|
-
promptCreateEnvironmentTool,
|
|
2677
|
-
promptCreateAssertionTool,
|
|
2678
|
-
promptCreatePlanTool,
|
|
2679
|
-
promptCreateRequestTool,
|
|
2680
|
-
promptUpdateRequestTool,
|
|
2681
|
-
promptCreateFolderTreeTool,
|
|
2682
|
-
promptAddPlanStepsTool,
|
|
2683
|
-
promptSetPlanVariablesTool,
|
|
2684
|
-
promptCreateMockServerTool,
|
|
2685
|
-
promptAddMockEndpointTool,
|
|
2686
|
-
promptSetEndpointValidationRulesTool,
|
|
2687
|
-
promptSetEndpointResponseRulesTool,
|
|
2688
|
-
promptSetEndpointMultipliersTool,
|
|
2689
|
-
mockCreateFromOpenApiTool,
|
|
2690
|
-
mockCreateFromPostmanTool,
|
|
2691
|
-
mockCreateFromInsomniaTool,
|
|
2692
|
-
mockCreateManualTool,
|
|
2693
|
-
mockListTool,
|
|
2694
|
-
mockListEndpointsTool,
|
|
2695
|
-
mockStartTool,
|
|
2696
|
-
mockStopTool,
|
|
2697
|
-
mockDeleteTool,
|
|
2698
|
-
mockAddEndpointTool,
|
|
2699
|
-
mockUpdateEndpointTool,
|
|
2700
|
-
mockDeleteEndpointTool,
|
|
2701
|
-
mockSetValidationRulesTool,
|
|
2702
|
-
mockSetResponseRulesTool,
|
|
2703
|
-
mockSetMultipliersTool,
|
|
2704
|
-
mockImportPostmanMockCollectionTool
|
|
2705
|
-
];
|
|
2706
|
-
function getTool(name) {
|
|
2707
|
-
return TOOL_REGISTRY.find((t) => t.name === name);
|
|
2708
|
-
}
|
|
2709
|
-
|
|
2710
|
-
// src/providers/Workspaces.ts
|
|
2711
|
-
var SingleWorkspaceAdapter = class {
|
|
2712
|
-
constructor(provider, workspaceId, displayName = "Workspace") {
|
|
2713
|
-
this.provider = provider;
|
|
2714
|
-
this.workspaceId = workspaceId;
|
|
2715
|
-
this.displayName = displayName;
|
|
2957
|
+
// src/tools/releases.ts
|
|
2958
|
+
import { z as z12 } from "zod";
|
|
2959
|
+
import { buildReleaseEntry, sortVersionsDesc } from "@apicircle/core";
|
|
2960
|
+
var releaseListTool = {
|
|
2961
|
+
name: "release.list",
|
|
2962
|
+
description: "List this workspace's published releases (newest first) with their notes, snapshot fingerprint, and deprecated / withdrawn flags. Returns the current version too.",
|
|
2963
|
+
inputSchema: z12.object({}),
|
|
2964
|
+
async handler(_input, ctx) {
|
|
2965
|
+
const state = await ctx.workspace.read();
|
|
2966
|
+
const ledger = state.synced.releases.self;
|
|
2967
|
+
if (!ledger) {
|
|
2968
|
+
return { currentVersion: null, count: 0, versions: [] };
|
|
2969
|
+
}
|
|
2970
|
+
const order = sortVersionsDesc(ledger.versions.map((v) => v.version));
|
|
2971
|
+
const byVersion = new Map(ledger.versions.map((v) => [v.version, v]));
|
|
2972
|
+
const versions = order.map((v) => byVersion.get(v)).filter((v) => v !== void 0).map((v) => ({
|
|
2973
|
+
version: v.version,
|
|
2974
|
+
publishedAt: v.publishedAt,
|
|
2975
|
+
notes: v.notes,
|
|
2976
|
+
workspaceSnapshot: v.workspaceSnapshot,
|
|
2977
|
+
deprecated: v.deprecated,
|
|
2978
|
+
yanked: v.yanked,
|
|
2979
|
+
...v.sha ? { sha: v.sha } : {},
|
|
2980
|
+
...v.tagName ? { tagName: v.tagName } : {}
|
|
2981
|
+
}));
|
|
2982
|
+
return { currentVersion: ledger.currentVersion, count: versions.length, versions };
|
|
2716
2983
|
}
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
}
|
|
2739
|
-
|
|
2984
|
+
};
|
|
2985
|
+
var releasePublishTool = {
|
|
2986
|
+
name: "release.publish",
|
|
2987
|
+
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.",
|
|
2988
|
+
inputSchema: z12.object({
|
|
2989
|
+
version: z12.string().min(1).describe('Semantic version, e.g. "1.2.0".'),
|
|
2990
|
+
notes: z12.string().default("").describe("Markdown release notes."),
|
|
2991
|
+
sha: z12.string().optional().describe("Optional source commit SHA for bookkeeping."),
|
|
2992
|
+
tagName: z12.string().optional().describe("Optional git tag name for bookkeeping.")
|
|
2993
|
+
}),
|
|
2994
|
+
async handler(input, ctx) {
|
|
2995
|
+
const state = await ctx.workspace.read();
|
|
2996
|
+
let entry;
|
|
2997
|
+
try {
|
|
2998
|
+
entry = await buildReleaseEntry(state.synced, {
|
|
2999
|
+
version: input.version,
|
|
3000
|
+
notes: input.notes,
|
|
3001
|
+
sha: input.sha,
|
|
3002
|
+
tagName: input.tagName
|
|
3003
|
+
});
|
|
3004
|
+
} catch (err) {
|
|
3005
|
+
return { ok: false, error: err instanceof Error ? err.message : "release.publish failed" };
|
|
3006
|
+
}
|
|
3007
|
+
try {
|
|
3008
|
+
const out = await ctx.workspace.apply({ kind: "release.publish", entry });
|
|
3009
|
+
const after = (await ctx.workspace.read()).synced.releases.self;
|
|
3010
|
+
return {
|
|
3011
|
+
ok: true,
|
|
3012
|
+
version: entry.version,
|
|
3013
|
+
currentVersion: after?.currentVersion ?? entry.version,
|
|
3014
|
+
workspaceSnapshot: entry.workspaceSnapshot,
|
|
3015
|
+
changedIds: out.changedIds
|
|
3016
|
+
};
|
|
3017
|
+
} catch (err) {
|
|
3018
|
+
return { ok: false, error: err instanceof Error ? err.message : "release.publish failed" };
|
|
3019
|
+
}
|
|
2740
3020
|
}
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
3021
|
+
};
|
|
3022
|
+
var releaseDeprecateTool = {
|
|
3023
|
+
name: "release.deprecate",
|
|
3024
|
+
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.",
|
|
3025
|
+
inputSchema: z12.object({ version: z12.string().min(1) }),
|
|
3026
|
+
async handler(input, ctx) {
|
|
3027
|
+
try {
|
|
3028
|
+
const out = await ctx.workspace.apply({ kind: "release.deprecate", version: input.version });
|
|
3029
|
+
return { ok: true, version: input.version, changedIds: out.changedIds };
|
|
3030
|
+
} catch (err) {
|
|
3031
|
+
return { ok: false, error: err instanceof Error ? err.message : "release.deprecate failed" };
|
|
2744
3032
|
}
|
|
2745
|
-
|
|
3033
|
+
}
|
|
3034
|
+
};
|
|
3035
|
+
var releaseYankTool = {
|
|
3036
|
+
name: "release.yank",
|
|
3037
|
+
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.",
|
|
3038
|
+
inputSchema: z12.object({ version: z12.string().min(1) }),
|
|
3039
|
+
async handler(input, ctx) {
|
|
3040
|
+
try {
|
|
3041
|
+
const out = await ctx.workspace.apply({ kind: "release.yank", version: input.version });
|
|
3042
|
+
return { ok: true, version: input.version, changedIds: out.changedIds };
|
|
3043
|
+
} catch (err) {
|
|
3044
|
+
return { ok: false, error: err instanceof Error ? err.message : "release.yank failed" };
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
};
|
|
3048
|
+
|
|
3049
|
+
// src/tools/linkedWorkspaces.ts
|
|
3050
|
+
import { z as z13 } from "zod";
|
|
3051
|
+
function summarize(link, currentVersion) {
|
|
3052
|
+
return {
|
|
3053
|
+
id: link.id,
|
|
3054
|
+
name: link.name,
|
|
3055
|
+
kind: link.kind,
|
|
3056
|
+
description: link.description,
|
|
3057
|
+
source: link.source,
|
|
3058
|
+
scope: link.scope,
|
|
3059
|
+
pinnedVersion: link.pinnedVersion,
|
|
3060
|
+
requiredSecretKeyIds: link.requiredSecretKeyIds,
|
|
3061
|
+
marketplace: link.marketplace,
|
|
3062
|
+
cachedCurrentVersion: currentVersion
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
var linkedListTool = {
|
|
3066
|
+
name: "linked.list",
|
|
3067
|
+
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.",
|
|
3068
|
+
inputSchema: z13.object({}),
|
|
3069
|
+
async handler(_input, ctx) {
|
|
3070
|
+
const state = await ctx.workspace.read();
|
|
3071
|
+
const links = Object.values(state.synced.linkedWorkspaces);
|
|
3072
|
+
return {
|
|
3073
|
+
count: links.length,
|
|
3074
|
+
links: links.map(
|
|
3075
|
+
(l) => summarize(l, state.synced.releases.perLink[l.id]?.currentVersion ?? null)
|
|
3076
|
+
)
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
};
|
|
3080
|
+
var linkedGetTool = {
|
|
3081
|
+
name: "linked.get",
|
|
3082
|
+
description: "Read one linked workspace by id, including its cached release ledger (the versions available to pin to).",
|
|
3083
|
+
inputSchema: z13.object({ id: z13.string() }),
|
|
3084
|
+
async handler(input, ctx) {
|
|
3085
|
+
const state = await ctx.workspace.read();
|
|
3086
|
+
const link = state.synced.linkedWorkspaces[input.id];
|
|
3087
|
+
if (!link) return { ok: false, error: `Linked workspace ${input.id} not found` };
|
|
3088
|
+
const ledger = state.synced.releases.perLink[input.id] ?? null;
|
|
3089
|
+
return {
|
|
3090
|
+
ok: true,
|
|
3091
|
+
link: summarize(link, ledger?.currentVersion ?? null),
|
|
3092
|
+
ledger
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
};
|
|
3096
|
+
var linkedSetConfigTool = {
|
|
3097
|
+
name: "linked.set_config",
|
|
3098
|
+
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.",
|
|
3099
|
+
inputSchema: z13.object({
|
|
3100
|
+
id: z13.string(),
|
|
3101
|
+
name: z13.string().optional(),
|
|
3102
|
+
description: z13.string().optional(),
|
|
3103
|
+
pinnedVersion: z13.string().nullable().optional().describe("null = unpin (track source HEAD)."),
|
|
3104
|
+
scope: z13.array(z13.enum(["collections", "environments"])).optional(),
|
|
3105
|
+
sessionMode: z13.enum(["workspace", "dedicated"]).optional(),
|
|
3106
|
+
requiredSecretKeyIds: z13.array(z13.string()).optional(),
|
|
3107
|
+
marketplace: z13.object({
|
|
3108
|
+
listedAs: z13.string(),
|
|
3109
|
+
tags: z13.array(z13.string()),
|
|
3110
|
+
summary: z13.string()
|
|
3111
|
+
}).nullable().optional().describe("null = clear marketplace metadata.")
|
|
3112
|
+
}),
|
|
3113
|
+
async handler(input, ctx) {
|
|
3114
|
+
const state = await ctx.workspace.read();
|
|
3115
|
+
const link = state.synced.linkedWorkspaces[input.id];
|
|
3116
|
+
if (!link) return { ok: false, error: `Linked workspace ${input.id} not found` };
|
|
3117
|
+
if (input.pinnedVersion !== void 0 && input.pinnedVersion !== null) {
|
|
3118
|
+
const cached = state.synced.releases.perLink[input.id]?.versions ?? [];
|
|
3119
|
+
if (!cached.some((v) => v.version === input.pinnedVersion)) {
|
|
3120
|
+
return {
|
|
3121
|
+
ok: false,
|
|
3122
|
+
error: `Version ${input.pinnedVersion} is not in the cached ledger \u2014 refresh the link first`
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3126
|
+
const next = {
|
|
3127
|
+
...link,
|
|
3128
|
+
...input.name !== void 0 ? { name: input.name } : {},
|
|
3129
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
3130
|
+
...input.pinnedVersion !== void 0 ? { pinnedVersion: input.pinnedVersion } : {},
|
|
3131
|
+
...input.scope !== void 0 ? { scope: input.scope } : {},
|
|
3132
|
+
...input.requiredSecretKeyIds !== void 0 ? { requiredSecretKeyIds: input.requiredSecretKeyIds } : {},
|
|
3133
|
+
...input.sessionMode !== void 0 ? { source: { ...link.source, sessionMode: input.sessionMode } } : {}
|
|
3134
|
+
};
|
|
3135
|
+
if (input.marketplace !== void 0) {
|
|
3136
|
+
if (input.marketplace === null) {
|
|
3137
|
+
delete next.marketplace;
|
|
3138
|
+
} else {
|
|
3139
|
+
next.marketplace = input.marketplace;
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
const out = await ctx.workspace.apply({ kind: "linkedWorkspace.upsert", link: next });
|
|
3143
|
+
return {
|
|
3144
|
+
ok: true,
|
|
3145
|
+
changedIds: out.changedIds,
|
|
3146
|
+
link: summarize(next, state.synced.releases.perLink[input.id]?.currentVersion ?? null)
|
|
3147
|
+
};
|
|
3148
|
+
}
|
|
3149
|
+
};
|
|
3150
|
+
var linkedUnlinkTool = {
|
|
3151
|
+
name: "linked.unlink",
|
|
3152
|
+
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.",
|
|
3153
|
+
inputSchema: z13.object({ id: z13.string() }),
|
|
3154
|
+
async handler(input, ctx) {
|
|
3155
|
+
const state = await ctx.workspace.read();
|
|
3156
|
+
if (!state.synced.linkedWorkspaces[input.id]) {
|
|
3157
|
+
return { ok: false, error: `Linked workspace ${input.id} not found` };
|
|
3158
|
+
}
|
|
3159
|
+
const out = await ctx.workspace.apply({ kind: "linkedWorkspace.remove", id: input.id });
|
|
3160
|
+
return { ok: true, changedIds: out.changedIds };
|
|
3161
|
+
}
|
|
3162
|
+
};
|
|
3163
|
+
|
|
3164
|
+
// src/tools/githubOps.ts
|
|
3165
|
+
import { z as z14 } from "zod";
|
|
3166
|
+
import {
|
|
3167
|
+
fetchRemoteWorkspaceJson,
|
|
3168
|
+
parseLinkedWorkspaceJson,
|
|
3169
|
+
buildLinkedSnapshot,
|
|
3170
|
+
ledgerFromProbe
|
|
3171
|
+
} from "@apicircle/core";
|
|
3172
|
+
import { generateId as generateId6 } from "@apicircle/shared";
|
|
3173
|
+
|
|
3174
|
+
// ../git/src/github/errors.ts
|
|
3175
|
+
var GitHubError = class extends Error {
|
|
3176
|
+
constructor(message, status, body) {
|
|
3177
|
+
super(message);
|
|
3178
|
+
this.status = status;
|
|
3179
|
+
this.body = body;
|
|
3180
|
+
this.name = "GitHubError";
|
|
3181
|
+
}
|
|
3182
|
+
status;
|
|
3183
|
+
body;
|
|
3184
|
+
};
|
|
3185
|
+
var MissingScopeError = class extends GitHubError {
|
|
3186
|
+
/** Scope strings the API said are missing, e.g. ['pull_request']. */
|
|
3187
|
+
missingScopes;
|
|
3188
|
+
/** Scope strings the token currently grants, parsed from x-oauth-scopes. */
|
|
3189
|
+
grantedScopes;
|
|
3190
|
+
constructor(message, status, missingScopes, grantedScopes) {
|
|
3191
|
+
super(message, status);
|
|
3192
|
+
this.name = "MissingScopeError";
|
|
3193
|
+
this.missingScopes = missingScopes;
|
|
3194
|
+
this.grantedScopes = grantedScopes;
|
|
3195
|
+
}
|
|
3196
|
+
};
|
|
3197
|
+
var RateLimitedError = class extends GitHubError {
|
|
3198
|
+
/** Unix timestamp (ms) when the rate-limit window resets. */
|
|
3199
|
+
resetAtMs;
|
|
3200
|
+
constructor(message, status, resetAtMs) {
|
|
3201
|
+
super(message, status);
|
|
3202
|
+
this.name = "RateLimitedError";
|
|
3203
|
+
this.resetAtMs = resetAtMs;
|
|
3204
|
+
}
|
|
3205
|
+
};
|
|
3206
|
+
var UnauthorizedError = class extends GitHubError {
|
|
3207
|
+
constructor(message, status) {
|
|
3208
|
+
super(message, status);
|
|
3209
|
+
this.name = "UnauthorizedError";
|
|
3210
|
+
}
|
|
3211
|
+
};
|
|
3212
|
+
var TimeoutError = class extends GitHubError {
|
|
3213
|
+
/** Timeout that fired, in ms. Useful for the UI message. */
|
|
3214
|
+
timeoutMs;
|
|
3215
|
+
constructor(message, timeoutMs) {
|
|
3216
|
+
super(message, 0);
|
|
3217
|
+
this.name = "TimeoutError";
|
|
3218
|
+
this.timeoutMs = timeoutMs;
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
3221
|
+
|
|
3222
|
+
// ../git/src/github/api.ts
|
|
3223
|
+
var API_BASE = "https://api.github.com";
|
|
3224
|
+
var LOGIN_BASE = "https://github.com";
|
|
3225
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
3226
|
+
var GitHubClient = class {
|
|
3227
|
+
baseUrl;
|
|
3228
|
+
loginBaseUrl;
|
|
3229
|
+
fetchImpl;
|
|
3230
|
+
timeoutMs;
|
|
3231
|
+
constructor(opts = {}) {
|
|
3232
|
+
this.baseUrl = opts.baseUrl ?? API_BASE;
|
|
3233
|
+
this.loginBaseUrl = (opts.loginBaseUrl ?? LOGIN_BASE).replace(/\/$/, "");
|
|
3234
|
+
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
3235
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
3236
|
+
}
|
|
3237
|
+
/**
|
|
3238
|
+
* Fetch the authenticated user. Doubles as a "verify token" probe — used
|
|
3239
|
+
* by the Secret Vault Sessions tab to refresh the granted-scopes list.
|
|
3240
|
+
*/
|
|
3241
|
+
async getViewer(token, opts = {}) {
|
|
3242
|
+
const { json, response } = await this.call(token, "/user", opts);
|
|
3243
|
+
return {
|
|
3244
|
+
viewer: {
|
|
3245
|
+
login: json.login,
|
|
3246
|
+
id: json.id,
|
|
3247
|
+
name: json.name ?? null,
|
|
3248
|
+
avatarUrl: json.avatar_url ?? null
|
|
3249
|
+
},
|
|
3250
|
+
scopes: parseScopes(response.headers)
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
/**
|
|
3254
|
+
* List repositories the authenticated user can access. Used by the repo
|
|
3255
|
+
* picker. Capped at 100 sorted by recent push; users with thousands of
|
|
3256
|
+
* repos can paginate later.
|
|
3257
|
+
*/
|
|
3258
|
+
async listAccessibleRepos(token, opts = {}) {
|
|
3259
|
+
const { json } = await this.call(
|
|
3260
|
+
token,
|
|
3261
|
+
"/user/repos?per_page=100&sort=pushed&affiliation=owner,collaborator,organization_member",
|
|
3262
|
+
opts
|
|
3263
|
+
);
|
|
3264
|
+
return json.map(normalizeRepo);
|
|
3265
|
+
}
|
|
3266
|
+
/**
|
|
3267
|
+
* Fetch a specific repo. Validates the user-supplied owner/name pair
|
|
3268
|
+
* exists + is accessible, and exposes the default branch.
|
|
3269
|
+
*/
|
|
3270
|
+
async getRepo(token, owner, name, opts = {}) {
|
|
3271
|
+
const { json } = await this.call(
|
|
3272
|
+
token,
|
|
3273
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,
|
|
3274
|
+
opts
|
|
3275
|
+
);
|
|
3276
|
+
return normalizeRepo(json);
|
|
3277
|
+
}
|
|
3278
|
+
/**
|
|
3279
|
+
* Read the head SHA of a branch. Used to seed a new working branch from
|
|
3280
|
+
* main before any edits land.
|
|
3281
|
+
*/
|
|
3282
|
+
async getBranchHead(token, owner, name, branch, opts = {}) {
|
|
3283
|
+
const { json } = await this.call(
|
|
3284
|
+
token,
|
|
3285
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/branches/${encodeURIComponent(branch)}`,
|
|
3286
|
+
opts
|
|
3287
|
+
);
|
|
3288
|
+
return { name: json.name, commitSha: json.commit.sha };
|
|
3289
|
+
}
|
|
3290
|
+
/**
|
|
3291
|
+
* List branches on a repo. Used by the Link Workspace repo-browser to
|
|
3292
|
+
* populate the branch dropdown after the user picks a repo. Capped at
|
|
3293
|
+
* 100 (GitHub's max page size); repos with more branches paginate.
|
|
3294
|
+
*/
|
|
3295
|
+
async listBranches(token, owner, name, opts = {}) {
|
|
3296
|
+
const { json } = await this.call(
|
|
3297
|
+
token,
|
|
3298
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/branches?per_page=100`,
|
|
3299
|
+
opts
|
|
3300
|
+
);
|
|
3301
|
+
return json.map((b) => ({ name: b.name, commitSha: b.commit.sha }));
|
|
3302
|
+
}
|
|
3303
|
+
/**
|
|
3304
|
+
* Create a new branch ref pointing at `sha`. The auto-branch flow calls
|
|
3305
|
+
* this with the head SHA from `getBranchHead(main)`.
|
|
3306
|
+
*
|
|
3307
|
+
* GitHub returns 422 with "Reference already exists" when the branch
|
|
3308
|
+
* already exists; that surfaces as a GitHubError(422) so the UI can
|
|
3309
|
+
* prompt for a different name.
|
|
3310
|
+
*/
|
|
3311
|
+
async createBranch(token, owner, name, branchName, sha, opts = {}) {
|
|
3312
|
+
const { json } = await this.call(
|
|
3313
|
+
token,
|
|
3314
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs`,
|
|
3315
|
+
{
|
|
3316
|
+
...opts,
|
|
3317
|
+
method: "POST",
|
|
3318
|
+
body: { ref: `refs/heads/${branchName}`, sha },
|
|
3319
|
+
requiredScopes: ["repo"]
|
|
3320
|
+
}
|
|
3321
|
+
);
|
|
3322
|
+
return { name: branchName, commitSha: json.object.sha };
|
|
3323
|
+
}
|
|
3324
|
+
/**
|
|
3325
|
+
* Read a branch ref's current commit SHA. Used at the start of push-to-
|
|
3326
|
+
* save to find the parent commit before building the new tree.
|
|
3327
|
+
*/
|
|
3328
|
+
async getRef(token, owner, name, branch, opts = {}) {
|
|
3329
|
+
const { json } = await this.call(
|
|
3330
|
+
token,
|
|
3331
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs/heads/${encodeURIComponent(branch)}`,
|
|
3332
|
+
opts
|
|
3333
|
+
);
|
|
3334
|
+
return { ref: json.ref, sha: json.object.sha };
|
|
3335
|
+
}
|
|
3336
|
+
/**
|
|
3337
|
+
* Read a commit's tree SHA. Used so the new tree can be built `base_tree`
|
|
3338
|
+
* — every path we don't override is inherited from the parent.
|
|
3339
|
+
*/
|
|
3340
|
+
async getCommit(token, owner, name, sha, opts = {}) {
|
|
3341
|
+
const { json } = await this.call(
|
|
3342
|
+
token,
|
|
3343
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/commits/${encodeURIComponent(sha)}`,
|
|
3344
|
+
opts
|
|
3345
|
+
);
|
|
3346
|
+
return {
|
|
3347
|
+
sha: json.sha,
|
|
3348
|
+
treeSha: json.tree.sha,
|
|
3349
|
+
message: json.message
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
/**
|
|
3353
|
+
* Upload a blob to the repo and return its SHA. Used by push-to-save
|
|
3354
|
+
* (P4.3b) for binary attachments — text files go straight into a tree
|
|
3355
|
+
* entry's `content`, but binary bytes have to go through a blob first.
|
|
3356
|
+
*
|
|
3357
|
+
* `content` is base64 when `encoding === 'base64'`. GitHub stores blobs
|
|
3358
|
+
* deduplicated by their git-sha1 (not our sha256), so re-uploading the
|
|
3359
|
+
* same bytes is cheap on their side; we save a roundtrip locally by
|
|
3360
|
+
* tracking lastPushedBlobSha per slot in a future revision.
|
|
3361
|
+
*/
|
|
3362
|
+
async createBlob(token, owner, name, args, opts = {}) {
|
|
3363
|
+
const { json } = await this.call(
|
|
3364
|
+
token,
|
|
3365
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/blobs`,
|
|
3366
|
+
{
|
|
3367
|
+
...opts,
|
|
3368
|
+
method: "POST",
|
|
3369
|
+
body: { content: args.content, encoding: args.encoding },
|
|
3370
|
+
requiredScopes: ["repo"]
|
|
3371
|
+
}
|
|
3372
|
+
);
|
|
3373
|
+
return { sha: json.sha, size: json.size ?? 0 };
|
|
3374
|
+
}
|
|
3375
|
+
/**
|
|
3376
|
+
* Build a new tree from `entries`, layered over `baseTreeSha`. Entries
|
|
3377
|
+
* with `content` are inlined (text path); entries with a pre-uploaded
|
|
3378
|
+
* `sha` reference an existing blob (binary path — used by attachments).
|
|
3379
|
+
*/
|
|
3380
|
+
async createTree(token, owner, name, args, opts = {}) {
|
|
3381
|
+
const tree = args.entries.map((e) => ({
|
|
3382
|
+
path: e.path,
|
|
3383
|
+
mode: e.mode ?? "100644",
|
|
3384
|
+
type: e.type ?? "blob",
|
|
3385
|
+
...e.content !== void 0 ? { content: e.content } : {},
|
|
3386
|
+
...e.sha !== void 0 ? { sha: e.sha } : {}
|
|
3387
|
+
}));
|
|
3388
|
+
const { json } = await this.call(
|
|
3389
|
+
token,
|
|
3390
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/trees`,
|
|
3391
|
+
{
|
|
3392
|
+
...opts,
|
|
3393
|
+
method: "POST",
|
|
3394
|
+
body: { base_tree: args.baseTreeSha, tree },
|
|
3395
|
+
requiredScopes: ["repo"]
|
|
3396
|
+
}
|
|
3397
|
+
);
|
|
3398
|
+
return { sha: json.sha };
|
|
3399
|
+
}
|
|
3400
|
+
/**
|
|
3401
|
+
* Create a new commit object pointing at the given tree, with the given
|
|
3402
|
+
* parents. Returns the new commit's SHA + the tree it points at.
|
|
3403
|
+
*/
|
|
3404
|
+
async createCommit(token, owner, name, args, opts = {}) {
|
|
3405
|
+
const { json } = await this.call(
|
|
3406
|
+
token,
|
|
3407
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/commits`,
|
|
3408
|
+
{
|
|
3409
|
+
...opts,
|
|
3410
|
+
method: "POST",
|
|
3411
|
+
body: {
|
|
3412
|
+
message: args.message,
|
|
3413
|
+
tree: args.treeSha,
|
|
3414
|
+
parents: args.parents
|
|
3415
|
+
},
|
|
3416
|
+
requiredScopes: ["repo"]
|
|
3417
|
+
}
|
|
3418
|
+
);
|
|
3419
|
+
return { sha: json.sha, treeSha: json.tree.sha };
|
|
3420
|
+
}
|
|
3421
|
+
/**
|
|
3422
|
+
* Fast-forward a branch ref to a new commit SHA. Pass `force: true` to
|
|
3423
|
+
* skip the FF check (we don't — push-to-save is always FF over the ref
|
|
3424
|
+
* we just read with getRef()).
|
|
3425
|
+
*/
|
|
3426
|
+
async updateRef(token, owner, name, args, opts = {}) {
|
|
3427
|
+
const { json } = await this.call(
|
|
3428
|
+
token,
|
|
3429
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs/heads/${encodeURIComponent(args.branch)}`,
|
|
3430
|
+
{
|
|
3431
|
+
...opts,
|
|
3432
|
+
method: "PATCH",
|
|
3433
|
+
body: { sha: args.sha, force: args.force ?? false },
|
|
3434
|
+
requiredScopes: ["repo"]
|
|
3435
|
+
}
|
|
3436
|
+
);
|
|
3437
|
+
return { ref: json.ref, sha: json.object.sha };
|
|
3438
|
+
}
|
|
3439
|
+
/**
|
|
3440
|
+
* Search GitHub for public API Circle workspaces. Appends
|
|
3441
|
+
* `topic:apicircle` to the user-supplied query so only repos carrying
|
|
3442
|
+
* the `apicircle` topic — the topic the Releases & Topics dialog
|
|
3443
|
+
* locks onto every workspace repo — surface in results. GitHub
|
|
3444
|
+
* matches the bare query against repository name, description, and
|
|
3445
|
+
* topics, so category words like `payments` narrow the marketplace by
|
|
3446
|
+
* topic. An empty query lists every public API Circle workspace. Top
|
|
3447
|
+
* 30 results. Token is optional — anonymous browsing is supported
|
|
3448
|
+
* (lower GitHub rate limits apply); pass a PAT when one is available
|
|
3449
|
+
* to lift them. `sort` controls ordering: omit for GitHub's
|
|
3450
|
+
* best-match relevance, or pass `'stars'` / `'updated'`.
|
|
3451
|
+
*/
|
|
3452
|
+
async searchMarketplaceRepos(token, query, opts = {}) {
|
|
3453
|
+
const { sort, ...callOpts } = opts;
|
|
3454
|
+
const fullQuery = `${query.trim()} topic:apicircle`.trim();
|
|
3455
|
+
const sortParam = sort ? `&sort=${sort}&order=desc` : "";
|
|
3456
|
+
const path2 = `/search/repositories?q=${encodeURIComponent(fullQuery)}&per_page=30${sortParam}`;
|
|
3457
|
+
const { json } = await this.call(token, path2, callOpts);
|
|
3458
|
+
const items = json.items ?? [];
|
|
3459
|
+
return items.map(normalizeMarketplaceRepo);
|
|
3460
|
+
}
|
|
3461
|
+
/**
|
|
3462
|
+
* Start GitHub's OAuth Device Flow. Returns a user-facing code the
|
|
3463
|
+
* user types into github.com/login/device + a device_code the app
|
|
3464
|
+
* polls with. Pure browser-safe: no client_secret involved (device
|
|
3465
|
+
* flow is the only OAuth path GitHub supports for public clients).
|
|
3466
|
+
*
|
|
3467
|
+
* Requires the OAuth App to have "Enable Device Flow" turned on in
|
|
3468
|
+
* its GitHub settings — surface 400 with `not_supported` to the user
|
|
3469
|
+
* if the App owner hasn't done that yet.
|
|
3470
|
+
*/
|
|
3471
|
+
async startDeviceFlow(clientId, scope, opts = {}) {
|
|
3472
|
+
const url = `${this.loginBaseUrl}/login/device/code`;
|
|
3473
|
+
const response = await this.fetchImpl(url, {
|
|
3474
|
+
method: "POST",
|
|
3475
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
3476
|
+
body: JSON.stringify({ client_id: clientId, scope }),
|
|
3477
|
+
signal: opts.signal
|
|
3478
|
+
});
|
|
3479
|
+
if (!response.ok) {
|
|
3480
|
+
throw new GitHubError(
|
|
3481
|
+
`Device-flow start failed: HTTP ${response.status}`,
|
|
3482
|
+
response.status,
|
|
3483
|
+
{}
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
3486
|
+
const json = await response.json();
|
|
3487
|
+
if (json.error) {
|
|
3488
|
+
throw new GitHubError(json.error_description ?? json.error, 400, json);
|
|
3489
|
+
}
|
|
3490
|
+
return {
|
|
3491
|
+
deviceCode: json.device_code,
|
|
3492
|
+
userCode: json.user_code,
|
|
3493
|
+
verificationUri: json.verification_uri,
|
|
3494
|
+
expiresIn: json.expires_in,
|
|
3495
|
+
interval: json.interval
|
|
3496
|
+
};
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* Poll for the access token after the user has authorized the device
|
|
3500
|
+
* code. GitHub returns `authorization_pending` until the user
|
|
3501
|
+
* completes the flow, `slow_down` if we polled too fast, then a real
|
|
3502
|
+
* token. Caller wraps this in a polling loop bounded by `expiresIn`.
|
|
3503
|
+
*/
|
|
3504
|
+
async pollDeviceToken(clientId, deviceCode, opts = {}) {
|
|
3505
|
+
const url = `${this.loginBaseUrl}/login/oauth/access_token`;
|
|
3506
|
+
const response = await this.fetchImpl(url, {
|
|
3507
|
+
method: "POST",
|
|
3508
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
3509
|
+
body: JSON.stringify({
|
|
3510
|
+
client_id: clientId,
|
|
3511
|
+
device_code: deviceCode,
|
|
3512
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
3513
|
+
}),
|
|
3514
|
+
signal: opts.signal
|
|
3515
|
+
});
|
|
3516
|
+
const json = await response.json();
|
|
3517
|
+
if (json.access_token) {
|
|
3518
|
+
return {
|
|
3519
|
+
kind: "granted",
|
|
3520
|
+
accessToken: json.access_token,
|
|
3521
|
+
tokenType: json.token_type ?? "bearer",
|
|
3522
|
+
scope: json.scope ?? ""
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
if (json.error === "authorization_pending") return { kind: "pending", slowDown: false };
|
|
3526
|
+
if (json.error === "slow_down") return { kind: "pending", slowDown: true };
|
|
3527
|
+
if (json.error === "expired_token") return { kind: "expired" };
|
|
3528
|
+
if (json.error === "access_denied")
|
|
3529
|
+
return { kind: "denied", reason: json.error_description ?? "User denied authorization" };
|
|
3530
|
+
throw new GitHubError(
|
|
3531
|
+
json.error_description ?? json.error ?? "Device-token poll failed",
|
|
3532
|
+
response.status,
|
|
3533
|
+
json
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
/**
|
|
3537
|
+
* Create a lightweight Git tag (a ref under `refs/tags/<name>`) on the
|
|
3538
|
+
* given commit SHA. Used by the publish-release flow when the user
|
|
3539
|
+
* opts in to "Create Git tag v<x.y.z>". Returns the resolved ref.
|
|
3540
|
+
*
|
|
3541
|
+
* GitHub returns 422 with "Reference already exists" when the tag is
|
|
3542
|
+
* a duplicate; that surfaces as a GitHubError(422) so the UI can warn
|
|
3543
|
+
* the user without ever overwriting an existing tag.
|
|
3544
|
+
*/
|
|
3545
|
+
async createTag(token, owner, name, args, opts = {}) {
|
|
3546
|
+
const { json } = await this.call(
|
|
3547
|
+
token,
|
|
3548
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs`,
|
|
3549
|
+
{
|
|
3550
|
+
...opts,
|
|
3551
|
+
method: "POST",
|
|
3552
|
+
body: { ref: `refs/tags/${args.tagName}`, sha: args.sha },
|
|
3553
|
+
requiredScopes: ["repo"]
|
|
3554
|
+
}
|
|
3555
|
+
);
|
|
3556
|
+
return { ref: json.ref, sha: json.object.sha };
|
|
3557
|
+
}
|
|
3558
|
+
/**
|
|
3559
|
+
* Compare two commits. Returns the relationship classification GitHub
|
|
3560
|
+
* gives us: `ahead` (head is descendant of base), `behind` (base is
|
|
3561
|
+
* descendant of head), `identical`, or `diverged` (the two histories
|
|
3562
|
+
* share a base but neither contains the other — typical of a force-push
|
|
3563
|
+
* that rewrote history under us).
|
|
3564
|
+
*
|
|
3565
|
+
* Used by the refresh path so we never silently 3-way-merge across a
|
|
3566
|
+
* history rewrite — divergence steers the user through an explicit
|
|
3567
|
+
* "history rewritten" modal instead of corrupting local state.
|
|
3568
|
+
*/
|
|
3569
|
+
async compareCommits(token, owner, name, base, head, opts = {}) {
|
|
3570
|
+
const { json } = await this.call(
|
|
3571
|
+
token,
|
|
3572
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/compare/${encodeURIComponent(
|
|
3573
|
+
base
|
|
3574
|
+
)}...${encodeURIComponent(head)}`,
|
|
3575
|
+
{ ...opts, requiredScopes: ["repo"] }
|
|
3576
|
+
);
|
|
3577
|
+
return {
|
|
3578
|
+
status: json.status,
|
|
3579
|
+
aheadBy: json.ahead_by,
|
|
3580
|
+
behindBy: json.behind_by
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
/**
|
|
3584
|
+
* Is `ancestor` reachable from `descendant`? Thin wrapper around
|
|
3585
|
+
* `compareCommits` — "ahead" or "identical" means yes; "behind" or
|
|
3586
|
+
* "diverged" means the histories don't fit, so the answer is no.
|
|
3587
|
+
*/
|
|
3588
|
+
async isAncestor(token, owner, name, ancestor, descendant, opts = {}) {
|
|
3589
|
+
if (ancestor === descendant) return true;
|
|
3590
|
+
const cmp = await this.compareCommits(token, owner, name, ancestor, descendant, opts);
|
|
3591
|
+
return cmp.status === "ahead" || cmp.status === "identical";
|
|
3592
|
+
}
|
|
3593
|
+
/**
|
|
3594
|
+
* Create a GitHub Release pointing at an existing tag. Used by the
|
|
3595
|
+
* publish-release flow when the user opts in to "Create GitHub
|
|
3596
|
+
* Release". Returns the release's HTML URL so the UI can show a
|
|
3597
|
+
* "Released — view on GitHub" link.
|
|
3598
|
+
*
|
|
3599
|
+
* Pass `prerelease: true` for semver pre-release identifiers (e.g.
|
|
3600
|
+
* `1.0.0-rc.1`); GitHub's Releases UI flags those distinctly.
|
|
3601
|
+
*/
|
|
3602
|
+
async createRelease(token, owner, name, args, opts = {}) {
|
|
3603
|
+
const { json } = await this.call(token, `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/releases`, {
|
|
3604
|
+
...opts,
|
|
3605
|
+
method: "POST",
|
|
3606
|
+
body: {
|
|
3607
|
+
tag_name: args.tagName,
|
|
3608
|
+
name: args.releaseName ?? args.tagName,
|
|
3609
|
+
body: args.body ?? "",
|
|
3610
|
+
draft: args.draft ?? false,
|
|
3611
|
+
prerelease: args.prerelease ?? false
|
|
3612
|
+
},
|
|
3613
|
+
requiredScopes: ["repo"]
|
|
3614
|
+
});
|
|
3615
|
+
return { id: json.id, htmlUrl: json.html_url, tagName: json.tag_name };
|
|
3616
|
+
}
|
|
3617
|
+
/**
|
|
3618
|
+
* Read a tag ref's current commit SHA. Used by the Release & topics
|
|
3619
|
+
* modal to detect whether a tag with the chosen name already exists
|
|
3620
|
+
* (so the UI can surface an "Override existing tag" toggle instead of
|
|
3621
|
+
* silently 422'ing through createTag).
|
|
3622
|
+
*
|
|
3623
|
+
* Returns `null` when the tag doesn't exist (404). Other failures
|
|
3624
|
+
* surface as typed errors.
|
|
3625
|
+
*/
|
|
3626
|
+
async getTagSha(token, owner, name, tagName, opts = {}) {
|
|
3627
|
+
try {
|
|
3628
|
+
const { json } = await this.call(
|
|
3629
|
+
token,
|
|
3630
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs/tags/${encodeURIComponent(tagName)}`,
|
|
3631
|
+
opts
|
|
3632
|
+
);
|
|
3633
|
+
return json.object.sha;
|
|
3634
|
+
} catch (err) {
|
|
3635
|
+
if (err instanceof GitHubError && err.status === 404) return null;
|
|
3636
|
+
throw err;
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
/**
|
|
3640
|
+
* Delete a ref. Used to support the "Override existing tag" path on
|
|
3641
|
+
* the Release & topics modal — we delete the existing tag ref, then
|
|
3642
|
+
* createTag against the new SHA. (GitHub doesn't have a single
|
|
3643
|
+
* "force-update tag" endpoint via the simple refs API.)
|
|
3644
|
+
*
|
|
3645
|
+
* `ref` is the bare suffix, e.g. `tags/v1.0.0` or `heads/feature-x`.
|
|
3646
|
+
*/
|
|
3647
|
+
async deleteRef(token, owner, name, ref, opts = {}) {
|
|
3648
|
+
await this.call(
|
|
3649
|
+
token,
|
|
3650
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/git/refs/${ref.split("/").map(encodeURIComponent).join("/")}`,
|
|
3651
|
+
{
|
|
3652
|
+
...opts,
|
|
3653
|
+
method: "DELETE",
|
|
3654
|
+
requiredScopes: ["repo"]
|
|
3655
|
+
}
|
|
3656
|
+
);
|
|
3657
|
+
}
|
|
3658
|
+
/**
|
|
3659
|
+
* Read the repo's current topic list. Topics drive marketplace
|
|
3660
|
+
* discoverability — public API Circle workspaces include `apicircle`
|
|
3661
|
+
* plus user-chosen category topics.
|
|
3662
|
+
*
|
|
3663
|
+
* Note: GitHub's topics API uses a custom Accept header, but we treat
|
|
3664
|
+
* that as transport detail; the `application/vnd.github.mercy-preview+json`
|
|
3665
|
+
* preview is now stable so the default Accept works.
|
|
3666
|
+
*/
|
|
3667
|
+
async listRepoTopics(token, owner, name, opts = {}) {
|
|
3668
|
+
const { json } = await this.call(
|
|
3669
|
+
token,
|
|
3670
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/topics`,
|
|
3671
|
+
opts
|
|
3672
|
+
);
|
|
3673
|
+
return Array.isArray(json.names) ? json.names : [];
|
|
3674
|
+
}
|
|
3675
|
+
/**
|
|
3676
|
+
* Replace the repo's full topic list. GitHub's `PUT /topics` endpoint
|
|
3677
|
+
* is a full replace (not a merge), so the caller must pass the
|
|
3678
|
+
* complete desired list. Caps at 20 topics; each must match
|
|
3679
|
+
* `^[a-z0-9][a-z0-9-]*$` and be ≤ 50 chars (GitHub enforces this with
|
|
3680
|
+
* a 422). Returns the persisted list.
|
|
3681
|
+
*/
|
|
3682
|
+
async setRepoTopics(token, owner, name, topics, opts = {}) {
|
|
3683
|
+
const { json } = await this.call(
|
|
3684
|
+
token,
|
|
3685
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/topics`,
|
|
3686
|
+
{
|
|
3687
|
+
...opts,
|
|
3688
|
+
method: "PUT",
|
|
3689
|
+
body: { names: topics },
|
|
3690
|
+
requiredScopes: ["repo"]
|
|
3691
|
+
}
|
|
3692
|
+
);
|
|
3693
|
+
return Array.isArray(json.names) ? json.names : [];
|
|
3694
|
+
}
|
|
3695
|
+
/**
|
|
3696
|
+
* Fetch a single file's contents from a branch / commit. Returns
|
|
3697
|
+
* `null` when GitHub answers 404 (file simply doesn't exist on that
|
|
3698
|
+
* ref — the common case for the very first pull). Other failures
|
|
3699
|
+
* surface as the usual typed errors.
|
|
3700
|
+
*
|
|
3701
|
+
* Used by the refresh flow to read remote `workspace.json` so the
|
|
3702
|
+
* 3-way diff can compare it against the local doc.
|
|
3703
|
+
*/
|
|
3704
|
+
async getContents(token, owner, name, path2, ref, opts = {}) {
|
|
3705
|
+
const query = `?ref=${encodeURIComponent(ref)}`;
|
|
3706
|
+
try {
|
|
3707
|
+
const { json } = await this.call(
|
|
3708
|
+
token,
|
|
3709
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/contents/${path2.split("/").map(encodeURIComponent).join("/")}${query}`,
|
|
3710
|
+
opts
|
|
3711
|
+
);
|
|
3712
|
+
if (Array.isArray(json) || json.type !== "file") {
|
|
3713
|
+
throw new GitHubError(`Path ${path2} is not a file`, 422, json);
|
|
3714
|
+
}
|
|
3715
|
+
const cleaned = json.content.replace(/\n/g, "");
|
|
3716
|
+
const decoded = decodeBase64Utf8(cleaned);
|
|
3717
|
+
return { content: decoded, sha: json.sha, path: json.path, size: json.size };
|
|
3718
|
+
} catch (err) {
|
|
3719
|
+
if (err instanceof GitHubError && err.status === 404) return null;
|
|
3720
|
+
throw err;
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
/**
|
|
3724
|
+
* Create or update a file via the Contents API. The killer feature here
|
|
3725
|
+
* vs. the git-data flow (createBlob → createTree → createCommit →
|
|
3726
|
+
* updateRef) is that this works on **truly empty repos**: GitHub's git
|
|
3727
|
+
* database isn't initialized until the first commit lands, so all the
|
|
3728
|
+
* `/git/*` endpoints reject with 409 "Git Repository is empty" — but
|
|
3729
|
+
* `PUT /contents/{path}` atomically initializes the database with a
|
|
3730
|
+
* single-file commit on the supplied branch (defaulting to the repo's
|
|
3731
|
+
* default branch).
|
|
3732
|
+
*
|
|
3733
|
+
* Used by the seed-initial-commit flow to bootstrap a freshly-created
|
|
3734
|
+
* empty repo with a scaffold `workspace.json`.
|
|
3735
|
+
*
|
|
3736
|
+
* `contentBase64` must already be base64-encoded — caller chooses the
|
|
3737
|
+
* encoder (TextEncoder for UTF-8 strings, raw bytes for binaries).
|
|
3738
|
+
*/
|
|
3739
|
+
async putContents(token, owner, name, path2, args, opts = {}) {
|
|
3740
|
+
const body = {
|
|
3741
|
+
message: args.message,
|
|
3742
|
+
content: args.contentBase64
|
|
3743
|
+
};
|
|
3744
|
+
if (args.branch) body.branch = args.branch;
|
|
3745
|
+
if (args.sha) body.sha = args.sha;
|
|
3746
|
+
const { json } = await this.call(
|
|
3747
|
+
token,
|
|
3748
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/contents/${path2.split("/").map(encodeURIComponent).join("/")}`,
|
|
3749
|
+
{
|
|
3750
|
+
...opts,
|
|
3751
|
+
method: "PUT",
|
|
3752
|
+
body,
|
|
3753
|
+
requiredScopes: ["repo"]
|
|
3754
|
+
}
|
|
3755
|
+
);
|
|
3756
|
+
return { commitSha: json.commit.sha, contentSha: json.content.sha };
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Same as `getContents` but returns the raw bytes instead of UTF-8
|
|
3760
|
+
* decoding the file. Used by the refresh flow to pull
|
|
3761
|
+
* `.apicircle/workspace-<id>/attachments/<slotId>` blobs into local IDB without
|
|
3762
|
+
* mangling binary data through TextDecoder.
|
|
3763
|
+
*/
|
|
3764
|
+
async getBinaryContents(token, owner, name, path2, ref, opts = {}) {
|
|
3765
|
+
const query = `?ref=${encodeURIComponent(ref)}`;
|
|
3766
|
+
try {
|
|
3767
|
+
const { json } = await this.call(
|
|
3768
|
+
token,
|
|
3769
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/contents/${path2.split("/").map(encodeURIComponent).join("/")}${query}`,
|
|
3770
|
+
opts
|
|
3771
|
+
);
|
|
3772
|
+
if (Array.isArray(json) || json.type !== "file") {
|
|
3773
|
+
throw new GitHubError(`Path ${path2} is not a file`, 422, json);
|
|
3774
|
+
}
|
|
3775
|
+
const cleaned = json.content.replace(/\n/g, "");
|
|
3776
|
+
const bytes = decodeBase64Bytes(cleaned);
|
|
3777
|
+
return { bytes, sha: json.sha, path: json.path, size: json.size };
|
|
3778
|
+
} catch (err) {
|
|
3779
|
+
if (err instanceof GitHubError && err.status === 404) return null;
|
|
3780
|
+
throw err;
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
/**
|
|
3784
|
+
* Open a pull request from `head` (the working branch) into `base` (the
|
|
3785
|
+
* repo's default branch). PR creation needs the `pull_request` scope on
|
|
3786
|
+
* top of `repo`; missing-scope errors flow through MissingScopeError so
|
|
3787
|
+
* the UI can prompt the user to update the token without losing branch
|
|
3788
|
+
* state (Plan §3.7).
|
|
3789
|
+
*
|
|
3790
|
+
* GitHub returns 422 when:
|
|
3791
|
+
* - head/base are equal (nothing to merge)
|
|
3792
|
+
* - a PR already exists between this head and base
|
|
3793
|
+
* - the head branch doesn't exist
|
|
3794
|
+
* All three surface as a plain GitHubError(422); the UI message is
|
|
3795
|
+
* picked up from response.body.message.
|
|
3796
|
+
*/
|
|
3797
|
+
/**
|
|
3798
|
+
* Fetch a single pull request by number. Used by the refresh flow to
|
|
3799
|
+
* detect whether a previously-opened PR has been merged on GitHub —
|
|
3800
|
+
* `merged: true` is what triggers the working-branch retirement path.
|
|
3801
|
+
*
|
|
3802
|
+
* Returns `null` on 404 (PR was deleted or never existed at this number);
|
|
3803
|
+
* other failures surface as the usual typed errors.
|
|
3804
|
+
*/
|
|
3805
|
+
async getPullRequest(token, owner, name, number, opts = {}) {
|
|
3806
|
+
try {
|
|
3807
|
+
const { json } = await this.call(
|
|
3808
|
+
token,
|
|
3809
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/pulls/${number}`,
|
|
3810
|
+
opts
|
|
3811
|
+
);
|
|
3812
|
+
return {
|
|
3813
|
+
number: json.number,
|
|
3814
|
+
htmlUrl: json.html_url,
|
|
3815
|
+
state: json.state,
|
|
3816
|
+
merged: json.merged === true
|
|
3817
|
+
};
|
|
3818
|
+
} catch (err) {
|
|
3819
|
+
if (err instanceof GitHubError && err.status === 404) return null;
|
|
3820
|
+
throw err;
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
/**
|
|
3824
|
+
* List pull requests on a repo. The capability-probe path uses this with
|
|
3825
|
+
* `perPage: 1` to determine whether the token can read PRs (and, by
|
|
3826
|
+
* extension on classic PATs, whether it can also create them).
|
|
3827
|
+
*
|
|
3828
|
+
* Caller declares `requiredScopes` to surface a `MissingScopeError` on
|
|
3829
|
+
* 403, so the capability probe can recognise the missing-scope case
|
|
3830
|
+
* cleanly vs. transient 5xx/network failures.
|
|
3831
|
+
*/
|
|
3832
|
+
async listPullRequests(token, owner, name, args = {}, opts = {}) {
|
|
3833
|
+
const params = new URLSearchParams();
|
|
3834
|
+
params.set("per_page", String(args.perPage ?? 30));
|
|
3835
|
+
if (args.state) params.set("state", args.state);
|
|
3836
|
+
const { json } = await this.call(
|
|
3837
|
+
token,
|
|
3838
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/pulls?${params.toString()}`,
|
|
3839
|
+
{
|
|
3840
|
+
...opts,
|
|
3841
|
+
requiredScopes: ["repo", "pull_request"]
|
|
3842
|
+
}
|
|
3843
|
+
);
|
|
3844
|
+
return json.map((pr) => ({
|
|
3845
|
+
number: pr.number,
|
|
3846
|
+
htmlUrl: pr.html_url,
|
|
3847
|
+
state: pr.state,
|
|
3848
|
+
title: pr.title
|
|
3849
|
+
}));
|
|
3850
|
+
}
|
|
3851
|
+
async createPullRequest(token, owner, name, args, opts = {}) {
|
|
3852
|
+
const { json } = await this.call(
|
|
3853
|
+
token,
|
|
3854
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/pulls`,
|
|
3855
|
+
{
|
|
3856
|
+
...opts,
|
|
3857
|
+
method: "POST",
|
|
3858
|
+
body: {
|
|
3859
|
+
title: args.title,
|
|
3860
|
+
body: args.body,
|
|
3861
|
+
head: args.head,
|
|
3862
|
+
base: args.base,
|
|
3863
|
+
draft: args.draft ?? false
|
|
3864
|
+
},
|
|
3865
|
+
requiredScopes: ["repo", "pull_request"]
|
|
3866
|
+
}
|
|
3867
|
+
);
|
|
3868
|
+
return {
|
|
3869
|
+
number: json.number,
|
|
3870
|
+
htmlUrl: json.html_url,
|
|
3871
|
+
state: json.state,
|
|
3872
|
+
title: json.title
|
|
3873
|
+
};
|
|
3874
|
+
}
|
|
3875
|
+
// --- low-level call ----------------------------------------------------
|
|
3876
|
+
async call(token, path2, opts = {}) {
|
|
3877
|
+
const url = path2.startsWith("http") ? path2 : `${this.baseUrl}${path2}`;
|
|
3878
|
+
const controller = new AbortController();
|
|
3879
|
+
const onExternalAbort = () => controller.abort(opts.signal.reason);
|
|
3880
|
+
if (opts.signal) {
|
|
3881
|
+
if (opts.signal.aborted) controller.abort(opts.signal.reason);
|
|
3882
|
+
else opts.signal.addEventListener("abort", onExternalAbort, { once: true });
|
|
3883
|
+
}
|
|
3884
|
+
const timeoutHandle = setTimeout(
|
|
3885
|
+
() => controller.abort(new Error(`GitHub request timed out after ${this.timeoutMs}ms`)),
|
|
3886
|
+
this.timeoutMs
|
|
3887
|
+
);
|
|
3888
|
+
let response;
|
|
3889
|
+
let timedOut = false;
|
|
3890
|
+
try {
|
|
3891
|
+
response = await this.fetchImpl(url, {
|
|
3892
|
+
method: opts.method ?? "GET",
|
|
3893
|
+
headers: {
|
|
3894
|
+
Accept: "application/vnd.github+json",
|
|
3895
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
3896
|
+
...token ? { Authorization: `Bearer ${token}` } : {},
|
|
3897
|
+
...opts.body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
3898
|
+
},
|
|
3899
|
+
cache: "no-store",
|
|
3900
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
3901
|
+
signal: controller.signal
|
|
3902
|
+
});
|
|
3903
|
+
} catch (err) {
|
|
3904
|
+
const isAbort = err instanceof DOMException && err.name === "AbortError";
|
|
3905
|
+
const callerAborted = opts.signal?.aborted ?? false;
|
|
3906
|
+
if (isAbort && !callerAborted) {
|
|
3907
|
+
timedOut = true;
|
|
3908
|
+
throw new TimeoutError(
|
|
3909
|
+
`GitHub request timed out after ${this.timeoutMs}ms. The write may have partially landed \u2014 refresh before retrying.`,
|
|
3910
|
+
this.timeoutMs
|
|
3911
|
+
);
|
|
3912
|
+
}
|
|
3913
|
+
throw err;
|
|
3914
|
+
} finally {
|
|
3915
|
+
clearTimeout(timeoutHandle);
|
|
3916
|
+
if (opts.signal) opts.signal.removeEventListener("abort", onExternalAbort);
|
|
3917
|
+
void timedOut;
|
|
3918
|
+
}
|
|
3919
|
+
if (response.ok) {
|
|
3920
|
+
if (response.status === 204 || response.status === 205) {
|
|
3921
|
+
return { json: {}, response };
|
|
3922
|
+
}
|
|
3923
|
+
const json = await response.json();
|
|
3924
|
+
return { json, response };
|
|
3925
|
+
}
|
|
3926
|
+
const errBody = await safeReadJson(response);
|
|
3927
|
+
throw classifyError(response, errBody, opts.requiredScopes ?? []);
|
|
3928
|
+
}
|
|
3929
|
+
};
|
|
3930
|
+
function normalizeMarketplaceRepo(raw) {
|
|
3931
|
+
return {
|
|
3932
|
+
fullName: raw.full_name,
|
|
3933
|
+
owner: raw.owner.login,
|
|
3934
|
+
name: raw.name,
|
|
3935
|
+
description: raw.description ?? "",
|
|
3936
|
+
topics: raw.topics ?? [],
|
|
3937
|
+
stargazers: raw.stargazers_count ?? 0,
|
|
3938
|
+
defaultBranch: raw.default_branch ?? "main"
|
|
3939
|
+
};
|
|
3940
|
+
}
|
|
3941
|
+
function decodeBase64Utf8(b64) {
|
|
3942
|
+
return new TextDecoder("utf-8").decode(decodeBase64Bytes(b64));
|
|
3943
|
+
}
|
|
3944
|
+
function decodeBase64Bytes(b64) {
|
|
3945
|
+
const binary = atob(b64);
|
|
3946
|
+
const bytes = new Uint8Array(binary.length);
|
|
3947
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
3948
|
+
return bytes;
|
|
3949
|
+
}
|
|
3950
|
+
function normalizeRepo(raw) {
|
|
3951
|
+
const visibility = raw.visibility ?? (raw.private === true ? "private" : "public");
|
|
3952
|
+
const isPrivate = raw.private ?? visibility !== "public";
|
|
3953
|
+
const pushable = raw.permissions?.push === true || raw.permissions?.admin === true;
|
|
3954
|
+
return {
|
|
3955
|
+
fullName: raw.full_name,
|
|
3956
|
+
owner: raw.owner.login,
|
|
3957
|
+
name: raw.name,
|
|
3958
|
+
defaultBranch: raw.default_branch,
|
|
3959
|
+
visibility,
|
|
3960
|
+
isPrivate,
|
|
3961
|
+
pushable
|
|
3962
|
+
};
|
|
3963
|
+
}
|
|
3964
|
+
function parseScopes(headers) {
|
|
3965
|
+
const raw = headers.get("x-oauth-scopes") ?? "";
|
|
3966
|
+
const granted = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3967
|
+
const acceptedHeader = headers.get("x-accepted-oauth-scopes") ?? "";
|
|
3968
|
+
const acceptedRequired = acceptedHeader.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3969
|
+
return acceptedRequired.length > 0 ? { granted, acceptedRequired } : { granted };
|
|
3970
|
+
}
|
|
3971
|
+
function classifyError(response, body, callerRequiredScopes) {
|
|
3972
|
+
const message = extractMessage(body) ?? response.statusText;
|
|
3973
|
+
const status = response.status;
|
|
3974
|
+
if (status === 401) {
|
|
3975
|
+
return new UnauthorizedError(message || "Unauthorized \u2014 token rejected", status);
|
|
3976
|
+
}
|
|
3977
|
+
if (status === 403) {
|
|
3978
|
+
const remaining = response.headers.get("x-ratelimit-remaining");
|
|
3979
|
+
const reset = response.headers.get("x-ratelimit-reset");
|
|
3980
|
+
if (remaining === "0" && reset) {
|
|
3981
|
+
const resetAtMs = Number(reset) * 1e3;
|
|
3982
|
+
const deltaMs = Math.max(0, resetAtMs - Date.now());
|
|
3983
|
+
const totalSeconds = Math.ceil(deltaMs / 1e3);
|
|
3984
|
+
const human = totalSeconds < 60 ? `${totalSeconds}s` : totalSeconds < 3600 ? `${Math.ceil(totalSeconds / 60)} min` : `${Math.ceil(totalSeconds / 3600)} h`;
|
|
3985
|
+
return new RateLimitedError(
|
|
3986
|
+
`GitHub rate limit reached. Resets in ${human} (at ${new Date(resetAtMs).toISOString()}).`,
|
|
3987
|
+
status,
|
|
3988
|
+
resetAtMs
|
|
3989
|
+
);
|
|
3990
|
+
}
|
|
3991
|
+
const accepted = (response.headers.get("x-accepted-oauth-scopes") ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3992
|
+
const granted = (response.headers.get("x-oauth-scopes") ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
3993
|
+
const missing = accepted.length > 0 ? accepted.filter((s) => !granted.includes(s)) : callerRequiredScopes.filter((s) => !granted.includes(s));
|
|
3994
|
+
if (missing.length > 0) {
|
|
3995
|
+
return new MissingScopeError(
|
|
3996
|
+
`GitHub denied this action: missing scopes ${missing.join(", ")}.`,
|
|
3997
|
+
status,
|
|
3998
|
+
missing,
|
|
3999
|
+
granted
|
|
4000
|
+
);
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
return new GitHubError(message || "GitHub API call failed", status, body);
|
|
4004
|
+
}
|
|
4005
|
+
function extractMessage(body) {
|
|
4006
|
+
if (typeof body === "object" && body !== null && "message" in body) {
|
|
4007
|
+
const m = body.message;
|
|
4008
|
+
if (typeof m === "string") return m;
|
|
4009
|
+
}
|
|
4010
|
+
return null;
|
|
4011
|
+
}
|
|
4012
|
+
async function safeReadJson(response) {
|
|
4013
|
+
try {
|
|
4014
|
+
return await response.json();
|
|
4015
|
+
} catch {
|
|
4016
|
+
return null;
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
|
|
4020
|
+
// src/tools/githubOps.ts
|
|
4021
|
+
function resolveToken(input) {
|
|
4022
|
+
const t = (input ?? process.env.GITHUB_TOKEN ?? "").trim();
|
|
4023
|
+
return t;
|
|
4024
|
+
}
|
|
4025
|
+
var TOKEN_HELP = "Pass `token`, or set the GITHUB_TOKEN env var on the MCP process.";
|
|
4026
|
+
var linkedLinkTool = {
|
|
4027
|
+
name: "linked.link",
|
|
4028
|
+
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,
|
|
4029
|
+
inputSchema: z14.object({
|
|
4030
|
+
repoFullName: z14.string().describe("owner/name of the source workspace repo."),
|
|
4031
|
+
branch: z14.string().default("main"),
|
|
4032
|
+
pinnedVersion: z14.string().nullable().optional().describe("null/omitted = source current version."),
|
|
4033
|
+
kind: z14.enum(["private", "public"]).default("private"),
|
|
4034
|
+
token: z14.string().optional()
|
|
4035
|
+
}),
|
|
4036
|
+
async handler(input, ctx) {
|
|
4037
|
+
const token = resolveToken(input.token);
|
|
4038
|
+
const repoFullName = input.repoFullName.trim();
|
|
4039
|
+
if (!repoFullName.includes("/")) return { ok: false, error: "repoFullName must be owner/name" };
|
|
4040
|
+
if (input.kind === "private" && !token)
|
|
4041
|
+
return { ok: false, error: `A token is required for private repos. ${TOKEN_HELP}` };
|
|
4042
|
+
const [owner, name] = repoFullName.split("/", 2);
|
|
4043
|
+
const state = await ctx.workspace.read();
|
|
4044
|
+
const dup = Object.values(state.synced.linkedWorkspaces).find(
|
|
4045
|
+
(l) => l.source.repoFullName === repoFullName && l.source.branch === input.branch
|
|
4046
|
+
);
|
|
4047
|
+
if (dup)
|
|
4048
|
+
return { ok: false, error: `Already linked to ${repoFullName}@${input.branch} (${dup.id})` };
|
|
4049
|
+
const client = new GitHubClient();
|
|
4050
|
+
let result;
|
|
4051
|
+
try {
|
|
4052
|
+
result = await fetchRemoteWorkspaceJson(async (p) => {
|
|
4053
|
+
const f = await client.getContents(token, owner, name, p, input.branch);
|
|
4054
|
+
return f?.content ?? null;
|
|
4055
|
+
});
|
|
4056
|
+
} catch (e) {
|
|
4057
|
+
return {
|
|
4058
|
+
ok: false,
|
|
4059
|
+
error: e instanceof GitHubError ? e.message : e instanceof Error ? e.message : "fetch failed"
|
|
4060
|
+
};
|
|
4061
|
+
}
|
|
4062
|
+
if ("error" in result)
|
|
4063
|
+
return { ok: false, error: `${repoFullName}@${input.branch}: ${result.error}` };
|
|
4064
|
+
const probe = parseLinkedWorkspaceJson(result.content);
|
|
4065
|
+
const ledger = ledgerFromProbe(probe);
|
|
4066
|
+
const link = {
|
|
4067
|
+
id: generateId6(),
|
|
4068
|
+
kind: input.kind,
|
|
4069
|
+
name: repoFullName,
|
|
4070
|
+
sourceWorkspaceId: result.workspaceId,
|
|
4071
|
+
source: { provider: "github", repoFullName, branch: input.branch, sessionMode: "workspace" },
|
|
4072
|
+
scope: ["collections", "environments"],
|
|
4073
|
+
pinnedVersion: input.pinnedVersion ?? ledger.currentVersion,
|
|
4074
|
+
updatePolicy: "manual",
|
|
4075
|
+
linkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4076
|
+
requiredSecretKeyIds: probe.secretKeys ? Object.keys(probe.secretKeys) : []
|
|
4077
|
+
};
|
|
4078
|
+
const snapshot = buildLinkedSnapshot(probe, link) ?? void 0;
|
|
4079
|
+
const out = await ctx.workspace.apply({
|
|
4080
|
+
kind: "linkedWorkspace.upsert",
|
|
4081
|
+
link,
|
|
4082
|
+
ledger,
|
|
4083
|
+
...snapshot ? { snapshot } : {}
|
|
4084
|
+
});
|
|
4085
|
+
return { ok: true, id: link.id, pinnedVersion: link.pinnedVersion, changedIds: out.changedIds };
|
|
4086
|
+
}
|
|
4087
|
+
};
|
|
4088
|
+
var linkedRefreshTool = {
|
|
4089
|
+
name: "linked.refresh",
|
|
4090
|
+
description: "Re-pull a linked workspace's cached release ledger (+ bootstrap snapshot if missing) from GitHub. " + TOKEN_HELP,
|
|
4091
|
+
inputSchema: z14.object({ id: z14.string(), token: z14.string().optional() }),
|
|
4092
|
+
async handler(input, ctx) {
|
|
4093
|
+
const state = await ctx.workspace.read();
|
|
4094
|
+
const link = state.synced.linkedWorkspaces[input.id];
|
|
4095
|
+
if (!link) return { ok: false, error: `Linked workspace ${input.id} not found` };
|
|
4096
|
+
const token = resolveToken(input.token);
|
|
4097
|
+
if (link.kind === "private" && !token)
|
|
4098
|
+
return { ok: false, error: `A token is required for private links. ${TOKEN_HELP}` };
|
|
4099
|
+
const [owner, name] = link.source.repoFullName.split("/", 2);
|
|
4100
|
+
const client = new GitHubClient();
|
|
4101
|
+
let result;
|
|
4102
|
+
try {
|
|
4103
|
+
result = await fetchRemoteWorkspaceJson(async (p) => {
|
|
4104
|
+
const f = await client.getContents(token, owner, name, p, link.source.branch);
|
|
4105
|
+
return f?.content ?? null;
|
|
4106
|
+
});
|
|
4107
|
+
} catch (e) {
|
|
4108
|
+
return { ok: false, error: e instanceof Error ? e.message : "fetch failed" };
|
|
4109
|
+
}
|
|
4110
|
+
if ("error" in result)
|
|
4111
|
+
return {
|
|
4112
|
+
ok: false,
|
|
4113
|
+
error: `${link.source.repoFullName}@${link.source.branch}: ${result.error}`
|
|
4114
|
+
};
|
|
4115
|
+
const probe = parseLinkedWorkspaceJson(result.content);
|
|
4116
|
+
const ledger = ledgerFromProbe(probe);
|
|
4117
|
+
const needsSnapshot = !state.local.linkedCollections[input.id];
|
|
4118
|
+
const snapshot = needsSnapshot ? buildLinkedSnapshot(probe, link) ?? void 0 : void 0;
|
|
4119
|
+
await ctx.workspace.apply({
|
|
4120
|
+
kind: "linkedWorkspace.upsert",
|
|
4121
|
+
link,
|
|
4122
|
+
ledger,
|
|
4123
|
+
...snapshot ? { snapshot } : {}
|
|
4124
|
+
});
|
|
4125
|
+
return {
|
|
4126
|
+
ok: true,
|
|
4127
|
+
currentVersion: ledger.currentVersion,
|
|
4128
|
+
versionCount: ledger.versions.length
|
|
4129
|
+
};
|
|
4130
|
+
}
|
|
4131
|
+
};
|
|
4132
|
+
var releaseTagTool = {
|
|
4133
|
+
name: "release.tag",
|
|
4134
|
+
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,
|
|
4135
|
+
inputSchema: z14.object({
|
|
4136
|
+
owner: z14.string(),
|
|
4137
|
+
name: z14.string(),
|
|
4138
|
+
version: z14.string(),
|
|
4139
|
+
createGitHubRelease: z14.boolean().default(false),
|
|
4140
|
+
notes: z14.string().optional(),
|
|
4141
|
+
overrideExisting: z14.boolean().default(false),
|
|
4142
|
+
token: z14.string().optional()
|
|
4143
|
+
}),
|
|
4144
|
+
async handler(input, _ctx) {
|
|
4145
|
+
const token = resolveToken(input.token);
|
|
4146
|
+
if (!token) return { ok: false, error: `A token is required to tag. ${TOKEN_HELP}` };
|
|
4147
|
+
const client = new GitHubClient();
|
|
4148
|
+
const tagName = `v${input.version.replace(/^v/, "")}`;
|
|
4149
|
+
try {
|
|
4150
|
+
const repo = await client.getRepo(token, input.owner, input.name);
|
|
4151
|
+
const ref = await client.getRef(token, input.owner, input.name, repo.defaultBranch);
|
|
4152
|
+
const existing = await client.getTagSha(token, input.owner, input.name, tagName);
|
|
4153
|
+
if (existing !== null) {
|
|
4154
|
+
if (!input.overrideExisting) {
|
|
4155
|
+
return {
|
|
4156
|
+
ok: false,
|
|
4157
|
+
error: `Tag ${tagName} already exists at ${existing.slice(0, 7)}. Pass overrideExisting:true to replace.`
|
|
4158
|
+
};
|
|
4159
|
+
}
|
|
4160
|
+
await client.deleteRef(token, input.owner, input.name, `tags/${tagName}`);
|
|
4161
|
+
}
|
|
4162
|
+
await client.createTag(token, input.owner, input.name, { tagName, sha: ref.sha });
|
|
4163
|
+
let releaseUrl;
|
|
4164
|
+
if (input.createGitHubRelease) {
|
|
4165
|
+
const release = await client.createRelease(token, input.owner, input.name, {
|
|
4166
|
+
tagName,
|
|
4167
|
+
releaseName: tagName,
|
|
4168
|
+
body: input.notes ?? ""
|
|
4169
|
+
});
|
|
4170
|
+
releaseUrl = release.htmlUrl;
|
|
4171
|
+
}
|
|
4172
|
+
return {
|
|
4173
|
+
ok: true,
|
|
4174
|
+
tagName,
|
|
4175
|
+
sha: ref.sha,
|
|
4176
|
+
branch: repo.defaultBranch,
|
|
4177
|
+
...releaseUrl ? { releaseUrl } : {}
|
|
4178
|
+
};
|
|
4179
|
+
} catch (e) {
|
|
4180
|
+
return { ok: false, error: e instanceof Error ? e.message : "tag failed" };
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
4183
|
+
};
|
|
4184
|
+
var marketplaceSearchTool = {
|
|
4185
|
+
name: "marketplace.search",
|
|
4186
|
+
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,
|
|
4187
|
+
inputSchema: z14.object({
|
|
4188
|
+
query: z14.string().default("").describe("Search query \u2014 matches repo name, description, and topics. Empty = browse all."),
|
|
4189
|
+
sort: z14.enum(["best-match", "stars", "updated"]).default("best-match").describe(
|
|
4190
|
+
"Sort order: best-match (default relevance), stars (most starred first), updated (recently pushed first)."
|
|
4191
|
+
),
|
|
4192
|
+
token: z14.string().optional()
|
|
4193
|
+
}),
|
|
4194
|
+
async handler(input, _ctx) {
|
|
4195
|
+
const token = resolveToken(input.token) || null;
|
|
4196
|
+
const client = new GitHubClient();
|
|
4197
|
+
try {
|
|
4198
|
+
const repos = await client.searchMarketplaceRepos(token, input.query, {
|
|
4199
|
+
sort: input.sort === "best-match" ? void 0 : input.sort
|
|
4200
|
+
});
|
|
4201
|
+
return { ok: true, count: repos.length, results: repos };
|
|
4202
|
+
} catch (e) {
|
|
4203
|
+
return {
|
|
4204
|
+
ok: false,
|
|
4205
|
+
error: e instanceof GitHubError ? e.message : e instanceof Error ? e.message : "search failed"
|
|
4206
|
+
};
|
|
4207
|
+
}
|
|
4208
|
+
}
|
|
4209
|
+
};
|
|
4210
|
+
var TOPIC_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
4211
|
+
var repoSetTopicsTool = {
|
|
4212
|
+
name: "repo.set_topics",
|
|
4213
|
+
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,
|
|
4214
|
+
inputSchema: z14.object({
|
|
4215
|
+
owner: z14.string(),
|
|
4216
|
+
name: z14.string(),
|
|
4217
|
+
topics: z14.array(z14.string()),
|
|
4218
|
+
token: z14.string().optional()
|
|
4219
|
+
}),
|
|
4220
|
+
async handler(input, _ctx) {
|
|
4221
|
+
const token = resolveToken(input.token);
|
|
4222
|
+
if (!token) return { ok: false, error: `A token is required to set topics. ${TOKEN_HELP}` };
|
|
4223
|
+
const requested = input.topics;
|
|
4224
|
+
const normalized = Array.from(
|
|
4225
|
+
/* @__PURE__ */ new Set(["apicircle", ...requested.map((t) => t.trim().toLowerCase()).filter(Boolean)])
|
|
4226
|
+
);
|
|
4227
|
+
for (const t of normalized) {
|
|
4228
|
+
if (!TOPIC_RE.test(t))
|
|
4229
|
+
return {
|
|
4230
|
+
ok: false,
|
|
4231
|
+
error: `Invalid topic "${t}" \u2014 lowercase letters/digits/"-", starting with a letter or digit.`
|
|
4232
|
+
};
|
|
4233
|
+
if (t.length > 50) return { ok: false, error: `Topic "${t}" exceeds 50 characters.` };
|
|
4234
|
+
}
|
|
4235
|
+
if (normalized.length > 20) return { ok: false, error: "GitHub allows at most 20 topics." };
|
|
4236
|
+
const client = new GitHubClient();
|
|
4237
|
+
try {
|
|
4238
|
+
const saved = await client.setRepoTopics(token, input.owner, input.name, normalized);
|
|
4239
|
+
return { ok: true, topics: saved };
|
|
4240
|
+
} catch (e) {
|
|
4241
|
+
return { ok: false, error: e instanceof Error ? e.message : "set topics failed" };
|
|
4242
|
+
}
|
|
4243
|
+
}
|
|
4244
|
+
};
|
|
4245
|
+
|
|
4246
|
+
// src/tools/registry.ts
|
|
4247
|
+
var TOOL_REGISTRY = [
|
|
4248
|
+
importCurlTool,
|
|
4249
|
+
importOpenApiTool,
|
|
4250
|
+
importPostmanTool,
|
|
4251
|
+
importInsomniaTool,
|
|
4252
|
+
importHarTool,
|
|
4253
|
+
generateCodeTool,
|
|
4254
|
+
workspaceListTool,
|
|
4255
|
+
workspaceReadTool,
|
|
4256
|
+
workspaceWriteTool,
|
|
4257
|
+
requestCreateTool,
|
|
4258
|
+
requestReadTool,
|
|
4259
|
+
requestUpdateTool,
|
|
4260
|
+
requestDeleteTool,
|
|
4261
|
+
folderCreateTool,
|
|
4262
|
+
folderReadTool,
|
|
4263
|
+
folderUpdateTool,
|
|
4264
|
+
folderDeleteTool,
|
|
4265
|
+
folderExportJsonTool,
|
|
4266
|
+
folderImportJsonTool,
|
|
4267
|
+
environmentCreateTool,
|
|
4268
|
+
environmentReadTool,
|
|
4269
|
+
environmentUpdateTool,
|
|
4270
|
+
environmentDeleteTool,
|
|
4271
|
+
environmentSetActiveTool,
|
|
4272
|
+
environmentSetPriorityTool,
|
|
4273
|
+
environmentExportTool,
|
|
4274
|
+
environmentImportTool,
|
|
4275
|
+
planCreateTool,
|
|
4276
|
+
planRunTool,
|
|
4277
|
+
planReadTool,
|
|
4278
|
+
planUpdateTool,
|
|
4279
|
+
planDeleteTool,
|
|
4280
|
+
planAddStepTool,
|
|
4281
|
+
planRemoveStepTool,
|
|
4282
|
+
planReorderStepsTool,
|
|
4283
|
+
planSetVariablesTool,
|
|
4284
|
+
assertionCreateTool,
|
|
4285
|
+
assertionReadTool,
|
|
4286
|
+
assertionUpdateTool,
|
|
4287
|
+
assertionDeleteTool,
|
|
4288
|
+
historyListRunsTool,
|
|
4289
|
+
historyGetRunTool,
|
|
4290
|
+
historyDeleteRunTool,
|
|
4291
|
+
historyPurgeTool,
|
|
4292
|
+
codebaseExtractCollectionTool,
|
|
4293
|
+
promptCreateEnvironmentTool,
|
|
4294
|
+
promptCreateAssertionTool,
|
|
4295
|
+
promptCreatePlanTool,
|
|
4296
|
+
promptCreateRequestTool,
|
|
4297
|
+
promptUpdateRequestTool,
|
|
4298
|
+
promptCreateFolderTreeTool,
|
|
4299
|
+
promptAddPlanStepsTool,
|
|
4300
|
+
promptSetPlanVariablesTool,
|
|
4301
|
+
promptCreateMockServerTool,
|
|
4302
|
+
promptAddMockEndpointTool,
|
|
4303
|
+
promptSetEndpointValidationRulesTool,
|
|
4304
|
+
promptSetEndpointResponseRulesTool,
|
|
4305
|
+
promptSetEndpointMultipliersTool,
|
|
4306
|
+
promptSetEndpointRequestSchemaTool,
|
|
4307
|
+
globalAssetsFilesListTool,
|
|
4308
|
+
globalAssetsFilesCreateTool,
|
|
4309
|
+
globalAssetsFilesUpdateTool,
|
|
4310
|
+
globalAssetsFilesDeleteTool,
|
|
4311
|
+
mockCreateFromOpenApiTool,
|
|
4312
|
+
mockCreateFromPostmanTool,
|
|
4313
|
+
mockCreateFromInsomniaTool,
|
|
4314
|
+
mockCreateManualTool,
|
|
4315
|
+
mockListTool,
|
|
4316
|
+
mockListEndpointsTool,
|
|
4317
|
+
mockStartTool,
|
|
4318
|
+
mockStopTool,
|
|
4319
|
+
mockDeleteTool,
|
|
4320
|
+
mockAddEndpointTool,
|
|
4321
|
+
mockUpdateEndpointTool,
|
|
4322
|
+
mockDeleteEndpointTool,
|
|
4323
|
+
mockSetValidationRulesTool,
|
|
4324
|
+
mockSetResponseRulesTool,
|
|
4325
|
+
mockSetMultipliersTool,
|
|
4326
|
+
mockSetRequestSchemaTool,
|
|
4327
|
+
mockSetDefaultPortTool,
|
|
4328
|
+
mockImportPostmanMockCollectionTool,
|
|
4329
|
+
releaseListTool,
|
|
4330
|
+
releasePublishTool,
|
|
4331
|
+
releaseDeprecateTool,
|
|
4332
|
+
releaseYankTool,
|
|
4333
|
+
linkedListTool,
|
|
4334
|
+
linkedGetTool,
|
|
4335
|
+
linkedSetConfigTool,
|
|
4336
|
+
linkedUnlinkTool,
|
|
4337
|
+
linkedLinkTool,
|
|
4338
|
+
linkedRefreshTool,
|
|
4339
|
+
releaseTagTool,
|
|
4340
|
+
repoSetTopicsTool,
|
|
4341
|
+
marketplaceSearchTool
|
|
4342
|
+
];
|
|
4343
|
+
function getTool(name) {
|
|
4344
|
+
return TOOL_REGISTRY.find((t) => t.name === name);
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
// src/providers/Workspaces.ts
|
|
4348
|
+
var SingleWorkspaceAdapter = class {
|
|
4349
|
+
constructor(provider, workspaceId, displayName = "Workspace") {
|
|
4350
|
+
this.provider = provider;
|
|
4351
|
+
this.workspaceId = workspaceId;
|
|
4352
|
+
this.displayName = displayName;
|
|
4353
|
+
}
|
|
4354
|
+
provider;
|
|
4355
|
+
workspaceId;
|
|
4356
|
+
displayName;
|
|
4357
|
+
async list() {
|
|
4358
|
+
const state = await this.provider.read();
|
|
4359
|
+
const s = state.synced;
|
|
4360
|
+
const id = s.workspaceId ?? this.workspaceId ?? "unknown";
|
|
4361
|
+
this.workspaceId = id;
|
|
4362
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4363
|
+
return [
|
|
4364
|
+
{
|
|
4365
|
+
id,
|
|
4366
|
+
name: this.displayName,
|
|
4367
|
+
isActive: true,
|
|
4368
|
+
createdAt: s.meta?.createdAt ?? now,
|
|
4369
|
+
lastOpenedAt: s.meta?.updatedAt ?? now,
|
|
4370
|
+
counts: s.collections ? {
|
|
4371
|
+
requests: Object.keys(s.collections.requests ?? {}).length,
|
|
4372
|
+
folders: Object.keys(s.collections.folders ?? {}).length,
|
|
4373
|
+
environments: Object.keys(s.environments?.items ?? {}).length,
|
|
4374
|
+
mockServers: Object.keys(s.mockServers ?? {}).length,
|
|
4375
|
+
plans: Object.keys(s.executionPlans ?? {}).length
|
|
4376
|
+
} : null
|
|
4377
|
+
}
|
|
4378
|
+
];
|
|
4379
|
+
}
|
|
4380
|
+
for(workspaceId) {
|
|
4381
|
+
if (this.workspaceId && workspaceId !== this.workspaceId) {
|
|
4382
|
+
throw new WorkspaceNotFoundError(workspaceId);
|
|
4383
|
+
}
|
|
4384
|
+
return this.provider;
|
|
2746
4385
|
}
|
|
2747
4386
|
activeId() {
|
|
2748
4387
|
return this.workspaceId;
|
|
@@ -2824,6 +4463,52 @@ var FileBackedWorkspaceProvider = class {
|
|
|
2824
4463
|
}
|
|
2825
4464
|
};
|
|
2826
4465
|
|
|
4466
|
+
// src/providers/GitBackedWorkspaceProvider.ts
|
|
4467
|
+
import { applyMutation as applyMutation3 } from "@apicircle/core";
|
|
4468
|
+
import { loadFromFile as loadFromFile2, saveToFile as saveToFile2, withWorkspace as withWorkspace2 } from "@apicircle/core/workspace/file-backed";
|
|
4469
|
+
var GIT_SYNCED_FILENAME = "workspace.json";
|
|
4470
|
+
var GitBackedWorkspaceProvider = class {
|
|
4471
|
+
constructor(dir) {
|
|
4472
|
+
this.dir = dir;
|
|
4473
|
+
}
|
|
4474
|
+
dir;
|
|
4475
|
+
async read() {
|
|
4476
|
+
const out = await loadFromFile2(this.dir, {
|
|
4477
|
+
syncedFilename: GIT_SYNCED_FILENAME,
|
|
4478
|
+
allowMissing: true
|
|
4479
|
+
});
|
|
4480
|
+
if (!out) {
|
|
4481
|
+
throw new Error(
|
|
4482
|
+
`No workspace found at ${this.dir}. Expected .apicircle/registry.json and .apicircle/workspace-<id>/workspace.json in the repo.`
|
|
4483
|
+
);
|
|
4484
|
+
}
|
|
4485
|
+
return out;
|
|
4486
|
+
}
|
|
4487
|
+
async apply(patch) {
|
|
4488
|
+
let captured = null;
|
|
4489
|
+
await withWorkspace2(
|
|
4490
|
+
this.dir,
|
|
4491
|
+
async (state) => {
|
|
4492
|
+
const result = applyMutation3(state, patch);
|
|
4493
|
+
captured = { state: result.next, changedIds: result.changedIds };
|
|
4494
|
+
return { next: result.next };
|
|
4495
|
+
},
|
|
4496
|
+
{ syncedFilename: GIT_SYNCED_FILENAME }
|
|
4497
|
+
);
|
|
4498
|
+
if (!captured) throw new Error("apply did not run");
|
|
4499
|
+
return captured;
|
|
4500
|
+
}
|
|
4501
|
+
async write(next) {
|
|
4502
|
+
const current = await this.read();
|
|
4503
|
+
const merged = {
|
|
4504
|
+
synced: next.synced ?? current.synced,
|
|
4505
|
+
local: next.local ?? current.local
|
|
4506
|
+
};
|
|
4507
|
+
await saveToFile2(this.dir, merged, { syncedFilename: GIT_SYNCED_FILENAME });
|
|
4508
|
+
return merged;
|
|
4509
|
+
}
|
|
4510
|
+
};
|
|
4511
|
+
|
|
2827
4512
|
// src/providers/MultiWorkspaceProvider.ts
|
|
2828
4513
|
import {
|
|
2829
4514
|
loadRegistry,
|
|
@@ -2916,13 +4601,14 @@ var MultiWorkspaceProvider = class {
|
|
|
2916
4601
|
let counts = null;
|
|
2917
4602
|
try {
|
|
2918
4603
|
const state = await loadWorkspaceById(this.registryRoot, entry.id);
|
|
2919
|
-
if (state) {
|
|
4604
|
+
if (state?.synced?.collections) {
|
|
4605
|
+
const s = state.synced;
|
|
2920
4606
|
counts = {
|
|
2921
|
-
requests: Object.keys(
|
|
2922
|
-
folders: Object.keys(
|
|
2923
|
-
environments: Object.keys(
|
|
2924
|
-
mockServers: Object.keys(
|
|
2925
|
-
plans: Object.keys(
|
|
4607
|
+
requests: Object.keys(s.collections.requests ?? {}).length,
|
|
4608
|
+
folders: Object.keys(s.collections.folders ?? {}).length,
|
|
4609
|
+
environments: Object.keys(s.environments?.items ?? {}).length,
|
|
4610
|
+
mockServers: Object.keys(s.mockServers ?? {}).length,
|
|
4611
|
+
plans: Object.keys(s.executionPlans ?? {}).length
|
|
2926
4612
|
};
|
|
2927
4613
|
}
|
|
2928
4614
|
} catch {
|
|
@@ -3005,7 +4691,85 @@ var InProcessMockController = class {
|
|
|
3005
4691
|
}
|
|
3006
4692
|
};
|
|
3007
4693
|
|
|
4694
|
+
// src/config/snippets.ts
|
|
4695
|
+
import * as path from "path";
|
|
4696
|
+
var AI_CLIENTS = [
|
|
4697
|
+
"claude-desktop",
|
|
4698
|
+
"claude-code",
|
|
4699
|
+
"codex",
|
|
4700
|
+
"cursor",
|
|
4701
|
+
"continue",
|
|
4702
|
+
"cline",
|
|
4703
|
+
"zed",
|
|
4704
|
+
"windsurf",
|
|
4705
|
+
"github-copilot",
|
|
4706
|
+
"chatgpt",
|
|
4707
|
+
"generic"
|
|
4708
|
+
];
|
|
4709
|
+
function buildSnippetVariants(client, binary, workspace) {
|
|
4710
|
+
const forwardWorkspace = workspace.replace(/\\/g, "/");
|
|
4711
|
+
const render = client === "codex" ? renderTomlSnippet : renderJsonSnippet;
|
|
4712
|
+
const escaped = render(binary, workspace);
|
|
4713
|
+
const forwardSlash = render(binary, forwardWorkspace);
|
|
4714
|
+
return {
|
|
4715
|
+
forwardSlash,
|
|
4716
|
+
escaped,
|
|
4717
|
+
identical: forwardSlash === escaped
|
|
4718
|
+
};
|
|
4719
|
+
}
|
|
4720
|
+
function renderJsonSnippet(binary, workspace) {
|
|
4721
|
+
const entry = {
|
|
4722
|
+
command: binary,
|
|
4723
|
+
args: ["--workspace", workspace],
|
|
4724
|
+
env: { APICIRCLE_WORKSPACE: workspace }
|
|
4725
|
+
};
|
|
4726
|
+
return JSON.stringify({ mcpServers: { apicircle: entry } }, null, 2);
|
|
4727
|
+
}
|
|
4728
|
+
function renderTomlSnippet(binary, workspace) {
|
|
4729
|
+
const esc = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
4730
|
+
return [
|
|
4731
|
+
`[mcp_servers.apicircle]`,
|
|
4732
|
+
`command = "${esc(binary)}"`,
|
|
4733
|
+
`args = ["--workspace", "${esc(workspace)}"]`,
|
|
4734
|
+
``,
|
|
4735
|
+
`[mcp_servers.apicircle.env]`,
|
|
4736
|
+
`APICIRCLE_WORKSPACE = "${esc(workspace)}"`
|
|
4737
|
+
].join("\n");
|
|
4738
|
+
}
|
|
4739
|
+
function resolveAiClientConfigPath(client, env) {
|
|
4740
|
+
const { homedir, platform, appdata } = env;
|
|
4741
|
+
switch (client) {
|
|
4742
|
+
case "claude-desktop":
|
|
4743
|
+
if (platform === "darwin") {
|
|
4744
|
+
return path.join(homedir, "Library/Application Support/Claude/claude_desktop_config.json");
|
|
4745
|
+
}
|
|
4746
|
+
if (platform === "win32") {
|
|
4747
|
+
return path.join(
|
|
4748
|
+
appdata ?? path.join(homedir, "AppData/Roaming"),
|
|
4749
|
+
"Claude/claude_desktop_config.json"
|
|
4750
|
+
);
|
|
4751
|
+
}
|
|
4752
|
+
return path.join(homedir, ".config/Claude/claude_desktop_config.json");
|
|
4753
|
+
case "claude-code":
|
|
4754
|
+
return path.join(homedir, ".claude/mcp.json");
|
|
4755
|
+
case "cursor":
|
|
4756
|
+
return path.join(homedir, ".cursor/mcp.json");
|
|
4757
|
+
case "continue":
|
|
4758
|
+
return path.join(homedir, ".continue/config.yaml");
|
|
4759
|
+
case "zed":
|
|
4760
|
+
return path.join(homedir, ".config/zed/settings.json");
|
|
4761
|
+
case "windsurf":
|
|
4762
|
+
return path.join(homedir, ".codeium/windsurf/mcp_config.json");
|
|
4763
|
+
case "codex":
|
|
4764
|
+
return path.join(homedir, ".codex/config.toml");
|
|
4765
|
+
default:
|
|
4766
|
+
return null;
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
|
|
3008
4770
|
// src/index.ts
|
|
4771
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4772
|
+
import { StdioServerTransport as StdioServerTransport2 } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3009
4773
|
function createMcpServer(options) {
|
|
3010
4774
|
const workspaces = options.workspaces ?? new SingleWorkspaceAdapter(
|
|
3011
4775
|
options.workspace,
|
|
@@ -3023,15 +4787,23 @@ function createMcpServer(options) {
|
|
|
3023
4787
|
});
|
|
3024
4788
|
}
|
|
3025
4789
|
export {
|
|
4790
|
+
AI_CLIENTS,
|
|
3026
4791
|
FileBackedWorkspaceProvider,
|
|
4792
|
+
GitBackedWorkspaceProvider,
|
|
3027
4793
|
InMemoryWorkspaceProvider,
|
|
3028
4794
|
InProcessMockController,
|
|
4795
|
+
MCP_PROMPTS,
|
|
4796
|
+
MCP_PROMPT_CATEGORIES,
|
|
3029
4797
|
McpHost,
|
|
3030
4798
|
MultiWorkspaceProvider,
|
|
3031
4799
|
SingleWorkspaceAdapter,
|
|
4800
|
+
StdioServerTransport2 as StdioServerTransport,
|
|
4801
|
+
StreamableHTTPServerTransport,
|
|
3032
4802
|
TOOL_REGISTRY,
|
|
3033
4803
|
WorkspaceNotFoundError,
|
|
4804
|
+
buildSnippetVariants,
|
|
3034
4805
|
createMcpServer,
|
|
3035
|
-
getTool
|
|
4806
|
+
getTool,
|
|
4807
|
+
resolveAiClientConfigPath
|
|
3036
4808
|
};
|
|
3037
4809
|
//# sourceMappingURL=index.js.map
|