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