@tempo-ai/mcp 0.0.106 → 0.0.107
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/dist/bin.js +1 -1
- package/dist/{call-N3UTMHLY.js → call-ESWO5SB3.js} +2 -2
- package/dist/{chunk-52IG2UKP.js → chunk-NPCH72RN.js} +4 -4
- package/dist/index.js +1 -1
- package/dist/{serve-7RSR6WI6.js → serve-IXP2WX42.js} +514 -31
- package/dist/serve-IXP2WX42.js.map +1 -0
- package/package.json +1 -1
- package/dist/serve-7RSR6WI6.js.map +0 -1
- /package/dist/{call-N3UTMHLY.js.map → call-ESWO5SB3.js.map} +0 -0
- /package/dist/{chunk-52IG2UKP.js.map → chunk-NPCH72RN.js.map} +0 -0
|
@@ -5907,14 +5907,15 @@ var TOOL_MANIFEST = [
|
|
|
5907
5907
|
"docs_move",
|
|
5908
5908
|
"docs_delete"
|
|
5909
5909
|
]),
|
|
5910
|
-
// ── comments (
|
|
5910
|
+
// ── comments (17) ───────────────────────────────────────────────────────
|
|
5911
5911
|
...read("comments", [
|
|
5912
5912
|
"comments_list_for_canvas",
|
|
5913
5913
|
"comments_get_thread",
|
|
5914
5914
|
"comments_list_unread_notifications",
|
|
5915
5915
|
"comments_list_activity_for_canvas",
|
|
5916
5916
|
"comments_list_threads_in_org",
|
|
5917
|
-
"comments_search"
|
|
5917
|
+
"comments_search",
|
|
5918
|
+
"comments_get_pr_visual_review"
|
|
5918
5919
|
]),
|
|
5919
5920
|
...write("comments", [
|
|
5920
5921
|
"comments_create_thread",
|
|
@@ -10752,9 +10753,169 @@ function refineByMessage(message, details) {
|
|
|
10752
10753
|
return { code: "internal", message, details };
|
|
10753
10754
|
}
|
|
10754
10755
|
|
|
10756
|
+
// ../canvas-comments-mcp/tools/pr-visual-review.ts
|
|
10757
|
+
var VISUAL_REVIEW_STATUS_CONTEXT = "visual-review";
|
|
10758
|
+
var VISUAL_REVIEW_STICKY_MARKER = "<!-- tempo-visual-review -->";
|
|
10759
|
+
var INTENTIONAL_NO_REPORT = /bypass|not applicable|no visual change/i;
|
|
10760
|
+
var REPORT_KEY = /^[A-Za-z0-9_-]{16,128}$/;
|
|
10761
|
+
var REPORT_KEY_IN_TEXT = /[?&]k=([A-Za-z0-9_-]{16,128})/;
|
|
10762
|
+
var GitHubApiError = class extends Error {
|
|
10763
|
+
constructor(message, options = {}) {
|
|
10764
|
+
super(message);
|
|
10765
|
+
this.name = "GitHubApiError";
|
|
10766
|
+
this.status = options.status;
|
|
10767
|
+
this.unavailable = options.unavailable ?? false;
|
|
10768
|
+
}
|
|
10769
|
+
};
|
|
10770
|
+
function normalizeGitHubRepo(value) {
|
|
10771
|
+
const trimmed2 = value.trim();
|
|
10772
|
+
if (!trimmed2) return null;
|
|
10773
|
+
const candidates = [trimmed2];
|
|
10774
|
+
if (/^github\.com\//i.test(trimmed2)) {
|
|
10775
|
+
candidates.push(`https://${trimmed2}`);
|
|
10776
|
+
} else if (/^[^/:@\s]+\/[^/\s]+$/.test(trimmed2)) {
|
|
10777
|
+
candidates.push(`https://github.com/${trimmed2}`);
|
|
10778
|
+
}
|
|
10779
|
+
for (const candidate of candidates) {
|
|
10780
|
+
const normalized = normalizeRepoUrl(candidate);
|
|
10781
|
+
if (normalized && /^github\.com\/[^/]+\/[^/]+$/.test(normalized)) {
|
|
10782
|
+
return normalized;
|
|
10783
|
+
}
|
|
10784
|
+
}
|
|
10785
|
+
return null;
|
|
10786
|
+
}
|
|
10787
|
+
function parseVisualReviewPrInput(pr, repository) {
|
|
10788
|
+
let prNumber = null;
|
|
10789
|
+
let repo = repository ?? null;
|
|
10790
|
+
if (typeof pr === "number") {
|
|
10791
|
+
prNumber = Number.isInteger(pr) && pr > 0 ? pr : null;
|
|
10792
|
+
} else {
|
|
10793
|
+
const trimmed2 = pr.trim();
|
|
10794
|
+
const numeric = trimmed2.match(/^#?(\d+)$/);
|
|
10795
|
+
if (numeric) {
|
|
10796
|
+
prNumber = Number(numeric[1]);
|
|
10797
|
+
} else {
|
|
10798
|
+
try {
|
|
10799
|
+
const url = new URL(trimmed2);
|
|
10800
|
+
const match = url.pathname.match(/^\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/);
|
|
10801
|
+
if (url.hostname.toLowerCase() !== "github.com" || !match) return null;
|
|
10802
|
+
repo = `${match[1]}/${match[2]}`;
|
|
10803
|
+
prNumber = Number(match[3]);
|
|
10804
|
+
} catch {
|
|
10805
|
+
return null;
|
|
10806
|
+
}
|
|
10807
|
+
}
|
|
10808
|
+
}
|
|
10809
|
+
const repoKey = repo ? normalizeGitHubRepo(repo) : null;
|
|
10810
|
+
return prNumber && repoKey ? { prNumber, repoKey } : null;
|
|
10811
|
+
}
|
|
10812
|
+
function githubRepoFromKey(repoKey) {
|
|
10813
|
+
const match = /^github\.com\/([^/]+)\/([^/]+)$/.exec(repoKey);
|
|
10814
|
+
return match ? `${match[1]}/${match[2]}` : null;
|
|
10815
|
+
}
|
|
10816
|
+
function parseReportKey(value) {
|
|
10817
|
+
const trimmed2 = value.trim();
|
|
10818
|
+
if (!trimmed2) return null;
|
|
10819
|
+
if (REPORT_KEY.test(trimmed2)) return trimmed2;
|
|
10820
|
+
try {
|
|
10821
|
+
const key = new URL(trimmed2).searchParams.get("k");
|
|
10822
|
+
return key && REPORT_KEY.test(key) ? key : null;
|
|
10823
|
+
} catch {
|
|
10824
|
+
return null;
|
|
10825
|
+
}
|
|
10826
|
+
}
|
|
10827
|
+
function reportKeyInText(text) {
|
|
10828
|
+
const match = text ? REPORT_KEY_IN_TEXT.exec(text) : null;
|
|
10829
|
+
return match ? match[1] : null;
|
|
10830
|
+
}
|
|
10831
|
+
async function resolvePrVisualReviewViaGitHub(githubApi, repository, prNumber) {
|
|
10832
|
+
const pull = await githubApi(`repos/${repository}/pulls/${prNumber}`);
|
|
10833
|
+
const headSha = pull.head?.sha;
|
|
10834
|
+
const pullRequest = {
|
|
10835
|
+
repository,
|
|
10836
|
+
number: prNumber,
|
|
10837
|
+
url: pull.html_url,
|
|
10838
|
+
title: pull.title,
|
|
10839
|
+
state: pull.state,
|
|
10840
|
+
draft: pull.draft,
|
|
10841
|
+
headSha
|
|
10842
|
+
};
|
|
10843
|
+
if (!headSha) {
|
|
10844
|
+
return {
|
|
10845
|
+
pullRequest,
|
|
10846
|
+
status: null,
|
|
10847
|
+
reportKey: null,
|
|
10848
|
+
reportKeySource: null,
|
|
10849
|
+
intentionalNoReport: false
|
|
10850
|
+
};
|
|
10851
|
+
}
|
|
10852
|
+
const combined = await githubApi(
|
|
10853
|
+
`repos/${repository}/commits/${headSha}/status`
|
|
10854
|
+
);
|
|
10855
|
+
const raw = (combined.statuses ?? []).find(
|
|
10856
|
+
(candidate) => candidate.context === VISUAL_REVIEW_STATUS_CONTEXT
|
|
10857
|
+
);
|
|
10858
|
+
const status = raw?.state ? {
|
|
10859
|
+
state: raw.state,
|
|
10860
|
+
description: raw.description ?? void 0,
|
|
10861
|
+
targetUrl: raw.target_url ?? void 0,
|
|
10862
|
+
updatedAt: raw.updated_at
|
|
10863
|
+
} : null;
|
|
10864
|
+
const intentionalNoReport = status?.state === "success" && INTENTIONAL_NO_REPORT.test(status.description ?? "");
|
|
10865
|
+
let reportKey = reportKeyInText(status?.targetUrl);
|
|
10866
|
+
let reportKeySource = reportKey ? "head-status" : null;
|
|
10867
|
+
if (!reportKey && !intentionalNoReport) {
|
|
10868
|
+
for (let page = 1; ; page += 1) {
|
|
10869
|
+
const pageSuffix = page === 1 ? "" : `&page=${page}`;
|
|
10870
|
+
const comments = await githubApi(
|
|
10871
|
+
`repos/${repository}/issues/${prNumber}/comments?per_page=100${pageSuffix}`
|
|
10872
|
+
);
|
|
10873
|
+
const sticky = comments.find(
|
|
10874
|
+
(comment) => typeof comment.body === "string" && comment.body.includes(VISUAL_REVIEW_STICKY_MARKER)
|
|
10875
|
+
);
|
|
10876
|
+
const key = reportKeyInText(sticky?.body);
|
|
10877
|
+
if (key) {
|
|
10878
|
+
reportKey = key;
|
|
10879
|
+
reportKeySource = "sticky-comment";
|
|
10880
|
+
break;
|
|
10881
|
+
}
|
|
10882
|
+
if (comments.length < 100) break;
|
|
10883
|
+
}
|
|
10884
|
+
}
|
|
10885
|
+
return { pullRequest, status, reportKey, reportKeySource, intentionalNoReport };
|
|
10886
|
+
}
|
|
10887
|
+
|
|
10755
10888
|
// ../canvas-comments-mcp/tools/read.ts
|
|
10889
|
+
function mapGitHubApiError(error) {
|
|
10890
|
+
if (error.unavailable) {
|
|
10891
|
+
return {
|
|
10892
|
+
code: "github_unavailable",
|
|
10893
|
+
message: `${error.message}. Pass \`reportUrl\` \u2014 the PR's \`visual-review\` check link (\u2026?k=\u2026) \u2014 instead.`
|
|
10894
|
+
};
|
|
10895
|
+
}
|
|
10896
|
+
if (error.status === 404) {
|
|
10897
|
+
return {
|
|
10898
|
+
code: "not_found",
|
|
10899
|
+
message: `GitHub returned 404 for that pull request: it does not exist, or your GitHub account cannot see it (${error.message}).`
|
|
10900
|
+
};
|
|
10901
|
+
}
|
|
10902
|
+
if (error.status === 401 || error.status === 403) {
|
|
10903
|
+
return {
|
|
10904
|
+
code: "forbidden",
|
|
10905
|
+
message: `GitHub refused the read as your account (${error.message}).`
|
|
10906
|
+
};
|
|
10907
|
+
}
|
|
10908
|
+
return { code: "github_error", message: error.message };
|
|
10909
|
+
}
|
|
10910
|
+
var EMPTY_COMMENTS = {
|
|
10911
|
+
openThreads: 0,
|
|
10912
|
+
resolvedThreads: 0,
|
|
10913
|
+
returnedThreads: 0,
|
|
10914
|
+
truncated: false,
|
|
10915
|
+
threads: []
|
|
10916
|
+
};
|
|
10756
10917
|
function registerReadTools2(server, deps) {
|
|
10757
|
-
const { convex, resolveCanvasContentPath } = deps;
|
|
10918
|
+
const { convex, resolveCanvasContentPath, getRepoUrl, githubApi } = deps;
|
|
10758
10919
|
const canvasKey = async (contentPath) => {
|
|
10759
10920
|
const resolved = resolveCanvasContentPath ? await resolveCanvasContentPath(contentPath) : contentPath;
|
|
10760
10921
|
return normalizeCanvasContentPath(resolved);
|
|
@@ -10897,6 +11058,198 @@ function registerReadTools2(server, deps) {
|
|
|
10897
11058
|
}
|
|
10898
11059
|
}
|
|
10899
11060
|
);
|
|
11061
|
+
server.tool(
|
|
11062
|
+
"comments_get_pr_visual_review",
|
|
11063
|
+
"Get a GitHub PR's visual review in one read-only call: the current `visual-review` commit status, the Tempo VRT report gate, actionable per-storyboard verdicts/approvals, and the canvas review comment threads. Pass `pr` (a GitHub PR URL is preferred; a PR number / '#123' needs `repository` unless the local Tempo workspace has a GitHub remote) \u2014 the PR's head status and report link are read with YOUR GitHub credentials through the `gh` CLI, so only PRs you can see on GitHub resolve. Without a usable `gh` (hosted / CLI sessions) pass `reportUrl` instead: the PR's `visual-review` check link (\u2026?k=\u2026). GitHub's head status is authoritative (bypass / no-visual-change PRs report correctly); while the current head is still capturing, the newest completed run is returned with report.stale=true so review threads stay visible. Only open threads are returned by default.",
|
|
11064
|
+
{
|
|
11065
|
+
pr: z10.union([z10.number().int().positive(), z10.string().min(1)]).optional().describe(
|
|
11066
|
+
"GitHub PR URL (recommended), or a positive PR number / '#123' (then `repository` or a workspace GitHub remote is required)."
|
|
11067
|
+
),
|
|
11068
|
+
repository: z10.string().optional().describe(
|
|
11069
|
+
"GitHub repository as owner/repo, an HTTPS/SSH remote, or a Tempo repoKey (github.com/owner/repo). Only needed with a bare PR number."
|
|
11070
|
+
),
|
|
11071
|
+
reportUrl: z10.string().optional().describe(
|
|
11072
|
+
"The PR's `visual-review` check link / Tempo report URL (\u2026?k=\u2026), or the bare report key. Required when no GitHub CLI is available; may be combined with `pr` to confirm freshness against the current head."
|
|
11073
|
+
),
|
|
11074
|
+
includeResolved: z10.boolean().default(false).describe("Include resolved review threads; defaults to false."),
|
|
11075
|
+
maxThreads: z10.number().int().min(1).max(100).default(50).describe("Maximum newest matching threads to return (default 50).")
|
|
11076
|
+
},
|
|
11077
|
+
async (args, ctx) => {
|
|
11078
|
+
try {
|
|
11079
|
+
const includeResolved = args.includeResolved ?? false;
|
|
11080
|
+
const maxThreads = Math.min(args.maxThreads ?? 50, 100);
|
|
11081
|
+
const warnings = [];
|
|
11082
|
+
let reportKey = null;
|
|
11083
|
+
if (args.reportUrl !== void 0) {
|
|
11084
|
+
reportKey = parseReportKey(args.reportUrl);
|
|
11085
|
+
if (!reportKey) {
|
|
11086
|
+
return errorResult3({
|
|
11087
|
+
code: "validation_error",
|
|
11088
|
+
message: "`reportUrl` must be a Tempo visual-review link carrying `?k=<key>` (the PR's `visual-review` check link) or a bare report key."
|
|
11089
|
+
});
|
|
11090
|
+
}
|
|
11091
|
+
}
|
|
11092
|
+
let parsedPr = null;
|
|
11093
|
+
if (args.pr !== void 0) {
|
|
11094
|
+
let repository = args.repository;
|
|
11095
|
+
if (!repository && getRepoUrl) {
|
|
11096
|
+
try {
|
|
11097
|
+
repository = await getRepoUrl();
|
|
11098
|
+
} catch {
|
|
11099
|
+
repository = null;
|
|
11100
|
+
}
|
|
11101
|
+
}
|
|
11102
|
+
parsedPr = parseVisualReviewPrInput(args.pr, repository);
|
|
11103
|
+
if (!parsedPr) {
|
|
11104
|
+
return errorResult3({
|
|
11105
|
+
code: "validation_error",
|
|
11106
|
+
message: "Pass a GitHub PR URL, or a PR number with `repository` (owner/repo, an HTTPS/SSH remote, or github.com/owner/repo)."
|
|
11107
|
+
});
|
|
11108
|
+
}
|
|
11109
|
+
}
|
|
11110
|
+
if (!reportKey && !parsedPr) {
|
|
11111
|
+
return errorResult3({
|
|
11112
|
+
code: "validation_error",
|
|
11113
|
+
message: "Pass `pr` (a GitHub PR URL) or `reportUrl` (the PR's `visual-review` check link)."
|
|
11114
|
+
});
|
|
11115
|
+
}
|
|
11116
|
+
let github = null;
|
|
11117
|
+
if (parsedPr) {
|
|
11118
|
+
const repository = githubRepoFromKey(parsedPr.repoKey);
|
|
11119
|
+
if (!repository) {
|
|
11120
|
+
return errorResult3({
|
|
11121
|
+
code: "validation_error",
|
|
11122
|
+
message: "Only GitHub repositories are supported."
|
|
11123
|
+
});
|
|
11124
|
+
}
|
|
11125
|
+
if (githubApi) {
|
|
11126
|
+
try {
|
|
11127
|
+
github = await resolvePrVisualReviewViaGitHub(
|
|
11128
|
+
githubApi,
|
|
11129
|
+
repository,
|
|
11130
|
+
parsedPr.prNumber
|
|
11131
|
+
);
|
|
11132
|
+
} catch (error) {
|
|
11133
|
+
if (!(error instanceof GitHubApiError)) throw error;
|
|
11134
|
+
if (!reportKey) return errorResult3(mapGitHubApiError(error));
|
|
11135
|
+
warnings.push(
|
|
11136
|
+
`GitHub lookup failed (${error.message}); freshness against the PR's current head is unknown.`
|
|
11137
|
+
);
|
|
11138
|
+
}
|
|
11139
|
+
if (github && !reportKey) reportKey = github.reportKey;
|
|
11140
|
+
} else if (!reportKey) {
|
|
11141
|
+
return errorResult3({
|
|
11142
|
+
code: "github_unavailable",
|
|
11143
|
+
message: "Resolving a PR needs an authenticated GitHub CLI (`gh`) on this machine, which this session does not have. Pass `reportUrl` \u2014 the PR's `visual-review` check link (\u2026?k=\u2026) \u2014 instead."
|
|
11144
|
+
});
|
|
11145
|
+
}
|
|
11146
|
+
}
|
|
11147
|
+
let detail = reportKey ? await convex.action(api3.visualReviewGithub.getPrReviewForMcp, {
|
|
11148
|
+
// biome-ignore lint/suspicious/noExplicitAny: cross-package id casting
|
|
11149
|
+
organizationId: ctx.orgId,
|
|
11150
|
+
reportKey,
|
|
11151
|
+
includeResolved,
|
|
11152
|
+
maxThreads
|
|
11153
|
+
}) : null;
|
|
11154
|
+
if (reportKey && !detail) {
|
|
11155
|
+
if (!github) {
|
|
11156
|
+
return errorResult3({
|
|
11157
|
+
code: "not_found",
|
|
11158
|
+
message: "No Tempo visual-review report exists for that key."
|
|
11159
|
+
});
|
|
11160
|
+
}
|
|
11161
|
+
warnings.push(
|
|
11162
|
+
`GitHub links report key ${reportKey}, but Tempo has no such report (it may have been swept).`
|
|
11163
|
+
);
|
|
11164
|
+
}
|
|
11165
|
+
if (detail && parsedPr && (detail.report.run.repoKey !== parsedPr.repoKey || detail.report.run.prNumber !== parsedPr.prNumber)) {
|
|
11166
|
+
warnings.push(
|
|
11167
|
+
`The resolved report belongs to ${detail.report.run.repoKey} PR #${detail.report.run.prNumber ?? "?"}, not the requested PR; it was ignored.`
|
|
11168
|
+
);
|
|
11169
|
+
detail = null;
|
|
11170
|
+
}
|
|
11171
|
+
if (detail && !github && githubApi && detail.report.run.prNumber !== void 0) {
|
|
11172
|
+
const repository = githubRepoFromKey(detail.report.run.repoKey);
|
|
11173
|
+
if (repository) {
|
|
11174
|
+
try {
|
|
11175
|
+
github = await resolvePrVisualReviewViaGitHub(
|
|
11176
|
+
githubApi,
|
|
11177
|
+
repository,
|
|
11178
|
+
detail.report.run.prNumber
|
|
11179
|
+
);
|
|
11180
|
+
if (github.reportKey && github.reportKey !== reportKey) {
|
|
11181
|
+
warnings.push(
|
|
11182
|
+
`GitHub currently links a different report for this PR (key ${github.reportKey}${github.status?.targetUrl ? `: ${github.status.targetUrl}` : ""}); you asked for ${reportKey}.`
|
|
11183
|
+
);
|
|
11184
|
+
}
|
|
11185
|
+
} catch (error) {
|
|
11186
|
+
if (!(error instanceof GitHubApiError)) throw error;
|
|
11187
|
+
warnings.push(
|
|
11188
|
+
`GitHub lookup failed (${error.message}); freshness against the PR's current head is unknown.`
|
|
11189
|
+
);
|
|
11190
|
+
}
|
|
11191
|
+
}
|
|
11192
|
+
}
|
|
11193
|
+
const headSha = github?.pullRequest.headSha;
|
|
11194
|
+
const stale = Boolean(
|
|
11195
|
+
detail && headSha && detail.report.run.headSha !== headSha
|
|
11196
|
+
);
|
|
11197
|
+
if (stale && detail) {
|
|
11198
|
+
warnings.push(
|
|
11199
|
+
`No visual-review run has completed for the current head ${headSha}; report verdicts are from the newest completed run at ${detail.report.run.headSha} (report.stale = true). Review threads are current.`
|
|
11200
|
+
);
|
|
11201
|
+
}
|
|
11202
|
+
const runRepoKey = detail?.report.run.repoKey ?? parsedPr?.repoKey;
|
|
11203
|
+
const pullRequest = github?.pullRequest ?? {
|
|
11204
|
+
repository: (runRepoKey && githubRepoFromKey(runRepoKey)) ?? runRepoKey ?? "",
|
|
11205
|
+
number: detail?.report.run.prNumber ?? parsedPr?.prNumber
|
|
11206
|
+
};
|
|
11207
|
+
const githubStatus = github?.status;
|
|
11208
|
+
const status = githubStatus ? {
|
|
11209
|
+
state: githubStatus.state === "error" ? "failure" : githubStatus.state,
|
|
11210
|
+
description: githubStatus.description ?? "GitHub visual-review status",
|
|
11211
|
+
source: "github",
|
|
11212
|
+
targetUrl: githubStatus.targetUrl,
|
|
11213
|
+
updatedAt: githubStatus.updatedAt
|
|
11214
|
+
} : github ? {
|
|
11215
|
+
// GitHub answered authoritatively for the current head and
|
|
11216
|
+
// the context is absent. Keep that distinct from Tempo's
|
|
11217
|
+
// stored gate, which remains available under `report.gate`.
|
|
11218
|
+
state: "missing",
|
|
11219
|
+
description: stale ? "No visual-review status is present on the current PR head yet." : "No visual-review status is present on the PR head.",
|
|
11220
|
+
source: "none"
|
|
11221
|
+
} : detail && !stale ? {
|
|
11222
|
+
// No GitHub read was possible (for example reportUrl-only
|
|
11223
|
+
// hosted usage), so the stored gate is the best available
|
|
11224
|
+
// status signal rather than an assertion about GitHub.
|
|
11225
|
+
...detail.derivedStatus,
|
|
11226
|
+
source: "tempo",
|
|
11227
|
+
targetUrl: detail.report.url
|
|
11228
|
+
} : {
|
|
11229
|
+
state: "unavailable",
|
|
11230
|
+
description: "GitHub status could not be read; only stored Tempo data is shown.",
|
|
11231
|
+
source: "none"
|
|
11232
|
+
};
|
|
11233
|
+
if (githubStatus && detail && !stale) {
|
|
11234
|
+
const published = githubStatus.state === "error" ? "failure" : githubStatus.state;
|
|
11235
|
+
if (published !== detail.derivedStatus.state) {
|
|
11236
|
+
warnings.push(
|
|
11237
|
+
`GitHub's published visual-review status is ${githubStatus.state}, but Tempo's current report gate derives ${detail.derivedStatus.state}; inspect report.run.lastPublishError for a stale-status failure.`
|
|
11238
|
+
);
|
|
11239
|
+
}
|
|
11240
|
+
}
|
|
11241
|
+
return jsonResult3({
|
|
11242
|
+
pullRequest,
|
|
11243
|
+
status,
|
|
11244
|
+
report: detail ? { ...detail.report, stale } : null,
|
|
11245
|
+
comments: detail?.comments ?? EMPTY_COMMENTS,
|
|
11246
|
+
warnings: [...warnings, ...detail?.warnings ?? []]
|
|
11247
|
+
});
|
|
11248
|
+
} catch (e) {
|
|
11249
|
+
return errorResult3(mapMcpError(e));
|
|
11250
|
+
}
|
|
11251
|
+
}
|
|
11252
|
+
);
|
|
10900
11253
|
}
|
|
10901
11254
|
|
|
10902
11255
|
// ../canvas-comments-mcp/tools/write.ts
|
|
@@ -11218,10 +11571,15 @@ function registerReadStateTools(server, deps) {
|
|
|
11218
11571
|
}
|
|
11219
11572
|
|
|
11220
11573
|
// ../canvas-comments-mcp/server.ts
|
|
11574
|
+
var COMMENTS_MCP_INSTRUCTIONS = [
|
|
11575
|
+
"Tempo canvas comments: review threads pinned to canvases and storyboards, org-scoped and keyed by (orgProjectId, branch, contentPath) where contentPath is the canvas FOLDER path.",
|
|
11576
|
+
"When the user asks for a pull request's visual-review (VRT) status, verdicts, or review feedback, call comments_get_pr_visual_review first: it joins the GitHub `visual-review` check, the Tempo report gate, and the review threads in one read-only call.",
|
|
11577
|
+
"The AI acts as the signed-in user \u2014 every write is attributed to them, never to a separate AI identity."
|
|
11578
|
+
].join("\n\n");
|
|
11221
11579
|
function createCommentsMcpServer(context) {
|
|
11222
11580
|
const server = new McpServer3(
|
|
11223
11581
|
{ name: "tempo-comments-tools", version: "1.0.0" },
|
|
11224
|
-
{ capabilities: { tools: {} } }
|
|
11582
|
+
{ capabilities: { tools: {} }, instructions: COMMENTS_MCP_INSTRUCTIONS }
|
|
11225
11583
|
);
|
|
11226
11584
|
const convex = new AuthedConvexClient({
|
|
11227
11585
|
url: context.convexUrl,
|
|
@@ -11245,8 +11603,13 @@ function createCommentsMcpServer(context) {
|
|
|
11245
11603
|
context.getCurrentOrgGeneration
|
|
11246
11604
|
);
|
|
11247
11605
|
const getWritePermission = context.getWritePermission ?? (() => true);
|
|
11248
|
-
const { resolveCanvasContentPath } = context;
|
|
11249
|
-
registerReadTools2(instrumented, {
|
|
11606
|
+
const { resolveCanvasContentPath, getRepoUrl, githubApi } = context;
|
|
11607
|
+
registerReadTools2(instrumented, {
|
|
11608
|
+
convex,
|
|
11609
|
+
resolveCanvasContentPath,
|
|
11610
|
+
getRepoUrl,
|
|
11611
|
+
githubApi
|
|
11612
|
+
});
|
|
11250
11613
|
registerWriteTools2(instrumented, {
|
|
11251
11614
|
convex,
|
|
11252
11615
|
getWritePermission,
|
|
@@ -24377,6 +24740,19 @@ function classifyDevServerOutput(line) {
|
|
|
24377
24740
|
observedAt: Date.now()
|
|
24378
24741
|
};
|
|
24379
24742
|
}
|
|
24743
|
+
function buildHostUnresponsiveHint(url, unreachableSinceMs, nowMs = Date.now()) {
|
|
24744
|
+
const unreachableForSeconds = Math.max(
|
|
24745
|
+
0,
|
|
24746
|
+
Math.round((nowMs - unreachableSinceMs) / 1e3)
|
|
24747
|
+
);
|
|
24748
|
+
return {
|
|
24749
|
+
class: "host-unresponsive",
|
|
24750
|
+
hint: `The canvas dev server is running but has stopped answering requests at ${url} (no response for ${unreachableForSeconds}s). It may be wedged after a dependency change or an interrupted install; retrying restarts it.`,
|
|
24751
|
+
errorCode: "not_reachable",
|
|
24752
|
+
line: `url health probe failed: ${url}`,
|
|
24753
|
+
observedAt: nowMs
|
|
24754
|
+
};
|
|
24755
|
+
}
|
|
24380
24756
|
function matchClass(line) {
|
|
24381
24757
|
if (isShellParseAbortOutput(line)) {
|
|
24382
24758
|
return {
|
|
@@ -28016,6 +28392,21 @@ ${output.trim()}`
|
|
|
28016
28392
|
this.name = "InstallDependencyConflictError";
|
|
28017
28393
|
}
|
|
28018
28394
|
};
|
|
28395
|
+
var InstallInterruptedError = class extends TempoDevServerError {
|
|
28396
|
+
constructor(command, output, evidence) {
|
|
28397
|
+
const explanation = evidence === "concurrent-install" ? "Install command was interrupted \u2014 node_modules changed while it ran, because another install was running in the same folder at the same time" : "Install command could not move a package aside because node_modules is in an inconsistent state (npm ENOTEMPTY) \u2014 left behind when an earlier install was cut short, or by another install running in the same folder";
|
|
28398
|
+
const remedy = evidence === "concurrent-install" ? "Wait for any other install in this folder to finish, then retry the canvas." : "If no other install is running, delete that folder's node_modules and retry the canvas.";
|
|
28399
|
+
super(
|
|
28400
|
+
"install_interrupted",
|
|
28401
|
+
`${explanation}: ${command}
|
|
28402
|
+
${output.trim()}
|
|
28403
|
+
|
|
28404
|
+
${remedy}`
|
|
28405
|
+
);
|
|
28406
|
+
this.name = "InstallInterruptedError";
|
|
28407
|
+
this.evidence = evidence;
|
|
28408
|
+
}
|
|
28409
|
+
};
|
|
28019
28410
|
function shellDialectRewriteHint(field) {
|
|
28020
28411
|
return `Hint: Tempo runs tempo.config.json's "${field}" through the platform shell, which is cmd.exe on Windows \u2014 it cannot parse PowerShell. Rewrite the command as a plain, cross-platform invocation with no shell conditionals (e.g. "npm install", or "npm install --prefix .. && npm install" \u2014 cmd supports \`&&\`). Avoid PowerShell-only syntax: \`if (...) { ... } else { ... }\`, \`Test-Path\`, \`$env:FOO\`. If you need cmd's own conditional, it is \`if exist package.json (...) else (...)\`.`;
|
|
28021
28412
|
}
|
|
@@ -28383,6 +28774,29 @@ var CanvasRegistry = class {
|
|
|
28383
28774
|
}
|
|
28384
28775
|
};
|
|
28385
28776
|
|
|
28777
|
+
// ../tempo-devserver/process-hint-slots.ts
|
|
28778
|
+
var ProcessHintSlots = class {
|
|
28779
|
+
constructor() {
|
|
28780
|
+
this.outputHint = null;
|
|
28781
|
+
this.supervisorHint = null;
|
|
28782
|
+
}
|
|
28783
|
+
current() {
|
|
28784
|
+
return this.supervisorHint ?? this.outputHint;
|
|
28785
|
+
}
|
|
28786
|
+
recordOutput(hint) {
|
|
28787
|
+
this.outputHint = hint;
|
|
28788
|
+
}
|
|
28789
|
+
recordSupervisor(hint) {
|
|
28790
|
+
this.supervisorHint = hint;
|
|
28791
|
+
}
|
|
28792
|
+
/** Withdraw the supervisor hint when it is of `hintClass`; false if it was not. */
|
|
28793
|
+
clearSupervisor(hintClass) {
|
|
28794
|
+
if (this.supervisorHint?.class !== hintClass) return false;
|
|
28795
|
+
this.supervisorHint = null;
|
|
28796
|
+
return true;
|
|
28797
|
+
}
|
|
28798
|
+
};
|
|
28799
|
+
|
|
28386
28800
|
// ../tempo-devserver/project-node-version.ts
|
|
28387
28801
|
import { access, readFile as readFile10, readdir as readdir5 } from "fs/promises";
|
|
28388
28802
|
import path16 from "path";
|
|
@@ -28965,10 +29379,11 @@ var TempoHostProcess = class _TempoHostProcess {
|
|
|
28965
29379
|
this.outputListeners = [];
|
|
28966
29380
|
// Collects recent process output so early-exit errors include diagnostic context
|
|
28967
29381
|
this.recentOutput = [];
|
|
28968
|
-
//
|
|
28969
|
-
//
|
|
28970
|
-
//
|
|
28971
|
-
|
|
29382
|
+
// User-actionable hints about this process — classified from stdout/stderr
|
|
29383
|
+
// (see `hint-classifier.ts`) or observed by the supervisor from outside —
|
|
29384
|
+
// drive the renderer's toolbar messaging when the canvas is stuck or
|
|
29385
|
+
// errored. Two slots, because the two sources have different lifetimes.
|
|
29386
|
+
this.hints = new ProcessHintSlots();
|
|
28972
29387
|
this.hintListeners = [];
|
|
28973
29388
|
this.explicitTempoHostUrl = options.explicitTempoHostUrl;
|
|
28974
29389
|
this.tempoHostUrlSource = options.tempoHostUrlSource;
|
|
@@ -29029,19 +29444,7 @@ var TempoHostProcess = class _TempoHostProcess {
|
|
|
29029
29444
|
hint: new MissingRootDepsError(this.missingRootDepsProjectRoot).message
|
|
29030
29445
|
} : classifiedHint;
|
|
29031
29446
|
if (hint) {
|
|
29032
|
-
this.
|
|
29033
|
-
devLog("process:hint", {
|
|
29034
|
-
pid: this.child.pid ?? null,
|
|
29035
|
-
class: hint.class,
|
|
29036
|
-
hint: hint.hint,
|
|
29037
|
-
line: text.slice(0, 500)
|
|
29038
|
-
});
|
|
29039
|
-
for (const listener of this.hintListeners) {
|
|
29040
|
-
try {
|
|
29041
|
-
listener(hint);
|
|
29042
|
-
} catch {
|
|
29043
|
-
}
|
|
29044
|
-
}
|
|
29447
|
+
this.recordHint(hint);
|
|
29045
29448
|
}
|
|
29046
29449
|
const matchedUrl = extractServerUrl2(text);
|
|
29047
29450
|
if (matchedUrl && !this.detectedBaseUrl) {
|
|
@@ -29157,15 +29560,69 @@ ${output}` : "";
|
|
|
29157
29560
|
}
|
|
29158
29561
|
onHint(listener) {
|
|
29159
29562
|
this.hintListeners.push(listener);
|
|
29160
|
-
|
|
29563
|
+
const current = this.hints.current();
|
|
29564
|
+
if (current) {
|
|
29161
29565
|
try {
|
|
29162
|
-
listener(
|
|
29566
|
+
listener(current);
|
|
29163
29567
|
} catch {
|
|
29164
29568
|
}
|
|
29165
29569
|
}
|
|
29166
29570
|
}
|
|
29167
29571
|
getLastHint() {
|
|
29168
|
-
return this.
|
|
29572
|
+
return this.hints.current();
|
|
29573
|
+
}
|
|
29574
|
+
/**
|
|
29575
|
+
* Record a hint classified from this process's stdout/stderr and notify
|
|
29576
|
+
* subscribers with whatever hint is now in effect (a supervisor hint, if one
|
|
29577
|
+
* is active, still takes precedence — see `ProcessHintSlots`).
|
|
29578
|
+
*/
|
|
29579
|
+
recordHint(hint) {
|
|
29580
|
+
this.hints.recordOutput(hint);
|
|
29581
|
+
this.logHint(hint);
|
|
29582
|
+
this.notifyHintListeners(this.hints.current());
|
|
29583
|
+
}
|
|
29584
|
+
/**
|
|
29585
|
+
* Record a hint the supervisor observed about this process from outside its
|
|
29586
|
+
* output (a live child whose URL stopped answering — see
|
|
29587
|
+
* `runUrlHealthProbeTick`). It is shown in place of any output hint until
|
|
29588
|
+
* `clearSupervisorHint` withdraws it.
|
|
29589
|
+
*/
|
|
29590
|
+
recordSupervisorHint(hint) {
|
|
29591
|
+
this.hints.recordSupervisor(hint);
|
|
29592
|
+
this.logHint(hint);
|
|
29593
|
+
this.notifyHintListeners(hint);
|
|
29594
|
+
}
|
|
29595
|
+
/**
|
|
29596
|
+
* Withdraw the supervisor hint of `hintClass` once the condition it
|
|
29597
|
+
* described has cleared (a URL probe succeeding again). Subscribers receive
|
|
29598
|
+
* the hint now in effect — the output hint that was showing before, or
|
|
29599
|
+
* `null` — so a consumer that mirrors "the current hint" (the Electron
|
|
29600
|
+
* adapter → renderer toolbar detail) stops reporting a recovered host as
|
|
29601
|
+
* stuck without losing a compile diagnostic that is still true.
|
|
29602
|
+
*/
|
|
29603
|
+
clearSupervisorHint(hintClass) {
|
|
29604
|
+
if (!this.hints.clearSupervisor(hintClass)) return;
|
|
29605
|
+
devLog("process:hint_cleared", {
|
|
29606
|
+
pid: this.child.pid ?? null,
|
|
29607
|
+
class: hintClass
|
|
29608
|
+
});
|
|
29609
|
+
this.notifyHintListeners(this.hints.current());
|
|
29610
|
+
}
|
|
29611
|
+
logHint(hint) {
|
|
29612
|
+
devLog("process:hint", {
|
|
29613
|
+
pid: this.child.pid ?? null,
|
|
29614
|
+
class: hint.class,
|
|
29615
|
+
hint: hint.hint,
|
|
29616
|
+
line: hint.line
|
|
29617
|
+
});
|
|
29618
|
+
}
|
|
29619
|
+
notifyHintListeners(hint) {
|
|
29620
|
+
for (const listener of this.hintListeners) {
|
|
29621
|
+
try {
|
|
29622
|
+
listener(hint);
|
|
29623
|
+
} catch {
|
|
29624
|
+
}
|
|
29625
|
+
}
|
|
29169
29626
|
}
|
|
29170
29627
|
getLivenessSource() {
|
|
29171
29628
|
return this.launcherFinished ? "probe" : "child";
|
|
@@ -29179,9 +29636,10 @@ ${output}` : "";
|
|
|
29179
29636
|
* no indication that the command never even parsed. See TEM-436.
|
|
29180
29637
|
*/
|
|
29181
29638
|
hintSuffix() {
|
|
29182
|
-
|
|
29639
|
+
const current = this.hints.current();
|
|
29640
|
+
return current ? `
|
|
29183
29641
|
|
|
29184
|
-
Hint: ${
|
|
29642
|
+
Hint: ${current.hint}` : "";
|
|
29185
29643
|
}
|
|
29186
29644
|
getPid() {
|
|
29187
29645
|
return this.child.pid ?? null;
|
|
@@ -29588,6 +30046,11 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
29588
30046
|
// local `process` var out of `startDevServer`.
|
|
29589
30047
|
this.activeHostProcess = null;
|
|
29590
30048
|
this.urlHealthProbeConsecutiveFailures = 0;
|
|
30049
|
+
// Bumped whenever the probe loop is (re)started or stopped, so a probe
|
|
30050
|
+
// that was already in flight for the PREVIOUS loop cannot report into this
|
|
30051
|
+
// one. URL equality is not enough of a fence: a replacement process on the
|
|
30052
|
+
// same URL would inherit the old process's last failure.
|
|
30053
|
+
this.urlHealthProbeGeneration = 0;
|
|
29591
30054
|
// Tempo-managed Next hosts never share the user's ordinary `.next` cache.
|
|
29592
30055
|
// This starts at the stable Tempo-owned directory and only receives a suffix
|
|
29593
30056
|
// after Next reports an observed lock conflict for that directory.
|
|
@@ -30788,6 +31251,7 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
30788
31251
|
if (this.disposed) return;
|
|
30789
31252
|
this.urlHealthProbeUrl = url;
|
|
30790
31253
|
this.urlHealthProbeConsecutiveFailures = 0;
|
|
31254
|
+
this.urlHealthProbeGeneration += 1;
|
|
30791
31255
|
this.urlHealthProbeTimer = setInterval(() => {
|
|
30792
31256
|
void this.runUrlHealthProbeTick();
|
|
30793
31257
|
}, _TempoDevServer.URL_HEALTH_PROBE_INTERVAL_MS);
|
|
@@ -30802,9 +31266,11 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
30802
31266
|
}
|
|
30803
31267
|
this.urlHealthProbeUrl = null;
|
|
30804
31268
|
this.urlHealthProbeConsecutiveFailures = 0;
|
|
31269
|
+
this.urlHealthProbeGeneration += 1;
|
|
30805
31270
|
}
|
|
30806
31271
|
async runUrlHealthProbeTick() {
|
|
30807
31272
|
const probedUrl = this.urlHealthProbeUrl;
|
|
31273
|
+
const generation = this.urlHealthProbeGeneration;
|
|
30808
31274
|
if (!probedUrl || this.disposed) return;
|
|
30809
31275
|
const reachable = await probeUrlOnce(
|
|
30810
31276
|
probedUrl,
|
|
@@ -30812,18 +31278,26 @@ var TempoDevServer = class _TempoDevServer {
|
|
|
30812
31278
|
this.projectRoot,
|
|
30813
31279
|
this.activeHostIdentity
|
|
30814
31280
|
);
|
|
30815
|
-
if (this.
|
|
31281
|
+
if (this.urlHealthProbeGeneration !== generation) return;
|
|
30816
31282
|
if (reachable) {
|
|
30817
31283
|
this.state.urlUnreachableSince = null;
|
|
30818
31284
|
this.urlHealthProbeConsecutiveFailures = 0;
|
|
31285
|
+
this.activeHostProcess?.clearSupervisorHint("host-unresponsive");
|
|
30819
31286
|
return;
|
|
30820
31287
|
}
|
|
30821
31288
|
if (this.activeHostProcess?.isAlive()) {
|
|
30822
31289
|
this.state.urlUnreachableSince ??= Date.now();
|
|
31290
|
+
this.urlHealthProbeConsecutiveFailures += 1;
|
|
30823
31291
|
devLog("supervisor:url_probe_failed_host_alive", {
|
|
30824
31292
|
url: probedUrl,
|
|
31293
|
+
consecutiveFailures: this.urlHealthProbeConsecutiveFailures,
|
|
30825
31294
|
unreachableSince: this.state.urlUnreachableSince
|
|
30826
31295
|
});
|
|
31296
|
+
if (this.urlHealthProbeConsecutiveFailures === _TempoDevServer.URL_HEALTH_FAILURES_TO_DEMOTE) {
|
|
31297
|
+
this.activeHostProcess.recordSupervisorHint(
|
|
31298
|
+
buildHostUnresponsiveHint(probedUrl, this.state.urlUnreachableSince)
|
|
31299
|
+
);
|
|
31300
|
+
}
|
|
30827
31301
|
return;
|
|
30828
31302
|
}
|
|
30829
31303
|
const probeBacked = this.activeHostLivenessSource() === "probe";
|
|
@@ -32063,6 +32537,11 @@ var INSTALL_LOCK_PATTERN = new RegExp(
|
|
|
32063
32537
|
`(?:EPERM|EACCES|EBUSY)[\\s\\S]{0,400}?(?:${INSTALL_LOCK_SYSCALLS})|(?:${INSTALL_LOCK_SYSCALLS})[\\s\\S]{0,400}?(?:EPERM|EACCES|EBUSY)|operation not permitted|permission denied|resource busy or locked`,
|
|
32064
32538
|
"i"
|
|
32065
32539
|
);
|
|
32540
|
+
function classifyInterruptedInstallOutput(output) {
|
|
32541
|
+
if (/\bTAR_ENTRY_ERROR\b/.test(output)) return "concurrent-install";
|
|
32542
|
+
if (/\bENOTEMPTY\b/.test(output)) return "inconsistent-tree";
|
|
32543
|
+
return null;
|
|
32544
|
+
}
|
|
32066
32545
|
function classifyInstallFailure(command, output) {
|
|
32067
32546
|
if (isShellParseAbortOutput(output)) {
|
|
32068
32547
|
return new InstallShellDialectMismatchError(command, output);
|
|
@@ -32070,6 +32549,10 @@ function classifyInstallFailure(command, output) {
|
|
|
32070
32549
|
if (/ERESOLVE|unable to resolve dependency tree|could not resolve/i.test(output)) {
|
|
32071
32550
|
return new InstallDependencyConflictError(command, output);
|
|
32072
32551
|
}
|
|
32552
|
+
const interruptedEvidence = classifyInterruptedInstallOutput(output);
|
|
32553
|
+
if (interruptedEvidence) {
|
|
32554
|
+
return new InstallInterruptedError(command, output, interruptedEvidence);
|
|
32555
|
+
}
|
|
32073
32556
|
if (/\bEBUSY\b|resource busy or locked/i.test(output)) {
|
|
32074
32557
|
return new InstallFileLockedError(command, output);
|
|
32075
32558
|
}
|
|
@@ -46700,7 +47183,7 @@ function buildBackends(runtime, toolsets, workspace) {
|
|
|
46700
47183
|
}
|
|
46701
47184
|
|
|
46702
47185
|
// src/version.ts
|
|
46703
|
-
var CLI_VERSION = "0.0.
|
|
47186
|
+
var CLI_VERSION = "0.0.107";
|
|
46704
47187
|
|
|
46705
47188
|
// src/canvas-hooks.ts
|
|
46706
47189
|
var import_pngjs4 = __toESM(require_png(), 1);
|
|
@@ -49214,4 +49697,4 @@ export {
|
|
|
49214
49697
|
runServe,
|
|
49215
49698
|
scopeParamsFor
|
|
49216
49699
|
};
|
|
49217
|
-
//# sourceMappingURL=serve-
|
|
49700
|
+
//# sourceMappingURL=serve-IXP2WX42.js.map
|