@stroq/cli 0.14.1 → 0.15.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/dist/index.js
CHANGED
|
@@ -549,10 +549,10 @@ function decodeLayer(text, depth, maxDepth) {
|
|
|
549
549
|
return found;
|
|
550
550
|
}
|
|
551
551
|
function expandVariants(text, maxDepth = 2) {
|
|
552
|
-
const
|
|
552
|
+
const normalized2 = normalizeText(text);
|
|
553
553
|
const base = [{ kind: "raw", depth: 0, text }];
|
|
554
|
-
if (
|
|
555
|
-
return [...base, ...decodeLayer(
|
|
554
|
+
if (normalized2 !== text) base.push({ kind: "normalized", depth: 0, text: normalized2 });
|
|
555
|
+
return [...base, ...decodeLayer(normalized2, 1, maxDepth)];
|
|
556
556
|
}
|
|
557
557
|
|
|
558
558
|
// ../core/src/rules/atr-types.ts
|
|
@@ -1255,7 +1255,14 @@ var SEVERITY_WEIGHT = {
|
|
|
1255
1255
|
};
|
|
1256
1256
|
var DEFAULT_THRESHOLD = 0.6;
|
|
1257
1257
|
var DEFAULT_MAX_CHARS = 2e5;
|
|
1258
|
-
var DEFAULT_BUDGET_MS =
|
|
1258
|
+
var DEFAULT_BUDGET_MS = 4e3;
|
|
1259
|
+
var SCAN_BUDGET_ENV = "STROQ_SCAN_BUDGET_MS";
|
|
1260
|
+
function budgetFromEnv() {
|
|
1261
|
+
const raw = process.env[SCAN_BUDGET_ENV];
|
|
1262
|
+
if (raw === void 0) return void 0;
|
|
1263
|
+
const parsed = Number(raw);
|
|
1264
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
1265
|
+
}
|
|
1259
1266
|
var ENCODED_FLOOR = 0.7;
|
|
1260
1267
|
var BUDGET_MATCH = {
|
|
1261
1268
|
ruleId: "STROQ-SCAN-BUDGET",
|
|
@@ -1275,7 +1282,7 @@ function timedOutResult(matches) {
|
|
|
1275
1282
|
function scanContent(rules, text, opts = {}, context = {}) {
|
|
1276
1283
|
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
1277
1284
|
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
1278
|
-
const budgetMs = opts.budgetMs ?? DEFAULT_BUDGET_MS;
|
|
1285
|
+
const budgetMs = opts.budgetMs ?? budgetFromEnv() ?? DEFAULT_BUDGET_MS;
|
|
1279
1286
|
const startedAt = performance.now();
|
|
1280
1287
|
const input = text.length > maxChars ? text.slice(0, maxChars) : text;
|
|
1281
1288
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1311,13 +1318,13 @@ function isDangerousRmTarget(target, cwd) {
|
|
|
1311
1318
|
if (t.startsWith("~") || t.startsWith("$") || t.startsWith("..")) return true;
|
|
1312
1319
|
if (UNC_PATH.test(t)) return true;
|
|
1313
1320
|
if (WINDOWS_DRIVE_PATH.test(t)) {
|
|
1314
|
-
const
|
|
1321
|
+
const normalized3 = foldPath(t).toLowerCase();
|
|
1315
1322
|
const root = foldPath(cwd).toLowerCase();
|
|
1316
|
-
return
|
|
1323
|
+
return normalized3 === "" || !normalized3.startsWith(`${root}/`);
|
|
1317
1324
|
}
|
|
1318
1325
|
if (!t.startsWith("/")) return false;
|
|
1319
|
-
const
|
|
1320
|
-
return !
|
|
1326
|
+
const normalized2 = t.replace(/\/+$/, "");
|
|
1327
|
+
return !normalized2.startsWith(`${cwd}/`);
|
|
1321
1328
|
}
|
|
1322
1329
|
|
|
1323
1330
|
// ../core/src/actions/classify-powershell.ts
|
|
@@ -1450,14 +1457,14 @@ function matchingParenEnd(text, start) {
|
|
|
1450
1457
|
}
|
|
1451
1458
|
function extractSubstitutions(command) {
|
|
1452
1459
|
const results = [];
|
|
1453
|
-
for (const
|
|
1454
|
-
let idx = command.indexOf(
|
|
1460
|
+
for (const open3 of OPEN_SUBSTITUTIONS) {
|
|
1461
|
+
let idx = command.indexOf(open3);
|
|
1455
1462
|
while (idx !== -1) {
|
|
1456
|
-
const start = idx +
|
|
1463
|
+
const start = idx + open3.length;
|
|
1457
1464
|
const end = matchingParenEnd(command, start);
|
|
1458
1465
|
if (end === -1) break;
|
|
1459
1466
|
results.push(command.slice(start, end));
|
|
1460
|
-
idx = command.indexOf(
|
|
1467
|
+
idx = command.indexOf(open3, end);
|
|
1461
1468
|
}
|
|
1462
1469
|
}
|
|
1463
1470
|
const backtickPairs = command.match(/`[^`]*`/g) ?? [];
|
|
@@ -1467,6 +1474,22 @@ function extractSubstitutions(command) {
|
|
|
1467
1474
|
function splitTop(command) {
|
|
1468
1475
|
return command.split(SEGMENT_SPLIT).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1469
1476
|
}
|
|
1477
|
+
var SEQUENCE_SPLIT = /\|\||&&|;|\n/;
|
|
1478
|
+
function pipelinesOf(command) {
|
|
1479
|
+
return command.split(SEQUENCE_SPLIT).map(
|
|
1480
|
+
(run) => run.split("|").map((stage) => stage.trim()).filter((stage) => stage.length > 0)
|
|
1481
|
+
).filter((stages) => stages.length > 0);
|
|
1482
|
+
}
|
|
1483
|
+
function splitPipelines(command) {
|
|
1484
|
+
return [
|
|
1485
|
+
...pipelinesOf(command),
|
|
1486
|
+
...extractSubstitutions(command).flatMap(pipelinesOf),
|
|
1487
|
+
...extractShCStrings(command).flatMap(pipelinesOf),
|
|
1488
|
+
...extractFindExecCommands(command).flatMap(pipelinesOf),
|
|
1489
|
+
...extractEvalArguments(command).flatMap(pipelinesOf),
|
|
1490
|
+
...extractGitForeachOrBisectRunCommands(command).flatMap(pipelinesOf)
|
|
1491
|
+
];
|
|
1492
|
+
}
|
|
1470
1493
|
var SH_C_QUOTE = /\b(?:sh|bash|zsh|dash|ksh)\s+-c\s+(["'])/g;
|
|
1471
1494
|
var IEX_QUOTE = /\b(?:iex|Invoke-Expression)\s+(["'])/gi;
|
|
1472
1495
|
function extractQuotedBodies(command, pattern) {
|
|
@@ -1963,19 +1986,28 @@ function isNetwork(seg) {
|
|
|
1963
1986
|
if (/\/dev\/tcp\//.test(seg)) return true;
|
|
1964
1987
|
return isUnknownWrapperNetworkCall(seg, word);
|
|
1965
1988
|
}
|
|
1966
|
-
function encodedExecSignals(segments) {
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1989
|
+
function encodedExecSignals(segments, pipelines) {
|
|
1990
|
+
const piped = pipelines.flatMap(
|
|
1991
|
+
(stages) => stages.flatMap((stage, i) => {
|
|
1992
|
+
const downstream = stages.slice(i + 1);
|
|
1993
|
+
const signals = [];
|
|
1994
|
+
if (DECODE.test(stage) && downstream.some(isShell)) signals.push("decode-pipe-shell");
|
|
1995
|
+
if (isNetwork(stage) && downstream.some(isShell)) signals.push("remote-pipe-shell");
|
|
1996
|
+
return signals;
|
|
1997
|
+
})
|
|
1998
|
+
);
|
|
1999
|
+
return [
|
|
2000
|
+
...piped,
|
|
2001
|
+
...segments.flatMap((seg) => {
|
|
2002
|
+
const signals = [];
|
|
2003
|
+
if (EVAL_DYNAMIC.test(seg)) signals.push("eval-dynamic");
|
|
2004
|
+
if (INLINE_INTERP.test(seg) && INLINE_PAYLOAD.test(seg))
|
|
2005
|
+
signals.push("inline-interpreter-payload");
|
|
2006
|
+
if (SHELL_C_REMOTE.test(seg)) signals.push("shell-c-remote");
|
|
2007
|
+
if (SHELL_PROC_SUB_REMOTE.test(seg)) signals.push("shell-proc-sub-remote");
|
|
2008
|
+
return signals;
|
|
2009
|
+
})
|
|
2010
|
+
];
|
|
1979
2011
|
}
|
|
1980
2012
|
function destructiveSignals(segments, cwd) {
|
|
1981
2013
|
return segments.flatMap((seg) => {
|
|
@@ -2008,7 +2040,10 @@ function classifyCommand(command, cwd) {
|
|
|
2008
2040
|
const selfConfig = selfTamperSignals(segments);
|
|
2009
2041
|
const ps = powershellSignals(segments, cwd);
|
|
2010
2042
|
const groups = [
|
|
2011
|
-
[
|
|
2043
|
+
[
|
|
2044
|
+
"shell.exec_encoded",
|
|
2045
|
+
[...encodedExecSignals(segments, splitPipelines(command)), ...ps.encoded]
|
|
2046
|
+
],
|
|
2012
2047
|
["shell.network", [...segments.filter(isNetwork).map(() => "network-command"), ...ps.network]],
|
|
2013
2048
|
["shell.destructive", [...destructiveSignals(segments, cwd), ...ps.destructive]],
|
|
2014
2049
|
["fs.secrets", [...secretSignals(segments), ...ps.secrets]],
|
|
@@ -3807,6 +3842,25 @@ function collectStrings(value, depth = 0) {
|
|
|
3807
3842
|
}
|
|
3808
3843
|
return [];
|
|
3809
3844
|
}
|
|
3845
|
+
function collectKeyedStrings(value, key2 = null, into = /* @__PURE__ */ new Map(), depth = 0) {
|
|
3846
|
+
if (depth > MAX_JSON_DEPTH) return into;
|
|
3847
|
+
if (typeof value === "string") {
|
|
3848
|
+
const keys = into.get(value) ?? /* @__PURE__ */ new Set();
|
|
3849
|
+
if (key2 !== null) keys.add(key2);
|
|
3850
|
+
into.set(value, keys);
|
|
3851
|
+
return into;
|
|
3852
|
+
}
|
|
3853
|
+
if (Array.isArray(value)) {
|
|
3854
|
+
for (const item of value) collectKeyedStrings(item, key2, into, depth + 1);
|
|
3855
|
+
return into;
|
|
3856
|
+
}
|
|
3857
|
+
if (value !== null && typeof value === "object") {
|
|
3858
|
+
for (const [childKey, item] of Object.entries(value)) {
|
|
3859
|
+
collectKeyedStrings(item, childKey, into, depth + 1);
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
return into;
|
|
3863
|
+
}
|
|
3810
3864
|
function mapStrings(value, fn, depth = 0) {
|
|
3811
3865
|
if (depth > MAX_JSON_DEPTH) return value;
|
|
3812
3866
|
if (typeof value === "string") return fn(value);
|
|
@@ -3822,19 +3876,182 @@ function mapStrings(value, fn, depth = 0) {
|
|
|
3822
3876
|
return value;
|
|
3823
3877
|
}
|
|
3824
3878
|
|
|
3879
|
+
// ../core/src/cloak/keyed.ts
|
|
3880
|
+
var normalizeKey = (key2) => key2.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3881
|
+
var PERSON_KEYS = /* @__PURE__ */ new Set([
|
|
3882
|
+
"firstname",
|
|
3883
|
+
"lastname",
|
|
3884
|
+
"fullname",
|
|
3885
|
+
"middlename",
|
|
3886
|
+
"givenname",
|
|
3887
|
+
"familyname",
|
|
3888
|
+
"surname",
|
|
3889
|
+
"displayname",
|
|
3890
|
+
"legalname",
|
|
3891
|
+
"personname",
|
|
3892
|
+
"contactname",
|
|
3893
|
+
"customername",
|
|
3894
|
+
"clientname",
|
|
3895
|
+
"employeename",
|
|
3896
|
+
"patientname",
|
|
3897
|
+
"authorname",
|
|
3898
|
+
"ownername",
|
|
3899
|
+
"recipientname",
|
|
3900
|
+
"sendername",
|
|
3901
|
+
"attendee",
|
|
3902
|
+
"attendees",
|
|
3903
|
+
"attendeename"
|
|
3904
|
+
]);
|
|
3905
|
+
var PLACE_KEYS = /* @__PURE__ */ new Set([
|
|
3906
|
+
"streetaddress",
|
|
3907
|
+
"street",
|
|
3908
|
+
"addressline1",
|
|
3909
|
+
"addressline2",
|
|
3910
|
+
"address1",
|
|
3911
|
+
"address2",
|
|
3912
|
+
"homeaddress",
|
|
3913
|
+
"billingaddress",
|
|
3914
|
+
"shippingaddress",
|
|
3915
|
+
"mailingaddress",
|
|
3916
|
+
"postaladdress",
|
|
3917
|
+
"deliveryaddress"
|
|
3918
|
+
]);
|
|
3919
|
+
var MAX_NAME_CHARS = 200;
|
|
3920
|
+
var MAX_ADDRESS_CHARS = 500;
|
|
3921
|
+
function detectKeyedFields(text, keys) {
|
|
3922
|
+
if (text.trim() === "") return [];
|
|
3923
|
+
let kind = null;
|
|
3924
|
+
for (const key2 of keys) {
|
|
3925
|
+
const normalized2 = normalizeKey(key2);
|
|
3926
|
+
if (PERSON_KEYS.has(normalized2)) {
|
|
3927
|
+
kind = "name";
|
|
3928
|
+
break;
|
|
3929
|
+
}
|
|
3930
|
+
if (PLACE_KEYS.has(normalized2)) kind = "address";
|
|
3931
|
+
}
|
|
3932
|
+
if (kind === null) return [];
|
|
3933
|
+
if (text.length > (kind === "name" ? MAX_NAME_CHARS : MAX_ADDRESS_CHARS)) return [];
|
|
3934
|
+
return [{ kind, start: 0, end: text.length, value: text, restorable: true }];
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
// ../core/src/cloak/prose-names.ts
|
|
3938
|
+
var MIN_PART_CHARS = 3;
|
|
3939
|
+
var MAX_NEEDLES = 64;
|
|
3940
|
+
var ALSO_ORDINARY = /* @__PURE__ */ new Set([
|
|
3941
|
+
"Amber",
|
|
3942
|
+
"Art",
|
|
3943
|
+
"Autumn",
|
|
3944
|
+
"Bill",
|
|
3945
|
+
"Bob",
|
|
3946
|
+
"Buck",
|
|
3947
|
+
"Carol",
|
|
3948
|
+
"Chase",
|
|
3949
|
+
"Chuck",
|
|
3950
|
+
"Daisy",
|
|
3951
|
+
"Dawn",
|
|
3952
|
+
"Don",
|
|
3953
|
+
"Drew",
|
|
3954
|
+
"Earl",
|
|
3955
|
+
"Faith",
|
|
3956
|
+
"Frank",
|
|
3957
|
+
"Grace",
|
|
3958
|
+
"Guy",
|
|
3959
|
+
"Hope",
|
|
3960
|
+
"Hunter",
|
|
3961
|
+
"Jack",
|
|
3962
|
+
"Joy",
|
|
3963
|
+
"June",
|
|
3964
|
+
"Major",
|
|
3965
|
+
"Mark",
|
|
3966
|
+
"Max",
|
|
3967
|
+
"May",
|
|
3968
|
+
"Mercy",
|
|
3969
|
+
"Nick",
|
|
3970
|
+
"Olive",
|
|
3971
|
+
"Page",
|
|
3972
|
+
"Pat",
|
|
3973
|
+
"Pearl",
|
|
3974
|
+
"Rose",
|
|
3975
|
+
"Ruby",
|
|
3976
|
+
"Sky",
|
|
3977
|
+
"Stone",
|
|
3978
|
+
"Sue",
|
|
3979
|
+
"Summer",
|
|
3980
|
+
"Wade",
|
|
3981
|
+
"Ward",
|
|
3982
|
+
"Will"
|
|
3983
|
+
]);
|
|
3984
|
+
var NOT_WORD = "[^\\p{L}\\p{N}_]";
|
|
3985
|
+
var SENTENCE_END = /* @__PURE__ */ new Set([".", "!", "?", "\n", "\r", ";", ":", "\u2022", "-", "*"]);
|
|
3986
|
+
var escapeLiteral = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3987
|
+
function nameNeedles(names) {
|
|
3988
|
+
const out = /* @__PURE__ */ new Set();
|
|
3989
|
+
for (const name of names) {
|
|
3990
|
+
const trimmed = name.trim();
|
|
3991
|
+
if (trimmed === "") continue;
|
|
3992
|
+
out.add(trimmed);
|
|
3993
|
+
for (const part of trimmed.split(/[^\p{L}\p{N}]+/u)) {
|
|
3994
|
+
if (part.length >= MIN_PART_CHARS) out.add(part);
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
return [...out].sort((a, b) => b.length - a.length || (a < b ? -1 : 1)).slice(0, MAX_NEEDLES);
|
|
3998
|
+
}
|
|
3999
|
+
function startsSentence(text, index) {
|
|
4000
|
+
for (let i = index - 1; i >= 0; i -= 1) {
|
|
4001
|
+
const char = text[i] ?? "";
|
|
4002
|
+
if (char === " " || char === " ") continue;
|
|
4003
|
+
return SENTENCE_END.has(char);
|
|
4004
|
+
}
|
|
4005
|
+
return true;
|
|
4006
|
+
}
|
|
4007
|
+
function compileNameMatcher(needles) {
|
|
4008
|
+
if (needles.length === 0) return null;
|
|
4009
|
+
const source = `(?<=^|${NOT_WORD})(?:${needles.map(escapeLiteral).join("|")})(?=$|${NOT_WORD})`;
|
|
4010
|
+
return (text) => {
|
|
4011
|
+
if (text === "") return [];
|
|
4012
|
+
const spans = [];
|
|
4013
|
+
for (const match of text.matchAll(new RegExp(source, "gu"))) {
|
|
4014
|
+
const value = match[0];
|
|
4015
|
+
const start = match.index;
|
|
4016
|
+
if (ALSO_ORDINARY.has(value) && startsSentence(text, start)) continue;
|
|
4017
|
+
spans.push({ kind: "name", start, end: start + value.length, value, restorable: true });
|
|
4018
|
+
}
|
|
4019
|
+
return spans;
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
|
|
3825
4023
|
// ../core/src/cloak/detector.ts
|
|
3826
4024
|
var MAX_CLOAK_CHARS = MAX_SCAN_CHARS;
|
|
3827
4025
|
function createCloakDetector(options) {
|
|
3828
4026
|
return {
|
|
3829
|
-
async detect(text) {
|
|
4027
|
+
async detect(text, keys) {
|
|
3830
4028
|
if (text === "" || text.length > MAX_CLOAK_CHARS) return [];
|
|
3831
|
-
const
|
|
3832
|
-
if (!options.secrets) return
|
|
4029
|
+
const shapes = [...detectPatterns(text), ...detectKeyedFields(text, keys ?? /* @__PURE__ */ new Set())];
|
|
4030
|
+
if (!options.secrets) return mergeSpans(shapes);
|
|
3833
4031
|
const matches = await options.secrets.lookup(candidatesFromText(text), options.cwd);
|
|
3834
|
-
return mergeSpans([...secretSpans(text, matches), ...
|
|
4032
|
+
return mergeSpans([...secretSpans(text, matches), ...shapes]);
|
|
3835
4033
|
}
|
|
3836
4034
|
};
|
|
3837
4035
|
}
|
|
4036
|
+
async function detectAcrossLeaves(leaves, detector) {
|
|
4037
|
+
const found = /* @__PURE__ */ new Map();
|
|
4038
|
+
const names = /* @__PURE__ */ new Set();
|
|
4039
|
+
for (const [text, keys] of leaves) {
|
|
4040
|
+
const spans = mergeSpans(await detector.detect(text, keys));
|
|
4041
|
+
if (spans.length > 0) found.set(text, spans);
|
|
4042
|
+
for (const span of spans) if (span.kind === "name") names.add(span.value);
|
|
4043
|
+
}
|
|
4044
|
+
if (names.size === 0) return found;
|
|
4045
|
+
const matcher = compileNameMatcher(nameNeedles(names));
|
|
4046
|
+
if (matcher === null) return found;
|
|
4047
|
+
for (const text of leaves.keys()) {
|
|
4048
|
+
const prose = matcher(text);
|
|
4049
|
+
if (prose.length === 0) continue;
|
|
4050
|
+
const merged = mergeSpans([...found.get(text) ?? [], ...prose]);
|
|
4051
|
+
found.set(text, merged);
|
|
4052
|
+
}
|
|
4053
|
+
return found;
|
|
4054
|
+
}
|
|
3838
4055
|
|
|
3839
4056
|
// ../core/src/cloak/store.ts
|
|
3840
4057
|
import { mkdir as mkdir6, readFile as readFile5, rename as rename4, writeFile as writeFile4 } from "fs/promises";
|
|
@@ -4556,7 +4773,7 @@ import { parseArgs as parseArgs2 } from "util";
|
|
|
4556
4773
|
|
|
4557
4774
|
// src/bench/run.ts
|
|
4558
4775
|
import { closeSync, existsSync as existsSync2, openSync, readSync, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
4559
|
-
import { join as join8 } from "path";
|
|
4776
|
+
import { basename as basename2, join as join8 } from "path";
|
|
4560
4777
|
import { fileURLToPath } from "url";
|
|
4561
4778
|
var TEXT = /\.(?:md|rst|txt|adoc)$/i;
|
|
4562
4779
|
var BENCH_BUDGET_MS = 6e4;
|
|
@@ -4600,15 +4817,23 @@ function runBench(dir) {
|
|
|
4600
4817
|
const flaggedFiles = [];
|
|
4601
4818
|
let bytes = 0;
|
|
4602
4819
|
let timedOut = 0;
|
|
4820
|
+
let slowestMs = 0;
|
|
4821
|
+
let slowestFile = "";
|
|
4603
4822
|
for (const file of files) {
|
|
4604
4823
|
const size = statSync4(file).size;
|
|
4605
4824
|
bytes += size;
|
|
4825
|
+
const startedAt = performance.now();
|
|
4606
4826
|
const result = scanContent(
|
|
4607
4827
|
rules,
|
|
4608
4828
|
readPrefix(file, MAX_READ_BYTES),
|
|
4609
4829
|
{ budgetMs: BENCH_BUDGET_MS },
|
|
4610
4830
|
{ target: "repo_content" }
|
|
4611
4831
|
);
|
|
4832
|
+
const elapsed = performance.now() - startedAt;
|
|
4833
|
+
if (elapsed > slowestMs) {
|
|
4834
|
+
slowestMs = elapsed;
|
|
4835
|
+
slowestFile = file;
|
|
4836
|
+
}
|
|
4612
4837
|
if (result.timedOut) timedOut += 1;
|
|
4613
4838
|
if (result.verdict !== "suspect") continue;
|
|
4614
4839
|
flaggedFiles.push(file);
|
|
@@ -4632,6 +4857,8 @@ function runBench(dir) {
|
|
|
4632
4857
|
flagged: flaggedFiles.length,
|
|
4633
4858
|
rate: files.length === 0 ? 0 : flaggedFiles.length / files.length,
|
|
4634
4859
|
timedOut,
|
|
4860
|
+
slowestMs,
|
|
4861
|
+
slowestFile,
|
|
4635
4862
|
byRule: [...perRule.values()].sort((a, b) => b.files - a.files),
|
|
4636
4863
|
flaggedFiles
|
|
4637
4864
|
};
|
|
@@ -4648,6 +4875,14 @@ function formatBench(report, opts = {}) {
|
|
|
4648
4875
|
`timedOut: ${report.timedOut} ${noun} hit the bench's scan budget and were forced suspect by that alone, not by a rule match \u2014 see docs/BENCH.md's Method section.`
|
|
4649
4876
|
);
|
|
4650
4877
|
}
|
|
4878
|
+
if (opts.timings === true) {
|
|
4879
|
+
const headroom = DEFAULT_BUDGET_MS / Math.max(report.slowestMs, 1e-3);
|
|
4880
|
+
const verdict = headroom < 2 ? " ** raise DEFAULT_BUDGET_MS **" : "";
|
|
4881
|
+
const which = opts.verbose === true ? ` (${basename2(report.slowestFile)})` : "";
|
|
4882
|
+
lines.push(
|
|
4883
|
+
`slowest: ${report.slowestMs.toFixed(0)} ms${which} against production's ${DEFAULT_BUDGET_MS} ms budget \u2014 ${headroom.toFixed(1)}x headroom${verdict}`
|
|
4884
|
+
);
|
|
4885
|
+
}
|
|
4651
4886
|
if (report.byRule.length === 0) {
|
|
4652
4887
|
lines.push("", "Nothing in this corpus trips a rule.");
|
|
4653
4888
|
} else {
|
|
@@ -4686,7 +4921,7 @@ async function runBenchCommand(args) {
|
|
|
4686
4921
|
const report = runBench(dir);
|
|
4687
4922
|
process.stdout.write(
|
|
4688
4923
|
values.json ? `${JSON.stringify(report, null, 2)}
|
|
4689
|
-
` : formatBench(report, { verbose: values.verbose })
|
|
4924
|
+
` : formatBench(report, { verbose: values.verbose, timings: true })
|
|
4690
4925
|
);
|
|
4691
4926
|
return 0;
|
|
4692
4927
|
}
|
|
@@ -6032,7 +6267,7 @@ function runOpenClawInstall(bin, dir, run = spawnCommand) {
|
|
|
6032
6267
|
|
|
6033
6268
|
// src/commands/init-mcp.ts
|
|
6034
6269
|
import { existsSync as existsSync8, statSync as statSync6 } from "fs";
|
|
6035
|
-
import { basename as
|
|
6270
|
+
import { basename as basename3, resolve as resolve3 } from "path";
|
|
6036
6271
|
|
|
6037
6272
|
// src/commands/mcp-config.ts
|
|
6038
6273
|
import { existsSync as existsSync7 } from "fs";
|
|
@@ -6198,7 +6433,7 @@ var MCP_NOTE = 'Restart the MCP client before this takes effect: it launches its
|
|
|
6198
6433
|
function mcpTarget(client, configPath, scope) {
|
|
6199
6434
|
if (client === void 0) {
|
|
6200
6435
|
const file = resolve3(configPath ?? "");
|
|
6201
|
-
return { file, label:
|
|
6436
|
+
return { file, label: basename3(file) };
|
|
6202
6437
|
}
|
|
6203
6438
|
if (!isMcpClient(client)) return null;
|
|
6204
6439
|
return { file: mcpConfigPath(client, scope), label: client };
|
|
@@ -7319,8 +7554,8 @@ var MAX_CONFIG_BYTES = 256 * 1024;
|
|
|
7319
7554
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".venv", "venv", "vendor", "target", "dist", "build"]);
|
|
7320
7555
|
var readTextOr = (file, fallback = "") => {
|
|
7321
7556
|
try {
|
|
7322
|
-
const
|
|
7323
|
-
if (!
|
|
7557
|
+
const stat4 = statSync8(file);
|
|
7558
|
+
if (!stat4.isFile() || stat4.size > MAX_CONFIG_BYTES) return fallback;
|
|
7324
7559
|
return readFileSync15(file, "utf8");
|
|
7325
7560
|
} catch {
|
|
7326
7561
|
return fallback;
|
|
@@ -7377,9 +7612,9 @@ function gitAttributesHits(root) {
|
|
|
7377
7612
|
}
|
|
7378
7613
|
function runsAsGitHook(file, plat) {
|
|
7379
7614
|
try {
|
|
7380
|
-
const
|
|
7381
|
-
if (!
|
|
7382
|
-
return plat === "win32" || (
|
|
7615
|
+
const stat4 = statSync8(file);
|
|
7616
|
+
if (!stat4.isFile()) return false;
|
|
7617
|
+
return plat === "win32" || (stat4.mode & 73) !== 0;
|
|
7383
7618
|
} catch {
|
|
7384
7619
|
return false;
|
|
7385
7620
|
}
|
|
@@ -8761,11 +8996,7 @@ var McpCloak = class {
|
|
|
8761
8996
|
kind: "refused",
|
|
8762
8997
|
reason: `the result serialises to ${serialised.length} characters, more than the ${MAX_CLOAK_CHARS} Stroq can scan whole`
|
|
8763
8998
|
};
|
|
8764
|
-
const spansByText =
|
|
8765
|
-
for (const text of new Set(collectStrings(result))) {
|
|
8766
|
-
const spans = mergeSpans(await this.opts.detector.detect(text));
|
|
8767
|
-
if (spans.length > 0) spansByText.set(text, spans);
|
|
8768
|
-
}
|
|
8999
|
+
const spansByText = await detectAcrossLeaves(collectKeyedStrings(result), this.opts.detector);
|
|
8769
9000
|
if (spansByText.size === 0) return { kind: "unchanged" };
|
|
8770
9001
|
const requests = [];
|
|
8771
9002
|
for (const spans of spansByText.values()) {
|
|
@@ -9753,7 +9984,9 @@ import { join as join23 } from "path";
|
|
|
9753
9984
|
import { parseArgs as parseArgs8 } from "util";
|
|
9754
9985
|
|
|
9755
9986
|
// src/replay/transcript.ts
|
|
9756
|
-
import {
|
|
9987
|
+
import { createReadStream } from "fs";
|
|
9988
|
+
import { readdir, stat as stat2 } from "fs/promises";
|
|
9989
|
+
import { createInterface } from "readline";
|
|
9757
9990
|
import { homedir as homedir19 } from "os";
|
|
9758
9991
|
import { join as join22 } from "path";
|
|
9759
9992
|
var isRecord2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -9772,24 +10005,24 @@ function blocksOf(record) {
|
|
|
9772
10005
|
const content = message["content"];
|
|
9773
10006
|
return Array.isArray(content) ? content.filter(isRecord2) : [];
|
|
9774
10007
|
}
|
|
9775
|
-
function
|
|
10008
|
+
function createTranscriptParser() {
|
|
9776
10009
|
const events = [];
|
|
9777
10010
|
const pending = /* @__PURE__ */ new Map();
|
|
9778
10011
|
let sessionId = "transcript";
|
|
9779
10012
|
let cwd = null;
|
|
9780
10013
|
let skipped = 0;
|
|
9781
|
-
|
|
9782
|
-
if (line3.trim().length === 0)
|
|
10014
|
+
function push(line3) {
|
|
10015
|
+
if (line3.trim().length === 0) return;
|
|
9783
10016
|
let record;
|
|
9784
10017
|
try {
|
|
9785
10018
|
record = JSON.parse(line3);
|
|
9786
10019
|
} catch {
|
|
9787
10020
|
skipped += 1;
|
|
9788
|
-
|
|
10021
|
+
return;
|
|
9789
10022
|
}
|
|
9790
10023
|
if (!isRecord2(record)) {
|
|
9791
10024
|
skipped += 1;
|
|
9792
|
-
|
|
10025
|
+
return;
|
|
9793
10026
|
}
|
|
9794
10027
|
if (typeof record["sessionId"] === "string") sessionId = record["sessionId"];
|
|
9795
10028
|
if (cwd === null && typeof record["cwd"] === "string") cwd = record["cwd"];
|
|
@@ -9816,7 +10049,19 @@ function parseTranscript(text) {
|
|
|
9816
10049
|
}
|
|
9817
10050
|
}
|
|
9818
10051
|
}
|
|
9819
|
-
return { sessionId, cwd, events, skipped };
|
|
10052
|
+
return { push, finish: () => ({ sessionId, cwd, events, skipped }) };
|
|
10053
|
+
}
|
|
10054
|
+
async function feedLines(path, parser) {
|
|
10055
|
+
const lines = createInterface({
|
|
10056
|
+
input: createReadStream(path, { encoding: "utf8" }),
|
|
10057
|
+
crlfDelay: Infinity
|
|
10058
|
+
});
|
|
10059
|
+
try {
|
|
10060
|
+
for await (const line3 of lines) parser.push(line3);
|
|
10061
|
+
} finally {
|
|
10062
|
+
lines.close();
|
|
10063
|
+
}
|
|
10064
|
+
return parser.finish();
|
|
9820
10065
|
}
|
|
9821
10066
|
function projectSlug(cwd) {
|
|
9822
10067
|
return cwd.replace(/[/.]/g, "-");
|
|
@@ -9856,7 +10101,7 @@ async function listJsonl(dir) {
|
|
|
9856
10101
|
return files;
|
|
9857
10102
|
}
|
|
9858
10103
|
async function readTranscript(path) {
|
|
9859
|
-
return
|
|
10104
|
+
return feedLines(path, createTranscriptParser());
|
|
9860
10105
|
}
|
|
9861
10106
|
|
|
9862
10107
|
// src/commands/replay.ts
|
|
@@ -10523,7 +10768,7 @@ stroq run \u2014 ${invocation.command}${agent === null ? "" : ` (${agent})`}`);
|
|
|
10523
10768
|
}
|
|
10524
10769
|
|
|
10525
10770
|
// src/commands/sent.ts
|
|
10526
|
-
import { homedir as
|
|
10771
|
+
import { homedir as homedir24 } from "os";
|
|
10527
10772
|
import { parseArgs as parseArgs9 } from "util";
|
|
10528
10773
|
|
|
10529
10774
|
// src/sent/format.ts
|
|
@@ -10646,19 +10891,664 @@ function formatSent(report) {
|
|
|
10646
10891
|
}
|
|
10647
10892
|
|
|
10648
10893
|
// src/sent/readers.ts
|
|
10894
|
+
import { open as open2 } from "fs/promises";
|
|
10895
|
+
import { homedir as homedir23 } from "os";
|
|
10896
|
+
|
|
10897
|
+
// src/sent/codex.ts
|
|
10898
|
+
import { open, readdir as readdir2, stat as stat3 } from "fs/promises";
|
|
10899
|
+
import { homedir as homedir21 } from "os";
|
|
10900
|
+
import { join as join26 } from "path";
|
|
10901
|
+
|
|
10902
|
+
// src/sent/codex-literal.ts
|
|
10903
|
+
var MAX_DEPTH = 64;
|
|
10904
|
+
var Unreadable = class extends Error {
|
|
10905
|
+
};
|
|
10906
|
+
function fail() {
|
|
10907
|
+
throw new Unreadable();
|
|
10908
|
+
}
|
|
10909
|
+
function readObjectLiteral(src, start) {
|
|
10910
|
+
let i = start;
|
|
10911
|
+
let depth = 0;
|
|
10912
|
+
const skip = () => {
|
|
10913
|
+
while (i < src.length && (src[i] === "," || /\s/.test(src[i]))) i += 1;
|
|
10914
|
+
};
|
|
10915
|
+
const skipSpace = () => {
|
|
10916
|
+
while (i < src.length && /\s/.test(src[i])) i += 1;
|
|
10917
|
+
};
|
|
10918
|
+
const ESCAPES = {
|
|
10919
|
+
n: "\n",
|
|
10920
|
+
t: " ",
|
|
10921
|
+
r: "\r",
|
|
10922
|
+
b: "\b",
|
|
10923
|
+
f: "\f",
|
|
10924
|
+
v: "\v",
|
|
10925
|
+
"0": "\0"
|
|
10926
|
+
};
|
|
10927
|
+
function readString(quote) {
|
|
10928
|
+
i += 1;
|
|
10929
|
+
let out = "";
|
|
10930
|
+
while (i < src.length) {
|
|
10931
|
+
const c = src[i];
|
|
10932
|
+
if (c === "\\") {
|
|
10933
|
+
const next = src[i + 1];
|
|
10934
|
+
if (next === void 0) fail();
|
|
10935
|
+
if (next === "\n") {
|
|
10936
|
+
i += 2;
|
|
10937
|
+
continue;
|
|
10938
|
+
}
|
|
10939
|
+
if (next === "u") {
|
|
10940
|
+
if (src[i + 2] === "{") {
|
|
10941
|
+
const close = src.indexOf("}", i + 3);
|
|
10942
|
+
if (close < 0) fail();
|
|
10943
|
+
const code2 = Number.parseInt(src.slice(i + 3, close), 16);
|
|
10944
|
+
if (Number.isNaN(code2)) fail();
|
|
10945
|
+
out += String.fromCodePoint(code2);
|
|
10946
|
+
i = close + 1;
|
|
10947
|
+
continue;
|
|
10948
|
+
}
|
|
10949
|
+
const code = Number.parseInt(src.slice(i + 2, i + 6), 16);
|
|
10950
|
+
if (Number.isNaN(code)) fail();
|
|
10951
|
+
out += String.fromCharCode(code);
|
|
10952
|
+
i += 6;
|
|
10953
|
+
continue;
|
|
10954
|
+
}
|
|
10955
|
+
if (next === "x") {
|
|
10956
|
+
const code = Number.parseInt(src.slice(i + 2, i + 4), 16);
|
|
10957
|
+
if (Number.isNaN(code)) fail();
|
|
10958
|
+
out += String.fromCharCode(code);
|
|
10959
|
+
i += 4;
|
|
10960
|
+
continue;
|
|
10961
|
+
}
|
|
10962
|
+
out += ESCAPES[next] ?? next;
|
|
10963
|
+
i += 2;
|
|
10964
|
+
continue;
|
|
10965
|
+
}
|
|
10966
|
+
if (c === quote) {
|
|
10967
|
+
i += 1;
|
|
10968
|
+
return out;
|
|
10969
|
+
}
|
|
10970
|
+
if (quote === "`" && c === "$" && src[i + 1] === "{") {
|
|
10971
|
+
let braces = 0;
|
|
10972
|
+
const from = i;
|
|
10973
|
+
while (i < src.length) {
|
|
10974
|
+
const t = src[i];
|
|
10975
|
+
if (t === "{") braces += 1;
|
|
10976
|
+
else if (t === "}") {
|
|
10977
|
+
braces -= 1;
|
|
10978
|
+
if (braces === 0) {
|
|
10979
|
+
i += 1;
|
|
10980
|
+
break;
|
|
10981
|
+
}
|
|
10982
|
+
}
|
|
10983
|
+
i += 1;
|
|
10984
|
+
}
|
|
10985
|
+
if (braces !== 0) fail();
|
|
10986
|
+
out += src.slice(from, i);
|
|
10987
|
+
continue;
|
|
10988
|
+
}
|
|
10989
|
+
out += c;
|
|
10990
|
+
i += 1;
|
|
10991
|
+
}
|
|
10992
|
+
return fail();
|
|
10993
|
+
}
|
|
10994
|
+
function readKey() {
|
|
10995
|
+
skip();
|
|
10996
|
+
const c = src[i];
|
|
10997
|
+
if (c === '"' || c === "'" || c === "`") return readString(c);
|
|
10998
|
+
const name = /^[A-Za-z_$][\w$]*/.exec(src.slice(i));
|
|
10999
|
+
if (!name) fail();
|
|
11000
|
+
i += name[0].length;
|
|
11001
|
+
return name[0];
|
|
11002
|
+
}
|
|
11003
|
+
function readValue() {
|
|
11004
|
+
skip();
|
|
11005
|
+
if (i >= src.length) fail();
|
|
11006
|
+
const c = src[i];
|
|
11007
|
+
if (c === "{") return readObject();
|
|
11008
|
+
if (c === "[") return readArray();
|
|
11009
|
+
if (c === '"' || c === "'" || c === "`") return readString(c);
|
|
11010
|
+
const rest = src.slice(i);
|
|
11011
|
+
for (const [word, value] of [
|
|
11012
|
+
["true", true],
|
|
11013
|
+
["false", false],
|
|
11014
|
+
["null", null],
|
|
11015
|
+
["undefined", null]
|
|
11016
|
+
]) {
|
|
11017
|
+
if (rest.startsWith(word) && !/[\w$]/.test(rest[word.length] ?? "")) {
|
|
11018
|
+
i += word.length;
|
|
11019
|
+
return value;
|
|
11020
|
+
}
|
|
11021
|
+
}
|
|
11022
|
+
const num = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(rest);
|
|
11023
|
+
if (num) {
|
|
11024
|
+
i += num[0].length;
|
|
11025
|
+
return Number(num[0]);
|
|
11026
|
+
}
|
|
11027
|
+
return fail();
|
|
11028
|
+
}
|
|
11029
|
+
function readObject() {
|
|
11030
|
+
depth += 1;
|
|
11031
|
+
if (depth > MAX_DEPTH) fail();
|
|
11032
|
+
i += 1;
|
|
11033
|
+
const out = {};
|
|
11034
|
+
for (; ; ) {
|
|
11035
|
+
skip();
|
|
11036
|
+
if (i >= src.length) fail();
|
|
11037
|
+
if (src[i] === "}") {
|
|
11038
|
+
i += 1;
|
|
11039
|
+
depth -= 1;
|
|
11040
|
+
return out;
|
|
11041
|
+
}
|
|
11042
|
+
if (src.startsWith("...", i)) {
|
|
11043
|
+
i += 3;
|
|
11044
|
+
const name = /^[A-Za-z_$][\w$.]*/.exec(src.slice(i));
|
|
11045
|
+
if (!name) fail();
|
|
11046
|
+
i += name[0].length;
|
|
11047
|
+
continue;
|
|
11048
|
+
}
|
|
11049
|
+
const key2 = readKey();
|
|
11050
|
+
skipSpace();
|
|
11051
|
+
if (src[i] === ":") {
|
|
11052
|
+
i += 1;
|
|
11053
|
+
out[key2] = readValue();
|
|
11054
|
+
continue;
|
|
11055
|
+
}
|
|
11056
|
+
if (src[i] === "}" || src[i] === ",") {
|
|
11057
|
+
out[key2] = null;
|
|
11058
|
+
continue;
|
|
11059
|
+
}
|
|
11060
|
+
fail();
|
|
11061
|
+
}
|
|
11062
|
+
}
|
|
11063
|
+
function readArray() {
|
|
11064
|
+
depth += 1;
|
|
11065
|
+
if (depth > MAX_DEPTH) fail();
|
|
11066
|
+
i += 1;
|
|
11067
|
+
const out = [];
|
|
11068
|
+
for (; ; ) {
|
|
11069
|
+
skip();
|
|
11070
|
+
if (i >= src.length) fail();
|
|
11071
|
+
if (src[i] === "]") {
|
|
11072
|
+
i += 1;
|
|
11073
|
+
depth -= 1;
|
|
11074
|
+
return out;
|
|
11075
|
+
}
|
|
11076
|
+
out.push(readValue());
|
|
11077
|
+
}
|
|
11078
|
+
}
|
|
11079
|
+
const opening = src[start];
|
|
11080
|
+
if (opening !== "{" && opening !== "[") return null;
|
|
11081
|
+
try {
|
|
11082
|
+
const value = readValue();
|
|
11083
|
+
return { value, end: i };
|
|
11084
|
+
} catch (err) {
|
|
11085
|
+
if (err instanceof Unreadable) return null;
|
|
11086
|
+
if (err instanceof RangeError) return null;
|
|
11087
|
+
throw err;
|
|
11088
|
+
}
|
|
11089
|
+
}
|
|
11090
|
+
|
|
11091
|
+
// src/sent/codex.ts
|
|
11092
|
+
var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11093
|
+
function outputText(value) {
|
|
11094
|
+
if (typeof value === "string") return value;
|
|
11095
|
+
if (!Array.isArray(value)) return "";
|
|
11096
|
+
return value.map((block) => {
|
|
11097
|
+
if (typeof block === "string") return block;
|
|
11098
|
+
if (isRecord3(block) && typeof block["text"] === "string") return block["text"];
|
|
11099
|
+
return "";
|
|
11100
|
+
}).filter((t) => t.length > 0).join("");
|
|
11101
|
+
}
|
|
11102
|
+
var TOOL_CALL = /\btools\.([A-Za-z_$][\w$]*)\s*\(/g;
|
|
11103
|
+
function invocationsIn(script) {
|
|
11104
|
+
const found = [];
|
|
11105
|
+
TOOL_CALL.lastIndex = 0;
|
|
11106
|
+
for (let m = TOOL_CALL.exec(script); m !== null; m = TOOL_CALL.exec(script)) {
|
|
11107
|
+
const afterParen = m.index + m[0].length;
|
|
11108
|
+
const brace = script.indexOf("{", afterParen - 1);
|
|
11109
|
+
const literal = brace >= 0 && brace <= afterParen + 2 ? readObjectLiteral(script, brace) : null;
|
|
11110
|
+
const args = isRecord3(literal?.value) ? { ...literal.value } : { raw: script };
|
|
11111
|
+
found.push({ tool: m[1], input: args });
|
|
11112
|
+
}
|
|
11113
|
+
return found;
|
|
11114
|
+
}
|
|
11115
|
+
function normalized(mapped, input) {
|
|
11116
|
+
if (mapped !== "Bash" || typeof input["command"] === "string") return input;
|
|
11117
|
+
const command = commandOf(input);
|
|
11118
|
+
return command === "" ? input : { ...input, command };
|
|
11119
|
+
}
|
|
11120
|
+
function eventsFor(callId, rawTool, rawInput, at) {
|
|
11121
|
+
const invocations = isPatchTool(rawTool) ? [{ tool: rawTool, input: { input: rawInput } }] : invocationsIn(rawInput);
|
|
11122
|
+
const list = invocations.length > 0 ? invocations : [{ tool: rawTool, input: { raw: rawInput } }];
|
|
11123
|
+
return list.map((inv, n) => {
|
|
11124
|
+
const tool = codexToolName(inv.tool);
|
|
11125
|
+
return {
|
|
11126
|
+
kind: "pre",
|
|
11127
|
+
/* One script, several calls: the ids stay distinct so the output can attach to
|
|
11128
|
+
exactly one of them, and the first keeps the recorded `call_id` so the
|
|
11129
|
+
common single-call case reads as the rollout wrote it. */
|
|
11130
|
+
id: n === 0 ? callId : `${callId}#${n}`,
|
|
11131
|
+
tool,
|
|
11132
|
+
input: normalized(tool, inv.input),
|
|
11133
|
+
at
|
|
11134
|
+
};
|
|
11135
|
+
});
|
|
11136
|
+
}
|
|
11137
|
+
function createCodexParser() {
|
|
11138
|
+
const events = [];
|
|
11139
|
+
const pending = /* @__PURE__ */ new Map();
|
|
11140
|
+
let sessionId = "transcript";
|
|
11141
|
+
let cwd = null;
|
|
11142
|
+
let skipped = 0;
|
|
11143
|
+
function push(line3) {
|
|
11144
|
+
if (line3.trim().length === 0) return;
|
|
11145
|
+
let envelope2;
|
|
11146
|
+
try {
|
|
11147
|
+
envelope2 = JSON.parse(line3);
|
|
11148
|
+
} catch {
|
|
11149
|
+
skipped += 1;
|
|
11150
|
+
return;
|
|
11151
|
+
}
|
|
11152
|
+
if (!isRecord3(envelope2)) {
|
|
11153
|
+
skipped += 1;
|
|
11154
|
+
return;
|
|
11155
|
+
}
|
|
11156
|
+
const at = typeof envelope2["timestamp"] === "string" ? envelope2["timestamp"] : "";
|
|
11157
|
+
const payload = isRecord3(envelope2["payload"]) ? envelope2["payload"] : envelope2;
|
|
11158
|
+
if (envelope2["type"] === "session_meta") {
|
|
11159
|
+
if (typeof payload["session_id"] === "string") sessionId = payload["session_id"];
|
|
11160
|
+
if (typeof payload["cwd"] === "string") cwd = payload["cwd"];
|
|
11161
|
+
return;
|
|
11162
|
+
}
|
|
11163
|
+
if (cwd === null && typeof payload["cwd"] === "string") cwd = payload["cwd"];
|
|
11164
|
+
const kind = payload["type"];
|
|
11165
|
+
const callId = payload["call_id"];
|
|
11166
|
+
if (typeof callId !== "string") return;
|
|
11167
|
+
if (kind === "custom_tool_call" || kind === "function_call") {
|
|
11168
|
+
const rawTool = typeof payload["name"] === "string" ? payload["name"] : "unknown";
|
|
11169
|
+
if (kind === "function_call") {
|
|
11170
|
+
let parsed = null;
|
|
11171
|
+
if (typeof payload["arguments"] === "string") {
|
|
11172
|
+
try {
|
|
11173
|
+
parsed = JSON.parse(payload["arguments"]);
|
|
11174
|
+
} catch {
|
|
11175
|
+
parsed = { raw: payload["arguments"] };
|
|
11176
|
+
}
|
|
11177
|
+
}
|
|
11178
|
+
const tool = codexToolName(rawTool);
|
|
11179
|
+
const input = isRecord3(parsed) ? parsed : {};
|
|
11180
|
+
const event = {
|
|
11181
|
+
kind: "pre",
|
|
11182
|
+
id: callId,
|
|
11183
|
+
tool,
|
|
11184
|
+
input: normalized(tool, input),
|
|
11185
|
+
at
|
|
11186
|
+
};
|
|
11187
|
+
events.push(event);
|
|
11188
|
+
pending.set(callId, { id: event.id, tool: event.tool, input: event.input });
|
|
11189
|
+
return;
|
|
11190
|
+
}
|
|
11191
|
+
const script = typeof payload["input"] === "string" ? payload["input"] : "";
|
|
11192
|
+
const produced = eventsFor(callId, rawTool, script, at);
|
|
11193
|
+
events.push(...produced);
|
|
11194
|
+
const last = produced[produced.length - 1];
|
|
11195
|
+
if (last) pending.set(callId, { id: last.id, tool: last.tool, input: last.input });
|
|
11196
|
+
return;
|
|
11197
|
+
}
|
|
11198
|
+
if (kind === "custom_tool_call_output" || kind === "function_call_output") {
|
|
11199
|
+
const call = pending.get(callId);
|
|
11200
|
+
if (!call) return;
|
|
11201
|
+
events.push({
|
|
11202
|
+
kind: "post",
|
|
11203
|
+
id: call.id,
|
|
11204
|
+
tool: call.tool,
|
|
11205
|
+
input: call.input,
|
|
11206
|
+
resultText: outputText(payload["output"]),
|
|
11207
|
+
at
|
|
11208
|
+
});
|
|
11209
|
+
}
|
|
11210
|
+
}
|
|
11211
|
+
return { push, finish: () => ({ sessionId, cwd, events, skipped }) };
|
|
11212
|
+
}
|
|
11213
|
+
var codexRoot = (home = homedir21()) => join26(home, ".codex", "sessions");
|
|
11214
|
+
var META_BYTES = 64 * 1024;
|
|
11215
|
+
async function rolloutCwd(path) {
|
|
11216
|
+
let head;
|
|
11217
|
+
try {
|
|
11218
|
+
const handle = await open(path, "r");
|
|
11219
|
+
try {
|
|
11220
|
+
const buffer = Buffer.alloc(META_BYTES);
|
|
11221
|
+
const { bytesRead } = await handle.read(buffer, 0, META_BYTES, 0);
|
|
11222
|
+
head = buffer.subarray(0, bytesRead).toString("utf8");
|
|
11223
|
+
} finally {
|
|
11224
|
+
await handle.close();
|
|
11225
|
+
}
|
|
11226
|
+
} catch {
|
|
11227
|
+
return null;
|
|
11228
|
+
}
|
|
11229
|
+
const first = head.split("\n")[0];
|
|
11230
|
+
if (first === void 0 || first.trim() === "") return null;
|
|
11231
|
+
try {
|
|
11232
|
+
const envelope2 = JSON.parse(first);
|
|
11233
|
+
if (!isRecord3(envelope2)) return null;
|
|
11234
|
+
const payload = isRecord3(envelope2["payload"]) ? envelope2["payload"] : envelope2;
|
|
11235
|
+
return typeof payload["cwd"] === "string" ? payload["cwd"] : null;
|
|
11236
|
+
} catch {
|
|
11237
|
+
return null;
|
|
11238
|
+
}
|
|
11239
|
+
}
|
|
11240
|
+
async function everyRollout(root) {
|
|
11241
|
+
const entries = await readdir2(root, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
11242
|
+
const files = [];
|
|
11243
|
+
for (const entry2 of entries) {
|
|
11244
|
+
if (!entry2.isFile()) continue;
|
|
11245
|
+
if (!entry2.name.startsWith("rollout-") || !entry2.name.endsWith(".jsonl")) continue;
|
|
11246
|
+
const path = join26(entry2.parentPath, entry2.name);
|
|
11247
|
+
try {
|
|
11248
|
+
files.push({ path, mtimeMs: (await stat3(path)).mtimeMs });
|
|
11249
|
+
} catch {
|
|
11250
|
+
}
|
|
11251
|
+
}
|
|
11252
|
+
return files.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
11253
|
+
}
|
|
11254
|
+
async function findCodexRollouts(cwd, home) {
|
|
11255
|
+
const all = await everyRollout(codexRoot(home));
|
|
11256
|
+
const here = [];
|
|
11257
|
+
for (const file of all) {
|
|
11258
|
+
if (await rolloutCwd(file.path) === cwd) here.push(file);
|
|
11259
|
+
}
|
|
11260
|
+
return here.length > 0 ? here : all;
|
|
11261
|
+
}
|
|
11262
|
+
async function readCodexRollout(path) {
|
|
11263
|
+
return feedLines(path, createCodexParser());
|
|
11264
|
+
}
|
|
11265
|
+
|
|
11266
|
+
// src/sent/cursor.ts
|
|
11267
|
+
import { homedir as homedir22 } from "os";
|
|
11268
|
+
import { join as join27 } from "path";
|
|
11269
|
+
var TOOL_NAMES = {
|
|
11270
|
+
run_terminal_command_v2: "Bash",
|
|
11271
|
+
run_terminal_command: "Bash",
|
|
11272
|
+
read_file_v2: "Read",
|
|
11273
|
+
read_file: "Read",
|
|
11274
|
+
edit_file_v2: "Write",
|
|
11275
|
+
edit_file: "Write",
|
|
11276
|
+
search_replace: "Write",
|
|
11277
|
+
create_file: "Write",
|
|
11278
|
+
delete_file: "Write"
|
|
11279
|
+
};
|
|
11280
|
+
var PATH_FIELDS2 = ["targetFile", "effectiveUri", "path", "targetDirectory", "absolutePath"];
|
|
11281
|
+
var RESULT_FIELDS = ["output", "contents", "result"];
|
|
11282
|
+
function parseJson(value) {
|
|
11283
|
+
if (typeof value !== "string" || value === "") return null;
|
|
11284
|
+
try {
|
|
11285
|
+
return JSON.parse(value);
|
|
11286
|
+
} catch {
|
|
11287
|
+
return null;
|
|
11288
|
+
}
|
|
11289
|
+
}
|
|
11290
|
+
function mcpPair(params) {
|
|
11291
|
+
if (!isRecord(params)) return null;
|
|
11292
|
+
const tools = params["tools"];
|
|
11293
|
+
if (!Array.isArray(tools)) return null;
|
|
11294
|
+
const first = tools[0];
|
|
11295
|
+
if (!isRecord(first)) return null;
|
|
11296
|
+
const tool = first["name"];
|
|
11297
|
+
const server = first["serverName"];
|
|
11298
|
+
if (typeof tool !== "string" || typeof server !== "string") return null;
|
|
11299
|
+
return { server, tool, args: first["parameters"] };
|
|
11300
|
+
}
|
|
11301
|
+
function callOf(name, params) {
|
|
11302
|
+
const mcp = mcpPair(params);
|
|
11303
|
+
if (mcp !== null) {
|
|
11304
|
+
return {
|
|
11305
|
+
tool: mcpToolName(mcp.server, mcp.tool),
|
|
11306
|
+
input: toolInputRecord(mcp.args),
|
|
11307
|
+
paths: []
|
|
11308
|
+
};
|
|
11309
|
+
}
|
|
11310
|
+
const record = isRecord(params) ? params : {};
|
|
11311
|
+
const paths = PATH_FIELDS2.map((field) => record[field]).filter(
|
|
11312
|
+
(value) => typeof value === "string" && value.startsWith("/")
|
|
11313
|
+
);
|
|
11314
|
+
const mapped = TOOL_NAMES[name] ?? name;
|
|
11315
|
+
if (mapped === "Bash") {
|
|
11316
|
+
const command = record["command"];
|
|
11317
|
+
return { tool: mapped, input: { command: typeof command === "string" ? command : "" }, paths };
|
|
11318
|
+
}
|
|
11319
|
+
if (mapped === "Read" || mapped === "Write") {
|
|
11320
|
+
const file = record["targetFile"] ?? record["relativeWorkspacePath"] ?? record["path"] ?? record["uri"];
|
|
11321
|
+
return {
|
|
11322
|
+
tool: mapped,
|
|
11323
|
+
input: { file_path: typeof file === "string" ? file : "" },
|
|
11324
|
+
paths
|
|
11325
|
+
};
|
|
11326
|
+
}
|
|
11327
|
+
return { tool: mapped, input: record, paths };
|
|
11328
|
+
}
|
|
11329
|
+
function resultTextOf(raw) {
|
|
11330
|
+
if (typeof raw !== "string" || raw === "") return null;
|
|
11331
|
+
const parsed = parseJson(raw);
|
|
11332
|
+
if (isRecord(parsed)) {
|
|
11333
|
+
for (const field of RESULT_FIELDS) {
|
|
11334
|
+
const value = parsed[field];
|
|
11335
|
+
if (typeof value === "string") return value;
|
|
11336
|
+
}
|
|
11337
|
+
}
|
|
11338
|
+
return raw;
|
|
11339
|
+
}
|
|
11340
|
+
function parseCursorStore(rows) {
|
|
11341
|
+
const bySession = /* @__PURE__ */ new Map();
|
|
11342
|
+
for (const row2 of rows) {
|
|
11343
|
+
const parts = row2.key.split(":");
|
|
11344
|
+
if (parts.length < 3 || parts[0] !== "bubbleId") continue;
|
|
11345
|
+
const id = parts[1] ?? "";
|
|
11346
|
+
if (id === "") continue;
|
|
11347
|
+
let session = bySession.get(id);
|
|
11348
|
+
if (session === void 0) {
|
|
11349
|
+
session = { events: [], roots: [], latest: 0, skipped: 0 };
|
|
11350
|
+
bySession.set(id, session);
|
|
11351
|
+
}
|
|
11352
|
+
const bubble = parseJson(row2.value);
|
|
11353
|
+
if (!isRecord(bubble)) {
|
|
11354
|
+
session.skipped += 1;
|
|
11355
|
+
continue;
|
|
11356
|
+
}
|
|
11357
|
+
const at = typeof bubble["createdAt"] === "string" ? bubble["createdAt"] : "";
|
|
11358
|
+
const moment = Date.parse(at);
|
|
11359
|
+
if (Number.isFinite(moment)) session.latest = Math.max(session.latest, moment);
|
|
11360
|
+
const former = bubble["toolFormerData"];
|
|
11361
|
+
if (!isRecord(former)) continue;
|
|
11362
|
+
const name = typeof former["name"] === "string" ? former["name"] : "";
|
|
11363
|
+
if (name === "") continue;
|
|
11364
|
+
const call = callOf(name, parseJson(former["params"]));
|
|
11365
|
+
for (const path of call.paths) if (!session.roots.includes(path)) session.roots.push(path);
|
|
11366
|
+
const callId = typeof former["toolCallId"] === "string" && former["toolCallId"] !== "" ? former["toolCallId"] : parts.slice(2).join(":") ?? "";
|
|
11367
|
+
session.events.push({ kind: "pre", id: callId, tool: call.tool, input: call.input, at });
|
|
11368
|
+
const text = resultTextOf(former["result"]);
|
|
11369
|
+
if (text === null) continue;
|
|
11370
|
+
session.events.push({
|
|
11371
|
+
kind: "post",
|
|
11372
|
+
id: callId,
|
|
11373
|
+
tool: call.tool,
|
|
11374
|
+
input: call.input,
|
|
11375
|
+
resultText: text,
|
|
11376
|
+
at
|
|
11377
|
+
});
|
|
11378
|
+
}
|
|
11379
|
+
return [...bySession.entries()].map(([id, s]) => ({
|
|
11380
|
+
id,
|
|
11381
|
+
mtimeMs: s.latest,
|
|
11382
|
+
roots: s.roots,
|
|
11383
|
+
transcript: {
|
|
11384
|
+
sessionId: id,
|
|
11385
|
+
cwd: s.roots[0] ?? null,
|
|
11386
|
+
events: s.events,
|
|
11387
|
+
skipped: s.skipped
|
|
11388
|
+
}
|
|
11389
|
+
})).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
11390
|
+
}
|
|
11391
|
+
function cursorStateDb(plat = process.platform, env = process.env, home = homedir22()) {
|
|
11392
|
+
const base = plat === "darwin" ? join27(home, "Library", "Application Support", "Cursor") : plat === "win32" ? join27(env["APPDATA"] ?? join27(home, "AppData", "Roaming"), "Cursor") : join27(home, ".config", "Cursor");
|
|
11393
|
+
return join27(base, "User", "globalStorage", "state.vscdb");
|
|
11394
|
+
}
|
|
11395
|
+
var cursorSessionPath = (db, session) => `${db}#${session}`;
|
|
11396
|
+
function splitCursorSessionPath(path) {
|
|
11397
|
+
const hash = path.lastIndexOf("#");
|
|
11398
|
+
if (hash <= 0) return { db: path, session: null };
|
|
11399
|
+
return { db: path.slice(0, hash), session: path.slice(hash + 1) };
|
|
11400
|
+
}
|
|
11401
|
+
var sqlitePromise = null;
|
|
11402
|
+
async function sqlite() {
|
|
11403
|
+
sqlitePromise ??= (async () => {
|
|
11404
|
+
const emit2 = process.emitWarning;
|
|
11405
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
11406
|
+
const name = typeof warning === "string" ? String(rest[0] ?? "") : warning.name;
|
|
11407
|
+
const text = typeof warning === "string" ? warning : warning.message;
|
|
11408
|
+
if (name === "ExperimentalWarning" && text.includes("SQLite")) return;
|
|
11409
|
+
emit2.call(process, warning, ...rest);
|
|
11410
|
+
});
|
|
11411
|
+
try {
|
|
11412
|
+
return await import("sqlite");
|
|
11413
|
+
} catch {
|
|
11414
|
+
return null;
|
|
11415
|
+
} finally {
|
|
11416
|
+
process.emitWarning = emit2;
|
|
11417
|
+
}
|
|
11418
|
+
})();
|
|
11419
|
+
return sqlitePromise;
|
|
11420
|
+
}
|
|
11421
|
+
async function cursorUnavailable() {
|
|
11422
|
+
if (await sqlite() !== null) return null;
|
|
11423
|
+
return `Cursor sessions need node:sqlite, which ${process.version} does not have \u2014 Node 22.23 or newer reads them`;
|
|
11424
|
+
}
|
|
11425
|
+
async function storeRows(db, session) {
|
|
11426
|
+
const mod = await sqlite();
|
|
11427
|
+
if (mod === null) return [];
|
|
11428
|
+
let handle;
|
|
11429
|
+
try {
|
|
11430
|
+
handle = new mod.DatabaseSync(db, { readOnly: true });
|
|
11431
|
+
} catch {
|
|
11432
|
+
return [];
|
|
11433
|
+
}
|
|
11434
|
+
try {
|
|
11435
|
+
const like = session === null ? "bubbleId:%" : `bubbleId:${session}:%`;
|
|
11436
|
+
const rows = handle.prepare("select key, value from cursorDiskKV where key like ?").all(like);
|
|
11437
|
+
return rows.flatMap((row2) => {
|
|
11438
|
+
const { key: key2, value } = row2;
|
|
11439
|
+
return typeof key2 === "string" && typeof value === "string" ? [{ key: key2, value }] : [];
|
|
11440
|
+
});
|
|
11441
|
+
} catch {
|
|
11442
|
+
return [];
|
|
11443
|
+
} finally {
|
|
11444
|
+
handle.close();
|
|
11445
|
+
}
|
|
11446
|
+
}
|
|
11447
|
+
function isUnder(path, dir) {
|
|
11448
|
+
if (dir === "") return false;
|
|
11449
|
+
const base = dir.endsWith("/") ? dir.slice(0, -1) : dir;
|
|
11450
|
+
return path === base || path.startsWith(`${base}/`);
|
|
11451
|
+
}
|
|
11452
|
+
async function findCursorSessions(cwd, db) {
|
|
11453
|
+
const store = db ?? cursorStateDb();
|
|
11454
|
+
const sessions = parseCursorStore(await storeRows(store, null));
|
|
11455
|
+
const files = (list) => list.map((s) => ({ path: cursorSessionPath(store, s.id), mtimeMs: s.mtimeMs }));
|
|
11456
|
+
const here = sessions.filter((s) => s.roots.some((root) => isUnder(root, cwd)));
|
|
11457
|
+
return here.length > 0 ? files(here) : files(sessions);
|
|
11458
|
+
}
|
|
11459
|
+
async function readCursorSession(path) {
|
|
11460
|
+
const { db, session } = splitCursorSessionPath(path);
|
|
11461
|
+
const sessions = parseCursorStore(await storeRows(db, session));
|
|
11462
|
+
const found = session === null ? sessions[0] : sessions.find((s) => s.id === session);
|
|
11463
|
+
return found?.transcript ?? { sessionId: session ?? "", cwd: null, events: [], skipped: 0 };
|
|
11464
|
+
}
|
|
11465
|
+
|
|
11466
|
+
// src/sent/readers.ts
|
|
11467
|
+
var HEAD_BYTES = 64 * 1024;
|
|
10649
11468
|
var claudeCodeReader = {
|
|
10650
11469
|
agent: "claude-code",
|
|
10651
11470
|
find: findTranscripts,
|
|
10652
|
-
read: readTranscript
|
|
11471
|
+
read: readTranscript,
|
|
11472
|
+
root: "~/.claude/projects",
|
|
11473
|
+
/* A Claude record is the message itself: `{type:'user'|'assistant', message:{…}}`,
|
|
11474
|
+
with `sessionId` on nearly every line. A Codex record is an envelope around a
|
|
11475
|
+
payload and has neither. */
|
|
11476
|
+
claims: (head) => {
|
|
11477
|
+
const record = tryParse(head);
|
|
11478
|
+
if (!isRecord4(record)) return false;
|
|
11479
|
+
return "message" in record || "sessionId" in record;
|
|
11480
|
+
}
|
|
11481
|
+
};
|
|
11482
|
+
var codexReader = {
|
|
11483
|
+
agent: "codex",
|
|
11484
|
+
find: (cwd) => findCodexRollouts(cwd),
|
|
11485
|
+
read: readCodexRollout,
|
|
11486
|
+
root: "~/.codex/sessions",
|
|
11487
|
+
/* Codex wraps every record: `{timestamp, ordinal, type, payload}`. The envelope
|
|
11488
|
+
is the format, so its presence is the test. */
|
|
11489
|
+
claims: (head) => {
|
|
11490
|
+
const record = tryParse(head);
|
|
11491
|
+
return isRecord4(record) && isRecord4(record["payload"]) && typeof record["type"] === "string";
|
|
11492
|
+
}
|
|
11493
|
+
};
|
|
11494
|
+
var cursorReader = {
|
|
11495
|
+
agent: "cursor",
|
|
11496
|
+
find: (cwd) => findCursorSessions(cwd),
|
|
11497
|
+
read: readCursorSession,
|
|
11498
|
+
root: tilde(cursorStateDb()),
|
|
11499
|
+
unavailable: cursorUnavailable,
|
|
11500
|
+
/* A SQLite file announces itself in its first 16 bytes, and that is all the
|
|
11501
|
+
dispatch needs: no other agent here writes one. */
|
|
11502
|
+
claims: (head) => head.startsWith("SQLite format 3")
|
|
10653
11503
|
};
|
|
10654
|
-
var READERS = [claudeCodeReader];
|
|
11504
|
+
var READERS = [claudeCodeReader, codexReader, cursorReader];
|
|
11505
|
+
async function readerNotices() {
|
|
11506
|
+
const notices = [];
|
|
11507
|
+
for (const reader of READERS) {
|
|
11508
|
+
const why = await reader.unavailable?.();
|
|
11509
|
+
if (why !== void 0 && why !== null) notices.push(why);
|
|
11510
|
+
}
|
|
11511
|
+
return notices;
|
|
11512
|
+
}
|
|
11513
|
+
function tilde(path) {
|
|
11514
|
+
const home = homedir23();
|
|
11515
|
+
return home !== "" && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
11516
|
+
}
|
|
11517
|
+
var isRecord4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11518
|
+
function tryParse(line3) {
|
|
11519
|
+
try {
|
|
11520
|
+
return JSON.parse(line3);
|
|
11521
|
+
} catch {
|
|
11522
|
+
return null;
|
|
11523
|
+
}
|
|
11524
|
+
}
|
|
11525
|
+
var READER_ROOTS = () => READERS.map((r) => r.root).join(" or ");
|
|
11526
|
+
async function readerForFile(path) {
|
|
11527
|
+
let head = "";
|
|
11528
|
+
try {
|
|
11529
|
+
const handle = await open2(splitCursorSessionPath(path).db, "r");
|
|
11530
|
+
try {
|
|
11531
|
+
const buffer = Buffer.alloc(HEAD_BYTES);
|
|
11532
|
+
const { bytesRead } = await handle.read(buffer, 0, HEAD_BYTES, 0);
|
|
11533
|
+
head = buffer.subarray(0, bytesRead).toString("utf8").split("\n")[0] ?? "";
|
|
11534
|
+
} finally {
|
|
11535
|
+
await handle.close();
|
|
11536
|
+
}
|
|
11537
|
+
} catch {
|
|
11538
|
+
return claudeCodeReader;
|
|
11539
|
+
}
|
|
11540
|
+
return READERS.find((reader) => reader.claims(head)) ?? claudeCodeReader;
|
|
11541
|
+
}
|
|
10655
11542
|
async function newestTranscript(cwd) {
|
|
11543
|
+
let best = null;
|
|
10656
11544
|
for (const reader of READERS) {
|
|
10657
|
-
const
|
|
10658
|
-
|
|
10659
|
-
if (
|
|
11545
|
+
const newest = (await reader.find(cwd))[0];
|
|
11546
|
+
if (!newest) continue;
|
|
11547
|
+
if (best === null || newest.mtimeMs > best.mtimeMs) {
|
|
11548
|
+
best = { reader, path: newest.path, mtimeMs: newest.mtimeMs };
|
|
11549
|
+
}
|
|
10660
11550
|
}
|
|
10661
|
-
return null;
|
|
11551
|
+
return best === null ? null : { reader: best.reader, path: best.path };
|
|
10662
11552
|
}
|
|
10663
11553
|
|
|
10664
11554
|
// src/sent/scan.ts
|
|
@@ -10882,7 +11772,7 @@ function emit(report, out) {
|
|
|
10882
11772
|
var USAGE = `stroq sent \u2014 which of your credentials already reached a model provider
|
|
10883
11773
|
|
|
10884
11774
|
stroq sent --last read the newest session in this directory
|
|
10885
|
-
stroq sent --transcript <path> read a specific transcript
|
|
11775
|
+
stroq sent --transcript <path> read a specific transcript or rollout
|
|
10886
11776
|
stroq sent [<session-id>] read a session Stroq itself recorded
|
|
10887
11777
|
|
|
10888
11778
|
Flags:
|
|
@@ -10890,9 +11780,10 @@ Flags:
|
|
|
10890
11780
|
--fail-on-finding exit 1 when a credential is found (for a scheduled job)
|
|
10891
11781
|
-h, --help show this
|
|
10892
11782
|
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
|
|
11783
|
+
Reads Claude Code transcripts and Codex CLI rollouts; --transcript works out
|
|
11784
|
+
which from the file itself. It reads this machine's credential files to know what
|
|
11785
|
+
to look for, and prints names and sources only, never a value. A finding exits 0
|
|
11786
|
+
by default: a session that already happened cannot be un-sent by today's commit.
|
|
10896
11787
|
`;
|
|
10897
11788
|
async function runSent(args) {
|
|
10898
11789
|
let parsed;
|
|
@@ -10924,7 +11815,7 @@ ${USAGE}`);
|
|
|
10924
11815
|
failOnFinding: values["fail-on-finding"] === true
|
|
10925
11816
|
};
|
|
10926
11817
|
const cwd = process.cwd();
|
|
10927
|
-
const home =
|
|
11818
|
+
const home = homedir24();
|
|
10928
11819
|
const index = new FileSecretIndex(secretsFile(), home);
|
|
10929
11820
|
const stats = await index.refresh(cwd);
|
|
10930
11821
|
const scope = {
|
|
@@ -10935,14 +11826,18 @@ ${USAGE}`);
|
|
|
10935
11826
|
};
|
|
10936
11827
|
if (values.transcript !== void 0 || values.last === true) {
|
|
10937
11828
|
const found = values.transcript === void 0 ? await newestTranscript(cwd) : (
|
|
10938
|
-
//
|
|
10939
|
-
// parsed
|
|
10940
|
-
//
|
|
10941
|
-
|
|
10942
|
-
{ reader: claudeCodeReader, path: values.transcript }
|
|
11829
|
+
// Chosen by what is in the file, not by where it is: a Codex rollout
|
|
11830
|
+
// parsed as a Claude transcript yields no events at all, which prints
|
|
11831
|
+
// as a clean session rather than as the mistake it is.
|
|
11832
|
+
{ reader: await readerForFile(values.transcript), path: values.transcript }
|
|
10943
11833
|
);
|
|
10944
11834
|
if (found === null) {
|
|
10945
|
-
|
|
11835
|
+
const notices = await readerNotices();
|
|
11836
|
+
process.stdout.write(
|
|
11837
|
+
`no agent transcript found \u2014 looked under ${READER_ROOTS()}
|
|
11838
|
+
` + notices.map((why) => ` note: ${why}
|
|
11839
|
+
`).join("")
|
|
11840
|
+
);
|
|
10946
11841
|
return 1;
|
|
10947
11842
|
}
|
|
10948
11843
|
const transcript = await found.reader.read(found.path);
|
|
@@ -11064,13 +11959,13 @@ function runTrust(argv, now = () => /* @__PURE__ */ new Date()) {
|
|
|
11064
11959
|
// src/commands/untaint.ts
|
|
11065
11960
|
import { existsSync as existsSync14, readdirSync as readdirSync7 } from "fs";
|
|
11066
11961
|
import { rm as rm6 } from "fs/promises";
|
|
11067
|
-
import { join as
|
|
11962
|
+
import { join as join28 } from "path";
|
|
11068
11963
|
import { parseArgs as parseArgs11 } from "util";
|
|
11069
11964
|
var USAGE2 = "usage: stroq untaint --session <id> | --all\n";
|
|
11070
11965
|
async function clearAll() {
|
|
11071
11966
|
const dir = sessionsDir();
|
|
11072
11967
|
if (!existsSync14(dir)) return;
|
|
11073
|
-
await Promise.all(readdirSync7(dir).map((f) => rm6(
|
|
11968
|
+
await Promise.all(readdirSync7(dir).map((f) => rm6(join28(dir, f), { force: true })));
|
|
11074
11969
|
}
|
|
11075
11970
|
async function runUntaint(args) {
|
|
11076
11971
|
const { values } = parseArgs11({
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "stroq",
|
|
3
3
|
"name": "Stroq",
|
|
4
4
|
"description": "Local action firewall for OpenClaw: scans what the agent reads, taints the session, blocks or asks before dangerous tool calls.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.15.0",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED