@agent-native/core 0.49.21 → 0.49.23
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/agent/production-agent.d.ts +1 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +15 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/tool-search.d.ts.map +1 -1
- package/dist/agent/tool-search.js +32 -7
- package/dist/agent/tool-search.js.map +1 -1
- package/dist/cli/connect.d.ts +2 -3
- package/dist/cli/connect.d.ts.map +1 -1
- package/dist/cli/connect.js +60 -37
- package/dist/cli/connect.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +5 -7
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +5 -7
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts +44 -52
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +420 -414
- package/dist/cli/recap.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +6 -3
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +1 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +23 -3
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +8 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +24 -1
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.js +4 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
- package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/DiffBlock.js +20 -7
- package/dist/client/blocks/library/DiffBlock.js.map +1 -1
- package/dist/client/blocks/library/annotation-rail.js +5 -5
- package/dist/client/blocks/library/annotation-rail.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +15 -2
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/coding-tools/run-code.d.ts.map +1 -1
- package/dist/coding-tools/run-code.js +69 -17
- package/dist/coding-tools/run-code.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +2 -0
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/mcp/build-server.d.ts +12 -10
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +53 -89
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/connect-route.d.ts.map +1 -1
- package/dist/mcp/connect-route.js +5 -4
- package/dist/mcp/connect-route.js.map +1 -1
- package/dist/mcp/oauth-token.d.ts +6 -5
- package/dist/mcp/oauth-token.d.ts.map +1 -1
- package/dist/mcp/oauth-token.js.map +1 -1
- package/dist/mcp/stdio.d.ts.map +1 -1
- package/dist/mcp/stdio.js +9 -2
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/provider-api/staging.d.ts.map +1 -1
- package/dist/provider-api/staging.js +6 -4
- package/dist/provider-api/staging.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +10 -7
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/docs/content/actions.md +1 -1
- package/docs/content/external-agents.md +53 -40
- package/docs/content/mcp-protocol.md +16 -11
- package/docs/content/pr-visual-recap.md +1 -1
- package/docs/content/template-plan.md +1 -1
- package/package.json +1 -1
package/dist/cli/recap.js
CHANGED
|
@@ -14,13 +14,12 @@
|
|
|
14
14
|
* collect-diff Collect the bounded base...head diff (excluding lockfiles,
|
|
15
15
|
* build output, snapshots), cap it at ~600KB, and classify the
|
|
16
16
|
* huge/tiny flags.
|
|
17
|
-
* mcp-config Write the plan MCP client config for the chosen backend
|
|
18
|
-
* (Claude Code JSON or Codex config.toml).
|
|
19
|
-
* mcp-smoke Verify the configured Plan MCP endpoint exposes the publish
|
|
20
|
-
* tools before spending runner time on Claude/Codex.
|
|
21
17
|
* scan Refuse to hand a secret-leaking diff to the agent.
|
|
18
|
+
* block-reference
|
|
19
|
+
* Fetch the live get-plan-blocks reference for the target app.
|
|
22
20
|
* build-prompt Assemble the agent prompt = latest visual-recap skill bundle
|
|
23
21
|
* + a task wrapper (or repo-pinned skill with --skill-source).
|
|
22
|
+
* publish Publish the agent-authored recap-source.json over HTTP.
|
|
24
23
|
* shot Screenshot the published plan and upload it to the plan app's
|
|
25
24
|
* signed public image route (for an inline PR-comment image).
|
|
26
25
|
* usage Parse and emit agent token-usage/cost from stdout.
|
|
@@ -36,7 +35,6 @@
|
|
|
36
35
|
*/
|
|
37
36
|
import { execFileSync } from "node:child_process";
|
|
38
37
|
import fs from "node:fs";
|
|
39
|
-
import os from "node:os";
|
|
40
38
|
import path from "node:path";
|
|
41
39
|
import { readPlanPublishAuth } from "./plan-publish-store.js";
|
|
42
40
|
import { PR_VISUAL_RECAP_WORKFLOW_YML } from "./pr-visual-recap-workflow.js";
|
|
@@ -196,12 +194,6 @@ export function writePrVisualRecapReusableCallerWorkflow(baseDir, options = {})
|
|
|
196
194
|
return { status: "written", path: rel, existed: false };
|
|
197
195
|
}
|
|
198
196
|
const DEFAULT_RECAP_APP_URL = "https://plan.agent-native.com";
|
|
199
|
-
const RECAP_MCP_CLIENT_HEADER = "agent-native-pr-visual-recap";
|
|
200
|
-
export const RECAP_MCP_REQUIRED_TOOLS = [
|
|
201
|
-
"get-plan-blocks",
|
|
202
|
-
"create-visual-recap",
|
|
203
|
-
"set-resource-visibility",
|
|
204
|
-
];
|
|
205
197
|
export function normalizeRecapAgent(value) {
|
|
206
198
|
const agent = (value || "claude").toLowerCase();
|
|
207
199
|
if (agent === "codex")
|
|
@@ -1109,371 +1101,6 @@ function runCollectDiff(args) {
|
|
|
1109
1101
|
process.stdout.write(`${JSON.stringify({ bytes, changed, huge, tiny })}\n`);
|
|
1110
1102
|
}
|
|
1111
1103
|
/* -------------------------------------------------------------------------- */
|
|
1112
|
-
/* MCP config writers — were the two `node -e` one-liners in the agent steps */
|
|
1113
|
-
/* -------------------------------------------------------------------------- */
|
|
1114
|
-
/**
|
|
1115
|
-
* The Claude Code MCP config the recap agent loads: a single HTTP `plan` server
|
|
1116
|
-
* pointing at the app's `/_agent-native/mcp` endpoint, authorized with the
|
|
1117
|
-
* PLAN_RECAP_TOKEN. Pure (returns the JSON string) so it can be unit-tested.
|
|
1118
|
-
*/
|
|
1119
|
-
export function buildRecapClaudeMcpConfig(appUrl, token) {
|
|
1120
|
-
const url = appUrl.replace(/\/$/, "") + "/_agent-native/mcp";
|
|
1121
|
-
return JSON.stringify({
|
|
1122
|
-
mcpServers: {
|
|
1123
|
-
plan: {
|
|
1124
|
-
type: "http",
|
|
1125
|
-
url,
|
|
1126
|
-
headers: {
|
|
1127
|
-
Authorization: "Bearer " + token,
|
|
1128
|
-
"X-Agent-Native-MCP-Client": RECAP_MCP_CLIENT_HEADER,
|
|
1129
|
-
"X-Agent-Native-MCP-Full-Catalog": "1",
|
|
1130
|
-
},
|
|
1131
|
-
},
|
|
1132
|
-
"agent-native-plans": {
|
|
1133
|
-
type: "http",
|
|
1134
|
-
url,
|
|
1135
|
-
headers: {
|
|
1136
|
-
Authorization: "Bearer " + token,
|
|
1137
|
-
"X-Agent-Native-MCP-Client": RECAP_MCP_CLIENT_HEADER,
|
|
1138
|
-
"X-Agent-Native-MCP-Full-Catalog": "1",
|
|
1139
|
-
},
|
|
1140
|
-
},
|
|
1141
|
-
},
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
/**
|
|
1145
|
-
* The Codex `config.toml` the recap agent loads. JSON.stringify the URL value so
|
|
1146
|
-
* a stray quote/newline in the app URL can't break out of the TOML basic string
|
|
1147
|
-
* (TOML shares JSON's escaping); the key and env-var name stay literal. Pure so
|
|
1148
|
-
* it can be unit-tested.
|
|
1149
|
-
*/
|
|
1150
|
-
export function buildRecapCodexMcpConfig(appUrl) {
|
|
1151
|
-
const url = appUrl.replace(/\/$/, "") + "/_agent-native/mcp";
|
|
1152
|
-
return ("[mcp_servers.plan]\n" +
|
|
1153
|
-
"url = " +
|
|
1154
|
-
JSON.stringify(url) +
|
|
1155
|
-
"\n" +
|
|
1156
|
-
'bearer_token_env_var = "PLAN_RECAP_TOKEN"\n' +
|
|
1157
|
-
'http_headers = { "X-Agent-Native-MCP-Client" = "agent-native-pr-visual-recap", "X-Agent-Native-MCP-Full-Catalog" = "1" }\n');
|
|
1158
|
-
}
|
|
1159
|
-
/**
|
|
1160
|
-
* Per-attempt timeout for the non-mutating MCP smoke probe. Short enough that
|
|
1161
|
-
* the workflow's 3-attempt retry loop can recover from a cold-start hang within
|
|
1162
|
-
* the job budget, long enough not to flake on a warm-but-slow response. The
|
|
1163
|
-
* plan app is serverless and intermittently 404s / stalls during a cold start
|
|
1164
|
-
* or mid-deploy; without a bound, undici's multi-minute default header/body
|
|
1165
|
-
* timeout would burn the whole job on a single stuck attempt.
|
|
1166
|
-
*/
|
|
1167
|
-
const RECAP_MCP_SMOKE_TIMEOUT_MS = 15_000;
|
|
1168
|
-
function recapMcpUrl(appUrl) {
|
|
1169
|
-
return appUrl.replace(/\/$/, "") + "/_agent-native/mcp";
|
|
1170
|
-
}
|
|
1171
|
-
function recapMcpSmokeHeaders(token) {
|
|
1172
|
-
return {
|
|
1173
|
-
authorization: `Bearer ${token}`,
|
|
1174
|
-
accept: "application/json, text/event-stream",
|
|
1175
|
-
"content-type": "application/json",
|
|
1176
|
-
"mcp-protocol-version": "2025-06-18",
|
|
1177
|
-
"x-agent-native-mcp-client": RECAP_MCP_CLIENT_HEADER,
|
|
1178
|
-
"x-agent-native-mcp-full-catalog": "1",
|
|
1179
|
-
};
|
|
1180
|
-
}
|
|
1181
|
-
function parseSseJsonPayload(raw) {
|
|
1182
|
-
const dataLines = raw
|
|
1183
|
-
.split(/\r?\n/)
|
|
1184
|
-
.map((line) => line.trim())
|
|
1185
|
-
.filter((line) => line.startsWith("data:"))
|
|
1186
|
-
.map((line) => line.slice("data:".length).trim())
|
|
1187
|
-
.filter((line) => line && line !== "[DONE]");
|
|
1188
|
-
for (let i = dataLines.length - 1; i >= 0; i -= 1) {
|
|
1189
|
-
try {
|
|
1190
|
-
return JSON.parse(dataLines[i]);
|
|
1191
|
-
}
|
|
1192
|
-
catch {
|
|
1193
|
-
// Keep looking for a parseable event payload.
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
return null;
|
|
1197
|
-
}
|
|
1198
|
-
function parseMcpJsonPayload(raw) {
|
|
1199
|
-
const trimmed = raw.trim();
|
|
1200
|
-
if (!trimmed)
|
|
1201
|
-
return null;
|
|
1202
|
-
try {
|
|
1203
|
-
return JSON.parse(trimmed);
|
|
1204
|
-
}
|
|
1205
|
-
catch {
|
|
1206
|
-
return parseSseJsonPayload(trimmed);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
function mcpRpcErrorMessage(payload) {
|
|
1210
|
-
if (!payload || typeof payload !== "object")
|
|
1211
|
-
return null;
|
|
1212
|
-
const error = payload.error;
|
|
1213
|
-
if (!error || typeof error !== "object")
|
|
1214
|
-
return null;
|
|
1215
|
-
const message = error.message;
|
|
1216
|
-
return typeof message === "string" && message.trim()
|
|
1217
|
-
? message.trim()
|
|
1218
|
-
: JSON.stringify(error);
|
|
1219
|
-
}
|
|
1220
|
-
async function postRecapMcpRpc(input) {
|
|
1221
|
-
const response = await input.fetchFn(input.mcpUrl, {
|
|
1222
|
-
method: "POST",
|
|
1223
|
-
headers: recapMcpSmokeHeaders(input.token),
|
|
1224
|
-
body: JSON.stringify({
|
|
1225
|
-
jsonrpc: "2.0",
|
|
1226
|
-
id: input.id,
|
|
1227
|
-
method: input.method,
|
|
1228
|
-
params: input.params ?? {},
|
|
1229
|
-
}),
|
|
1230
|
-
// Bound each attempt so a cold-start hang fails fast and the workflow's
|
|
1231
|
-
// smoke retry loop can re-probe a (by-then warm) endpoint, instead of
|
|
1232
|
-
// blocking on undici's multi-minute default timeout.
|
|
1233
|
-
signal: AbortSignal.timeout(RECAP_MCP_SMOKE_TIMEOUT_MS),
|
|
1234
|
-
});
|
|
1235
|
-
const raw = await response.text().catch((err) => String(err));
|
|
1236
|
-
if (!response.ok) {
|
|
1237
|
-
const detail = sanitizeAgentFailureSummary(raw, 300);
|
|
1238
|
-
return {
|
|
1239
|
-
ok: false,
|
|
1240
|
-
status: response.status,
|
|
1241
|
-
raw,
|
|
1242
|
-
reason: `Plan MCP ${input.method} returned HTTP ${response.status}${detail ? `: ${detail}` : ""}`,
|
|
1243
|
-
};
|
|
1244
|
-
}
|
|
1245
|
-
const payload = parseMcpJsonPayload(raw);
|
|
1246
|
-
if (!payload) {
|
|
1247
|
-
return {
|
|
1248
|
-
ok: false,
|
|
1249
|
-
raw,
|
|
1250
|
-
reason: `Plan MCP ${input.method} returned an unreadable response`,
|
|
1251
|
-
};
|
|
1252
|
-
}
|
|
1253
|
-
const rpcError = mcpRpcErrorMessage(payload);
|
|
1254
|
-
if (rpcError) {
|
|
1255
|
-
return {
|
|
1256
|
-
ok: false,
|
|
1257
|
-
raw,
|
|
1258
|
-
reason: `Plan MCP ${input.method} failed: ${sanitizeAgentFailureSummary(rpcError, 300)}`,
|
|
1259
|
-
};
|
|
1260
|
-
}
|
|
1261
|
-
return { ok: true, payload };
|
|
1262
|
-
}
|
|
1263
|
-
function extractMcpToolNames(payload) {
|
|
1264
|
-
const result = payload && typeof payload === "object"
|
|
1265
|
-
? payload.result
|
|
1266
|
-
: undefined;
|
|
1267
|
-
const tools = result && typeof result === "object"
|
|
1268
|
-
? result.tools
|
|
1269
|
-
: undefined;
|
|
1270
|
-
if (!Array.isArray(tools))
|
|
1271
|
-
return [];
|
|
1272
|
-
return tools
|
|
1273
|
-
.map((tool) => tool && typeof tool === "object"
|
|
1274
|
-
? tool.name
|
|
1275
|
-
: undefined)
|
|
1276
|
-
.filter((name) => typeof name === "string" && !!name)
|
|
1277
|
-
.sort();
|
|
1278
|
-
}
|
|
1279
|
-
function recapMcpSmokeFailure(input) {
|
|
1280
|
-
const tools = input.tools ?? [];
|
|
1281
|
-
const reason = sanitizeAgentFailureSummary(input.reason, 500);
|
|
1282
|
-
return {
|
|
1283
|
-
ok: false,
|
|
1284
|
-
appUrl: input.appUrl,
|
|
1285
|
-
mcpUrl: input.mcpUrl,
|
|
1286
|
-
toolCount: tools.length,
|
|
1287
|
-
tools,
|
|
1288
|
-
requiredTools: [...input.requiredTools],
|
|
1289
|
-
reason,
|
|
1290
|
-
summary: `Plan MCP smoke check failed: ${reason}`,
|
|
1291
|
-
};
|
|
1292
|
-
}
|
|
1293
|
-
/**
|
|
1294
|
-
* Non-mutating live contract check for PR Visual Recap publishing.
|
|
1295
|
-
*
|
|
1296
|
-
* The previous workflow discovered a broken Plan MCP catalog only after the
|
|
1297
|
-
* agent tried to publish and failed to create `recap-url.txt`. This probes the
|
|
1298
|
-
* same authenticated MCP endpoint first and requires the three tools that the
|
|
1299
|
-
* visual-recap skill needs to publish a hosted recap.
|
|
1300
|
-
*/
|
|
1301
|
-
export async function smokeRecapMcpTools(input) {
|
|
1302
|
-
const appUrl = input.appUrl ?? DEFAULT_RECAP_APP_URL;
|
|
1303
|
-
const mcpUrl = recapMcpUrl(appUrl);
|
|
1304
|
-
const requiredTools = input.requiredTools ?? RECAP_MCP_REQUIRED_TOOLS;
|
|
1305
|
-
const token = input.token?.trim();
|
|
1306
|
-
if (!token) {
|
|
1307
|
-
return recapMcpSmokeFailure({
|
|
1308
|
-
appUrl,
|
|
1309
|
-
mcpUrl,
|
|
1310
|
-
requiredTools,
|
|
1311
|
-
reason: "PLAN_RECAP_TOKEN is empty",
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
const fetchFn = input.fetchFn ?? fetch;
|
|
1315
|
-
try {
|
|
1316
|
-
const init = await postRecapMcpRpc({
|
|
1317
|
-
fetchFn,
|
|
1318
|
-
mcpUrl,
|
|
1319
|
-
token,
|
|
1320
|
-
method: "initialize",
|
|
1321
|
-
id: 1,
|
|
1322
|
-
params: {
|
|
1323
|
-
protocolVersion: "2025-06-18",
|
|
1324
|
-
capabilities: {},
|
|
1325
|
-
clientInfo: {
|
|
1326
|
-
name: RECAP_MCP_CLIENT_HEADER,
|
|
1327
|
-
version: "1.0.0",
|
|
1328
|
-
},
|
|
1329
|
-
},
|
|
1330
|
-
});
|
|
1331
|
-
if (!init.ok) {
|
|
1332
|
-
return recapMcpSmokeFailure({
|
|
1333
|
-
appUrl,
|
|
1334
|
-
mcpUrl,
|
|
1335
|
-
requiredTools,
|
|
1336
|
-
reason: init.reason,
|
|
1337
|
-
});
|
|
1338
|
-
}
|
|
1339
|
-
const listed = await postRecapMcpRpc({
|
|
1340
|
-
fetchFn,
|
|
1341
|
-
mcpUrl,
|
|
1342
|
-
token,
|
|
1343
|
-
method: "tools/list",
|
|
1344
|
-
id: 2,
|
|
1345
|
-
});
|
|
1346
|
-
if (!listed.ok) {
|
|
1347
|
-
return recapMcpSmokeFailure({
|
|
1348
|
-
appUrl,
|
|
1349
|
-
mcpUrl,
|
|
1350
|
-
requiredTools,
|
|
1351
|
-
reason: listed.reason,
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
|
-
const tools = extractMcpToolNames(listed.payload);
|
|
1355
|
-
const missing = requiredTools.filter((name) => !tools.includes(name));
|
|
1356
|
-
if (missing.length > 0) {
|
|
1357
|
-
return recapMcpSmokeFailure({
|
|
1358
|
-
appUrl,
|
|
1359
|
-
mcpUrl,
|
|
1360
|
-
tools,
|
|
1361
|
-
requiredTools,
|
|
1362
|
-
reason: `Plan MCP tools/list returned ${tools.length} tools but is missing required publishing tools: ${missing.join(", ")}`,
|
|
1363
|
-
});
|
|
1364
|
-
}
|
|
1365
|
-
return {
|
|
1366
|
-
ok: true,
|
|
1367
|
-
appUrl,
|
|
1368
|
-
mcpUrl,
|
|
1369
|
-
toolCount: tools.length,
|
|
1370
|
-
tools,
|
|
1371
|
-
requiredTools: [...requiredTools],
|
|
1372
|
-
summary: `Plan MCP smoke check passed: tools/list exposes ${requiredTools.join(", ")}.`,
|
|
1373
|
-
};
|
|
1374
|
-
}
|
|
1375
|
-
catch (err) {
|
|
1376
|
-
return recapMcpSmokeFailure({
|
|
1377
|
-
appUrl,
|
|
1378
|
-
mcpUrl,
|
|
1379
|
-
requiredTools,
|
|
1380
|
-
reason: `Plan MCP smoke check could not reach ${mcpUrl}: ${String(err)}`,
|
|
1381
|
-
});
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
async function runMcpSmoke(args) {
|
|
1385
|
-
const appUrl = optionalArg(args, "app-url") ??
|
|
1386
|
-
process.env.PLAN_RECAP_APP_URL ??
|
|
1387
|
-
DEFAULT_RECAP_APP_URL;
|
|
1388
|
-
const token = optionalArg(args, "token") ?? process.env.PLAN_RECAP_TOKEN;
|
|
1389
|
-
const result = await smokeRecapMcpTools({ appUrl, token });
|
|
1390
|
-
writeGitHubOutput("ok", result.ok ? "true" : "false");
|
|
1391
|
-
writeGitHubOutput("summary", result.summary);
|
|
1392
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
1393
|
-
if (!result.ok)
|
|
1394
|
-
process.exitCode = 1;
|
|
1395
|
-
}
|
|
1396
|
-
/**
|
|
1397
|
-
* `recap mcp-config` — write the plan MCP client config for the chosen backend,
|
|
1398
|
-
* replacing the two `node -e '...'` one-liners that previously lived inline in
|
|
1399
|
-
* the agent steps. PLAN_RECAP_TOKEN is read from the environment (claude only),
|
|
1400
|
-
* exactly as before.
|
|
1401
|
-
*/
|
|
1402
|
-
function runMcpConfig(args) {
|
|
1403
|
-
const agent = stringArg(args, "agent").toLowerCase();
|
|
1404
|
-
const appUrl = stringArg(args, "app-url");
|
|
1405
|
-
const force = Boolean(args["force"]);
|
|
1406
|
-
if (agent === "claude") {
|
|
1407
|
-
const token = process.env.PLAN_RECAP_TOKEN;
|
|
1408
|
-
if (!token) {
|
|
1409
|
-
process.stderr.write(`recap mcp-config: PLAN_RECAP_TOKEN is not set.\n` +
|
|
1410
|
-
`Set it in the workflow environment before running this step.\n`);
|
|
1411
|
-
process.exit(1);
|
|
1412
|
-
}
|
|
1413
|
-
const out = stringArg(args, "out");
|
|
1414
|
-
fs.writeFileSync(path.resolve(out), buildRecapClaudeMcpConfig(appUrl, token));
|
|
1415
|
-
process.stdout.write(`${JSON.stringify({ ok: true, agent, out })}\n`);
|
|
1416
|
-
return;
|
|
1417
|
-
}
|
|
1418
|
-
if (agent === "codex") {
|
|
1419
|
-
const out = optionalArg(args, "out") ??
|
|
1420
|
-
path.join(os.homedir(), ".codex", "config.toml");
|
|
1421
|
-
const absOut = path.resolve(out);
|
|
1422
|
-
fs.mkdirSync(path.dirname(absOut), { recursive: true });
|
|
1423
|
-
const newEntry = buildRecapCodexMcpConfig(appUrl);
|
|
1424
|
-
const SECTION_MARKER = "[mcp_servers.plan]";
|
|
1425
|
-
// If the file already exists and is non-empty, merge rather than overwrite.
|
|
1426
|
-
let existing = "";
|
|
1427
|
-
try {
|
|
1428
|
-
const raw = fs.readFileSync(absOut, "utf8");
|
|
1429
|
-
if (raw.trim())
|
|
1430
|
-
existing = raw;
|
|
1431
|
-
}
|
|
1432
|
-
catch {
|
|
1433
|
-
/* file absent — write fresh */
|
|
1434
|
-
}
|
|
1435
|
-
if (existing) {
|
|
1436
|
-
if (existing.includes(SECTION_MARKER)) {
|
|
1437
|
-
// Section already present — skip unless --force was passed.
|
|
1438
|
-
if (!force) {
|
|
1439
|
-
process.stdout.write(`${JSON.stringify({ ok: true, agent, out, skipped: true, reason: "plan entry already present; pass --force to overwrite" })}\n`);
|
|
1440
|
-
return;
|
|
1441
|
-
}
|
|
1442
|
-
// --force: replace the existing [mcp_servers.plan] block.
|
|
1443
|
-
// Remove lines from the section header until the next `[` header or EOF.
|
|
1444
|
-
const lines = existing.split("\n");
|
|
1445
|
-
const startIdx = lines.findIndex((l) => l.trim() === SECTION_MARKER);
|
|
1446
|
-
let endIdx = lines.length;
|
|
1447
|
-
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
1448
|
-
if (lines[i].trimStart().startsWith("[")) {
|
|
1449
|
-
endIdx = i;
|
|
1450
|
-
break;
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
const without = [
|
|
1454
|
-
...lines.slice(0, startIdx),
|
|
1455
|
-
...lines.slice(endIdx),
|
|
1456
|
-
].join("\n");
|
|
1457
|
-
const merged = (without.trimEnd() ? without.trimEnd() + "\n\n" : "") + newEntry;
|
|
1458
|
-
fs.writeFileSync(absOut, merged, { mode: 0o600 });
|
|
1459
|
-
}
|
|
1460
|
-
else {
|
|
1461
|
-
// Append the new section to the existing config.
|
|
1462
|
-
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1463
|
-
fs.writeFileSync(absOut, existing + separator + newEntry, {
|
|
1464
|
-
mode: 0o600,
|
|
1465
|
-
});
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
else {
|
|
1469
|
-
fs.writeFileSync(absOut, newEntry);
|
|
1470
|
-
}
|
|
1471
|
-
process.stdout.write(`${JSON.stringify({ ok: true, agent, out })}\n`);
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
throw new Error(`Unknown --agent "${agent}" (expected "claude" or "codex")`);
|
|
1475
|
-
}
|
|
1476
|
-
/* -------------------------------------------------------------------------- */
|
|
1477
1104
|
/* Prompt builder — repo SKILL.md + task wrapper */
|
|
1478
1105
|
/* -------------------------------------------------------------------------- */
|
|
1479
1106
|
/**
|
|
@@ -1591,6 +1218,9 @@ export function buildRecapPrompt(input) {
|
|
|
1591
1218
|
}
|
|
1592
1219
|
if (input.statPath)
|
|
1593
1220
|
lines.push(`- Diff stat: \`${input.statPath}\` (read this file)`);
|
|
1221
|
+
if (!input.localFiles) {
|
|
1222
|
+
lines.push(`- Live plan block reference: \`${input.blockReferencePath ?? "recap-blocks.md"}\` (read this before authoring; it is the workflow-fetched \`get-plan-blocks\` output for the target Plan app).`);
|
|
1223
|
+
}
|
|
1594
1224
|
if (input.huge) {
|
|
1595
1225
|
lines.push(`- The diff is LARGE — produce a **summarized** recap (top files + schema/API deltas), not an exhaustive one. The diff was truncated at the size cap — \`${input.statPath ?? "recap.stat"}\` contains the complete file list with per-file stats; for any file missing from \`${input.diffPath}\`, fetch it directly with \`git diff <base>...<head> -- <path>\`.`);
|
|
1596
1226
|
}
|
|
@@ -1603,21 +1233,18 @@ export function buildRecapPrompt(input) {
|
|
|
1603
1233
|
lines.push("3. Write the returned `url` from that command to `recap-url.txt` at the repo root, containing exactly one line. This file is the workflow's only hand-off.");
|
|
1604
1234
|
}
|
|
1605
1235
|
else {
|
|
1606
|
-
lines.push("##
|
|
1607
|
-
lines.push(`The
|
|
1608
|
-
lines.push("
|
|
1609
|
-
lines.push("
|
|
1610
|
-
lines.push(
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
? `, and also passing \`sourceUrl: "${prSourceUrl}"\` so the hosted recap page can link back to the PR`
|
|
1614
|
-
: ""}.`);
|
|
1615
|
-
lines.push("If `create-visual-recap` returns validation feedback about empty or invalid wireframes, make one immediate correction pass in this same process: revise the named WireframeBlock/Artboard MDX so each frame has real visible product text/controls, then call `create-visual-recap` again. Do not write `recap-url.txt` until the tool succeeds.");
|
|
1616
|
-
lines.push(`2. Write the plan URL to a file named \`recap-url.txt\` at the repo root, containing exactly one line: \`${appUrl}/recaps/<the returned plan id>\`. This file is the workflow's only hand-off — do not print anything else as the deliverable.`);
|
|
1617
|
-
lines.push(`3. (Fallback only — skip if step 1 succeeded) If \`create-visual-recap\` does not accept a \`visibility\` parameter (older server), call the **set-resource-visibility** tool with \`{ resourceType: "plan", resourceId: <the returned plan id>, visibility: "org" }\` after publishing.`);
|
|
1236
|
+
lines.push("## Author Source (this is the only way to produce output)");
|
|
1237
|
+
lines.push(`The workflow has already fetched the live \`get-plan-blocks\` output into \`${input.blockReferencePath ?? "recap-blocks.md"}\`. Read that file and treat it as the authoritative block/tag/schema reference for this run.`);
|
|
1238
|
+
lines.push("Do NOT call the Plan MCP server and do NOT try to publish the recap yourself. CI publishes deterministically after you write the source file, which avoids host MCP registration flake.");
|
|
1239
|
+
lines.push("This is a one-shot GitHub Actions run. Do not wait, sleep, back off, schedule wakeups, reminders, follow-ups, or retries in another turn. Either write `recap-source.json` in this process, or report why source authoring failed plainly.");
|
|
1240
|
+
lines.push("1. Author grounded MDX recap source derived ONLY from the real diff. The final file must be valid JSON, not Markdown, not prose, and not a tool-call transcript.");
|
|
1241
|
+
lines.push('2. Write a file named `recap-source.json` at the repo root with exactly this shape: `{ "title": string, "brief": string, "mdx": { "plan.mdx": string, "canvas.mdx"?: string, "prototype.mdx"?: string, ".plan-state.json"?: string, "assets/"?: { [filename: string]: string } } }`.');
|
|
1242
|
+
lines.push("3. Do not write `recap-url.txt`; the deterministic CLI publisher writes that after it successfully POSTs your source to `create-visual-recap`.");
|
|
1618
1243
|
}
|
|
1619
1244
|
lines.push("");
|
|
1620
|
-
lines.push(
|
|
1245
|
+
lines.push(input.localFiles
|
|
1246
|
+
? "Do not invent file names, schema fields, or endpoints. Redact anything that looks like a secret. If the diff has no reviewable substance, still create a minimal local recap and write recap-url.txt from the local preview command. (CI already gated tiny diffs before invoking you — ignore the skill's advice to skip small diffs; always produce output.)"
|
|
1247
|
+
: "Do not invent file names, schema fields, or endpoints. Redact anything that looks like a secret. If the diff has no reviewable substance, still write a minimal `recap-source.json`. (CI already gated tiny diffs before invoking you — ignore the skill's advice to skip small diffs; always produce output.)");
|
|
1621
1248
|
lines.push("");
|
|
1622
1249
|
lines.push("## Depth preflight");
|
|
1623
1250
|
lines.push("Before authoring the recap, read the diff/stat and make a quick surface/state inventory of changed files, routes/actions, rendered UI surfaces, popovers/dialogs, role/access states, empty/error states, and shared abstractions. The published recap must cover each meaningful item with a structured block or intentionally omit it because it is tiny, redundant, or not user-visible.");
|
|
@@ -1625,7 +1252,9 @@ export function buildRecapPrompt(input) {
|
|
|
1625
1252
|
lines.push("");
|
|
1626
1253
|
lines.push("---");
|
|
1627
1254
|
lines.push("");
|
|
1628
|
-
lines.push("# visual-recap skill
|
|
1255
|
+
lines.push("# visual-recap skill — use for recap CONTENT and structure");
|
|
1256
|
+
lines.push("");
|
|
1257
|
+
lines.push("Follow the skill below for WHAT makes a good recap: which blocks to use, grounding, house style, and review depth. IGNORE its publishing and hand-off instructions — in this run you have NO Plan MCP tools and must NOT publish the recap yourself. Publishing is handled exactly as described above (write the source file; CI publishes it deterministically).");
|
|
1629
1258
|
lines.push("");
|
|
1630
1259
|
lines.push(input.skillMd.trim());
|
|
1631
1260
|
lines.push("");
|
|
@@ -1664,6 +1293,20 @@ async function githubRequest(token, apiPath, init = {}, fetchFn = fetch) {
|
|
|
1664
1293
|
return undefined;
|
|
1665
1294
|
return (await res.json());
|
|
1666
1295
|
}
|
|
1296
|
+
export async function isPullRequestHeadCurrent(input) {
|
|
1297
|
+
const expected = input.headSha.trim();
|
|
1298
|
+
if (!expected)
|
|
1299
|
+
return null;
|
|
1300
|
+
const fn = input.fetchFn ?? fetch;
|
|
1301
|
+
try {
|
|
1302
|
+
const pr = await githubRequest(input.token, `/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(input.repo)}/pulls/${encodeURIComponent(input.issue)}`, {}, fn);
|
|
1303
|
+
const current = pr.head?.sha?.trim();
|
|
1304
|
+
return current ? current === expected : null;
|
|
1305
|
+
}
|
|
1306
|
+
catch {
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1667
1310
|
export async function findExistingComment(input) {
|
|
1668
1311
|
const fn = input.fetchFn ?? fetch;
|
|
1669
1312
|
for (let page = 1;; page += 1) {
|
|
@@ -1738,6 +1381,10 @@ function trustedRecapImageUrl(raw, base) {
|
|
|
1738
1381
|
/** Build the sticky comment body from the workflow's environment. */
|
|
1739
1382
|
export function buildCommentBody(env = process.env) {
|
|
1740
1383
|
const lines = [MARKER];
|
|
1384
|
+
const headSha = (env.HEAD_SHA || "").trim();
|
|
1385
|
+
const headMarker = /^[a-f0-9]{7,64}$/i.test(headSha)
|
|
1386
|
+
? `<!-- head-sha: ${headSha} -->`
|
|
1387
|
+
: "";
|
|
1741
1388
|
// Last-known plan id threaded from the previous run (supplied via PREV_PLAN_ID
|
|
1742
1389
|
// when the comment is rebuilt from scratch, or parsed from the env on upsert).
|
|
1743
1390
|
// We always emit the plan-id marker when any plan id is known so that a
|
|
@@ -1760,6 +1407,8 @@ export function buildCommentBody(env = process.env) {
|
|
|
1760
1407
|
lines.push(`Reason: \`${reason}\`.`);
|
|
1761
1408
|
if (prevPlanId)
|
|
1762
1409
|
lines.push("", `<!-- plan-id: ${prevPlanId} -->`);
|
|
1410
|
+
if (headMarker)
|
|
1411
|
+
lines.push("", headMarker);
|
|
1763
1412
|
return lines.join("\n");
|
|
1764
1413
|
}
|
|
1765
1414
|
// Tiny diffs aren't worth a recap. The workflow upserts this state as a sticky
|
|
@@ -1771,6 +1420,8 @@ export function buildCommentBody(env = process.env) {
|
|
|
1771
1420
|
lines.push("The change in this pull request is too small to be worth a visual recap. This is informational only and does **not** block the PR.");
|
|
1772
1421
|
if (prevPlanId)
|
|
1773
1422
|
lines.push("", `<!-- plan-id: ${prevPlanId} -->`);
|
|
1423
|
+
if (headMarker)
|
|
1424
|
+
lines.push("", headMarker);
|
|
1774
1425
|
return lines.join("\n");
|
|
1775
1426
|
}
|
|
1776
1427
|
const planUrl = (env.PLAN_URL || "").trim();
|
|
@@ -1813,6 +1464,8 @@ export function buildCommentBody(env = process.env) {
|
|
|
1813
1464
|
}
|
|
1814
1465
|
if (markerPlanId)
|
|
1815
1466
|
lines.push("", `<!-- plan-id: ${markerPlanId} -->`);
|
|
1467
|
+
if (headMarker)
|
|
1468
|
+
lines.push("", headMarker);
|
|
1816
1469
|
return lines.join("\n");
|
|
1817
1470
|
}
|
|
1818
1471
|
// Image URLs are produced by our own recap-image route, but validate each is
|
|
@@ -1840,6 +1493,8 @@ export function buildCommentBody(env = process.env) {
|
|
|
1840
1493
|
lines.push("> Large diff — this recap is a **summarized** view (top files + schema/API deltas).");
|
|
1841
1494
|
}
|
|
1842
1495
|
lines.push("", `<!-- plan-id: ${planId} -->`);
|
|
1496
|
+
if (headMarker)
|
|
1497
|
+
lines.push("", headMarker);
|
|
1843
1498
|
return lines.join("\n");
|
|
1844
1499
|
}
|
|
1845
1500
|
/* -------------------------------------------------------------------------- */
|
|
@@ -1898,6 +1553,7 @@ function runBuildPrompt(args) {
|
|
|
1898
1553
|
appUrl: optionalArg(args, "app-url") ?? "https://plan.agent-native.com",
|
|
1899
1554
|
diffPath,
|
|
1900
1555
|
statPath: optionalArg(args, "stat"),
|
|
1556
|
+
blockReferencePath: optionalArg(args, "block-reference"),
|
|
1901
1557
|
prevPlanId: optionalArg(args, "prev-plan-id"),
|
|
1902
1558
|
huge: args.huge === true || args.huge === "true",
|
|
1903
1559
|
localFiles: args["local-files"] === true || args["local-files"] === "true",
|
|
@@ -1910,6 +1566,278 @@ function runBuildPrompt(args) {
|
|
|
1910
1566
|
fs.writeFileSync(path.resolve(out), prompt);
|
|
1911
1567
|
process.stdout.write(`${JSON.stringify({ ok: true, out, skillSource: skill.source, bytes: prompt.length })}\n`);
|
|
1912
1568
|
}
|
|
1569
|
+
const RECAP_SOURCE_FILENAME = "recap-source.json";
|
|
1570
|
+
const RECAP_URL_REASON_FILENAME = "recap-url-reason.txt";
|
|
1571
|
+
const RECAP_HTTP_TIMEOUT_MS = 45_000;
|
|
1572
|
+
function writeRecapUrlReason(reason, cwd = process.cwd()) {
|
|
1573
|
+
fs.writeFileSync(path.join(cwd, RECAP_URL_REASON_FILENAME), `${sanitizeAgentFailureSummary(reason, 1000)}\n`);
|
|
1574
|
+
}
|
|
1575
|
+
function readRecapUrlReason(cwd = process.cwd()) {
|
|
1576
|
+
return readTextIfExists(path.join(cwd, RECAP_URL_REASON_FILENAME));
|
|
1577
|
+
}
|
|
1578
|
+
function validateRecapSourcePayload(value) {
|
|
1579
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1580
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} must contain a JSON object.`);
|
|
1581
|
+
}
|
|
1582
|
+
const obj = value;
|
|
1583
|
+
if (obj.title !== undefined && typeof obj.title !== "string") {
|
|
1584
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} title must be a string.`);
|
|
1585
|
+
}
|
|
1586
|
+
if (obj.brief !== undefined && typeof obj.brief !== "string") {
|
|
1587
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} brief must be a string.`);
|
|
1588
|
+
}
|
|
1589
|
+
if (!obj.mdx || typeof obj.mdx !== "object" || Array.isArray(obj.mdx)) {
|
|
1590
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} must include an mdx object.`);
|
|
1591
|
+
}
|
|
1592
|
+
const mdx = obj.mdx;
|
|
1593
|
+
if (typeof mdx["plan.mdx"] !== "string" || !mdx["plan.mdx"].trim()) {
|
|
1594
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} mdx["plan.mdx"] must be a non-empty string.`);
|
|
1595
|
+
}
|
|
1596
|
+
for (const key of ["canvas.mdx", "prototype.mdx", ".plan-state.json"]) {
|
|
1597
|
+
if (mdx[key] !== undefined && typeof mdx[key] !== "string") {
|
|
1598
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} mdx["${key}"] must be a string when present.`);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
const assets = mdx["assets/"];
|
|
1602
|
+
if (assets !== undefined) {
|
|
1603
|
+
if (!assets || typeof assets !== "object" || Array.isArray(assets)) {
|
|
1604
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} mdx["assets/"] must be an object when present.`);
|
|
1605
|
+
}
|
|
1606
|
+
for (const [name, body] of Object.entries(assets)) {
|
|
1607
|
+
if (typeof body !== "string") {
|
|
1608
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} asset ${JSON.stringify(name)} must be a string.`);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
return {
|
|
1613
|
+
...(typeof obj.title === "string" ? { title: obj.title } : {}),
|
|
1614
|
+
...(typeof obj.brief === "string" ? { brief: obj.brief } : {}),
|
|
1615
|
+
mdx,
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
export function readRecapSourcePayload(filePath = RECAP_SOURCE_FILENAME) {
|
|
1619
|
+
const abs = path.resolve(filePath);
|
|
1620
|
+
let text;
|
|
1621
|
+
try {
|
|
1622
|
+
text = fs.readFileSync(abs, "utf8");
|
|
1623
|
+
}
|
|
1624
|
+
catch (err) {
|
|
1625
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} was not created by the agent (${String(err)}).`);
|
|
1626
|
+
}
|
|
1627
|
+
let parsed;
|
|
1628
|
+
try {
|
|
1629
|
+
parsed = JSON.parse(text);
|
|
1630
|
+
}
|
|
1631
|
+
catch (err) {
|
|
1632
|
+
throw new Error(`${RECAP_SOURCE_FILENAME} was not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
1633
|
+
}
|
|
1634
|
+
return validateRecapSourcePayload(parsed);
|
|
1635
|
+
}
|
|
1636
|
+
function recapActionEndpoint(appUrl, action) {
|
|
1637
|
+
return `${appUrl.replace(/\/$/, "")}/_agent-native/actions/${action}`;
|
|
1638
|
+
}
|
|
1639
|
+
async function fetchJsonWithTimeout(url, init, fetchFn) {
|
|
1640
|
+
return await fetchFn(url, {
|
|
1641
|
+
...init,
|
|
1642
|
+
signal: init.signal ?? AbortSignal.timeout(RECAP_HTTP_TIMEOUT_MS),
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
export async function fetchRecapBlockReference(input) {
|
|
1646
|
+
const fetchFn = input.fetchFn ?? fetch;
|
|
1647
|
+
const out = input.out ?? "recap-blocks.md";
|
|
1648
|
+
const endpoint = new URL(recapActionEndpoint(input.appUrl, "get-plan-blocks"));
|
|
1649
|
+
endpoint.searchParams.set("format", "reference");
|
|
1650
|
+
const response = await fetchJsonWithTimeout(endpoint.toString(), { method: "GET", headers: { accept: "application/json" } }, fetchFn);
|
|
1651
|
+
if (!response.ok) {
|
|
1652
|
+
const detail = await response.text().catch(() => "");
|
|
1653
|
+
throw new Error(`get-plan-blocks failed ${response.status} ${response.statusText}: ${sanitizeAgentFailureSummary(detail, 500)}`);
|
|
1654
|
+
}
|
|
1655
|
+
const json = (await response.json().catch(() => null));
|
|
1656
|
+
if (!json?.reference) {
|
|
1657
|
+
throw new Error("get-plan-blocks returned no reference text.");
|
|
1658
|
+
}
|
|
1659
|
+
fs.writeFileSync(path.resolve(out), json.reference);
|
|
1660
|
+
return { ok: true, out, count: json.count };
|
|
1661
|
+
}
|
|
1662
|
+
function recapUrlFromPublishResult(result, appUrl) {
|
|
1663
|
+
const candidates = [];
|
|
1664
|
+
const ids = [];
|
|
1665
|
+
const visit = (value, depth = 0) => {
|
|
1666
|
+
if (!value || typeof value !== "object" || depth > 3)
|
|
1667
|
+
return;
|
|
1668
|
+
const obj = value;
|
|
1669
|
+
for (const key of ["webUrl", "url", "path", "href"]) {
|
|
1670
|
+
const candidate = obj[key];
|
|
1671
|
+
if (typeof candidate === "string")
|
|
1672
|
+
candidates.push(candidate);
|
|
1673
|
+
}
|
|
1674
|
+
for (const key of ["planId", "id"]) {
|
|
1675
|
+
const candidate = obj[key];
|
|
1676
|
+
if (typeof candidate === "string" &&
|
|
1677
|
+
/^[A-Za-z0-9_-]{1,80}$/.test(candidate)) {
|
|
1678
|
+
ids.push(candidate);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
for (const key of ["plan", "openLink", "link", "result"]) {
|
|
1682
|
+
visit(obj[key], depth + 1);
|
|
1683
|
+
}
|
|
1684
|
+
};
|
|
1685
|
+
visit(result);
|
|
1686
|
+
for (const candidate of candidates) {
|
|
1687
|
+
const canonical = canonicalRecapUrl(candidate, appUrl);
|
|
1688
|
+
if (canonical)
|
|
1689
|
+
return canonical;
|
|
1690
|
+
}
|
|
1691
|
+
for (const id of ids) {
|
|
1692
|
+
const canonical = canonicalRecapUrl(`/recaps/${id}`, appUrl);
|
|
1693
|
+
if (canonical)
|
|
1694
|
+
return canonical;
|
|
1695
|
+
}
|
|
1696
|
+
return "";
|
|
1697
|
+
}
|
|
1698
|
+
function shouldRetryRecapPublish(status) {
|
|
1699
|
+
return (status === 408 ||
|
|
1700
|
+
status === 409 ||
|
|
1701
|
+
status === 425 ||
|
|
1702
|
+
status === 429 ||
|
|
1703
|
+
status >= 500);
|
|
1704
|
+
}
|
|
1705
|
+
export async function publishRecapSource(input) {
|
|
1706
|
+
const cwd = input.cwd ?? process.cwd();
|
|
1707
|
+
const sourcePath = input.sourcePath ?? path.join(cwd, RECAP_SOURCE_FILENAME);
|
|
1708
|
+
const out = input.out ?? path.join(cwd, "recap-url.txt");
|
|
1709
|
+
const token = input.token.trim();
|
|
1710
|
+
if (!token)
|
|
1711
|
+
throw new Error("PLAN_RECAP_TOKEN is empty.");
|
|
1712
|
+
const source = readRecapSourcePayload(sourcePath);
|
|
1713
|
+
const sourceUrl = input.sourceUrl ??
|
|
1714
|
+
(input.repo && input.pr
|
|
1715
|
+
? `https://github.com/${input.repo}/pull/${input.pr}`
|
|
1716
|
+
: undefined);
|
|
1717
|
+
const body = {
|
|
1718
|
+
...(input.prevPlanId ? { planId: input.prevPlanId } : {}),
|
|
1719
|
+
...(source.title ? { title: source.title } : {}),
|
|
1720
|
+
...(source.brief ? { brief: source.brief } : {}),
|
|
1721
|
+
visibility: "org",
|
|
1722
|
+
source: "imported",
|
|
1723
|
+
...(input.repo ? { repoPath: input.repo } : {}),
|
|
1724
|
+
...(sourceUrl ? { sourceUrl } : {}),
|
|
1725
|
+
currentFocus: "visual recap review",
|
|
1726
|
+
status: "review",
|
|
1727
|
+
mdx: source.mdx,
|
|
1728
|
+
};
|
|
1729
|
+
const endpoint = recapActionEndpoint(input.appUrl, "create-visual-recap");
|
|
1730
|
+
const fetchFn = input.fetchFn ?? fetch;
|
|
1731
|
+
let lastError = "";
|
|
1732
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
1733
|
+
try {
|
|
1734
|
+
const response = await fetchJsonWithTimeout(endpoint, {
|
|
1735
|
+
method: "POST",
|
|
1736
|
+
headers: {
|
|
1737
|
+
accept: "application/json",
|
|
1738
|
+
"content-type": "application/json",
|
|
1739
|
+
authorization: `Bearer ${token}`,
|
|
1740
|
+
},
|
|
1741
|
+
body: JSON.stringify(body),
|
|
1742
|
+
}, fetchFn);
|
|
1743
|
+
const text = await response.text().catch((err) => String(err));
|
|
1744
|
+
if (!response.ok) {
|
|
1745
|
+
lastError = `create-visual-recap failed ${response.status} ${response.statusText}: ${sanitizeAgentFailureSummary(text, 800)}`;
|
|
1746
|
+
if (attempt < 3 && shouldRetryRecapPublish(response.status)) {
|
|
1747
|
+
await delay(attempt * 2000);
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
throw new Error(lastError);
|
|
1751
|
+
}
|
|
1752
|
+
let result = null;
|
|
1753
|
+
try {
|
|
1754
|
+
result = text ? JSON.parse(text) : null;
|
|
1755
|
+
}
|
|
1756
|
+
catch {
|
|
1757
|
+
throw new Error("create-visual-recap returned non-JSON output.");
|
|
1758
|
+
}
|
|
1759
|
+
const url = recapUrlFromPublishResult(result, input.appUrl);
|
|
1760
|
+
if (!url) {
|
|
1761
|
+
throw new Error("create-visual-recap succeeded but did not return a usable /recaps/<id> URL or plan id.");
|
|
1762
|
+
}
|
|
1763
|
+
fs.writeFileSync(path.resolve(out), `${url}\n`);
|
|
1764
|
+
try {
|
|
1765
|
+
fs.rmSync(path.join(cwd, RECAP_URL_REASON_FILENAME), { force: true });
|
|
1766
|
+
}
|
|
1767
|
+
catch {
|
|
1768
|
+
/* ignore */
|
|
1769
|
+
}
|
|
1770
|
+
return { ok: true, url, out };
|
|
1771
|
+
}
|
|
1772
|
+
catch (err) {
|
|
1773
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
1774
|
+
if (attempt < 3 &&
|
|
1775
|
+
/fetch failed|network|timeout|timed out|ECONNRESET|ETIMEDOUT/i.test(lastError)) {
|
|
1776
|
+
await delay(attempt * 2000);
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
throw new Error(lastError);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
throw new Error(lastError || "create-visual-recap failed.");
|
|
1783
|
+
}
|
|
1784
|
+
async function runBlockReference(args) {
|
|
1785
|
+
const appUrl = optionalArg(args, "app-url") ??
|
|
1786
|
+
process.env.PLAN_RECAP_APP_URL ??
|
|
1787
|
+
DEFAULT_RECAP_APP_URL;
|
|
1788
|
+
const out = optionalArg(args, "out") ?? "recap-blocks.md";
|
|
1789
|
+
try {
|
|
1790
|
+
const result = await fetchRecapBlockReference({ appUrl, out });
|
|
1791
|
+
writeGitHubOutput("ok", "true");
|
|
1792
|
+
writeGitHubOutput("out", result.out);
|
|
1793
|
+
writeGitHubOutput("reason", "");
|
|
1794
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
1795
|
+
}
|
|
1796
|
+
catch (err) {
|
|
1797
|
+
const reason = sanitizeAgentFailureSummary(err instanceof Error ? err.message : String(err), 1000);
|
|
1798
|
+
writeRecapUrlReason(reason);
|
|
1799
|
+
writeGitHubOutput("ok", "false");
|
|
1800
|
+
writeGitHubOutput("out", "");
|
|
1801
|
+
writeGitHubOutput("reason", reason);
|
|
1802
|
+
process.stdout.write(`${JSON.stringify({ ok: false, reason })}\n`);
|
|
1803
|
+
process.exitCode = 1;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
async function runPublish(args) {
|
|
1807
|
+
const appUrl = optionalArg(args, "app-url") ??
|
|
1808
|
+
process.env.PLAN_RECAP_APP_URL ??
|
|
1809
|
+
DEFAULT_RECAP_APP_URL;
|
|
1810
|
+
const token = optionalArg(args, "token") ?? process.env.PLAN_RECAP_TOKEN ?? "";
|
|
1811
|
+
const out = optionalArg(args, "out") ?? "recap-url.txt";
|
|
1812
|
+
const done = (obj) => {
|
|
1813
|
+
process.stdout.write(`${JSON.stringify(obj)}\n`);
|
|
1814
|
+
};
|
|
1815
|
+
try {
|
|
1816
|
+
const result = await publishRecapSource({
|
|
1817
|
+
appUrl,
|
|
1818
|
+
token,
|
|
1819
|
+
sourcePath: optionalArg(args, "source") ?? RECAP_SOURCE_FILENAME,
|
|
1820
|
+
out,
|
|
1821
|
+
prevPlanId: optionalArg(args, "prev-plan-id"),
|
|
1822
|
+
repo: optionalArg(args, "repo") ?? process.env.GITHUB_REPOSITORY,
|
|
1823
|
+
pr: optionalArg(args, "pr") ?? process.env.PR_NUMBER,
|
|
1824
|
+
sourceUrl: optionalArg(args, "source-url"),
|
|
1825
|
+
});
|
|
1826
|
+
writeGitHubOutput("ok", "true");
|
|
1827
|
+
writeGitHubOutput("plan_url", result.url);
|
|
1828
|
+
writeGitHubOutput("reason", "");
|
|
1829
|
+
done(result);
|
|
1830
|
+
}
|
|
1831
|
+
catch (err) {
|
|
1832
|
+
const reason = sanitizeAgentFailureSummary(err instanceof Error ? err.message : String(err), 1000);
|
|
1833
|
+
writeRecapUrlReason(reason);
|
|
1834
|
+
writeGitHubOutput("ok", "false");
|
|
1835
|
+
writeGitHubOutput("plan_url", "");
|
|
1836
|
+
writeGitHubOutput("reason", reason);
|
|
1837
|
+
done({ ok: false, reason });
|
|
1838
|
+
process.exitCode = 1;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1913
1841
|
function delay(ms) {
|
|
1914
1842
|
return ms > 0
|
|
1915
1843
|
? new Promise((resolve) => setTimeout(resolve, ms))
|
|
@@ -2012,6 +1940,47 @@ async function defaultImportPlaywright() {
|
|
|
2012
1940
|
return (await import("@playwright/test"));
|
|
2013
1941
|
}
|
|
2014
1942
|
}
|
|
1943
|
+
const RECAP_SYSTEM_CHROME_EXECUTABLES = [
|
|
1944
|
+
"/usr/bin/google-chrome-stable",
|
|
1945
|
+
"/usr/bin/google-chrome",
|
|
1946
|
+
"/usr/bin/chromium-browser",
|
|
1947
|
+
"/usr/bin/chromium",
|
|
1948
|
+
];
|
|
1949
|
+
function shouldTrySystemChromeFallback(err) {
|
|
1950
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1951
|
+
return /Executable doesn't exist|playwright install|browser.*not found|chromium.*not found/i.test(message);
|
|
1952
|
+
}
|
|
1953
|
+
export async function launchRecapChromium(chromium) {
|
|
1954
|
+
const launchOptions = { args: ["--no-sandbox"] };
|
|
1955
|
+
try {
|
|
1956
|
+
return await chromium.launch(launchOptions);
|
|
1957
|
+
}
|
|
1958
|
+
catch (err) {
|
|
1959
|
+
if (!shouldTrySystemChromeFallback(err))
|
|
1960
|
+
throw err;
|
|
1961
|
+
const fallbackErrors = [];
|
|
1962
|
+
for (const executablePath of RECAP_SYSTEM_CHROME_EXECUTABLES) {
|
|
1963
|
+
if (!fs.existsSync(executablePath))
|
|
1964
|
+
continue;
|
|
1965
|
+
try {
|
|
1966
|
+
process.stderr.write(`[recap shot] Playwright browser unavailable; trying system Chrome at ${executablePath}\n`);
|
|
1967
|
+
return await chromium.launch({ ...launchOptions, executablePath });
|
|
1968
|
+
}
|
|
1969
|
+
catch (fallbackErr) {
|
|
1970
|
+
const message = fallbackErr instanceof Error
|
|
1971
|
+
? fallbackErr.message
|
|
1972
|
+
: String(fallbackErr);
|
|
1973
|
+
fallbackErrors.push(`${executablePath}: ${message}`);
|
|
1974
|
+
process.stderr.write(`[recap shot] system Chrome launch failed at ${executablePath}: ${message}\n`);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (fallbackErrors.length) {
|
|
1978
|
+
const originalMessage = err instanceof Error ? err.message : String(err);
|
|
1979
|
+
throw new Error(`${originalMessage}; system Chrome fallback failed (${fallbackErrors.join("; ")})`, { cause: err });
|
|
1980
|
+
}
|
|
1981
|
+
throw err;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
2015
1984
|
function parseRecapScreenshotTheme(value) {
|
|
2016
1985
|
if (value === undefined)
|
|
2017
1986
|
return undefined;
|
|
@@ -2080,13 +2049,14 @@ importPlaywright = defaultImportPlaywright) {
|
|
|
2080
2049
|
return;
|
|
2081
2050
|
}
|
|
2082
2051
|
let captured = false;
|
|
2052
|
+
let reason = "";
|
|
2083
2053
|
let browser;
|
|
2084
2054
|
const hardTimer = setTimeout(() => {
|
|
2085
2055
|
done({ ok: false, reason: "hard 60s timeout reached" });
|
|
2086
2056
|
process.exit(0);
|
|
2087
2057
|
}, 60_000);
|
|
2088
2058
|
try {
|
|
2089
|
-
browser = await chromium
|
|
2059
|
+
browser = await launchRecapChromium(chromium);
|
|
2090
2060
|
const context = await browser.newContext({
|
|
2091
2061
|
viewport: RECAP_SHOT_VIEWPORT,
|
|
2092
2062
|
deviceScaleFactor: RECAP_SHOT_DEVICE_SCALE_FACTOR,
|
|
@@ -2211,12 +2181,20 @@ importPlaywright = defaultImportPlaywright) {
|
|
|
2211
2181
|
});
|
|
2212
2182
|
await page.waitForTimeout(250);
|
|
2213
2183
|
await page.screenshot({ path: out });
|
|
2214
|
-
// If the captured PNG is over the upload cap,
|
|
2215
|
-
//
|
|
2184
|
+
// If the captured PNG is over the upload cap, retry at CSS-pixel scale
|
|
2185
|
+
// before giving up. The server route rejects oversized files, and the
|
|
2186
|
+
// GitHub comment can only embed an image after a successful upload.
|
|
2216
2187
|
const firstSize = fs.existsSync(out) ? fs.statSync(out).size : 0;
|
|
2217
2188
|
if (firstSize > RECAP_SHOT_MAX_BYTES) {
|
|
2218
|
-
process.stderr.write(`[recap shot] PNG is ${firstSize} bytes (cap ${RECAP_SHOT_MAX_BYTES}) —
|
|
2189
|
+
process.stderr.write(`[recap shot] PNG is ${firstSize} bytes (cap ${RECAP_SHOT_MAX_BYTES}) — retrying at CSS-pixel scale\n`);
|
|
2219
2190
|
fs.unlinkSync(out);
|
|
2191
|
+
await page.screenshot({ path: out, scale: "css" });
|
|
2192
|
+
const retrySize = fs.existsSync(out) ? fs.statSync(out).size : 0;
|
|
2193
|
+
if (retrySize > RECAP_SHOT_MAX_BYTES) {
|
|
2194
|
+
reason = `screenshot PNG exceeded upload cap (${retrySize} bytes > ${RECAP_SHOT_MAX_BYTES})`;
|
|
2195
|
+
process.stderr.write(`[recap shot] ${reason}; skipping upload\n`);
|
|
2196
|
+
fs.unlinkSync(out);
|
|
2197
|
+
}
|
|
2220
2198
|
}
|
|
2221
2199
|
captured = fs.existsSync(out);
|
|
2222
2200
|
await browser.close();
|
|
@@ -2240,8 +2218,12 @@ importPlaywright = defaultImportPlaywright) {
|
|
|
2240
2218
|
let imageUrl = null;
|
|
2241
2219
|
if (captured && token && appUrl) {
|
|
2242
2220
|
imageUrl = await uploadRecapImage({ appUrl, token, pngPath: out });
|
|
2221
|
+
if (!imageUrl) {
|
|
2222
|
+
reason = "screenshot captured but image upload failed";
|
|
2223
|
+
}
|
|
2243
2224
|
}
|
|
2244
|
-
|
|
2225
|
+
const ok = captured && (!(token && appUrl) || !!imageUrl);
|
|
2226
|
+
done({ ok, out, imageUrl, ...(reason ? { reason } : {}) });
|
|
2245
2227
|
}
|
|
2246
2228
|
async function runComment(args, sub) {
|
|
2247
2229
|
const token = stringArg(args, "token");
|
|
@@ -2259,6 +2241,24 @@ async function runComment(args, sub) {
|
|
|
2259
2241
|
return;
|
|
2260
2242
|
}
|
|
2261
2243
|
if (sub === "upsert") {
|
|
2244
|
+
const headSha = optionalArg(args, "head-sha") ?? process.env.HEAD_SHA ?? "";
|
|
2245
|
+
if (headSha) {
|
|
2246
|
+
const current = await isPullRequestHeadCurrent({
|
|
2247
|
+
token,
|
|
2248
|
+
owner,
|
|
2249
|
+
repo,
|
|
2250
|
+
issue,
|
|
2251
|
+
headSha,
|
|
2252
|
+
});
|
|
2253
|
+
if (current === false) {
|
|
2254
|
+
process.stdout.write(`${JSON.stringify({
|
|
2255
|
+
action: "skipped",
|
|
2256
|
+
id: 0,
|
|
2257
|
+
reason: "stale head sha",
|
|
2258
|
+
})}\n`);
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
2262
|
const result = await upsertComment({
|
|
2263
2263
|
token,
|
|
2264
2264
|
owner,
|
|
@@ -2617,13 +2617,16 @@ export function canonicalRecapUrl(rawUrl, appUrl) {
|
|
|
2617
2617
|
}
|
|
2618
2618
|
}
|
|
2619
2619
|
export function inferLocalRecapUrlFailureReason(input = {}) {
|
|
2620
|
-
const
|
|
2620
|
+
const cwd = input.cwd ?? process.cwd();
|
|
2621
|
+
const explicitReason = readRecapUrlReason(cwd);
|
|
2622
|
+
const recapUrlPath = path.join(cwd, "recap-url.txt");
|
|
2621
2623
|
const raw = readTextIfExists(recapUrlPath);
|
|
2622
|
-
if (raw === null)
|
|
2623
|
-
return "recap-url.txt was not created
|
|
2624
|
+
if (raw === null) {
|
|
2625
|
+
return explicitReason?.trim() || "recap-url.txt was not created.";
|
|
2626
|
+
}
|
|
2624
2627
|
const value = raw.replace(/[\r\n\s]/g, "");
|
|
2625
2628
|
if (!value)
|
|
2626
|
-
return "recap-url.txt was empty.";
|
|
2629
|
+
return explicitReason?.trim() || "recap-url.txt was empty.";
|
|
2627
2630
|
const appUrl = input.appUrl ||
|
|
2628
2631
|
process.env.PLAN_RECAP_APP_URL ||
|
|
2629
2632
|
"https://plan.agent-native.com";
|
|
@@ -2637,10 +2640,12 @@ export function inferLocalRecapUrlFailureReason(input = {}) {
|
|
|
2637
2640
|
if (parsed.origin !== trusted.origin) {
|
|
2638
2641
|
return `recap-url.txt points at ${parsed.origin}, expected ${trusted.origin}.`;
|
|
2639
2642
|
}
|
|
2640
|
-
return
|
|
2643
|
+
return (explicitReason?.trim() ||
|
|
2644
|
+
"recap-url.txt did not contain a valid /plans/<id> or /recaps/<id> URL for the configured plan app.");
|
|
2641
2645
|
}
|
|
2642
2646
|
catch {
|
|
2643
|
-
return
|
|
2647
|
+
return (explicitReason?.trim() ||
|
|
2648
|
+
"recap-url.txt was not a valid URL or recap path.");
|
|
2644
2649
|
}
|
|
2645
2650
|
}
|
|
2646
2651
|
export function buildRecapFailureDiagnostic(input) {
|
|
@@ -3063,10 +3068,10 @@ Usage:
|
|
|
3063
3068
|
npx @agent-native/core@latest recap setup [--repo owner/name] [--agent claude|codex] [--app-url <url>] [--skip-secrets] [--dry-run] [--force]
|
|
3064
3069
|
npx @agent-native/core@latest recap doctor [--repo owner/name] [--agent claude|codex] [--app-url <url>]
|
|
3065
3070
|
npx @agent-native/core@latest recap collect-diff --base <baseSha> --head <headSha> [--out recap.diff] [--stat recap.stat]
|
|
3066
|
-
npx @agent-native/core@latest recap
|
|
3067
|
-
npx @agent-native/core@latest recap mcp-smoke [--app-url <url>] [--token <planToken>]
|
|
3071
|
+
npx @agent-native/core@latest recap block-reference [--app-url <url>] [--out recap-blocks.md]
|
|
3068
3072
|
npx @agent-native/core@latest recap scan --diff <path> [--mode off|high-confidence|strict]
|
|
3069
|
-
npx @agent-native/core@latest recap build-prompt --pr <n> [--repo owner/name] [--head <sha>] [--app-url <url>] [--diff <path>] [--stat <path>] [--prev-plan-id <id>] [--huge] [--local-files] [--local-dir <folder>] [--skill-source auto|latest|repo] [--out <path>]
|
|
3073
|
+
npx @agent-native/core@latest recap build-prompt --pr <n> [--repo owner/name] [--head <sha>] [--app-url <url>] [--diff <path>] [--stat <path>] [--block-reference recap-blocks.md] [--prev-plan-id <id>] [--huge] [--local-files] [--local-dir <folder>] [--skill-source auto|latest|repo] [--out <path>]
|
|
3074
|
+
npx @agent-native/core@latest recap publish [--source recap-source.json] [--out recap-url.txt] [--repo owner/name] [--pr <n>] [--prev-plan-id <id>] [--app-url <url>] [--token <planToken>]
|
|
3070
3075
|
npx @agent-native/core@latest recap shot --url <planUrl> [--token <planToken>] [--app-url <url>] [--out recap.png] [--theme light|dark]
|
|
3071
3076
|
npx @agent-native/core@latest recap usage --plan-url <planUrl> --result-file <path> --app-url <url> --token <planToken> [--agent claude|codex] [--model <id>]
|
|
3072
3077
|
npx @agent-native/core@latest recap agent-summary --result-file <path> [--stderr-file <path>] [--exit-code-file <path>] [--agent claude|codex]
|
|
@@ -3103,11 +3108,12 @@ Usage:
|
|
|
3103
3108
|
shapes such as private key blocks and known provider token prefixes. Set
|
|
3104
3109
|
VISUAL_RECAP_SECRET_SCAN=strict, or pass --mode strict, to restore generic
|
|
3105
3110
|
TOKEN/SECRET assignment suppression; set off to disable this preflight.
|
|
3106
|
-
npx @agent-native/core@latest recap
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
+
npx @agent-native/core@latest recap block-reference
|
|
3112
|
+
Fetch the target Plan app's live get-plan-blocks reference over the public
|
|
3113
|
+
action route and write it to recap-blocks.md for the CI agent to read.
|
|
3114
|
+
npx @agent-native/core@latest recap publish
|
|
3115
|
+
Validate recap-source.json from the CI agent, publish it by POSTing the
|
|
3116
|
+
authenticated create-visual-recap action, and write recap-url.txt.
|
|
3111
3117
|
npx @agent-native/core@latest recap setup
|
|
3112
3118
|
Write/refresh .github/workflows/pr-visual-recap.yml, then configure GitHub
|
|
3113
3119
|
Actions secrets and variables with gh when values are available from env or
|
|
@@ -3130,11 +3136,8 @@ export async function runRecap(argv) {
|
|
|
3130
3136
|
case "collect-diff":
|
|
3131
3137
|
runCollectDiff(args);
|
|
3132
3138
|
return;
|
|
3133
|
-
case "
|
|
3134
|
-
|
|
3135
|
-
return;
|
|
3136
|
-
case "mcp-smoke":
|
|
3137
|
-
await runMcpSmoke(args);
|
|
3139
|
+
case "block-reference":
|
|
3140
|
+
await runBlockReference(args);
|
|
3138
3141
|
return;
|
|
3139
3142
|
case "scan":
|
|
3140
3143
|
runScan(args);
|
|
@@ -3142,6 +3145,9 @@ export async function runRecap(argv) {
|
|
|
3142
3145
|
case "build-prompt":
|
|
3143
3146
|
runBuildPrompt(args);
|
|
3144
3147
|
return;
|
|
3148
|
+
case "publish":
|
|
3149
|
+
await runPublish(args);
|
|
3150
|
+
return;
|
|
3145
3151
|
case "shot":
|
|
3146
3152
|
await runShot(args);
|
|
3147
3153
|
return;
|