@myspec/mcp-server 0.3.0-next.90 → 0.4.0-next.100
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 +20 -4
- package/dist/index.js +867 -148
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -255,6 +255,7 @@ async function exchangeCode(userAuthUrl, code, fetchImpl) {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// src/auth/credentials-store.ts
|
|
258
|
+
import { randomUUID } from "crypto";
|
|
258
259
|
import { promises as fs } from "fs";
|
|
259
260
|
import os from "os";
|
|
260
261
|
import path from "path";
|
|
@@ -359,15 +360,26 @@ async function loadSettings(target) {
|
|
|
359
360
|
throw err;
|
|
360
361
|
}
|
|
361
362
|
}
|
|
363
|
+
async function readSettingsUserAuthUrl(settingsPath = defaultSettingsPath()) {
|
|
364
|
+
const settings = await loadSettings(settingsPath);
|
|
365
|
+
return settings?.userAuthUrl;
|
|
366
|
+
}
|
|
362
367
|
async function writeJsonFile(target, data, opts) {
|
|
363
368
|
const dir = path.dirname(target);
|
|
364
369
|
await fs.mkdir(dir, { recursive: true, mode: 448 });
|
|
365
370
|
if (process.platform !== "win32") {
|
|
366
371
|
await fs.chmod(dir, 448);
|
|
367
372
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
await fs.
|
|
373
|
+
const tmp = `${target}.${randomUUID()}.tmp`;
|
|
374
|
+
try {
|
|
375
|
+
await fs.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 384 });
|
|
376
|
+
if (opts.secret && process.platform !== "win32") {
|
|
377
|
+
await fs.chmod(tmp, 384);
|
|
378
|
+
}
|
|
379
|
+
await fs.rename(tmp, target);
|
|
380
|
+
} catch (err) {
|
|
381
|
+
await unlinkIfExists(tmp);
|
|
382
|
+
throw err;
|
|
371
383
|
}
|
|
372
384
|
}
|
|
373
385
|
async function unlinkIfExists(target) {
|
|
@@ -584,6 +596,37 @@ var TokenManager = class {
|
|
|
584
596
|
now;
|
|
585
597
|
cached = null;
|
|
586
598
|
refreshInFlight = null;
|
|
599
|
+
/**
|
|
600
|
+
* The in-flight disk read, shared by every concurrent caller.
|
|
601
|
+
*
|
|
602
|
+
* `refresh` below has always deduped; this did not, so N concurrent platform
|
|
603
|
+
* calls each issued their own `store.load()` before any of them populated
|
|
604
|
+
* `cached`. That is N redundant reads on every cold call, and it is what made
|
|
605
|
+
* a torn read reachable at all: one caller could be refreshing (and so
|
|
606
|
+
* rewriting the credential files) while the others were still reading them.
|
|
607
|
+
*
|
|
608
|
+
* The atomic write in credentials-store.ts is the real guarantee. This is the
|
|
609
|
+
* other half — it removes the redundant reads rather than merely surviving
|
|
610
|
+
* them.
|
|
611
|
+
*/
|
|
612
|
+
loadInFlight = null;
|
|
613
|
+
/**
|
|
614
|
+
* Bumped by clear(), so a read that started before it cannot publish its
|
|
615
|
+
* result to `cached` afterwards.
|
|
616
|
+
*
|
|
617
|
+
* Scope, precisely: the caller already awaiting that read still receives the
|
|
618
|
+
* pre-clear credentials as its own return value and proceeds with them —
|
|
619
|
+
* nothing can retract a value from a call already in flight. What the guard
|
|
620
|
+
* buys is that the value stops there instead of being cached, so no
|
|
621
|
+
* SUBSEQUENT caller is handed credentials the user asked us to forget.
|
|
622
|
+
*
|
|
623
|
+
* Dropping `loadInFlight` alone would not achieve even that: the awaiting
|
|
624
|
+
* caller resumes after the clear and would assign `cached` itself.
|
|
625
|
+
* Pre-existing — the old code assigned `cached` straight after an un-deduped
|
|
626
|
+
* `await` — but the fix belongs with the dedupe rather than left as a claim
|
|
627
|
+
* the comment cannot back up.
|
|
628
|
+
*/
|
|
629
|
+
loadGeneration = 0;
|
|
587
630
|
/**
|
|
588
631
|
* Set once the exchange endpoint rejects the API token outright.
|
|
589
632
|
*
|
|
@@ -616,17 +659,25 @@ var TokenManager = class {
|
|
|
616
659
|
}
|
|
617
660
|
async clear() {
|
|
618
661
|
this.cached = null;
|
|
662
|
+
this.loadInFlight = null;
|
|
663
|
+
this.loadGeneration += 1;
|
|
619
664
|
await this.store.clear();
|
|
620
665
|
}
|
|
621
666
|
async ensureLoaded() {
|
|
622
667
|
if (this.cached) {
|
|
623
668
|
return this.cached;
|
|
624
669
|
}
|
|
625
|
-
const
|
|
670
|
+
const generation = this.loadGeneration;
|
|
671
|
+
this.loadInFlight ??= this.store.load().finally(() => {
|
|
672
|
+
this.loadInFlight = null;
|
|
673
|
+
});
|
|
674
|
+
const loaded = await this.loadInFlight;
|
|
626
675
|
if (!loaded) {
|
|
627
676
|
throw new NeedsLoginError();
|
|
628
677
|
}
|
|
629
|
-
this.
|
|
678
|
+
if (generation === this.loadGeneration) {
|
|
679
|
+
this.cached = loaded;
|
|
680
|
+
}
|
|
630
681
|
return loaded;
|
|
631
682
|
}
|
|
632
683
|
async refresh(creds) {
|
|
@@ -679,7 +730,7 @@ var TokenManager = class {
|
|
|
679
730
|
if (response.status === 401 || response.status === 403) {
|
|
680
731
|
const reason = response.status === 403 ? "the token owner no longer has access to its organization" : "the token is invalid, disabled, or expired";
|
|
681
732
|
const rejection = new ApiTokenRejectedError(
|
|
682
|
-
`API token exchange failed: ${reason}.
|
|
733
|
+
`API token exchange failed at ${url}: ${reason}. Check that URL names the environment the token was created in. With MYSPEC_API_TOKEN it comes from MYSPEC_USER_AUTH_URL (or --user-auth-url), then userAuthUrl in ~/.myspec/settings.json, defaulting to https://auth.myspec.dev; with an apiToken in oauth_creds.json it comes from that settings.json value alone, which only \`npx @myspec/mcp-server login --user-auth-url <url>\` rewrites. If the host is right, create a new API token in MySpec and set it as MYSPEC_API_TOKEN (or write it to the apiToken field of your credentials file).`
|
|
683
734
|
);
|
|
684
735
|
this.apiTokenRejection = rejection;
|
|
685
736
|
throw rejection;
|
|
@@ -689,7 +740,7 @@ var TokenManager = class {
|
|
|
689
740
|
throw new HttpStatusError(
|
|
690
741
|
response.status,
|
|
691
742
|
body,
|
|
692
|
-
`Token exchange failed: ${url} returned 404, so no exchange endpoint is there. Check that URL names the environment the token was created in. With MYSPEC_API_TOKEN it comes from MYSPEC_USER_AUTH_URL (or --user-auth-url), defaulting to https://auth.myspec.dev. Otherwise it comes from
|
|
743
|
+
`Token exchange failed: ${url} returned 404, so no exchange endpoint is there. Check that URL names the environment the token was created in. With MYSPEC_API_TOKEN it comes from MYSPEC_USER_AUTH_URL (or --user-auth-url), then userAuthUrl in ~/.myspec/settings.json, defaulting to https://auth.myspec.dev. Otherwise it comes from that same settings.json value, which only \`npx @myspec/mcp-server login --user-auth-url <url>\` rewrites \u2014 the flag and the variable do not redirect a file-configured exchange on their own, and userAuthUrl in oauth_creds.json is ignored entirely.`
|
|
693
744
|
);
|
|
694
745
|
}
|
|
695
746
|
if (!response.ok) {
|
|
@@ -1333,6 +1384,26 @@ var PlatformPreconditionRequiredError = class extends PlatformHttpError {
|
|
|
1333
1384
|
this.name = "PlatformPreconditionRequiredError";
|
|
1334
1385
|
}
|
|
1335
1386
|
};
|
|
1387
|
+
var QUOTA_EXCEEDED_CODE = "quota_exceeded";
|
|
1388
|
+
var PlatformQuotaError = class extends PlatformHttpError {
|
|
1389
|
+
/** The platform's machine-readable code, e.g. `quota_exceeded`. */
|
|
1390
|
+
code;
|
|
1391
|
+
constructor(message, status, details, code) {
|
|
1392
|
+
super(message, status, details);
|
|
1393
|
+
this.name = "PlatformQuotaError";
|
|
1394
|
+
this.code = code;
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
function isPlatformQuotaError(err) {
|
|
1398
|
+
if (statusOf(err) !== 429) {
|
|
1399
|
+
return false;
|
|
1400
|
+
}
|
|
1401
|
+
if (typeof err !== "object" || err === null) {
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
const code = err.code;
|
|
1405
|
+
return typeof code === "string" && code.toLowerCase() === QUOTA_EXCEEDED_CODE;
|
|
1406
|
+
}
|
|
1336
1407
|
function isPlatformHttpError(err) {
|
|
1337
1408
|
return typeof err === "object" && err !== null && err[PLATFORM_ERROR_BRAND] === true;
|
|
1338
1409
|
}
|
|
@@ -1409,13 +1480,16 @@ function toConflictDetails(details) {
|
|
|
1409
1480
|
changedAt: stringField(details, "last_modified_at")
|
|
1410
1481
|
};
|
|
1411
1482
|
}
|
|
1412
|
-
function createPlatformError(message, status, details) {
|
|
1483
|
+
function createPlatformError(message, status, details, code) {
|
|
1413
1484
|
if (status === 409) {
|
|
1414
1485
|
return new PlatformConflictError(message, status, details, toConflictDetails(details));
|
|
1415
1486
|
}
|
|
1416
1487
|
if (status === 428) {
|
|
1417
1488
|
return new PlatformPreconditionRequiredError(message, details);
|
|
1418
1489
|
}
|
|
1490
|
+
if (status === 429 && typeof code === "string" && code.toLowerCase() === QUOTA_EXCEEDED_CODE) {
|
|
1491
|
+
return new PlatformQuotaError(message, status, details, code.toLowerCase());
|
|
1492
|
+
}
|
|
1419
1493
|
return new PlatformHttpError(message, status, details);
|
|
1420
1494
|
}
|
|
1421
1495
|
function truncate(text) {
|
|
@@ -1426,17 +1500,19 @@ async function throwFromResponse(response, fallbackMessage) {
|
|
|
1426
1500
|
const rawBody = await response.text().catch(() => "");
|
|
1427
1501
|
let message = fallbackMessage;
|
|
1428
1502
|
let details;
|
|
1503
|
+
let code;
|
|
1429
1504
|
if (rawBody) {
|
|
1430
1505
|
try {
|
|
1431
1506
|
const parsed = JSON.parse(rawBody);
|
|
1432
1507
|
details = parsed.details;
|
|
1508
|
+
code = typeof parsed.error === "string" ? parsed.error : void 0;
|
|
1433
1509
|
const suffix = details ? ` [details: ${JSON.stringify(details)}]` : "";
|
|
1434
1510
|
message = truncate(parsed.message ?? rawBody) + suffix;
|
|
1435
1511
|
} catch {
|
|
1436
1512
|
message = truncate(rawBody);
|
|
1437
1513
|
}
|
|
1438
1514
|
}
|
|
1439
|
-
throw createPlatformError(`HTTP ${response.status}: ${message}`, response.status, details);
|
|
1515
|
+
throw createPlatformError(`HTTP ${response.status}: ${message}`, response.status, details, code);
|
|
1440
1516
|
}
|
|
1441
1517
|
|
|
1442
1518
|
// ../../packages/shared/logger.ts
|
|
@@ -1644,6 +1720,10 @@ var BaseHttpClient = class {
|
|
|
1644
1720
|
this.logger.debug("Retrying after abort/timeout", { error: error.message });
|
|
1645
1721
|
return true;
|
|
1646
1722
|
}
|
|
1723
|
+
if (isPlatformQuotaError(error)) {
|
|
1724
|
+
this.logger.debug("Not retrying quota rejection", { status: 429 });
|
|
1725
|
+
return false;
|
|
1726
|
+
}
|
|
1647
1727
|
const httpError = error;
|
|
1648
1728
|
if (httpError.status !== void 0) {
|
|
1649
1729
|
const shouldRetry = RETRYABLE_STATUS_CODES.includes(httpError.status);
|
|
@@ -1700,6 +1780,9 @@ function transformSpecFileResponse(raw) {
|
|
|
1700
1780
|
deletedAt: raw.deleted_at ? new Date(raw.deleted_at) : void 0
|
|
1701
1781
|
};
|
|
1702
1782
|
}
|
|
1783
|
+
function toRevisionAuthorKind(raw) {
|
|
1784
|
+
return raw === "ai" ? "ai" : "human";
|
|
1785
|
+
}
|
|
1703
1786
|
function transformFileRevisionResponse(raw) {
|
|
1704
1787
|
return {
|
|
1705
1788
|
id: raw.id,
|
|
@@ -1710,6 +1793,7 @@ function transformFileRevisionResponse(raw) {
|
|
|
1710
1793
|
fileSizeBytes: raw.file_size_bytes,
|
|
1711
1794
|
checksum: raw.checksum,
|
|
1712
1795
|
changedBy: raw.changed_by,
|
|
1796
|
+
changedByKind: toRevisionAuthorKind(raw.changed_by_kind),
|
|
1713
1797
|
createdAt: new Date(raw.created_at)
|
|
1714
1798
|
};
|
|
1715
1799
|
}
|
|
@@ -2387,6 +2471,32 @@ var PlatformClient = class {
|
|
|
2387
2471
|
};
|
|
2388
2472
|
return this.withTokenRetry((jwt, c) => c.session.listSpecSessions(projectId, jwt, merged));
|
|
2389
2473
|
}
|
|
2474
|
+
async getSpecSession(sessionId) {
|
|
2475
|
+
return this.withTokenRetry((jwt, c) => c.session.getSpecSession(sessionId, jwt));
|
|
2476
|
+
}
|
|
2477
|
+
/**
|
|
2478
|
+
* Writes ONLY the session's `context` field (rename's read-merge-write).
|
|
2479
|
+
* Deliberately narrow: status, chatHistory and the counters stay
|
|
2480
|
+
* unreachable from the MCP surface. The platform permits context-only
|
|
2481
|
+
* updates on archived sessions, so rename works there too.
|
|
2482
|
+
*/
|
|
2483
|
+
async updateSpecSessionContext(sessionId, context) {
|
|
2484
|
+
return this.withTokenRetry((jwt, c) => c.session.updateSpecSession(sessionId, { context }, jwt));
|
|
2485
|
+
}
|
|
2486
|
+
/** Idempotent: re-archiving keeps the original archivedAt. */
|
|
2487
|
+
async archiveSpecSession(sessionId) {
|
|
2488
|
+
return this.withTokenRetry((jwt, c) => c.session.archiveSpecSession(sessionId, jwt));
|
|
2489
|
+
}
|
|
2490
|
+
async unarchiveSpecSession(sessionId) {
|
|
2491
|
+
return this.withTokenRetry((jwt, c) => c.session.unarchiveSpecSession(sessionId, jwt));
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Soft-deletes a spec session. The platform rejects this unless the session
|
|
2495
|
+
* is already archived, surfacing a validation error the caller reports.
|
|
2496
|
+
*/
|
|
2497
|
+
async deleteSpecSession(sessionId) {
|
|
2498
|
+
await this.withTokenRetry((jwt, c) => c.session.deleteSpecSession(sessionId, jwt));
|
|
2499
|
+
}
|
|
2390
2500
|
async listFiles(projectId, opts = {}) {
|
|
2391
2501
|
return this.withTokenRetry((jwt, c) => c.file.listFiles(projectId, jwt, opts));
|
|
2392
2502
|
}
|
|
@@ -2660,6 +2770,14 @@ function jsonResult(value) {
|
|
|
2660
2770
|
function errorResult(message) {
|
|
2661
2771
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
2662
2772
|
}
|
|
2773
|
+
function imageResult(dataBase64, mimeType, metadata) {
|
|
2774
|
+
return {
|
|
2775
|
+
content: [
|
|
2776
|
+
{ type: "text", text: JSON.stringify(metadata, null, 2) },
|
|
2777
|
+
{ type: "image", data: dataBase64, mimeType }
|
|
2778
|
+
]
|
|
2779
|
+
};
|
|
2780
|
+
}
|
|
2663
2781
|
|
|
2664
2782
|
// src/server/tools/archive_project.ts
|
|
2665
2783
|
import { z } from "zod";
|
|
@@ -2691,16 +2809,63 @@ var archiveProjectTool = {
|
|
|
2691
2809
|
}
|
|
2692
2810
|
};
|
|
2693
2811
|
|
|
2694
|
-
// src/server/tools/
|
|
2812
|
+
// src/server/tools/archive_spec_session.ts
|
|
2695
2813
|
import { z as z2 } from "zod";
|
|
2814
|
+
|
|
2815
|
+
// src/server/tools/session-output.ts
|
|
2816
|
+
function sessionNotFound(err, sessionId) {
|
|
2817
|
+
return statusOf(err) === 404 ? errorResult(`Session ${sessionId} not found.`) : null;
|
|
2818
|
+
}
|
|
2819
|
+
function sessionMetadata(s) {
|
|
2820
|
+
return {
|
|
2821
|
+
id: s.id,
|
|
2822
|
+
project_id: s.projectId,
|
|
2823
|
+
org: s.org,
|
|
2824
|
+
user_id: s.userId,
|
|
2825
|
+
status: s.status,
|
|
2826
|
+
message_count: s.messageCount,
|
|
2827
|
+
token_count: s.tokenCount,
|
|
2828
|
+
started_at: s.startedAt,
|
|
2829
|
+
completed_at: s.completedAt ?? null,
|
|
2830
|
+
archived_at: s.archivedAt ?? null,
|
|
2831
|
+
created_at: s.createdAt,
|
|
2832
|
+
updated_at: s.updatedAt
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
// src/server/tools/archive_spec_session.ts
|
|
2696
2837
|
var inputSchema2 = {
|
|
2697
|
-
|
|
2698
|
-
|
|
2838
|
+
session_id: z2.string().min(1).describe("UUID of the spec session to archive")
|
|
2839
|
+
};
|
|
2840
|
+
var archiveSpecSessionTool = {
|
|
2841
|
+
name: "archive_spec_session",
|
|
2842
|
+
description: "Archive a spec session. Archiving is the confirmation step required before delete_spec_session, and is reversible via unarchive_spec_session. Idempotent: archiving an already-archived session succeeds and keeps the original archived_at.",
|
|
2843
|
+
inputSchema: inputSchema2,
|
|
2844
|
+
handler: async (args, ctx) => {
|
|
2845
|
+
let session;
|
|
2846
|
+
try {
|
|
2847
|
+
session = await ctx.client.archiveSpecSession(args.session_id);
|
|
2848
|
+
} catch (err) {
|
|
2849
|
+
const notFound = sessionNotFound(err, args.session_id);
|
|
2850
|
+
if (notFound) {
|
|
2851
|
+
return notFound;
|
|
2852
|
+
}
|
|
2853
|
+
throw err;
|
|
2854
|
+
}
|
|
2855
|
+
return jsonResult({ session: sessionMetadata(session) });
|
|
2856
|
+
}
|
|
2857
|
+
};
|
|
2858
|
+
|
|
2859
|
+
// src/server/tools/create_project.ts
|
|
2860
|
+
import { z as z3 } from "zod";
|
|
2861
|
+
var inputSchema3 = {
|
|
2862
|
+
name: z3.string().describe("Name of the project to create."),
|
|
2863
|
+
description: z3.string().optional().describe("Optional project description.")
|
|
2699
2864
|
};
|
|
2700
2865
|
var createProjectTool = {
|
|
2701
2866
|
name: "create_project",
|
|
2702
2867
|
description: "Create a new project. Returns the created project id and metadata.",
|
|
2703
|
-
inputSchema:
|
|
2868
|
+
inputSchema: inputSchema3,
|
|
2704
2869
|
handler: async (args, ctx) => {
|
|
2705
2870
|
const name = args.name.trim();
|
|
2706
2871
|
if (name === "") {
|
|
@@ -2715,14 +2880,14 @@ var createProjectTool = {
|
|
|
2715
2880
|
};
|
|
2716
2881
|
|
|
2717
2882
|
// src/server/tools/delete_project.ts
|
|
2718
|
-
import { z as
|
|
2719
|
-
var
|
|
2720
|
-
project_id:
|
|
2883
|
+
import { z as z4 } from "zod";
|
|
2884
|
+
var inputSchema4 = {
|
|
2885
|
+
project_id: z4.string().min(1).describe("UUID of the project to delete.")
|
|
2721
2886
|
};
|
|
2722
2887
|
var deleteProjectTool = {
|
|
2723
2888
|
name: "delete_project",
|
|
2724
2889
|
description: "Soft-delete a project. Only a project that is already archived can be deleted. If the project is still active, this tool refuses and you must NOT archive it on the user's behalf to make the deletion go through \u2014 instead, reconfirm with the user whether deleting the project is truly intended.",
|
|
2725
|
-
inputSchema:
|
|
2890
|
+
inputSchema: inputSchema4,
|
|
2726
2891
|
handler: async (args, ctx) => {
|
|
2727
2892
|
let status;
|
|
2728
2893
|
try {
|
|
@@ -2747,9 +2912,38 @@ var deleteProjectTool = {
|
|
|
2747
2912
|
}
|
|
2748
2913
|
};
|
|
2749
2914
|
|
|
2915
|
+
// src/server/tools/delete_spec_session.ts
|
|
2916
|
+
import { z as z5 } from "zod";
|
|
2917
|
+
var inputSchema5 = {
|
|
2918
|
+
session_id: z5.string().min(1).describe("UUID of the spec session to delete")
|
|
2919
|
+
};
|
|
2920
|
+
var deleteSpecSessionTool = {
|
|
2921
|
+
name: "delete_spec_session",
|
|
2922
|
+
description: "Soft-delete a spec session. Only an ARCHIVED session can be deleted \u2014 archive it first with archive_spec_session. This tool never archives on your behalf: archiving is the user's confirmation step, and collapsing the two-step destructive flow would remove the only checkpoint the platform provides. Check archived_at via get_spec_session to see whether delete is permitted.",
|
|
2923
|
+
inputSchema: inputSchema5,
|
|
2924
|
+
handler: async (args, ctx) => {
|
|
2925
|
+
try {
|
|
2926
|
+
await ctx.client.deleteSpecSession(args.session_id);
|
|
2927
|
+
} catch (err) {
|
|
2928
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2929
|
+
if (/must be archived/i.test(message)) {
|
|
2930
|
+
return errorResult(
|
|
2931
|
+
`Session ${args.session_id} was NOT deleted: session must be archived before it can be deleted. Archive it first with archive_spec_session \u2014 after reconfirming with the user that deleting this session is intended.`
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
const notFound = sessionNotFound(err, args.session_id);
|
|
2935
|
+
if (notFound) {
|
|
2936
|
+
return notFound;
|
|
2937
|
+
}
|
|
2938
|
+
throw err;
|
|
2939
|
+
}
|
|
2940
|
+
return jsonResult({ deleted: true, session_id: args.session_id });
|
|
2941
|
+
}
|
|
2942
|
+
};
|
|
2943
|
+
|
|
2750
2944
|
// src/server/tools/download_spec_file.ts
|
|
2751
2945
|
import path3 from "path";
|
|
2752
|
-
import { z as
|
|
2946
|
+
import { z as z6 } from "zod";
|
|
2753
2947
|
|
|
2754
2948
|
// src/server/download-path.ts
|
|
2755
2949
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
@@ -2883,18 +3077,18 @@ async function writeBytesAtomically(target, bytes) {
|
|
|
2883
3077
|
|
|
2884
3078
|
// src/server/tools/download_spec_file.ts
|
|
2885
3079
|
var MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
|
|
2886
|
-
var
|
|
2887
|
-
file_id:
|
|
2888
|
-
revision:
|
|
2889
|
-
destination_path:
|
|
3080
|
+
var inputSchema6 = {
|
|
3081
|
+
file_id: z6.string().min(1).describe("UUID of the file"),
|
|
3082
|
+
revision: z6.number().int().positive().optional().describe("Specific revision number to download (defaults to the latest revision)."),
|
|
3083
|
+
destination_path: z6.string().min(1).optional().describe(
|
|
2890
3084
|
"Local path to save the file to. Optional: omit it to save the file under `.specs/` in the MCP server's working directory, mirroring its remote path (`specs/add-oauth/proposal.md` \u2192 `.specs/add-oauth/proposal.md`). When given, it is absolute or relative to that working directory; a relative path rooted at `specs/` is redirected to `.specs/`, so pass an absolute path to save into a literal `specs/` directory. If it points to an existing directory (or ends with a path separator), the spec file's basename is appended."
|
|
2891
3085
|
),
|
|
2892
|
-
overwrite:
|
|
3086
|
+
overwrite: z6.boolean().optional().describe("Overwrite the destination file if it already exists (default: false).")
|
|
2893
3087
|
};
|
|
2894
3088
|
var downloadSpecFileTool = {
|
|
2895
3089
|
name: "download_spec_file",
|
|
2896
3090
|
description: "Download a spec file's raw bytes to a local file. By default it lands under `.specs/` in the MCP server's working directory, mirroring the file's remote path \u2014 pass `destination_path` only to save it somewhere else. Unlike read_spec_file, this handles binary files and files up to 50 MiB, and the response returns only metadata (saved path, size, revision) \u2014 not the content. Use it to save a large or binary file locally, then search or read it in chunks with other tools.",
|
|
2897
|
-
inputSchema:
|
|
3091
|
+
inputSchema: inputSchema6,
|
|
2898
3092
|
handler: async (args, ctx) => {
|
|
2899
3093
|
const meta = await ctx.client.getFileMetadata(args.file_id);
|
|
2900
3094
|
const targetRevision = args.revision ?? meta.revisionCount;
|
|
@@ -2992,15 +3186,15 @@ function buildOversizeMessage(fileId, byteLength) {
|
|
|
2992
3186
|
}
|
|
2993
3187
|
|
|
2994
3188
|
// src/server/tools/get_attachment.ts
|
|
2995
|
-
import { z as
|
|
2996
|
-
var
|
|
2997
|
-
attachment_id:
|
|
2998
|
-
include_download_url:
|
|
3189
|
+
import { z as z7 } from "zod";
|
|
3190
|
+
var inputSchema7 = {
|
|
3191
|
+
attachment_id: z7.string().min(1).describe("UUID of the attachment"),
|
|
3192
|
+
include_download_url: z7.number().int().nonnegative().optional().describe("Set to any positive value to include a signed download URL. Omit or 0 to skip.")
|
|
2999
3193
|
};
|
|
3000
3194
|
var getAttachmentTool = {
|
|
3001
3195
|
name: "get_attachment",
|
|
3002
3196
|
description: "Get attachment metadata. With `include_download_url>0`, also returns a signed `download_url`.",
|
|
3003
|
-
inputSchema:
|
|
3197
|
+
inputSchema: inputSchema7,
|
|
3004
3198
|
handler: async (args, ctx) => {
|
|
3005
3199
|
const attachment = await ctx.client.getAttachmentMetadata(args.attachment_id);
|
|
3006
3200
|
if (!attachment) {
|
|
@@ -3029,14 +3223,14 @@ var getAttachmentTool = {
|
|
|
3029
3223
|
};
|
|
3030
3224
|
|
|
3031
3225
|
// src/server/tools/get_project.ts
|
|
3032
|
-
import { z as
|
|
3033
|
-
var
|
|
3034
|
-
project_id:
|
|
3226
|
+
import { z as z8 } from "zod";
|
|
3227
|
+
var inputSchema8 = {
|
|
3228
|
+
project_id: z8.string().min(1).describe("UUID of the project to fetch")
|
|
3035
3229
|
};
|
|
3036
3230
|
var getProjectTool = {
|
|
3037
3231
|
name: "get_project",
|
|
3038
3232
|
description: "Get details for a project including all active spec sessions, files (id + metadata), and attachments (id + metadata).",
|
|
3039
|
-
inputSchema:
|
|
3233
|
+
inputSchema: inputSchema8,
|
|
3040
3234
|
handler: async (args, ctx) => {
|
|
3041
3235
|
const [project, sessions, files, attachments] = await Promise.all([
|
|
3042
3236
|
ctx.client.getProject(args.project_id),
|
|
@@ -3082,7 +3276,7 @@ var getProjectTool = {
|
|
|
3082
3276
|
};
|
|
3083
3277
|
|
|
3084
3278
|
// src/server/tools/get_spec_file.ts
|
|
3085
|
-
import { z as
|
|
3279
|
+
import { z as z9 } from "zod";
|
|
3086
3280
|
|
|
3087
3281
|
// src/server/tools/spec-file-output.ts
|
|
3088
3282
|
function specFileSummary(file) {
|
|
@@ -3102,16 +3296,16 @@ function specFileSummary(file) {
|
|
|
3102
3296
|
}
|
|
3103
3297
|
|
|
3104
3298
|
// src/server/tools/get_spec_file.ts
|
|
3105
|
-
var
|
|
3106
|
-
file_id:
|
|
3107
|
-
include_download_url:
|
|
3299
|
+
var inputSchema9 = {
|
|
3300
|
+
file_id: z9.string().min(1).describe("UUID of the file"),
|
|
3301
|
+
include_download_url: z9.number().int().nonnegative().optional().describe(
|
|
3108
3302
|
"Revision number to include a download URL for. Returns a signed, time-limited URL for that revision (latest or historical) that needs no credentials. Omit or 0 to skip."
|
|
3109
3303
|
)
|
|
3110
3304
|
};
|
|
3111
3305
|
var getSpecFileTool = {
|
|
3112
3306
|
name: "get_spec_file",
|
|
3113
3307
|
description: "Get spec file metadata. With `include_download_url=N`, also returns a signed, time-limited download URL for revision N (latest or historical). The URL is self-authenticating, so no credentials are returned.",
|
|
3114
|
-
inputSchema:
|
|
3308
|
+
inputSchema: inputSchema9,
|
|
3115
3309
|
handler: async (args, ctx) => {
|
|
3116
3310
|
const meta = await ctx.client.getFileMetadata(args.file_id);
|
|
3117
3311
|
const base = specFileSummary(meta);
|
|
@@ -3135,19 +3329,209 @@ var getSpecFileTool = {
|
|
|
3135
3329
|
}
|
|
3136
3330
|
};
|
|
3137
3331
|
|
|
3332
|
+
// src/server/tools/get_spec_session.ts
|
|
3333
|
+
import { z as z10 } from "zod";
|
|
3334
|
+
|
|
3335
|
+
// src/util/bytes.ts
|
|
3336
|
+
function encodeBase64(bytes) {
|
|
3337
|
+
const CHUNK = 32768;
|
|
3338
|
+
let binary = "";
|
|
3339
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
3340
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
3341
|
+
}
|
|
3342
|
+
return btoa(binary);
|
|
3343
|
+
}
|
|
3344
|
+
function isBinary(bytes) {
|
|
3345
|
+
return bytes.includes(0);
|
|
3346
|
+
}
|
|
3347
|
+
function decodeUtf8Strict(bytes) {
|
|
3348
|
+
try {
|
|
3349
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }).decode(bytes);
|
|
3350
|
+
} catch {
|
|
3351
|
+
return null;
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
function utf8ByteLength(value) {
|
|
3355
|
+
return new TextEncoder().encode(value).byteLength;
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
// src/server/session-context.ts
|
|
3359
|
+
var MAX_CONTEXT_RESPONSE_BYTES = 1 * 1024 * 1024;
|
|
3360
|
+
var SESSION_SUMMARY_MAX_LENGTH = 500;
|
|
3361
|
+
function jsonTypeOf(value) {
|
|
3362
|
+
if (value === null) {
|
|
3363
|
+
return "null";
|
|
3364
|
+
}
|
|
3365
|
+
if (Array.isArray(value)) {
|
|
3366
|
+
return "array";
|
|
3367
|
+
}
|
|
3368
|
+
const t = typeof value;
|
|
3369
|
+
if (t === "string" || t === "number" || t === "boolean") {
|
|
3370
|
+
return t;
|
|
3371
|
+
}
|
|
3372
|
+
return "object";
|
|
3373
|
+
}
|
|
3374
|
+
function isPlainObject(value) {
|
|
3375
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3376
|
+
}
|
|
3377
|
+
function introspectContext(context) {
|
|
3378
|
+
if (context === null || context === void 0) {
|
|
3379
|
+
return { session_summary: null, context_size_bytes: 0, context_keys: [] };
|
|
3380
|
+
}
|
|
3381
|
+
const sizeBytes = contextSizeBytes(context);
|
|
3382
|
+
if (!isPlainObject(context)) {
|
|
3383
|
+
const type = jsonTypeOf(context);
|
|
3384
|
+
return {
|
|
3385
|
+
session_summary: null,
|
|
3386
|
+
context_size_bytes: sizeBytes,
|
|
3387
|
+
context_keys: [],
|
|
3388
|
+
...type !== "object" && type !== "null" ? { context_type: type } : {}
|
|
3389
|
+
};
|
|
3390
|
+
}
|
|
3391
|
+
const summary = context.sessionSummary;
|
|
3392
|
+
return {
|
|
3393
|
+
session_summary: typeof summary === "string" ? summary : null,
|
|
3394
|
+
context_size_bytes: sizeBytes,
|
|
3395
|
+
context_keys: Object.entries(context).map(([key, value]) => ({
|
|
3396
|
+
key,
|
|
3397
|
+
size_bytes: value === void 0 ? 0 : utf8ByteLength(JSON.stringify(value))
|
|
3398
|
+
}))
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
function contextSizeBytes(context) {
|
|
3402
|
+
if (context === null || context === void 0) {
|
|
3403
|
+
return 0;
|
|
3404
|
+
}
|
|
3405
|
+
return utf8ByteLength(JSON.stringify(context));
|
|
3406
|
+
}
|
|
3407
|
+
function mergeSessionSummary(context, sessionSummary) {
|
|
3408
|
+
if (context === null || context === void 0) {
|
|
3409
|
+
return { ok: true, context: { sessionSummary } };
|
|
3410
|
+
}
|
|
3411
|
+
if (isPlainObject(context)) {
|
|
3412
|
+
return { ok: true, context: { ...context, sessionSummary } };
|
|
3413
|
+
}
|
|
3414
|
+
return { ok: false, actualType: jsonTypeOf(context) };
|
|
3415
|
+
}
|
|
3416
|
+
|
|
3417
|
+
// src/server/tools/get_spec_session.ts
|
|
3418
|
+
var inputSchema10 = {
|
|
3419
|
+
session_id: z10.string().min(1).describe("UUID of the spec session"),
|
|
3420
|
+
include_context: z10.boolean().optional().describe(
|
|
3421
|
+
"When true, include the whole context JSONB (subject to a 1 MiB ceiling \u2014 above it the tool errors with the actual size). Default false: the response instead carries context_size_bytes and context_keys so you can see the size and shape before asking."
|
|
3422
|
+
)
|
|
3423
|
+
};
|
|
3424
|
+
var getSpecSessionTool = {
|
|
3425
|
+
name: "get_spec_session",
|
|
3426
|
+
description: "Get one spec session's metadata: status, counters, timestamps, archived_at (delete eligibility), session_summary, plus context_size_bytes and context_keys describing the context without returning it. Set include_context=true for the full context (1 MiB ceiling). The chat transcript is never returned.",
|
|
3427
|
+
inputSchema: inputSchema10,
|
|
3428
|
+
handler: async (args, ctx) => {
|
|
3429
|
+
let session;
|
|
3430
|
+
try {
|
|
3431
|
+
session = await ctx.client.getSpecSession(args.session_id);
|
|
3432
|
+
} catch (err) {
|
|
3433
|
+
const notFound = sessionNotFound(err, args.session_id);
|
|
3434
|
+
if (notFound) {
|
|
3435
|
+
return notFound;
|
|
3436
|
+
}
|
|
3437
|
+
throw err;
|
|
3438
|
+
}
|
|
3439
|
+
const summary = introspectContext(session.context);
|
|
3440
|
+
const base = {
|
|
3441
|
+
...sessionMetadata(session),
|
|
3442
|
+
session_summary: summary.session_summary,
|
|
3443
|
+
context_size_bytes: summary.context_size_bytes,
|
|
3444
|
+
context_keys: summary.context_keys,
|
|
3445
|
+
...summary.context_type === void 0 ? {} : { context_type: summary.context_type }
|
|
3446
|
+
};
|
|
3447
|
+
if (args.include_context !== true) {
|
|
3448
|
+
return jsonResult(base);
|
|
3449
|
+
}
|
|
3450
|
+
const size = contextSizeBytes(session.context);
|
|
3451
|
+
if (size > MAX_CONTEXT_RESPONSE_BYTES) {
|
|
3452
|
+
return errorResult(
|
|
3453
|
+
`Session ${session.id} context is ${String(size)} bytes, above the ${String(MAX_CONTEXT_RESPONSE_BYTES)}-byte include_context ceiling. Structured JSON is not truncated; use context_keys to locate what you need, or read the session in the MySpec webapp.`
|
|
3454
|
+
);
|
|
3455
|
+
}
|
|
3456
|
+
return jsonResult({ ...base, context: session.context ?? null });
|
|
3457
|
+
}
|
|
3458
|
+
};
|
|
3459
|
+
|
|
3460
|
+
// src/server/tools/list_attachments.ts
|
|
3461
|
+
import { z as z11 } from "zod";
|
|
3462
|
+
|
|
3463
|
+
// src/util/list-paging.ts
|
|
3464
|
+
function clampLimit(limit, defaultLimit, maxLimit) {
|
|
3465
|
+
const requested = limit ?? defaultLimit;
|
|
3466
|
+
return Math.min(Math.max(1, requested), maxLimit);
|
|
3467
|
+
}
|
|
3468
|
+
function sliceWindow(items, limit, offset) {
|
|
3469
|
+
const total = items.length;
|
|
3470
|
+
const start = Math.min(Math.max(0, offset), total);
|
|
3471
|
+
const slice = items.slice(start, start + limit);
|
|
3472
|
+
const end = start + slice.length;
|
|
3473
|
+
return {
|
|
3474
|
+
items: [...slice],
|
|
3475
|
+
total,
|
|
3476
|
+
limit,
|
|
3477
|
+
offset: start,
|
|
3478
|
+
...end < total ? { next_offset: end } : {}
|
|
3479
|
+
};
|
|
3480
|
+
}
|
|
3481
|
+
function nextOffsetOf(offset, returned, total) {
|
|
3482
|
+
const end = offset + returned;
|
|
3483
|
+
return end < total ? end : void 0;
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
// src/server/tools/list_attachments.ts
|
|
3487
|
+
var LIST_DEFAULT = 50;
|
|
3488
|
+
var LIST_MAX = 200;
|
|
3489
|
+
var inputSchema11 = {
|
|
3490
|
+
project_id: z11.string().min(1).describe("UUID of the project whose attachments to list."),
|
|
3491
|
+
limit: z11.number().int().positive().optional().describe(
|
|
3492
|
+
`Max attachments to return (default ${String(LIST_DEFAULT)}, capped at ${String(LIST_MAX)}).`
|
|
3493
|
+
),
|
|
3494
|
+
offset: z11.number().int().nonnegative().optional().describe("Pagination offset (default 0)")
|
|
3495
|
+
};
|
|
3496
|
+
var listAttachmentsTool = {
|
|
3497
|
+
name: "list_attachments",
|
|
3498
|
+
description: "List a project's attachments (metadata only, always including file_size_bytes so a caller can check the size before read_attachment). Returns `total` and, when more records remain, `next_offset`.",
|
|
3499
|
+
inputSchema: inputSchema11,
|
|
3500
|
+
handler: async (args, ctx) => {
|
|
3501
|
+
const all = await ctx.client.listAttachments(args.project_id);
|
|
3502
|
+
const limit = clampLimit(args.limit, LIST_DEFAULT, LIST_MAX);
|
|
3503
|
+
const window = sliceWindow(all, limit, args.offset ?? 0);
|
|
3504
|
+
return jsonResult({
|
|
3505
|
+
project_id: args.project_id,
|
|
3506
|
+
attachments: window.items.map((a) => ({
|
|
3507
|
+
id: a.id,
|
|
3508
|
+
name: a.name,
|
|
3509
|
+
mime_type: a.mimeType,
|
|
3510
|
+
file_size_bytes: a.fileSizeBytes,
|
|
3511
|
+
checksum: a.checksum,
|
|
3512
|
+
created_at: a.createdAt
|
|
3513
|
+
})),
|
|
3514
|
+
total: window.total,
|
|
3515
|
+
limit: window.limit,
|
|
3516
|
+
offset: window.offset,
|
|
3517
|
+
...window.next_offset === void 0 ? {} : { next_offset: window.next_offset }
|
|
3518
|
+
});
|
|
3519
|
+
}
|
|
3520
|
+
};
|
|
3521
|
+
|
|
3138
3522
|
// src/server/tools/list_projects.ts
|
|
3139
|
-
import { z as
|
|
3140
|
-
var
|
|
3141
|
-
query:
|
|
3523
|
+
import { z as z12 } from "zod";
|
|
3524
|
+
var inputSchema12 = {
|
|
3525
|
+
query: z12.string().optional().describe(
|
|
3142
3526
|
"Search term to filter projects by name. Omit to list all projects."
|
|
3143
3527
|
),
|
|
3144
|
-
limit:
|
|
3145
|
-
offset:
|
|
3528
|
+
limit: z12.number().int().positive().max(100).optional().describe("Max projects to return (default 20)"),
|
|
3529
|
+
offset: z12.number().int().nonnegative().optional().describe("Pagination offset (default 0)")
|
|
3146
3530
|
};
|
|
3147
3531
|
var listProjectsTool = {
|
|
3148
3532
|
name: "list_projects",
|
|
3149
3533
|
description: "List or search MySpec projects accessible to the authenticated user.",
|
|
3150
|
-
inputSchema:
|
|
3534
|
+
inputSchema: inputSchema12,
|
|
3151
3535
|
handler: async (args, ctx) => {
|
|
3152
3536
|
const result = await ctx.client.listProjects({
|
|
3153
3537
|
query: args.query,
|
|
@@ -3170,21 +3554,21 @@ var listProjectsTool = {
|
|
|
3170
3554
|
};
|
|
3171
3555
|
|
|
3172
3556
|
// src/server/tools/list_spec_file.ts
|
|
3173
|
-
import { z as
|
|
3557
|
+
import { z as z13 } from "zod";
|
|
3174
3558
|
var PAGE_SIZE = 100;
|
|
3175
|
-
var
|
|
3176
|
-
project_id:
|
|
3177
|
-
path:
|
|
3559
|
+
var inputSchema13 = {
|
|
3560
|
+
project_id: z13.string().min(1).describe("UUID of the project whose spec files to list."),
|
|
3561
|
+
path: z13.string().optional().describe(
|
|
3178
3562
|
'Directory prefix to filter by, e.g. ".spec" or ".spec/design". Matches the exact file and everything under that directory. Pass "", "/", or omit to list all files.'
|
|
3179
3563
|
),
|
|
3180
|
-
trashed:
|
|
3564
|
+
trashed: z13.boolean().optional().describe(
|
|
3181
3565
|
"When true, list files in the project Trash Bin instead. When false or omitted, list only live files (trashed files excluded)."
|
|
3182
3566
|
)
|
|
3183
3567
|
};
|
|
3184
3568
|
var listSpecFileTool = {
|
|
3185
3569
|
name: "list_spec_file",
|
|
3186
3570
|
description: 'List a project\'s spec files with id, path, and metadata (revision count, file size of the latest revision, file type, owning session, last update). Filter to a directory with `path` (exact file or everything under it; pass "", "/", or omit for all). Set `trashed=true` to list files in the Trash Bin instead of live files.',
|
|
3187
|
-
inputSchema:
|
|
3571
|
+
inputSchema: inputSchema13,
|
|
3188
3572
|
handler: async (args, ctx) => {
|
|
3189
3573
|
const trashed = args.trashed ?? false;
|
|
3190
3574
|
const files = trashed ? await ctx.client.listTrashedFiles(args.project_id) : await listAllLiveFiles(ctx.client, args.project_id);
|
|
@@ -3240,34 +3624,255 @@ function isUnderPath(filePath, prefix) {
|
|
|
3240
3624
|
return filePath === prefix || filePath.startsWith(prefix + "/");
|
|
3241
3625
|
}
|
|
3242
3626
|
|
|
3627
|
+
// src/server/tools/list_spec_sessions.ts
|
|
3628
|
+
import { z as z14 } from "zod";
|
|
3629
|
+
var LIST_DEFAULT2 = 10;
|
|
3630
|
+
var LIST_MAX2 = 20;
|
|
3631
|
+
var inputSchema14 = {
|
|
3632
|
+
project_id: z14.string().min(1).describe("UUID of the project whose spec sessions to list."),
|
|
3633
|
+
archive_filter: z14.enum(["active", "archived", "all"]).optional().describe("Which sessions to list: 'active' (default), 'archived', or 'all'."),
|
|
3634
|
+
status: z14.enum(["generating", "pending_input", "completed"]).optional().describe("Filter by session status."),
|
|
3635
|
+
query: z14.string().optional().describe("Search term matched against the session summary."),
|
|
3636
|
+
limit: z14.number().int().positive().optional().describe(
|
|
3637
|
+
`Max sessions to return (default ${String(LIST_DEFAULT2)}, capped at ${String(LIST_MAX2)}).`
|
|
3638
|
+
),
|
|
3639
|
+
offset: z14.number().int().nonnegative().optional().describe("Pagination offset (default 0)")
|
|
3640
|
+
};
|
|
3641
|
+
var listSpecSessionsTool = {
|
|
3642
|
+
name: "list_spec_sessions",
|
|
3643
|
+
description: `List a project's spec sessions (metadata plus session_summary and context_size_bytes \u2014 never the context itself or the chat transcript). Filter with archive_filter ('active'/'archived'/'all'), status, and query (searches the session summary). Capped at ${String(LIST_MAX2)} per page; returns \`total\` and \`next_offset\`.`,
|
|
3644
|
+
inputSchema: inputSchema14,
|
|
3645
|
+
handler: async (args, ctx) => {
|
|
3646
|
+
const limit = clampLimit(args.limit, LIST_DEFAULT2, LIST_MAX2);
|
|
3647
|
+
const offset = args.offset ?? 0;
|
|
3648
|
+
const result = await ctx.client.listProjectSessions(args.project_id, {
|
|
3649
|
+
archiveFilter: args.archive_filter ?? "active",
|
|
3650
|
+
...args.status === void 0 ? {} : { status: args.status },
|
|
3651
|
+
...args.query === void 0 ? {} : { query: args.query },
|
|
3652
|
+
limit,
|
|
3653
|
+
offset
|
|
3654
|
+
});
|
|
3655
|
+
const next = nextOffsetOf(offset, result.sessions.length, result.total);
|
|
3656
|
+
return jsonResult({
|
|
3657
|
+
project_id: args.project_id,
|
|
3658
|
+
sessions: result.sessions.map((s) => {
|
|
3659
|
+
const summary = introspectContext(s.context);
|
|
3660
|
+
return {
|
|
3661
|
+
...sessionMetadata(s),
|
|
3662
|
+
session_summary: summary.session_summary,
|
|
3663
|
+
context_size_bytes: summary.context_size_bytes
|
|
3664
|
+
};
|
|
3665
|
+
}),
|
|
3666
|
+
total: result.total,
|
|
3667
|
+
limit,
|
|
3668
|
+
offset,
|
|
3669
|
+
...next === void 0 ? {} : { next_offset: next }
|
|
3670
|
+
});
|
|
3671
|
+
}
|
|
3672
|
+
};
|
|
3673
|
+
|
|
3243
3674
|
// src/server/tools/move_spec_file_to_trash.ts
|
|
3244
|
-
import { z as
|
|
3245
|
-
var
|
|
3246
|
-
file_id:
|
|
3675
|
+
import { z as z15 } from "zod";
|
|
3676
|
+
var inputSchema15 = {
|
|
3677
|
+
file_id: z15.string().min(1).describe("UUID of the spec file to move to the Trash Bin.")
|
|
3247
3678
|
};
|
|
3248
3679
|
var moveSpecFileToTrashTool = {
|
|
3249
3680
|
name: "move_spec_file_to_trash",
|
|
3250
3681
|
description: "Move a spec file to the Trash Bin. This is recoverable \u2014 the file is hidden from the default file list and AI-agent tools but kept restorable indefinitely via `restore_spec_file_from_trash`. It is NOT a permanent delete.",
|
|
3251
|
-
inputSchema:
|
|
3682
|
+
inputSchema: inputSchema15,
|
|
3252
3683
|
handler: async (args, ctx) => {
|
|
3253
3684
|
await ctx.client.trashFile(args.file_id);
|
|
3254
3685
|
return jsonResult({ trashed: true, file_id: args.file_id });
|
|
3255
3686
|
}
|
|
3256
3687
|
};
|
|
3257
3688
|
|
|
3689
|
+
// src/server/tools/read_attachment.ts
|
|
3690
|
+
import { z as z16 } from "zod";
|
|
3691
|
+
|
|
3692
|
+
// src/util/paginate-lines.ts
|
|
3693
|
+
var MAX_LINES_PER_CALL = 2e3;
|
|
3694
|
+
var MAX_RESPONSE_BYTES = 1 * 1024 * 1024;
|
|
3695
|
+
function normalizeNewlines(text) {
|
|
3696
|
+
const withoutBom = text.startsWith("\uFEFF") ? text.slice(1) : text;
|
|
3697
|
+
return withoutBom.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3698
|
+
}
|
|
3699
|
+
function splitIntoLines(text) {
|
|
3700
|
+
const parts = text.split("\n");
|
|
3701
|
+
if (parts.length > 0 && parts[parts.length - 1] === "") {
|
|
3702
|
+
parts.pop();
|
|
3703
|
+
}
|
|
3704
|
+
return parts;
|
|
3705
|
+
}
|
|
3706
|
+
function truncateToBytes(value, maxBytes) {
|
|
3707
|
+
const encoded = new TextEncoder().encode(value);
|
|
3708
|
+
if (encoded.byteLength <= maxBytes) {
|
|
3709
|
+
return value;
|
|
3710
|
+
}
|
|
3711
|
+
const decoded = new TextDecoder("utf-8").decode(encoded.subarray(0, maxBytes));
|
|
3712
|
+
return decoded.replace(/�+$/, "");
|
|
3713
|
+
}
|
|
3714
|
+
function paginateText(text, offsetArg, limitArg) {
|
|
3715
|
+
const lines = splitIntoLines(normalizeNewlines(text));
|
|
3716
|
+
const totalLines = lines.length;
|
|
3717
|
+
const rawOffset = offsetArg ?? 1;
|
|
3718
|
+
const offset = rawOffset === 0 ? 1 : rawOffset;
|
|
3719
|
+
if (offset < 1) {
|
|
3720
|
+
return {
|
|
3721
|
+
ok: false,
|
|
3722
|
+
error: `Invalid offset ${String(rawOffset)}; offset is 1-based and must be >= 1.`
|
|
3723
|
+
};
|
|
3724
|
+
}
|
|
3725
|
+
if (totalLines === 0) {
|
|
3726
|
+
return {
|
|
3727
|
+
ok: true,
|
|
3728
|
+
window: {
|
|
3729
|
+
content: "",
|
|
3730
|
+
start_line: 1,
|
|
3731
|
+
end_line: 0,
|
|
3732
|
+
read_lines: 0,
|
|
3733
|
+
total_lines: 0,
|
|
3734
|
+
truncated: false
|
|
3735
|
+
}
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3738
|
+
if (offset > totalLines) {
|
|
3739
|
+
return {
|
|
3740
|
+
ok: false,
|
|
3741
|
+
error: `Offset ${String(rawOffset)} is past the end of the file (total ${String(totalLines)} lines).`
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
const effectiveLimit = Math.min(Math.max(1, limitArg ?? MAX_LINES_PER_CALL), MAX_LINES_PER_CALL);
|
|
3745
|
+
const startIdx = offset - 1;
|
|
3746
|
+
const lineWindowEnd = Math.min(startIdx + effectiveLimit, totalLines);
|
|
3747
|
+
const out = [];
|
|
3748
|
+
let usedBytes = 0;
|
|
3749
|
+
let index = startIdx;
|
|
3750
|
+
let byteCeilingHit = false;
|
|
3751
|
+
for (; index < lineWindowEnd; index++) {
|
|
3752
|
+
const line = lines[index] ?? "";
|
|
3753
|
+
const lineBytes = utf8ByteLength(line);
|
|
3754
|
+
const separatorBytes = out.length > 0 ? 1 : 0;
|
|
3755
|
+
if (usedBytes + separatorBytes + lineBytes > MAX_RESPONSE_BYTES) {
|
|
3756
|
+
if (out.length === 0) {
|
|
3757
|
+
out.push(
|
|
3758
|
+
`${truncateToBytes(line, MAX_RESPONSE_BYTES)}\u2026 [truncated, ${String(lineBytes)} bytes total]`
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3761
|
+
byteCeilingHit = true;
|
|
3762
|
+
break;
|
|
3763
|
+
}
|
|
3764
|
+
usedBytes += separatorBytes + lineBytes;
|
|
3765
|
+
out.push(line);
|
|
3766
|
+
}
|
|
3767
|
+
const endLine = startIdx + out.length;
|
|
3768
|
+
const truncated = byteCeilingHit || endLine < totalLines;
|
|
3769
|
+
return {
|
|
3770
|
+
ok: true,
|
|
3771
|
+
window: {
|
|
3772
|
+
content: out.join("\n"),
|
|
3773
|
+
start_line: offset,
|
|
3774
|
+
end_line: endLine,
|
|
3775
|
+
read_lines: out.length,
|
|
3776
|
+
total_lines: totalLines,
|
|
3777
|
+
truncated,
|
|
3778
|
+
...truncated && endLine < totalLines ? { next_offset: endLine + 1 } : {}
|
|
3779
|
+
}
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3783
|
+
// src/server/tools/read_attachment.ts
|
|
3784
|
+
var MAX_ATTACHMENT_MB = MAX_ATTACHMENT_BYTES / (1024 * 1024);
|
|
3785
|
+
var IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
3786
|
+
"image/png",
|
|
3787
|
+
"image/jpeg",
|
|
3788
|
+
"image/gif",
|
|
3789
|
+
"image/webp"
|
|
3790
|
+
]);
|
|
3791
|
+
var TEXT_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
3792
|
+
"application/json",
|
|
3793
|
+
"application/xml",
|
|
3794
|
+
"application/x-yaml",
|
|
3795
|
+
"application/yaml",
|
|
3796
|
+
"image/svg+xml"
|
|
3797
|
+
]);
|
|
3798
|
+
var GENERIC_MIME_TYPES = /* @__PURE__ */ new Set(["application/octet-stream", ""]);
|
|
3799
|
+
var inputSchema16 = {
|
|
3800
|
+
attachment_id: z16.string().min(1).describe("UUID of the attachment to read"),
|
|
3801
|
+
offset: z16.number().int().nonnegative().optional().describe(
|
|
3802
|
+
"1-based line number to start reading from (text attachments only; default 1; 0 is an alias for 1)."
|
|
3803
|
+
),
|
|
3804
|
+
limit: z16.number().int().positive().optional().describe("Max lines to return (text attachments only; default 2000, capped at 2000).")
|
|
3805
|
+
};
|
|
3806
|
+
var readAttachmentTool = {
|
|
3807
|
+
name: "read_attachment",
|
|
3808
|
+
description: `Return an attachment's content inline. UTF-8 text is paginated by line (offset/limit, 1 MiB per response). Images (png, jpeg, gif, webp) are returned whole as an MCP image block \u2014 never paginated \u2014 up to the ${String(MAX_ATTACHMENT_MB)} MiB attachment limit; a 10 MiB image is roughly 13.4 MiB of base64, so check file_size_bytes (from get_attachment or list_attachments) before reading a large one. Other binary types (pdf, docx, xlsx, \u2026) cannot be read here; view them in the MySpec webapp.`,
|
|
3809
|
+
inputSchema: inputSchema16,
|
|
3810
|
+
handler: async (args, ctx) => {
|
|
3811
|
+
const meta = await ctx.client.getAttachmentMetadata(args.attachment_id);
|
|
3812
|
+
if (!meta) {
|
|
3813
|
+
return errorResult(`Attachment ${args.attachment_id} not found.`);
|
|
3814
|
+
}
|
|
3815
|
+
const mime = meta.mimeType;
|
|
3816
|
+
if (IMAGE_MIME_TYPES.has(mime)) {
|
|
3817
|
+
if (meta.fileSizeBytes > MAX_ATTACHMENT_BYTES) {
|
|
3818
|
+
return errorResult(
|
|
3819
|
+
`Attachment ${meta.id} is ${String(meta.fileSizeBytes)} bytes, above the ${String(MAX_ATTACHMENT_MB)} MiB image ceiling.`
|
|
3820
|
+
);
|
|
3821
|
+
}
|
|
3822
|
+
const bytes2 = await ctx.client.downloadAttachment(meta.id);
|
|
3823
|
+
if (bytes2.byteLength > MAX_ATTACHMENT_BYTES) {
|
|
3824
|
+
return errorResult(
|
|
3825
|
+
`Attachment ${meta.id} is ${String(bytes2.byteLength)} bytes, above the ${String(MAX_ATTACHMENT_MB)} MiB image ceiling.`
|
|
3826
|
+
);
|
|
3827
|
+
}
|
|
3828
|
+
return imageResult(encodeBase64(bytes2), mime, {
|
|
3829
|
+
attachment_id: meta.id,
|
|
3830
|
+
file_name: meta.name,
|
|
3831
|
+
mime_type: mime,
|
|
3832
|
+
file_size_bytes: bytes2.byteLength
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
const declaredText = mime.startsWith("text/") || TEXT_MIME_TYPES.has(mime);
|
|
3836
|
+
const generic = GENERIC_MIME_TYPES.has(mime);
|
|
3837
|
+
if (!declaredText && !generic) {
|
|
3838
|
+
return errorResult(
|
|
3839
|
+
`Attachment ${meta.id} has MIME type ${mime}, which is neither UTF-8 text nor a supported image type (png, jpeg, gif, webp). View it in the MySpec webapp, or use get_attachment with include_download_url=1 for a signed URL to download it locally.`
|
|
3840
|
+
);
|
|
3841
|
+
}
|
|
3842
|
+
const bytes = await ctx.client.downloadAttachment(meta.id);
|
|
3843
|
+
const text = isBinary(bytes) ? null : decodeUtf8Strict(bytes);
|
|
3844
|
+
if (text === null) {
|
|
3845
|
+
return errorResult(
|
|
3846
|
+
`Attachment ${meta.id} (MIME type ${mime}) is not valid UTF-8 text and is not a supported image type. View it in the MySpec webapp, or use get_attachment with include_download_url=1 for a signed URL to download it locally.`
|
|
3847
|
+
);
|
|
3848
|
+
}
|
|
3849
|
+
const paginated = paginateText(text, args.offset, args.limit);
|
|
3850
|
+
if (!paginated.ok) {
|
|
3851
|
+
return errorResult(paginated.error);
|
|
3852
|
+
}
|
|
3853
|
+
return jsonResult({
|
|
3854
|
+
attachment_id: meta.id,
|
|
3855
|
+
file_name: meta.name,
|
|
3856
|
+
mime_type: mime,
|
|
3857
|
+
file_size_bytes: meta.fileSizeBytes,
|
|
3858
|
+
...paginated.window
|
|
3859
|
+
});
|
|
3860
|
+
}
|
|
3861
|
+
};
|
|
3862
|
+
|
|
3258
3863
|
// src/server/tools/read_spec_file.ts
|
|
3259
3864
|
import { promises as fs3 } from "fs";
|
|
3260
3865
|
import { createHash as createHash2 } from "crypto";
|
|
3261
|
-
import { z as
|
|
3866
|
+
import { z as z17 } from "zod";
|
|
3262
3867
|
var MAX_READ_BYTES = 1 * 1024 * 1024;
|
|
3263
|
-
var
|
|
3264
|
-
file_id:
|
|
3265
|
-
revision:
|
|
3868
|
+
var inputSchema17 = {
|
|
3869
|
+
file_id: z17.string().min(1).describe("UUID of the file"),
|
|
3870
|
+
revision: z17.number().int().positive().optional().describe("Specific revision number to read (defaults to the latest revision).")
|
|
3266
3871
|
};
|
|
3267
3872
|
var readSpecFileTool = {
|
|
3268
3873
|
name: "read_spec_file",
|
|
3269
3874
|
description: "Return a spec file's UTF-8 content. Bytes are cached on disk under the configured cache root (default ~/.myspec) at `project/<projectId>/file/<fileId>/rev/<rev>/<basename>` and reused on subsequent reads of the same revision. Fails for binary files or files larger than 1 MiB \u2014 use download_spec_file to save those to a local file instead.",
|
|
3270
|
-
inputSchema:
|
|
3875
|
+
inputSchema: inputSchema17,
|
|
3271
3876
|
handler: async (args, ctx) => {
|
|
3272
3877
|
const meta = await ctx.client.getFileMetadata(args.file_id);
|
|
3273
3878
|
const targetRevision = args.revision ?? meta.revisionCount;
|
|
@@ -3305,7 +3910,7 @@ var readSpecFileTool = {
|
|
|
3305
3910
|
if (bytes.byteLength > MAX_READ_BYTES) {
|
|
3306
3911
|
return errorResult(buildOversizeMessage2(args.file_id, bytes.byteLength));
|
|
3307
3912
|
}
|
|
3308
|
-
if (
|
|
3913
|
+
if (isBinary2(bytes)) {
|
|
3309
3914
|
return errorResult(
|
|
3310
3915
|
`File ${args.file_id} appears to be binary (file_type=${meta.fileType}, size=${String(bytes.byteLength)} bytes). Use download_spec_file to save it to a local file.`
|
|
3311
3916
|
);
|
|
@@ -3338,7 +3943,7 @@ var readSpecFileTool = {
|
|
|
3338
3943
|
function buildOversizeMessage2(fileId, byteLength) {
|
|
3339
3944
|
return `File ${fileId} is ${String(byteLength)} bytes which exceeds the read cap of ${String(MAX_READ_BYTES)} bytes. Use download_spec_file to save it to a local file and read it in chunks.`;
|
|
3340
3945
|
}
|
|
3341
|
-
function
|
|
3946
|
+
function isBinary2(bytes) {
|
|
3342
3947
|
for (const byte of bytes) {
|
|
3343
3948
|
if (byte === 0) {
|
|
3344
3949
|
return true;
|
|
@@ -3350,15 +3955,59 @@ function sha256Prefixed2(content) {
|
|
|
3350
3955
|
return "sha256:" + createHash2("sha256").update(content).digest("hex");
|
|
3351
3956
|
}
|
|
3352
3957
|
|
|
3958
|
+
// src/server/tools/rename_spec_session.ts
|
|
3959
|
+
import { z as z18 } from "zod";
|
|
3960
|
+
var inputSchema18 = {
|
|
3961
|
+
session_id: z18.string().min(1).describe("UUID of the spec session to rename"),
|
|
3962
|
+
session_summary: z18.string().min(1).max(SESSION_SUMMARY_MAX_LENGTH).describe(
|
|
3963
|
+
`New human-readable session title (max ${String(SESSION_SUMMARY_MAX_LENGTH)} characters). Stored as context.sessionSummary \u2014 the field the webapp displays and searches.`
|
|
3964
|
+
)
|
|
3965
|
+
};
|
|
3966
|
+
var renameSpecSessionTool = {
|
|
3967
|
+
name: "rename_spec_session",
|
|
3968
|
+
description: "Rename a spec session by updating its context.sessionSummary \u2014 the session's human-readable title (there is no first-class name field). Permitted on both active and archived sessions. All other context keys are preserved untouched.",
|
|
3969
|
+
inputSchema: inputSchema18,
|
|
3970
|
+
handler: async (args, ctx) => {
|
|
3971
|
+
if (args.session_summary.length > SESSION_SUMMARY_MAX_LENGTH) {
|
|
3972
|
+
return errorResult(
|
|
3973
|
+
`session_summary is ${String(args.session_summary.length)} characters; the limit is ${String(SESSION_SUMMARY_MAX_LENGTH)}.`
|
|
3974
|
+
);
|
|
3975
|
+
}
|
|
3976
|
+
let session;
|
|
3977
|
+
try {
|
|
3978
|
+
session = await ctx.client.getSpecSession(args.session_id);
|
|
3979
|
+
} catch (err) {
|
|
3980
|
+
const notFound = sessionNotFound(err, args.session_id);
|
|
3981
|
+
if (notFound) {
|
|
3982
|
+
return notFound;
|
|
3983
|
+
}
|
|
3984
|
+
throw err;
|
|
3985
|
+
}
|
|
3986
|
+
const merged = mergeSessionSummary(session.context, args.session_summary);
|
|
3987
|
+
if (!merged.ok) {
|
|
3988
|
+
return errorResult(
|
|
3989
|
+
`Session ${args.session_id} was NOT renamed: its context is a JSON ${merged.actualType}, not an object. This shape is unexpected, the tool cannot know what it encodes, and overwriting it would be unrecoverable. Inspect the session in the MySpec webapp.`
|
|
3990
|
+
);
|
|
3991
|
+
}
|
|
3992
|
+
const updated = await ctx.client.updateSpecSessionContext(args.session_id, merged.context);
|
|
3993
|
+
const summary = introspectContext(updated.context);
|
|
3994
|
+
return jsonResult({
|
|
3995
|
+
renamed: true,
|
|
3996
|
+
session_id: updated.id,
|
|
3997
|
+
session_summary: summary.session_summary
|
|
3998
|
+
});
|
|
3999
|
+
}
|
|
4000
|
+
};
|
|
4001
|
+
|
|
3353
4002
|
// src/server/tools/restore_spec_file_from_trash.ts
|
|
3354
|
-
import { z as
|
|
3355
|
-
var
|
|
3356
|
-
file_id:
|
|
4003
|
+
import { z as z19 } from "zod";
|
|
4004
|
+
var inputSchema19 = {
|
|
4005
|
+
file_id: z19.string().min(1).describe("UUID of the spec file to restore from the Trash Bin.")
|
|
3357
4006
|
};
|
|
3358
4007
|
var restoreSpecFileFromTrashTool = {
|
|
3359
4008
|
name: "restore_spec_file_from_trash",
|
|
3360
4009
|
description: "Restore a spec file from the Trash Bin back to the active file list, reversing `move_spec_file_to_trash`. The file becomes visible again to the default file list and AI-agent tools.",
|
|
3361
|
-
inputSchema:
|
|
4010
|
+
inputSchema: inputSchema19,
|
|
3362
4011
|
handler: async (args, ctx) => {
|
|
3363
4012
|
await ctx.client.restoreFile(args.file_id);
|
|
3364
4013
|
return jsonResult({ restored: true, file_id: args.file_id });
|
|
@@ -3366,34 +4015,58 @@ var restoreSpecFileFromTrashTool = {
|
|
|
3366
4015
|
};
|
|
3367
4016
|
|
|
3368
4017
|
// src/server/tools/unarchive_project.ts
|
|
3369
|
-
import { z as
|
|
3370
|
-
var
|
|
3371
|
-
project_id:
|
|
4018
|
+
import { z as z20 } from "zod";
|
|
4019
|
+
var inputSchema20 = {
|
|
4020
|
+
project_id: z20.string().min(1).describe("UUID of the project to unarchive.")
|
|
3372
4021
|
};
|
|
3373
4022
|
var unarchiveProjectTool = {
|
|
3374
4023
|
name: "unarchive_project",
|
|
3375
4024
|
description: 'Unarchive a project (sets status to "active"). Idempotent: unarchiving an already-active project succeeds.',
|
|
3376
|
-
inputSchema:
|
|
4025
|
+
inputSchema: inputSchema20,
|
|
3377
4026
|
handler: async (args, ctx) => {
|
|
3378
4027
|
const project = await ctx.client.unarchiveProject(args.project_id);
|
|
3379
4028
|
return jsonResult({ project: projectSummary(project) });
|
|
3380
4029
|
}
|
|
3381
4030
|
};
|
|
3382
4031
|
|
|
4032
|
+
// src/server/tools/unarchive_spec_session.ts
|
|
4033
|
+
import { z as z21 } from "zod";
|
|
4034
|
+
var inputSchema21 = {
|
|
4035
|
+
session_id: z21.string().min(1).describe("UUID of the spec session to unarchive")
|
|
4036
|
+
};
|
|
4037
|
+
var unarchiveSpecSessionTool = {
|
|
4038
|
+
name: "unarchive_spec_session",
|
|
4039
|
+
description: "Unarchive a spec session, returning it to the active list.",
|
|
4040
|
+
inputSchema: inputSchema21,
|
|
4041
|
+
handler: async (args, ctx) => {
|
|
4042
|
+
let session;
|
|
4043
|
+
try {
|
|
4044
|
+
session = await ctx.client.unarchiveSpecSession(args.session_id);
|
|
4045
|
+
} catch (err) {
|
|
4046
|
+
const notFound = sessionNotFound(err, args.session_id);
|
|
4047
|
+
if (notFound) {
|
|
4048
|
+
return notFound;
|
|
4049
|
+
}
|
|
4050
|
+
throw err;
|
|
4051
|
+
}
|
|
4052
|
+
return jsonResult({ session: sessionMetadata(session) });
|
|
4053
|
+
}
|
|
4054
|
+
};
|
|
4055
|
+
|
|
3383
4056
|
// src/server/tools/update_project.ts
|
|
3384
|
-
import { z as
|
|
3385
|
-
var
|
|
3386
|
-
project_id:
|
|
3387
|
-
name:
|
|
3388
|
-
description:
|
|
3389
|
-
project_type:
|
|
4057
|
+
import { z as z22 } from "zod";
|
|
4058
|
+
var inputSchema22 = {
|
|
4059
|
+
project_id: z22.string().min(1).describe("UUID of the project to update."),
|
|
4060
|
+
name: z22.string().min(1).optional().describe("New project name."),
|
|
4061
|
+
description: z22.string().optional().describe("New project description."),
|
|
4062
|
+
project_type: z22.enum(["greenfield", "brownfield", "auto"]).optional().describe(
|
|
3390
4063
|
"Codebase type. 'greenfield' or 'brownfield' sets it explicitly; 'auto' clears it so the workflow decides. Omit to leave unchanged."
|
|
3391
4064
|
)
|
|
3392
4065
|
};
|
|
3393
4066
|
var updateProjectTool = {
|
|
3394
4067
|
name: "update_project",
|
|
3395
4068
|
description: "Update a project's metadata (name, description, project_type). Returns the updated metadata.",
|
|
3396
|
-
inputSchema:
|
|
4069
|
+
inputSchema: inputSchema22,
|
|
3397
4070
|
handler: async (args, ctx) => {
|
|
3398
4071
|
if (args.name === void 0 && args.description === void 0 && args.project_type === void 0) {
|
|
3399
4072
|
return errorResult(
|
|
@@ -3416,7 +4089,7 @@ var updateProjectTool = {
|
|
|
3416
4089
|
};
|
|
3417
4090
|
|
|
3418
4091
|
// src/server/tools/update_spec_file.ts
|
|
3419
|
-
import { z as
|
|
4092
|
+
import { z as z23 } from "zod";
|
|
3420
4093
|
|
|
3421
4094
|
// src/server/tools/spec-content-source.ts
|
|
3422
4095
|
import { readFile, stat } from "fs/promises";
|
|
@@ -3472,24 +4145,24 @@ async function resolveSpecContent(input, maxBytes = MAX_LOCAL_SPEC_FILE_BYTES) {
|
|
|
3472
4145
|
}
|
|
3473
4146
|
|
|
3474
4147
|
// src/server/tools/update_spec_file.ts
|
|
3475
|
-
var
|
|
3476
|
-
file_id:
|
|
3477
|
-
project_id:
|
|
3478
|
-
file_path:
|
|
3479
|
-
content:
|
|
4148
|
+
var inputSchema23 = {
|
|
4149
|
+
file_id: z23.string().min(1).optional().describe("UUID of the spec file to overwrite. Provide this or (project_id + file_path)."),
|
|
4150
|
+
project_id: z23.string().min(1).optional().describe("UUID of the project. Required (with file_path) when file_id is not given."),
|
|
4151
|
+
file_path: z23.string().min(1).optional().describe("Path of the spec file. Required (with project_id) when file_id is not given."),
|
|
4152
|
+
content: z23.string().min(1).optional().describe(
|
|
3480
4153
|
"Full UTF-8 replacement content, saved as a new revision. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
|
|
3481
4154
|
),
|
|
3482
|
-
local_file_path:
|
|
4155
|
+
local_file_path: z23.string().min(1).optional().describe(
|
|
3483
4156
|
"Path to a local file whose bytes become the new revision; the server reads it directly. Provide this OR content (exactly one). Absolute paths are most reliable; a relative path resolves against the MCP server working directory. Point this only at an intended spec file \u2014 its bytes are uploaded to the project as-is; never use it for secrets or unrelated files."
|
|
3484
4157
|
),
|
|
3485
|
-
expected_version:
|
|
4158
|
+
expected_version: z23.number().int().nonnegative().optional().describe(
|
|
3486
4159
|
"The content_version the new body was based on, from the get_spec_file or read_spec_file you worked from. The update is rejected with an actionable error if someone else changed the file since. Omit only when overwriting unconditionally is genuinely intended \u2014 the project may be configured to reject unversioned updates."
|
|
3487
4160
|
)
|
|
3488
4161
|
};
|
|
3489
4162
|
var updateSpecFileTool = {
|
|
3490
4163
|
name: "update_spec_file",
|
|
3491
4164
|
description: "Overwrite an existing spec file by saving a new revision. Provide the new body inline via `content` or by pointing at a local file with `local_file_path` \u2014 exactly one is required. Prefer `local_file_path` when the body is already an existing local file (the server reads it directly); use `content` for text generated in-memory. Reference the file by file_id or by (project_id + file_path). Pass `expected_version` from the get_spec_file you based the new body on so a concurrent edit by someone else is reported instead of silently overwritten.",
|
|
3492
|
-
inputSchema:
|
|
4165
|
+
inputSchema: inputSchema23,
|
|
3493
4166
|
handler: async (args, ctx) => {
|
|
3494
4167
|
const resolved = await resolveSpecContent({
|
|
3495
4168
|
content: args.content,
|
|
@@ -3554,9 +4227,9 @@ function conflictMessage(filePath, conflict) {
|
|
|
3554
4227
|
import { createHash as createHash3 } from "crypto";
|
|
3555
4228
|
import { lstat, readFile as readFile2 } from "fs/promises";
|
|
3556
4229
|
import { basename, extname, isAbsolute as isAbsolute2 } from "path";
|
|
3557
|
-
import { z as
|
|
4230
|
+
import { z as z24 } from "zod";
|
|
3558
4231
|
var DEFAULT_MIME = "application/octet-stream";
|
|
3559
|
-
var
|
|
4232
|
+
var MAX_ATTACHMENT_MB2 = MAX_ATTACHMENT_BYTES / (1024 * 1024);
|
|
3560
4233
|
var STRUCTURED_EXTENSION_MIME = {
|
|
3561
4234
|
".pdf": "application/pdf",
|
|
3562
4235
|
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
@@ -3659,23 +4332,23 @@ function inferMimeFromName(name) {
|
|
|
3659
4332
|
}
|
|
3660
4333
|
return DEFAULT_MIME;
|
|
3661
4334
|
}
|
|
3662
|
-
var
|
|
3663
|
-
project_id:
|
|
3664
|
-
file_path:
|
|
4335
|
+
var inputSchema24 = {
|
|
4336
|
+
project_id: z24.string().min(1).describe("UUID of the project to attach the file to."),
|
|
4337
|
+
file_path: z24.string().min(1).describe(
|
|
3665
4338
|
"Absolute path to the local file to upload. The MCP server reads this path from its own host."
|
|
3666
4339
|
),
|
|
3667
|
-
file_name:
|
|
3668
|
-
mime_type:
|
|
4340
|
+
file_name: z24.string().min(1).optional().describe("Override the filename recorded on the attachment. Defaults to the path basename."),
|
|
4341
|
+
mime_type: z24.string().min(1).optional().describe(
|
|
3669
4342
|
"Override the MIME type sent to the platform. The platform re-detects from content, so this is only a hint. If omitted, inferred from the file extension \u2014 structured (.pdf, .docx, .xlsx) keep their canonical MIME; common UTF-8 text formats (.xml, .json, .yaml, .html, .csv, .md, source code, ...) are sent as text/plain."
|
|
3670
4343
|
),
|
|
3671
|
-
override:
|
|
4344
|
+
override: z24.boolean().optional().describe(
|
|
3672
4345
|
"When true, any existing (non-deleted) attachment on the project whose recorded filename exactly matches `file_name` (or the path basename) is soft-deleted before the new upload, so the new attachment keeps the original name instead of getting an auto-dedup `(1)` suffix. The platform has no in-place content-replace API, so the new upload always gets a new `attachment_id`; any replaced ids are returned as `overridden_attachment_ids` (array, possibly empty) so callers can refresh stored references. Defaults to false (let the platform auto-dedup)."
|
|
3673
4346
|
)
|
|
3674
4347
|
};
|
|
3675
4348
|
var uploadAttachmentTool = {
|
|
3676
4349
|
name: "upload_attachment",
|
|
3677
|
-
description: `Upload a local file as a new attachment on a project. Reads \`file_path\` from the host running the MCP server, computes its SHA256, then performs the two-phase platform upload. Max ${String(
|
|
3678
|
-
inputSchema:
|
|
4350
|
+
description: `Upload a local file as a new attachment on a project. Reads \`file_path\` from the host running the MCP server, computes its SHA256, then performs the two-phase platform upload. Max ${String(MAX_ATTACHMENT_MB2)} MB. Supported content: PDF, DOCX, XLSX, images (PNG, JPEG, GIF, WEBP), and any UTF-8 text file (XML, JSON, YAML, HTML, SVG, Markdown, CSV, source code, ...). Other binary files are rejected by the platform.`,
|
|
4351
|
+
inputSchema: inputSchema24,
|
|
3679
4352
|
handler: async (args, ctx) => {
|
|
3680
4353
|
if (!isAbsolute2(args.file_path)) {
|
|
3681
4354
|
return errorResult(`file_path must be an absolute path; got "${args.file_path}".`);
|
|
@@ -3708,7 +4381,7 @@ var uploadAttachmentTool = {
|
|
|
3708
4381
|
}
|
|
3709
4382
|
if (stats.size > MAX_ATTACHMENT_BYTES) {
|
|
3710
4383
|
return errorResult(
|
|
3711
|
-
`File size ${String(stats.size)} bytes exceeds the ${String(
|
|
4384
|
+
`File size ${String(stats.size)} bytes exceeds the ${String(MAX_ATTACHMENT_MB2)} MB attachment limit.`
|
|
3712
4385
|
);
|
|
3713
4386
|
}
|
|
3714
4387
|
let buf;
|
|
@@ -3727,7 +4400,7 @@ var uploadAttachmentTool = {
|
|
|
3727
4400
|
}
|
|
3728
4401
|
if (buf.byteLength > MAX_ATTACHMENT_BYTES) {
|
|
3729
4402
|
return errorResult(
|
|
3730
|
-
`File size ${String(buf.byteLength)} bytes exceeds the ${String(
|
|
4403
|
+
`File size ${String(buf.byteLength)} bytes exceeds the ${String(MAX_ATTACHMENT_MB2)} MB attachment limit.`
|
|
3731
4404
|
);
|
|
3732
4405
|
}
|
|
3733
4406
|
const fileName = args.file_name ?? basename(args.file_path);
|
|
@@ -3799,7 +4472,7 @@ var uploadAttachmentTool = {
|
|
|
3799
4472
|
};
|
|
3800
4473
|
|
|
3801
4474
|
// src/server/tools/upload_spec_file.ts
|
|
3802
|
-
import { z as
|
|
4475
|
+
import { z as z25 } from "zod";
|
|
3803
4476
|
|
|
3804
4477
|
// src/server/spec-path.ts
|
|
3805
4478
|
var MAX_SPEC_DIR_DEPTH = 3;
|
|
@@ -3852,23 +4525,23 @@ var SPEC_FILE_TYPES = [
|
|
|
3852
4525
|
"proposal",
|
|
3853
4526
|
"openspec-spec"
|
|
3854
4527
|
];
|
|
3855
|
-
var
|
|
3856
|
-
project_id:
|
|
3857
|
-
file_path:
|
|
4528
|
+
var inputSchema25 = {
|
|
4529
|
+
project_id: z25.string().min(1).describe("UUID of the project to upload the file into."),
|
|
4530
|
+
file_path: z25.string().min(1).describe(
|
|
3858
4531
|
'Path of the new spec file, rooted at "specs" or "openspec" with at most 3 directory levels below the root (e.g. specs/changes/add-oauth-login/proposal.md).'
|
|
3859
4532
|
),
|
|
3860
|
-
content:
|
|
4533
|
+
content: z25.string().min(1).optional().describe(
|
|
3861
4534
|
"Full UTF-8 text content of the spec file. Provide this OR local_file_path (exactly one). Use for in-memory / generated content."
|
|
3862
4535
|
),
|
|
3863
|
-
local_file_path:
|
|
4536
|
+
local_file_path: z25.string().min(1).optional().describe(
|
|
3864
4537
|
"Path to a local file whose bytes become the spec file body; the server reads it directly. Provide this OR content (exactly one). Absolute paths are most reliable; a relative path resolves against the MCP server working directory. Point this only at an intended spec file \u2014 its bytes are uploaded to the project as-is; never use it for secrets or unrelated files."
|
|
3865
4538
|
),
|
|
3866
|
-
file_type:
|
|
4539
|
+
file_type: z25.enum(SPEC_FILE_TYPES).optional().describe("Spec file type. Omit to derive from the filename (e.g. proposal.md \u2192 proposal).")
|
|
3867
4540
|
};
|
|
3868
4541
|
var uploadSpecFileTool = {
|
|
3869
4542
|
name: "upload_spec_file",
|
|
3870
4543
|
description: "Upload a new, session-less spec file into a project. Provide the file body inline via `content` or by pointing at a local file with `local_file_path` \u2014 exactly one is required. Prefer `local_file_path` when the body is already an existing local file (the server reads it directly, avoiding sending large content through the model); use `content` for text generated in-memory. Rejects if a file already exists at the same path (use update_spec_file to overwrite).",
|
|
3871
|
-
inputSchema:
|
|
4544
|
+
inputSchema: inputSchema25,
|
|
3872
4545
|
handler: async (args, ctx) => {
|
|
3873
4546
|
const pathError = validateSpecFilePath(args.file_path);
|
|
3874
4547
|
if (pathError) {
|
|
@@ -3921,6 +4594,14 @@ function buildServer(deps) {
|
|
|
3921
4594
|
registerTool(server, updateSpecFileTool, deps.client);
|
|
3922
4595
|
registerTool(server, moveSpecFileToTrashTool, deps.client);
|
|
3923
4596
|
registerTool(server, restoreSpecFileFromTrashTool, deps.client);
|
|
4597
|
+
registerTool(server, listAttachmentsTool, deps.client);
|
|
4598
|
+
registerTool(server, readAttachmentTool, deps.client);
|
|
4599
|
+
registerTool(server, listSpecSessionsTool, deps.client);
|
|
4600
|
+
registerTool(server, getSpecSessionTool, deps.client);
|
|
4601
|
+
registerTool(server, renameSpecSessionTool, deps.client);
|
|
4602
|
+
registerTool(server, archiveSpecSessionTool, deps.client);
|
|
4603
|
+
registerTool(server, unarchiveSpecSessionTool, deps.client);
|
|
4604
|
+
registerTool(server, deleteSpecSessionTool, deps.client);
|
|
3924
4605
|
return server;
|
|
3925
4606
|
}
|
|
3926
4607
|
async function startStdioServer(deps) {
|
|
@@ -4074,7 +4755,7 @@ async function canonicaliseRoot(root) {
|
|
|
4074
4755
|
import { promises as fs4 } from "fs";
|
|
4075
4756
|
import { join as join2, relative } from "path";
|
|
4076
4757
|
import { spawn } from "child_process";
|
|
4077
|
-
import { z as
|
|
4758
|
+
import { z as z26 } from "zod";
|
|
4078
4759
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4079
4760
|
|
|
4080
4761
|
// src/reverse/ignore.ts
|
|
@@ -4171,7 +4852,7 @@ var PackCache = class {
|
|
|
4171
4852
|
if (oldestKey === void 0) break;
|
|
4172
4853
|
this.entries.delete(oldestKey);
|
|
4173
4854
|
}
|
|
4174
|
-
const lines =
|
|
4855
|
+
const lines = splitIntoLines2(content);
|
|
4175
4856
|
const pages = planPages(lines);
|
|
4176
4857
|
const entry = {
|
|
4177
4858
|
outputId,
|
|
@@ -4199,7 +4880,7 @@ var PackCache = class {
|
|
|
4199
4880
|
}
|
|
4200
4881
|
}
|
|
4201
4882
|
};
|
|
4202
|
-
function
|
|
4883
|
+
function splitIntoLines2(text) {
|
|
4203
4884
|
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
4204
4885
|
const parts = normalized.split("\n");
|
|
4205
4886
|
if (parts.length > 0 && parts[parts.length - 1] === "") parts.pop();
|
|
@@ -4379,37 +5060,37 @@ var READ_FILE_MAX_LINES = 2e3;
|
|
|
4379
5060
|
var LIST_DIR_HARD_CAP = 5e3;
|
|
4380
5061
|
var LIST_DIR_DEFAULT_CAP = 1e3;
|
|
4381
5062
|
var GREP_MAX_MATCHES_DEFAULT = 200;
|
|
4382
|
-
var listDirInput =
|
|
4383
|
-
path:
|
|
4384
|
-
recursive:
|
|
4385
|
-
maxEntries:
|
|
4386
|
-
includeIgnored:
|
|
5063
|
+
var listDirInput = z26.object({
|
|
5064
|
+
path: z26.string(),
|
|
5065
|
+
recursive: z26.boolean().optional(),
|
|
5066
|
+
maxEntries: z26.number().int().positive().optional(),
|
|
5067
|
+
includeIgnored: z26.boolean().optional()
|
|
4387
5068
|
});
|
|
4388
|
-
var readFileInput =
|
|
4389
|
-
path:
|
|
4390
|
-
offset:
|
|
4391
|
-
limit:
|
|
4392
|
-
includeIgnored:
|
|
5069
|
+
var readFileInput = z26.object({
|
|
5070
|
+
path: z26.string(),
|
|
5071
|
+
offset: z26.number().int().nonnegative().optional(),
|
|
5072
|
+
limit: z26.number().int().positive().optional(),
|
|
5073
|
+
includeIgnored: z26.boolean().optional()
|
|
4393
5074
|
});
|
|
4394
|
-
var grepInput =
|
|
4395
|
-
pattern:
|
|
5075
|
+
var grepInput = z26.object({
|
|
5076
|
+
pattern: z26.string().describe(
|
|
4396
5077
|
"Search pattern. Interpreted as a regular expression by default (ripgrep syntax), e.g. `foo|bar` or `[Rr]ole`. Set `isRegex: false` to match the pattern as an exact literal string instead."
|
|
4397
5078
|
),
|
|
4398
|
-
path:
|
|
4399
|
-
isRegex:
|
|
4400
|
-
caseSensitive:
|
|
4401
|
-
maxMatches:
|
|
4402
|
-
contextLines:
|
|
4403
|
-
includeIgnored:
|
|
5079
|
+
path: z26.string().optional().describe("File or directory under --root to search. Defaults to the whole root."),
|
|
5080
|
+
isRegex: z26.boolean().optional().describe("Treat `pattern` as a regex (default true). Set false for literal-string search."),
|
|
5081
|
+
caseSensitive: z26.boolean().optional(),
|
|
5082
|
+
maxMatches: z26.number().int().positive().optional(),
|
|
5083
|
+
contextLines: z26.number().int().nonnegative().optional(),
|
|
5084
|
+
includeIgnored: z26.boolean().optional()
|
|
4404
5085
|
});
|
|
4405
|
-
var packCodebaseInput =
|
|
4406
|
-
subpath:
|
|
4407
|
-
includePatterns:
|
|
4408
|
-
ignorePatterns:
|
|
5086
|
+
var packCodebaseInput = z26.object({
|
|
5087
|
+
subpath: z26.string().optional(),
|
|
5088
|
+
includePatterns: z26.string().optional(),
|
|
5089
|
+
ignorePatterns: z26.string().optional()
|
|
4409
5090
|
});
|
|
4410
|
-
var packCodebaseReadPageInput =
|
|
4411
|
-
outputId:
|
|
4412
|
-
page:
|
|
5091
|
+
var packCodebaseReadPageInput = z26.object({
|
|
5092
|
+
outputId: z26.string(),
|
|
5093
|
+
page: z26.number().int().positive()
|
|
4413
5094
|
});
|
|
4414
5095
|
function structuredError(error, message, extra) {
|
|
4415
5096
|
const payload = { error, message, ...extra ?? {} };
|
|
@@ -4525,19 +5206,19 @@ async function handleRead(args, opts) {
|
|
|
4525
5206
|
`Binary content detected in ${args.path}; line-based pagination doesn't apply. Skip this file or use local_fs_grep on neighbouring text files.`
|
|
4526
5207
|
);
|
|
4527
5208
|
}
|
|
4528
|
-
const text =
|
|
4529
|
-
const lines =
|
|
5209
|
+
const text = normalizeNewlines2(raw.toString("utf-8"));
|
|
5210
|
+
const lines = splitIntoLines3(text);
|
|
4530
5211
|
const page = paginateLines(lines, args.offset, args.limit);
|
|
4531
5212
|
if ("error" in page) {
|
|
4532
5213
|
return structuredError("invalid_offset", page.error);
|
|
4533
5214
|
}
|
|
4534
5215
|
return structuredOk(page);
|
|
4535
5216
|
}
|
|
4536
|
-
function
|
|
5217
|
+
function normalizeNewlines2(s) {
|
|
4537
5218
|
const noBom = s.charCodeAt(0) === 65279 ? s.slice(1) : s;
|
|
4538
5219
|
return noBom.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
4539
5220
|
}
|
|
4540
|
-
function
|
|
5221
|
+
function splitIntoLines3(text) {
|
|
4541
5222
|
const parts = text.split("\n");
|
|
4542
5223
|
if (parts.length > 0 && parts[parts.length - 1] === "") parts.pop();
|
|
4543
5224
|
return parts;
|
|
@@ -4850,7 +5531,7 @@ function registerLocalFsTools(server, opts) {
|
|
|
4850
5531
|
},
|
|
4851
5532
|
{
|
|
4852
5533
|
name: "local_pack_codebase",
|
|
4853
|
-
description: "Pack the
|
|
5534
|
+
description: "Pack the local codebase (under the granted --root) into a single compressed XML bundle (embedded repomix, `--style xml --compress`; file bodies use `\u22EE----` delimiters). Prefer this over many `local_fs_read_file` calls when you need broad codebase context. Output is paginated at ~2500 lines per page; a `<file>` block is never split across a page boundary, but the `<files>` root wrapper spans pages (its opener is on the first files page and its closer on the last) \u2014 never treat a missing `</files>` as truncation. The response includes `outputId`, `page`, `totalPages`, `totalLines`, `pageLines`, `hasMore`, and a `nextPage` hint; page 1 opens with the full directory structure. Fetch further pages with `local_pack_codebase_read_page` while `totalPages` is small; for a large pack (roughly 10+ pages) do NOT page through everything \u2014 use page 1 as the repo map, then re-pack narrowed with `subpath`/`includePatterns` or switch to `local_fs_grep`/`local_fs_read_file` for targeted reads. Repeat calls with the same `subpath`/`includePatterns`/`ignorePatterns` return the same `outputId` and are served instantly from the 30-minute cache without re-running repomix.",
|
|
4854
5535
|
inputSchema: jsonSchemaFromZod(packCodebaseInput),
|
|
4855
5536
|
handler: async (args) => {
|
|
4856
5537
|
try {
|
|
@@ -4863,7 +5544,7 @@ function registerLocalFsTools(server, opts) {
|
|
|
4863
5544
|
},
|
|
4864
5545
|
{
|
|
4865
5546
|
name: "local_pack_codebase_read_page",
|
|
4866
|
-
description: "Read a specific page of a previously packed codebase output. Provide the `outputId` returned by `local_pack_codebase` and the 1-indexed `page` to fetch. Each page is up to ~2500 lines of XML; no `<file>` block is split across pages, but the `<files>` root wrapper spans pages \u2014
|
|
5547
|
+
description: "Read a specific page of a previously packed codebase output. Provide the `outputId` returned by `local_pack_codebase` and the 1-indexed `page` to fetch. Each page is up to ~2500 lines of the compressed XML bundle; no `<file>` block is split across pages, but the `<files>` root wrapper spans pages \u2014 never treat a missing `</files>` as truncation. Cache entries expire after 30 minutes; on cache miss, re-run `local_pack_codebase` (same args return the same outputId \u2014 re-pack is cheap, served from cache).",
|
|
4867
5548
|
inputSchema: jsonSchemaFromZod(packCodebaseReadPageInput),
|
|
4868
5549
|
handler: (args) => {
|
|
4869
5550
|
try {
|
|
@@ -4937,7 +5618,7 @@ function jsonSchemaFromZod(schema) {
|
|
|
4937
5618
|
return zodToJson(schema);
|
|
4938
5619
|
}
|
|
4939
5620
|
function zodToJson(schema) {
|
|
4940
|
-
if (schema instanceof
|
|
5621
|
+
if (schema instanceof z26.ZodObject) {
|
|
4941
5622
|
const shape = schema.shape;
|
|
4942
5623
|
const properties = {};
|
|
4943
5624
|
const required = [];
|
|
@@ -4952,19 +5633,19 @@ function zodToJson(schema) {
|
|
|
4952
5633
|
if (required.length > 0) out.required = required;
|
|
4953
5634
|
return out;
|
|
4954
5635
|
}
|
|
4955
|
-
if (schema instanceof
|
|
5636
|
+
if (schema instanceof z26.ZodOptional) {
|
|
4956
5637
|
return zodToJson(schema.unwrap());
|
|
4957
5638
|
}
|
|
4958
|
-
if (schema instanceof
|
|
5639
|
+
if (schema instanceof z26.ZodString) {
|
|
4959
5640
|
return { type: "string" };
|
|
4960
5641
|
}
|
|
4961
|
-
if (schema instanceof
|
|
5642
|
+
if (schema instanceof z26.ZodNumber) {
|
|
4962
5643
|
return { type: "number" };
|
|
4963
5644
|
}
|
|
4964
|
-
if (schema instanceof
|
|
5645
|
+
if (schema instanceof z26.ZodBoolean) {
|
|
4965
5646
|
return { type: "boolean" };
|
|
4966
5647
|
}
|
|
4967
|
-
if (schema instanceof
|
|
5648
|
+
if (schema instanceof z26.ZodEnum) {
|
|
4968
5649
|
return { type: "string", enum: schema.options };
|
|
4969
5650
|
}
|
|
4970
5651
|
return { type: "string" };
|
|
@@ -5407,21 +6088,59 @@ async function resolveReverseAgentUrl(sources) {
|
|
|
5407
6088
|
throw err;
|
|
5408
6089
|
}
|
|
5409
6090
|
}
|
|
6091
|
+
function ignoringSettings(settingsPath, reason) {
|
|
6092
|
+
return `myspec-mcp: ignoring the userAuthUrl in ${settingsPath} for the MYSPEC_API_TOKEN exchange URL \u2014 ${reason}.`;
|
|
6093
|
+
}
|
|
6094
|
+
function usableAuthUrl(candidate, settingsPath, warn) {
|
|
6095
|
+
if (candidate === void 0) {
|
|
6096
|
+
return void 0;
|
|
6097
|
+
}
|
|
6098
|
+
try {
|
|
6099
|
+
resolveConfig({ env: {}, storedUserAuthUrl: candidate });
|
|
6100
|
+
return candidate;
|
|
6101
|
+
} catch (err) {
|
|
6102
|
+
if (!(err instanceof ConfigError)) {
|
|
6103
|
+
throw err;
|
|
6104
|
+
}
|
|
6105
|
+
warn(ignoringSettings(settingsPath, err.message));
|
|
6106
|
+
return void 0;
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
5410
6109
|
async function buildCredentialsStore(opts = {}) {
|
|
5411
6110
|
const env = opts.env ?? process.env;
|
|
5412
|
-
const
|
|
6111
|
+
const settingsPath = opts.settingsPath ?? defaultSettingsPath();
|
|
6112
|
+
const fileStore = opts.fileStore ?? createFileCredentialsStore({ settingsPath });
|
|
6113
|
+
const warn = opts.warn ?? ((message) => {
|
|
6114
|
+
process.stderr.write(message + "\n");
|
|
6115
|
+
});
|
|
5413
6116
|
const usingEnvApiToken = hasEnvApiToken(env);
|
|
5414
6117
|
const tolerate = opts.toleratesUnreadableFile ?? usingEnvApiToken;
|
|
5415
6118
|
const stored = tolerate ? await fileStore.load().catch(() => null) : await fileStore.load();
|
|
6119
|
+
const overridden = (opts.cliUserAuthUrl ?? env.MYSPEC_USER_AUTH_URL) !== void 0;
|
|
6120
|
+
const fromSettings = usingEnvApiToken && !overridden && stored?.userAuthUrl === void 0 ? await readSettingsUserAuthUrl(settingsPath).catch((err) => {
|
|
6121
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6122
|
+
warn(ignoringSettings(settingsPath, `it could not be read (${message})`));
|
|
6123
|
+
return void 0;
|
|
6124
|
+
}) : void 0;
|
|
5416
6125
|
const config = resolveConfig({
|
|
5417
6126
|
env,
|
|
5418
6127
|
cliUserAuthUrl: opts.cliUserAuthUrl,
|
|
5419
|
-
|
|
6128
|
+
// Validated on its own before it joins the chain, for the same reason the
|
|
6129
|
+
// unreadable file above is forgiven: parseSettings only requires a
|
|
6130
|
+
// non-empty string, so a hand-written `htps://…` or a bare host in a
|
|
6131
|
+
// settings.json that is not even the credential would otherwise turn a
|
|
6132
|
+
// pure-env install that used to start on the prod default into a
|
|
6133
|
+
// ConfigError exit — and one that never names the file it came from.
|
|
6134
|
+
storedUserAuthUrl: stored?.userAuthUrl ?? usableAuthUrl(fromSettings, settingsPath, warn)
|
|
5420
6135
|
});
|
|
5421
6136
|
return {
|
|
5422
6137
|
store: createEnvApiTokenStore({
|
|
5423
6138
|
fileStore,
|
|
5424
|
-
|
|
6139
|
+
// `warn` rather than the default sink, so the "cannot be sent as a
|
|
6140
|
+
// header" warning lands wherever the caller's other warnings do.
|
|
6141
|
+
envConfig: readEnvApiToken(config.userAuthUrl, env, warn),
|
|
6142
|
+
settingsPath,
|
|
6143
|
+
warn
|
|
5425
6144
|
}),
|
|
5426
6145
|
config,
|
|
5427
6146
|
usingEnvApiToken
|