@muggleai/works 5.30.0-staging.264 → 5.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.codex-mcp.json +1 -1
- package/dist/plugin/.codex-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/dist/plugin/.mcp.json +1 -1
- package/dist/plugin/scripts/guardrails.mjs +118 -2
- package/dist/release-manifest.json +3 -3
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-mcp.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/scripts/guardrails.mjs +118 -2
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"name": "muggleai",
|
|
10
10
|
"source": "./plugin",
|
|
11
11
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
12
|
-
"version": "5.30.0
|
|
12
|
+
"version": "5.30.0"
|
|
13
13
|
}
|
|
14
14
|
]
|
|
15
15
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
4
|
-
"version": "5.30.0
|
|
4
|
+
"version": "5.30.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
|
-
"version": "5.30.0
|
|
3
|
+
"version": "5.30.0",
|
|
4
4
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
5
|
-
"version": "5.30.0
|
|
5
|
+
"version": "5.30.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
package/dist/plugin/.mcp.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync, execFileSync, spawn } from 'child_process';
|
|
2
|
+
import { readFileSync, rmSync, existsSync, readdirSync, mkdirSync, openSync, fstatSync, readSync, closeSync, writeFileSync, renameSync, statSync, writeSync, unlinkSync } from 'fs';
|
|
2
3
|
import { join, isAbsolute, resolve, basename, dirname } from 'path';
|
|
3
4
|
import { homedir, tmpdir, hostname } from 'os';
|
|
4
|
-
import { execFileSync, spawn } from 'child_process';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
7
|
|
|
@@ -131,6 +131,12 @@ var REWAKE_ANCHOR_ROLE_BY_EVENT = {
|
|
|
131
131
|
Stop: "assistant",
|
|
132
132
|
UserPromptSubmit: "user"
|
|
133
133
|
};
|
|
134
|
+
var RECONCILED_HOOK_EVENT = "PostToolUse";
|
|
135
|
+
var RECONCILED_RECORD_SUFFIX = ".reconciled-record";
|
|
136
|
+
var RECONCILE_REPLAY_TIMEOUT_MS = 15e3;
|
|
137
|
+
var GUARDRAIL_SUBCOMMAND_ASSIGNMENT = /^GUARDRAIL_SUBCOMMAND="([\w-]+)"/m;
|
|
138
|
+
var HOOK_COMMAND_SCRIPTS = /^set -- ([^;]+);/;
|
|
139
|
+
var GUARDRAIL_DISPATCH_SCRIPT = "guardrail-dispatch.sh";
|
|
134
140
|
|
|
135
141
|
// src/guardrails/store/fileLock.ts
|
|
136
142
|
function isProcessAlive(pid) {
|
|
@@ -280,6 +286,20 @@ function takeSyncHookOutput(sessionId2, hookEventName, payloadDigest2, dirOverri
|
|
|
280
286
|
return null;
|
|
281
287
|
}
|
|
282
288
|
}
|
|
289
|
+
function readReconciledRecord(sessionId2, dirOverride) {
|
|
290
|
+
try {
|
|
291
|
+
return readFileSync(fileFor(sessionId2, dirOverride, RECONCILED_RECORD_SUFFIX), "utf-8").trim() || void 0;
|
|
292
|
+
} catch {
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function writeReconciledRecord(sessionId2, recordUuid, dirOverride) {
|
|
297
|
+
mkdirSync(baseDir(dirOverride), { recursive: true });
|
|
298
|
+
const target = fileFor(sessionId2, dirOverride, RECONCILED_RECORD_SUFFIX);
|
|
299
|
+
const staging = `${target}.${process.pid}.tmp`;
|
|
300
|
+
writeFileSync(staging, recordUuid);
|
|
301
|
+
renameSync(staging, target);
|
|
302
|
+
}
|
|
283
303
|
var lockFor = (sessionId2, dirOverride) => `${fileFor(sessionId2, dirOverride)}.lock`;
|
|
284
304
|
function commitUnderHeldLock(sessionId2, mutate, dirOverride) {
|
|
285
305
|
const current = readState(sessionId2, dirOverride);
|
|
@@ -2923,6 +2943,75 @@ function rewakeMessage(hookEventName, hookOutput) {
|
|
|
2923
2943
|
${deliveredText}`;
|
|
2924
2944
|
}
|
|
2925
2945
|
|
|
2946
|
+
// src/guardrails/reconcile.ts
|
|
2947
|
+
function parseTranscriptLines(transcriptTail) {
|
|
2948
|
+
return transcriptTail.split("\n").flatMap((line) => {
|
|
2949
|
+
try {
|
|
2950
|
+
return [JSON.parse(line)];
|
|
2951
|
+
} catch {
|
|
2952
|
+
return [];
|
|
2953
|
+
}
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
function contentBlocks(line) {
|
|
2957
|
+
const content = line.message?.content;
|
|
2958
|
+
return Array.isArray(content) ? content : [];
|
|
2959
|
+
}
|
|
2960
|
+
function toolResponseFor(line, block) {
|
|
2961
|
+
if (line.toolUseResult && typeof line.toolUseResult === "object") return line.toolUseResult;
|
|
2962
|
+
if (typeof block.content === "string") return { output: block.content };
|
|
2963
|
+
return { content: block.content };
|
|
2964
|
+
}
|
|
2965
|
+
function isCancelledSyncRecorderRecord(record) {
|
|
2966
|
+
if (record?.type !== TRANSCRIPT_HOOK_CANCELLED_TYPE || record.hookEvent !== RECONCILED_HOOK_EVENT) return false;
|
|
2967
|
+
const command = record.command ?? "";
|
|
2968
|
+
return command.includes(GUARDRAIL_HOOK_COMMAND_MARKER) && !command.includes(REWAKE_HOOK_SCRIPT);
|
|
2969
|
+
}
|
|
2970
|
+
function findCancelledRecorderEvents(transcriptTail, sessionInput, afterRecordUuid) {
|
|
2971
|
+
const lines = parseTranscriptLines(transcriptTail);
|
|
2972
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
2973
|
+
const toolResults = /* @__PURE__ */ new Map();
|
|
2974
|
+
for (const line of lines) {
|
|
2975
|
+
for (const block of contentBlocks(line)) {
|
|
2976
|
+
if (block.type === "tool_use" && block.id) toolCalls.set(block.id, { name: block.name, input: block.input });
|
|
2977
|
+
if (block.type === "tool_result" && block.tool_use_id) toolResults.set(block.tool_use_id, { line, block });
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const watermarkIndex = afterRecordUuid ? lines.findIndex((line) => line.uuid === afterRecordUuid) : -1;
|
|
2981
|
+
return lines.slice(watermarkIndex + 1).flatMap((line) => {
|
|
2982
|
+
const record = line.attachment;
|
|
2983
|
+
if (!line.uuid || !record || !isCancelledSyncRecorderRecord(record) || !record.toolUseID) return [];
|
|
2984
|
+
const toolCall = toolCalls.get(record.toolUseID);
|
|
2985
|
+
const toolResult = toolResults.get(record.toolUseID);
|
|
2986
|
+
if (!toolCall || !toolResult) return [];
|
|
2987
|
+
return [
|
|
2988
|
+
{
|
|
2989
|
+
recordUuid: line.uuid,
|
|
2990
|
+
command: record.command ?? "",
|
|
2991
|
+
replayPayload: {
|
|
2992
|
+
session_id: sessionInput.session_id,
|
|
2993
|
+
transcript_path: sessionInput.transcript_path,
|
|
2994
|
+
cwd: toolResult.line.cwd ?? line.cwd ?? sessionInput.cwd,
|
|
2995
|
+
hook_event_name: RECONCILED_HOOK_EVENT,
|
|
2996
|
+
tool_name: toolCall.name,
|
|
2997
|
+
tool_input: toolCall.input,
|
|
2998
|
+
tool_response: toolResponseFor(toolResult.line, toolResult.block),
|
|
2999
|
+
tool_use_id: record.toolUseID
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
];
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
function recorderSubcommands(command, readScript) {
|
|
3006
|
+
const scriptList = HOOK_COMMAND_SCRIPTS.exec(command)?.[1] ?? "";
|
|
3007
|
+
const scriptNames = scriptList.split(/\s+/).filter((scriptName) => scriptName && scriptName !== GUARDRAIL_DISPATCH_SCRIPT);
|
|
3008
|
+
const subcommands = scriptNames.flatMap((scriptName) => {
|
|
3009
|
+
const subcommand = GUARDRAIL_SUBCOMMAND_ASSIGNMENT.exec(readScript(scriptName) ?? "")?.[1];
|
|
3010
|
+
return subcommand ? [subcommand] : [];
|
|
3011
|
+
});
|
|
3012
|
+
return [...new Set(subcommands)];
|
|
3013
|
+
}
|
|
3014
|
+
|
|
2926
3015
|
// src/guardrails/hookHost.ts
|
|
2927
3016
|
function resolveHookHost(env = process.env) {
|
|
2928
3017
|
if (env[HOOK_HOST_ENV_VAR] === "codex") return "codex";
|
|
@@ -3540,8 +3629,35 @@ function awaitSyncHookOutcome(hookEventName) {
|
|
|
3540
3629
|
waitSync(REWAKE_OUTCOME_POLL_MS);
|
|
3541
3630
|
}
|
|
3542
3631
|
}
|
|
3632
|
+
function readGuardrailScript(scriptName) {
|
|
3633
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ?? process.env.CURSOR_PLUGIN_ROOT;
|
|
3634
|
+
const scriptsDir = pluginRoot ? join(pluginRoot, "scripts") : dirname(process.argv[1]);
|
|
3635
|
+
try {
|
|
3636
|
+
return readFileSync(join(scriptsDir, scriptName), "utf-8");
|
|
3637
|
+
} catch {
|
|
3638
|
+
return null;
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
function reconcileCancelledRecorders() {
|
|
3642
|
+
const transcriptPath = input.transcript_path;
|
|
3643
|
+
if (!transcriptPath) return;
|
|
3644
|
+
const transcriptTail = readFileTail(transcriptPath, REWAKE_TRANSCRIPT_TAIL_BYTES) ?? "";
|
|
3645
|
+
for (const event of findCancelledRecorderEvents(transcriptTail, input, readReconciledRecord(sessionId))) {
|
|
3646
|
+
const subcommands = recorderSubcommands(event.command, readGuardrailScript);
|
|
3647
|
+
if (subcommands.length > 0) {
|
|
3648
|
+
spawnSync(process.execPath, [...process.execArgv, process.argv[1], "run", ...subcommands], {
|
|
3649
|
+
input: JSON.stringify(event.replayPayload),
|
|
3650
|
+
timeout: RECONCILE_REPLAY_TIMEOUT_MS,
|
|
3651
|
+
windowsHide: true,
|
|
3652
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3655
|
+
writeReconciledRecord(sessionId, event.recordUuid);
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3543
3658
|
function rewake() {
|
|
3544
3659
|
const hookEventName = input.hook_event_name ?? "";
|
|
3660
|
+
if (hookEventName === "Stop") reconcileCancelledRecorders();
|
|
3545
3661
|
const outcome = awaitSyncHookOutcome(hookEventName);
|
|
3546
3662
|
const savedOutput = takeSyncHookOutput(sessionId, hookEventName, payloadDigest(rawPayload));
|
|
3547
3663
|
if (outcome === "delivered" /* Delivered */) return "";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"release": "5.26.0",
|
|
3
|
-
"buildId": "run-
|
|
4
|
-
"commitSha": "
|
|
5
|
-
"buildTime": "2026-10-
|
|
3
|
+
"buildId": "run-266-1",
|
|
4
|
+
"commitSha": "c79ba47696868d4d7e5d1d884f4edf77ee3fc393",
|
|
5
|
+
"buildTime": "2026-10-31T06:04:17Z",
|
|
6
6
|
"serviceName": "muggle-ai-works-mcp"
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muggleai/works",
|
|
3
3
|
"mcpName": "io.github.multiplex-ai/muggle",
|
|
4
|
-
"version": "5.30.0
|
|
4
|
+
"version": "5.30.0",
|
|
5
5
|
"description": "13 years of Microsoft hyperscale engineering, packaged as a harness for your AI coding agent.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"muggleConfig": {
|
|
65
65
|
"electronAppVersion": "1.17.0",
|
|
66
66
|
"downloadBaseUrl": "https://github.com/multiplex-ai/muggle-ai-works/releases/download",
|
|
67
|
-
"runtimeTargetDefault": "
|
|
67
|
+
"runtimeTargetDefault": "production",
|
|
68
68
|
"checksumsByStream": {
|
|
69
69
|
"production": {
|
|
70
70
|
"win32-x64": "03687e6c511f407875354954cb1b0f8e95d4ad0602174d76388a52d1aa1554e4",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
4
|
-
"version": "5.30.0
|
|
4
|
+
"version": "5.30.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
7
7
|
"email": "support@muggle-ai.com"
|
package/plugin/.codex-mcp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muggle",
|
|
3
|
-
"version": "5.30.0
|
|
3
|
+
"version": "5.30.0",
|
|
4
4
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Muggle AI",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "muggle",
|
|
3
3
|
"displayName": "Muggle AI",
|
|
4
4
|
"description": "Real-browser E2E acceptance for your coding agent: test the flows you changed and open the PR with screenshots. Built for flows other tools can't reach: managed test identities with live inboxes (magic links, email OTP and 2FA, verification mail, password resets), an encrypted secret store for login and test credentials, and a test graph that runs prerequisite flows first. Plain-English tests become replayable scripts on localhost or preview URLs.",
|
|
5
|
-
"version": "5.30.0
|
|
5
|
+
"version": "5.30.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
package/plugin/.mcp.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync, execFileSync, spawn } from 'child_process';
|
|
2
|
+
import { readFileSync, rmSync, existsSync, readdirSync, mkdirSync, openSync, fstatSync, readSync, closeSync, writeFileSync, renameSync, statSync, writeSync, unlinkSync } from 'fs';
|
|
2
3
|
import { join, isAbsolute, resolve, basename, dirname } from 'path';
|
|
3
4
|
import { homedir, tmpdir, hostname } from 'os';
|
|
4
|
-
import { execFileSync, spawn } from 'child_process';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
7
|
|
|
@@ -131,6 +131,12 @@ var REWAKE_ANCHOR_ROLE_BY_EVENT = {
|
|
|
131
131
|
Stop: "assistant",
|
|
132
132
|
UserPromptSubmit: "user"
|
|
133
133
|
};
|
|
134
|
+
var RECONCILED_HOOK_EVENT = "PostToolUse";
|
|
135
|
+
var RECONCILED_RECORD_SUFFIX = ".reconciled-record";
|
|
136
|
+
var RECONCILE_REPLAY_TIMEOUT_MS = 15e3;
|
|
137
|
+
var GUARDRAIL_SUBCOMMAND_ASSIGNMENT = /^GUARDRAIL_SUBCOMMAND="([\w-]+)"/m;
|
|
138
|
+
var HOOK_COMMAND_SCRIPTS = /^set -- ([^;]+);/;
|
|
139
|
+
var GUARDRAIL_DISPATCH_SCRIPT = "guardrail-dispatch.sh";
|
|
134
140
|
|
|
135
141
|
// src/guardrails/store/fileLock.ts
|
|
136
142
|
function isProcessAlive(pid) {
|
|
@@ -280,6 +286,20 @@ function takeSyncHookOutput(sessionId2, hookEventName, payloadDigest2, dirOverri
|
|
|
280
286
|
return null;
|
|
281
287
|
}
|
|
282
288
|
}
|
|
289
|
+
function readReconciledRecord(sessionId2, dirOverride) {
|
|
290
|
+
try {
|
|
291
|
+
return readFileSync(fileFor(sessionId2, dirOverride, RECONCILED_RECORD_SUFFIX), "utf-8").trim() || void 0;
|
|
292
|
+
} catch {
|
|
293
|
+
return void 0;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function writeReconciledRecord(sessionId2, recordUuid, dirOverride) {
|
|
297
|
+
mkdirSync(baseDir(dirOverride), { recursive: true });
|
|
298
|
+
const target = fileFor(sessionId2, dirOverride, RECONCILED_RECORD_SUFFIX);
|
|
299
|
+
const staging = `${target}.${process.pid}.tmp`;
|
|
300
|
+
writeFileSync(staging, recordUuid);
|
|
301
|
+
renameSync(staging, target);
|
|
302
|
+
}
|
|
283
303
|
var lockFor = (sessionId2, dirOverride) => `${fileFor(sessionId2, dirOverride)}.lock`;
|
|
284
304
|
function commitUnderHeldLock(sessionId2, mutate, dirOverride) {
|
|
285
305
|
const current = readState(sessionId2, dirOverride);
|
|
@@ -2923,6 +2943,75 @@ function rewakeMessage(hookEventName, hookOutput) {
|
|
|
2923
2943
|
${deliveredText}`;
|
|
2924
2944
|
}
|
|
2925
2945
|
|
|
2946
|
+
// src/guardrails/reconcile.ts
|
|
2947
|
+
function parseTranscriptLines(transcriptTail) {
|
|
2948
|
+
return transcriptTail.split("\n").flatMap((line) => {
|
|
2949
|
+
try {
|
|
2950
|
+
return [JSON.parse(line)];
|
|
2951
|
+
} catch {
|
|
2952
|
+
return [];
|
|
2953
|
+
}
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
function contentBlocks(line) {
|
|
2957
|
+
const content = line.message?.content;
|
|
2958
|
+
return Array.isArray(content) ? content : [];
|
|
2959
|
+
}
|
|
2960
|
+
function toolResponseFor(line, block) {
|
|
2961
|
+
if (line.toolUseResult && typeof line.toolUseResult === "object") return line.toolUseResult;
|
|
2962
|
+
if (typeof block.content === "string") return { output: block.content };
|
|
2963
|
+
return { content: block.content };
|
|
2964
|
+
}
|
|
2965
|
+
function isCancelledSyncRecorderRecord(record) {
|
|
2966
|
+
if (record?.type !== TRANSCRIPT_HOOK_CANCELLED_TYPE || record.hookEvent !== RECONCILED_HOOK_EVENT) return false;
|
|
2967
|
+
const command = record.command ?? "";
|
|
2968
|
+
return command.includes(GUARDRAIL_HOOK_COMMAND_MARKER) && !command.includes(REWAKE_HOOK_SCRIPT);
|
|
2969
|
+
}
|
|
2970
|
+
function findCancelledRecorderEvents(transcriptTail, sessionInput, afterRecordUuid) {
|
|
2971
|
+
const lines = parseTranscriptLines(transcriptTail);
|
|
2972
|
+
const toolCalls = /* @__PURE__ */ new Map();
|
|
2973
|
+
const toolResults = /* @__PURE__ */ new Map();
|
|
2974
|
+
for (const line of lines) {
|
|
2975
|
+
for (const block of contentBlocks(line)) {
|
|
2976
|
+
if (block.type === "tool_use" && block.id) toolCalls.set(block.id, { name: block.name, input: block.input });
|
|
2977
|
+
if (block.type === "tool_result" && block.tool_use_id) toolResults.set(block.tool_use_id, { line, block });
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const watermarkIndex = afterRecordUuid ? lines.findIndex((line) => line.uuid === afterRecordUuid) : -1;
|
|
2981
|
+
return lines.slice(watermarkIndex + 1).flatMap((line) => {
|
|
2982
|
+
const record = line.attachment;
|
|
2983
|
+
if (!line.uuid || !record || !isCancelledSyncRecorderRecord(record) || !record.toolUseID) return [];
|
|
2984
|
+
const toolCall = toolCalls.get(record.toolUseID);
|
|
2985
|
+
const toolResult = toolResults.get(record.toolUseID);
|
|
2986
|
+
if (!toolCall || !toolResult) return [];
|
|
2987
|
+
return [
|
|
2988
|
+
{
|
|
2989
|
+
recordUuid: line.uuid,
|
|
2990
|
+
command: record.command ?? "",
|
|
2991
|
+
replayPayload: {
|
|
2992
|
+
session_id: sessionInput.session_id,
|
|
2993
|
+
transcript_path: sessionInput.transcript_path,
|
|
2994
|
+
cwd: toolResult.line.cwd ?? line.cwd ?? sessionInput.cwd,
|
|
2995
|
+
hook_event_name: RECONCILED_HOOK_EVENT,
|
|
2996
|
+
tool_name: toolCall.name,
|
|
2997
|
+
tool_input: toolCall.input,
|
|
2998
|
+
tool_response: toolResponseFor(toolResult.line, toolResult.block),
|
|
2999
|
+
tool_use_id: record.toolUseID
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
];
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
function recorderSubcommands(command, readScript) {
|
|
3006
|
+
const scriptList = HOOK_COMMAND_SCRIPTS.exec(command)?.[1] ?? "";
|
|
3007
|
+
const scriptNames = scriptList.split(/\s+/).filter((scriptName) => scriptName && scriptName !== GUARDRAIL_DISPATCH_SCRIPT);
|
|
3008
|
+
const subcommands = scriptNames.flatMap((scriptName) => {
|
|
3009
|
+
const subcommand = GUARDRAIL_SUBCOMMAND_ASSIGNMENT.exec(readScript(scriptName) ?? "")?.[1];
|
|
3010
|
+
return subcommand ? [subcommand] : [];
|
|
3011
|
+
});
|
|
3012
|
+
return [...new Set(subcommands)];
|
|
3013
|
+
}
|
|
3014
|
+
|
|
2926
3015
|
// src/guardrails/hookHost.ts
|
|
2927
3016
|
function resolveHookHost(env = process.env) {
|
|
2928
3017
|
if (env[HOOK_HOST_ENV_VAR] === "codex") return "codex";
|
|
@@ -3540,8 +3629,35 @@ function awaitSyncHookOutcome(hookEventName) {
|
|
|
3540
3629
|
waitSync(REWAKE_OUTCOME_POLL_MS);
|
|
3541
3630
|
}
|
|
3542
3631
|
}
|
|
3632
|
+
function readGuardrailScript(scriptName) {
|
|
3633
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT ?? process.env.CURSOR_PLUGIN_ROOT;
|
|
3634
|
+
const scriptsDir = pluginRoot ? join(pluginRoot, "scripts") : dirname(process.argv[1]);
|
|
3635
|
+
try {
|
|
3636
|
+
return readFileSync(join(scriptsDir, scriptName), "utf-8");
|
|
3637
|
+
} catch {
|
|
3638
|
+
return null;
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
function reconcileCancelledRecorders() {
|
|
3642
|
+
const transcriptPath = input.transcript_path;
|
|
3643
|
+
if (!transcriptPath) return;
|
|
3644
|
+
const transcriptTail = readFileTail(transcriptPath, REWAKE_TRANSCRIPT_TAIL_BYTES) ?? "";
|
|
3645
|
+
for (const event of findCancelledRecorderEvents(transcriptTail, input, readReconciledRecord(sessionId))) {
|
|
3646
|
+
const subcommands = recorderSubcommands(event.command, readGuardrailScript);
|
|
3647
|
+
if (subcommands.length > 0) {
|
|
3648
|
+
spawnSync(process.execPath, [...process.execArgv, process.argv[1], "run", ...subcommands], {
|
|
3649
|
+
input: JSON.stringify(event.replayPayload),
|
|
3650
|
+
timeout: RECONCILE_REPLAY_TIMEOUT_MS,
|
|
3651
|
+
windowsHide: true,
|
|
3652
|
+
stdio: ["pipe", "ignore", "ignore"]
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3655
|
+
writeReconciledRecord(sessionId, event.recordUuid);
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3543
3658
|
function rewake() {
|
|
3544
3659
|
const hookEventName = input.hook_event_name ?? "";
|
|
3660
|
+
if (hookEventName === "Stop") reconcileCancelledRecorders();
|
|
3545
3661
|
const outcome = awaitSyncHookOutcome(hookEventName);
|
|
3546
3662
|
const savedOutput = takeSyncHookOutput(sessionId, hookEventName, payloadDigest(rawPayload));
|
|
3547
3663
|
if (outcome === "delivered" /* Delivered */) return "";
|