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