@apicircle/mcp-server 1.0.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/bin/mcp-server.cjs +2084 -223
- 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 +1845 -47
- 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 +1684 -44
- 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,6 +2205,53 @@ 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
2256
|
// src/tools/globalAssets.ts
|
|
2147
2257
|
var import_zod10 = require("zod");
|
|
@@ -2392,10 +2502,14 @@ var mockListTool = {
|
|
|
2392
2502
|
};
|
|
2393
2503
|
var mockStartTool = {
|
|
2394
2504
|
name: "mock.start",
|
|
2395
|
-
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.",
|
|
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`.",
|
|
2396
2506
|
inputSchema: import_zod11.z.object({
|
|
2397
2507
|
id: import_zod11.z.string(),
|
|
2398
|
-
|
|
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()
|
|
2399
2513
|
}),
|
|
2400
2514
|
async handler(input, ctx) {
|
|
2401
2515
|
const state = await ctx.workspace.read();
|
|
@@ -2435,13 +2549,44 @@ var mockDeleteTool = {
|
|
|
2435
2549
|
return { ok: true, changedIds: out.changedIds };
|
|
2436
2550
|
}
|
|
2437
2551
|
};
|
|
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
|
+
};
|
|
2438
2580
|
var HTTP_METHOD3 = import_zod11.z.enum(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
2439
2581
|
var mockCreateManualTool = {
|
|
2440
2582
|
name: "mock.create_manual",
|
|
2441
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.",
|
|
2442
2584
|
inputSchema: import_zod11.z.object({
|
|
2443
2585
|
name: import_zod11.z.string().min(1),
|
|
2444
|
-
|
|
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()
|
|
2445
2590
|
}),
|
|
2446
2591
|
async handler(input, ctx) {
|
|
2447
2592
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2643,7 +2788,10 @@ var RESPONSE_RULE = import_zod11.z.object({
|
|
|
2643
2788
|
id: import_zod11.z.string().optional(),
|
|
2644
2789
|
name: import_zod11.z.string(),
|
|
2645
2790
|
enabled: import_zod11.z.boolean().default(true),
|
|
2646
|
-
when
|
|
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),
|
|
2647
2795
|
response: import_zod11.z.object({
|
|
2648
2796
|
status: import_zod11.z.number().int().min(100).max(599).default(200),
|
|
2649
2797
|
jsonBody: import_zod11.z.string().default("{}")
|
|
@@ -2745,9 +2893,61 @@ var mockSetResponseRulesTool = {
|
|
|
2745
2893
|
return { ok: true, changedIds: out.changedIds };
|
|
2746
2894
|
}
|
|
2747
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
|
+
};
|
|
2748
2948
|
var mockSetMultipliersTool = {
|
|
2749
2949
|
name: "mock.set_multipliers",
|
|
2750
|
-
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
|
|
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.",
|
|
2751
2951
|
inputSchema: import_zod11.z.object({
|
|
2752
2952
|
mockId: import_zod11.z.string(),
|
|
2753
2953
|
endpointId: import_zod11.z.string(),
|
|
@@ -2758,6 +2958,9 @@ var mockSetMultipliersTool = {
|
|
|
2758
2958
|
const mock = state.synced.mockServers[input.mockId];
|
|
2759
2959
|
if (!mock) return { ok: false, error: "mock not found" };
|
|
2760
2960
|
const multipliers = input.multipliers;
|
|
2961
|
+
if (multipliers.length > import_shared5.MAX_RESPONSE_MULTIPLIERS) {
|
|
2962
|
+
return { ok: false, error: "too many multipliers" };
|
|
2963
|
+
}
|
|
2761
2964
|
const next = patchEndpoint2(mock, input.endpointId, (e) => ({
|
|
2762
2965
|
...e,
|
|
2763
2966
|
defaultResponse: {
|
|
@@ -2779,6 +2982,1290 @@ var mockSetMultipliersTool = {
|
|
|
2779
2982
|
}
|
|
2780
2983
|
};
|
|
2781
2984
|
|
|
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: [] };
|
|
2997
|
+
}
|
|
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 };
|
|
3011
|
+
}
|
|
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" };
|
|
3047
|
+
}
|
|
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
|
+
|
|
2782
4269
|
// src/tools/registry.ts
|
|
2783
4270
|
var TOOL_REGISTRY = [
|
|
2784
4271
|
importCurlTool,
|
|
@@ -2839,6 +4326,7 @@ var TOOL_REGISTRY = [
|
|
|
2839
4326
|
promptSetEndpointValidationRulesTool,
|
|
2840
4327
|
promptSetEndpointResponseRulesTool,
|
|
2841
4328
|
promptSetEndpointMultipliersTool,
|
|
4329
|
+
promptSetEndpointRequestSchemaTool,
|
|
2842
4330
|
globalAssetsFilesListTool,
|
|
2843
4331
|
globalAssetsFilesCreateTool,
|
|
2844
4332
|
globalAssetsFilesUpdateTool,
|
|
@@ -2858,7 +4346,22 @@ var TOOL_REGISTRY = [
|
|
|
2858
4346
|
mockSetValidationRulesTool,
|
|
2859
4347
|
mockSetResponseRulesTool,
|
|
2860
4348
|
mockSetMultipliersTool,
|
|
2861
|
-
|
|
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
|
|
2862
4365
|
];
|
|
2863
4366
|
function getTool(name) {
|
|
2864
4367
|
return TOOL_REGISTRY.find((t) => t.name === name);
|
|
@@ -2876,22 +4379,24 @@ var SingleWorkspaceAdapter = class {
|
|
|
2876
4379
|
displayName;
|
|
2877
4380
|
async list() {
|
|
2878
4381
|
const state = await this.provider.read();
|
|
2879
|
-
const
|
|
4382
|
+
const s = state.synced;
|
|
4383
|
+
const id = s.workspaceId ?? this.workspaceId ?? "unknown";
|
|
2880
4384
|
this.workspaceId = id;
|
|
4385
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2881
4386
|
return [
|
|
2882
4387
|
{
|
|
2883
4388
|
id,
|
|
2884
4389
|
name: this.displayName,
|
|
2885
4390
|
isActive: true,
|
|
2886
|
-
createdAt:
|
|
2887
|
-
lastOpenedAt:
|
|
2888
|
-
counts: {
|
|
2889
|
-
requests: Object.keys(
|
|
2890
|
-
folders: Object.keys(
|
|
2891
|
-
environments: Object.keys(
|
|
2892
|
-
mockServers: Object.keys(
|
|
2893
|
-
plans: Object.keys(
|
|
2894
|
-
}
|
|
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
|
|
2895
4400
|
}
|
|
2896
4401
|
];
|
|
2897
4402
|
}
|
|
@@ -2922,7 +4427,7 @@ var WorkspaceNotFoundError = class extends Error {
|
|
|
2922
4427
|
};
|
|
2923
4428
|
|
|
2924
4429
|
// src/providers/InMemoryWorkspaceProvider.ts
|
|
2925
|
-
var
|
|
4430
|
+
var import_core6 = require("@apicircle/core");
|
|
2926
4431
|
var InMemoryWorkspaceProvider = class {
|
|
2927
4432
|
state;
|
|
2928
4433
|
constructor(initial) {
|
|
@@ -2932,7 +4437,7 @@ var InMemoryWorkspaceProvider = class {
|
|
|
2932
4437
|
return this.state;
|
|
2933
4438
|
}
|
|
2934
4439
|
async apply(patch) {
|
|
2935
|
-
const out = (0,
|
|
4440
|
+
const out = (0, import_core6.applyMutation)(this.state, patch);
|
|
2936
4441
|
this.state = out.next;
|
|
2937
4442
|
return { state: this.state, changedIds: out.changedIds };
|
|
2938
4443
|
}
|
|
@@ -2946,7 +4451,7 @@ var InMemoryWorkspaceProvider = class {
|
|
|
2946
4451
|
};
|
|
2947
4452
|
|
|
2948
4453
|
// src/providers/FileBackedWorkspaceProvider.ts
|
|
2949
|
-
var
|
|
4454
|
+
var import_core7 = require("@apicircle/core");
|
|
2950
4455
|
var import_file_backed = require("@apicircle/core/workspace/file-backed");
|
|
2951
4456
|
var FileBackedWorkspaceProvider = class {
|
|
2952
4457
|
constructor(dir) {
|
|
@@ -2963,7 +4468,7 @@ var FileBackedWorkspaceProvider = class {
|
|
|
2963
4468
|
async apply(patch) {
|
|
2964
4469
|
let captured = null;
|
|
2965
4470
|
await (0, import_file_backed.withWorkspace)(this.dir, async (state) => {
|
|
2966
|
-
const result = (0,
|
|
4471
|
+
const result = (0, import_core7.applyMutation)(state, patch);
|
|
2967
4472
|
captured = { state: result.next, changedIds: result.changedIds };
|
|
2968
4473
|
return { next: result.next };
|
|
2969
4474
|
});
|
|
@@ -2981,6 +4486,52 @@ var FileBackedWorkspaceProvider = class {
|
|
|
2981
4486
|
}
|
|
2982
4487
|
};
|
|
2983
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
|
+
|
|
2984
4535
|
// src/providers/MultiWorkspaceProvider.ts
|
|
2985
4536
|
var import_registry = require("@apicircle/core/workspace/registry");
|
|
2986
4537
|
var LazyActiveWorkspaceProvider = class {
|
|
@@ -3067,13 +4618,14 @@ var MultiWorkspaceProvider = class {
|
|
|
3067
4618
|
let counts = null;
|
|
3068
4619
|
try {
|
|
3069
4620
|
const state = await (0, import_registry.loadWorkspaceById)(this.registryRoot, entry.id);
|
|
3070
|
-
if (state) {
|
|
4621
|
+
if (state?.synced?.collections) {
|
|
4622
|
+
const s = state.synced;
|
|
3071
4623
|
counts = {
|
|
3072
|
-
requests: Object.keys(
|
|
3073
|
-
folders: Object.keys(
|
|
3074
|
-
environments: Object.keys(
|
|
3075
|
-
mockServers: Object.keys(
|
|
3076
|
-
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
|
|
3077
4629
|
};
|
|
3078
4630
|
}
|
|
3079
4631
|
} catch {
|
|
@@ -3153,7 +4705,245 @@ var InProcessMockController = class {
|
|
|
3153
4705
|
}
|
|
3154
4706
|
};
|
|
3155
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
|
+
|
|
3156
4944
|
// src/index.ts
|
|
4945
|
+
var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
4946
|
+
var import_stdio2 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
3157
4947
|
function createMcpServer(options) {
|
|
3158
4948
|
const workspaces = options.workspaces ?? new SingleWorkspaceAdapter(
|
|
3159
4949
|
options.workspace,
|
|
@@ -3172,15 +4962,23 @@ function createMcpServer(options) {
|
|
|
3172
4962
|
}
|
|
3173
4963
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3174
4964
|
0 && (module.exports = {
|
|
4965
|
+
AI_CLIENTS,
|
|
3175
4966
|
FileBackedWorkspaceProvider,
|
|
4967
|
+
GitBackedWorkspaceProvider,
|
|
3176
4968
|
InMemoryWorkspaceProvider,
|
|
3177
4969
|
InProcessMockController,
|
|
4970
|
+
MCP_PROMPTS,
|
|
4971
|
+
MCP_PROMPT_CATEGORIES,
|
|
3178
4972
|
McpHost,
|
|
3179
4973
|
MultiWorkspaceProvider,
|
|
3180
4974
|
SingleWorkspaceAdapter,
|
|
4975
|
+
StdioServerTransport,
|
|
4976
|
+
StreamableHTTPServerTransport,
|
|
3181
4977
|
TOOL_REGISTRY,
|
|
3182
4978
|
WorkspaceNotFoundError,
|
|
4979
|
+
buildSnippetVariants,
|
|
3183
4980
|
createMcpServer,
|
|
3184
|
-
getTool
|
|
4981
|
+
getTool,
|
|
4982
|
+
resolveAiClientConfigPath
|
|
3185
4983
|
});
|
|
3186
4984
|
//# sourceMappingURL=index.cjs.map
|