@feigi/fleet-ctl 3.93.1 → 3.93.3
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/package.json +1 -1
- package/scripts/member-record.mjs +248 -11
package/package.json
CHANGED
|
@@ -460,25 +460,262 @@ export function foldOmpTranscript(jsonlText, filePath) {
|
|
|
460
460
|
|
|
461
461
|
// The report files one transcript tool call writes, as { file, content } with
|
|
462
462
|
// `file` the written path's basename: the `write` tool's arguments, or each
|
|
463
|
-
//
|
|
464
|
-
// `
|
|
465
|
-
const HEREDOC_RE = /cat\s*>\s*(\S+)\s*<<\s*['"]?([A-Za-z_][A-Za-z0-9_]*)['"]?[^\n]*\n([\s\S]*?)\n\2[ \t]*(?=\n|$)/g;
|
|
463
|
+
// report a `bash` call writes with a heredoc (`heredocReportWrites`). Only a
|
|
464
|
+
// path ending `/reports/<name>.json` counts.
|
|
466
465
|
const REPORT_PATH_RE = /\/reports\/[^/\s]+\.json$/;
|
|
467
|
-
function reportWritesOf(c) {
|
|
466
|
+
export function reportWritesOf(c) {
|
|
468
467
|
if (c?.type !== "toolCall") return [];
|
|
469
468
|
const a = c.arguments ?? {};
|
|
470
469
|
if (c.name === "write" && typeof a.path === "string" && typeof a.content === "string") {
|
|
471
470
|
return REPORT_PATH_RE.test(a.path) ? [{ file: basename(a.path), content: a.content }] : [];
|
|
472
471
|
}
|
|
473
|
-
if (c.name === "bash" && typeof a.command === "string")
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
472
|
+
if (c.name === "bash" && typeof a.command === "string") return heredocReportWrites(a.command);
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// One linear pass over a shell command, yielding { file, content } for each
|
|
477
|
+
// heredoc that writes a report, in command order:
|
|
478
|
+
// cat > P <<TAG cat <<TAG > P tee P [>/dev/null] <<TAG
|
|
479
|
+
// cat <<TAG | tee P
|
|
480
|
+
// with `<<` or `<<-` and a bare, 'single' or "double" quoted TAG. The command
|
|
481
|
+
// may follow leading keywords and the wrappers in WRAPPERS (`env FOO=1`,
|
|
482
|
+
// `sudo -u x`, `timeout 5`, …), and may sit inside a `$(…)` or be the string
|
|
483
|
+
// of a `bash -c` / `sh -c`. P is a literal path (quoted or not), a whole-word
|
|
484
|
+
// `$NAME` / `${NAME}` / `"$NAME"` that a `NAME=<value>` earlier in the same
|
|
485
|
+
// command set, or a word whose `/reports/<name>.json` tail is literal
|
|
486
|
+
// (`$D/reports/x.json`): the basename is all a report write is read for.
|
|
487
|
+
// The body is the lines up to the first terminator line, a terminator line
|
|
488
|
+
// being TAG with any trailing blanks or CR; under `<<-` every line is read
|
|
489
|
+
// with its leading tabs stripped. The body is otherwise verbatim (an unquoted
|
|
490
|
+
// TAG does not expand it). A heredoc whose stdin a later `<` or `<<<`
|
|
491
|
+
// replaces writes nothing. A heredoc with no terminator writes nothing — and
|
|
492
|
+
// since the shell would swallow the rest of the command into it, the scan
|
|
493
|
+
// stops there. Bodies are never scanned for commands.
|
|
494
|
+
const REDIR_RE = /&>>|&>|>>|>&|>\||>|<<<|<<-|<<|<&|</y;
|
|
495
|
+
const PARAM_RE = /\$(?:[A-Za-z_][A-Za-z0-9_]*|[0-9@*#?$!-]|\{[^}]*\}?)/y;
|
|
496
|
+
const WHOLE_PARAM_RE = /^"?\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})"?$/;
|
|
497
|
+
const ASSIGN_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
498
|
+
const DECLARE_COMMANDS = new Set(["export", "local", "declare", "readonly", "typeset"]);
|
|
499
|
+
const LEADING_KEYWORDS = new Set(["{", "!", "then", "do", "else", "exec", "command", "rtk", "time"]);
|
|
500
|
+
// Commands that run the command after them: the single-letter options of
|
|
501
|
+
// theirs that take a separate argument, and how many operands come before that
|
|
502
|
+
// command. `env` and `sudo` also take NAME=value words before it.
|
|
503
|
+
const WRAPPERS = new Map([
|
|
504
|
+
["env", { args: "uCS", operands: 0 }],
|
|
505
|
+
["sudo", { args: "ugpCUrtDRT", operands: 0 }],
|
|
506
|
+
["timeout", { args: "sk", operands: 1 }],
|
|
507
|
+
["nice", { args: "n", operands: 0 }],
|
|
508
|
+
["stdbuf", { args: "ioe", operands: 0 }],
|
|
509
|
+
["nohup", { args: "", operands: 0 }],
|
|
510
|
+
]);
|
|
511
|
+
const SHELLS = new Set(["sh", "bash", "zsh", "dash", "ksh"]);
|
|
512
|
+
const WORD_BREAKS = " \t\r\n;&|<>()";
|
|
513
|
+
// `$(…)` and `-c` strings nested deeper than this are not scanned: the rest
|
|
514
|
+
// of the command reads as one word.
|
|
515
|
+
const MAX_NESTING = 32;
|
|
516
|
+
|
|
517
|
+
function heredocReportWrites(cmd) {
|
|
518
|
+
return scan(cmd, 0, false, 0).writes;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// The index of the command word in `words`, from `k` past leading keywords
|
|
522
|
+
// and wrappers with their options, operands and assignments.
|
|
523
|
+
function commandIndex(words, k) {
|
|
524
|
+
while (k < words.length) {
|
|
525
|
+
const t = words[k].text;
|
|
526
|
+
if (LEADING_KEYWORDS.has(t)) { k++; continue; }
|
|
527
|
+
const wrap = WRAPPERS.get(t);
|
|
528
|
+
if (!wrap) break;
|
|
529
|
+
k++;
|
|
530
|
+
while (k < words.length && words[k].text.startsWith("-")) {
|
|
531
|
+
const o = words[k++].text;
|
|
532
|
+
if (o === "--") break;
|
|
533
|
+
if (o.length === 2 && wrap.args.includes(o[1])) k++;
|
|
478
534
|
}
|
|
479
|
-
|
|
535
|
+
if (t === "env" || t === "sudo") while (k < words.length && ASSIGN_RE.test(words[k].raw)) k++;
|
|
536
|
+
k += wrap.operands;
|
|
480
537
|
}
|
|
481
|
-
return
|
|
538
|
+
return k;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// One shell word starting at `i`: its source text, its text with quotes
|
|
542
|
+
// removed, whether any `$` or backtick expansion is in it, where in `text` the
|
|
543
|
+
// literal tail after its last expansion starts, and the end index. A `$(…)` in
|
|
544
|
+
// it is scanned as a command of its own, whose report writes go to `writes`.
|
|
545
|
+
function scanWord(cmd, i, writes, depth) {
|
|
546
|
+
const n = cmd.length;
|
|
547
|
+
let j = i, text = "", dynamic = false, litFrom = 0;
|
|
548
|
+
const expansion = (e) => { text += cmd.slice(j, e); dynamic = true; litFrom = text.length; j = e; };
|
|
549
|
+
const substitution = () => {
|
|
550
|
+
if (depth >= MAX_NESTING) return expansion(n);
|
|
551
|
+
const sub = scan(cmd, j + 2, true, depth + 1);
|
|
552
|
+
for (const w of sub.writes) writes.push(w);
|
|
553
|
+
expansion(sub.end);
|
|
554
|
+
};
|
|
555
|
+
const parameter = () => {
|
|
556
|
+
PARAM_RE.lastIndex = j;
|
|
557
|
+
expansion(PARAM_RE.test(cmd) ? PARAM_RE.lastIndex : j + 1);
|
|
558
|
+
};
|
|
559
|
+
const backtick = () => {
|
|
560
|
+
const e = cmd.indexOf("`", j + 1);
|
|
561
|
+
expansion(e < 0 ? n : e + 1);
|
|
562
|
+
};
|
|
563
|
+
while (j < n) {
|
|
564
|
+
const ch = cmd[j];
|
|
565
|
+
if (ch === "$" && cmd[j + 1] === "(") substitution();
|
|
566
|
+
else if (ch === "$") parameter();
|
|
567
|
+
else if (ch === "`") backtick();
|
|
568
|
+
else if (ch === "\\") {
|
|
569
|
+
if (cmd[j + 1] === "\n") j += 2;
|
|
570
|
+
else { text += cmd[j + 1] ?? ""; j += 2; }
|
|
571
|
+
} else if (ch === "'") {
|
|
572
|
+
const e = cmd.indexOf("'", j + 1);
|
|
573
|
+
text += cmd.slice(j + 1, e < 0 ? n : e); j = e < 0 ? n : e + 1;
|
|
574
|
+
} else if (ch === '"') {
|
|
575
|
+
j++;
|
|
576
|
+
while (j < n && cmd[j] !== '"') {
|
|
577
|
+
if (cmd[j] === "\\" && j + 1 < n) { text += cmd[j + 1]; j += 2; }
|
|
578
|
+
else if (cmd[j] === "$" && cmd[j + 1] === "(") substitution();
|
|
579
|
+
else if (cmd[j] === "$") parameter();
|
|
580
|
+
else if (cmd[j] === "`") backtick();
|
|
581
|
+
else text += cmd[j++];
|
|
582
|
+
}
|
|
583
|
+
j++;
|
|
584
|
+
} else if (WORD_BREAKS.includes(ch)) break;
|
|
585
|
+
else { text += ch; j++; }
|
|
586
|
+
}
|
|
587
|
+
const end = Math.min(j, n);
|
|
588
|
+
return { raw: cmd.slice(i, end), text, dynamic, litFrom, end };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function stripTrailingBlanks(s) {
|
|
592
|
+
let e = s.length;
|
|
593
|
+
while (e > 0 && (s[e - 1] === " " || s[e - 1] === "\t" || s[e - 1] === "\r")) e--;
|
|
594
|
+
return s.slice(0, e);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function stripLeadingTabs(s) {
|
|
598
|
+
let b = 0;
|
|
599
|
+
while (b < s.length && s[b] === "\t") b++;
|
|
600
|
+
return s.slice(b);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const isReportPath = (t) => t !== null && REPORT_PATH_RE.test(t);
|
|
604
|
+
const isStdin = (r) => (r.fd === null || r.fd === "0") && (r.op === "<" || r.op === "<<<" || r.op === "<&" || r.op === "<<" || r.op === "<<-");
|
|
605
|
+
|
|
606
|
+
// The scan proper, from `start`. `nested` when `start` is just inside a `$(`,
|
|
607
|
+
// whose unmatched `)` then ends the scan. Returns the writes and the index
|
|
608
|
+
// the scan ended at (after that `)`).
|
|
609
|
+
function scan(cmd, start, nested, depth) {
|
|
610
|
+
const writes = [];
|
|
611
|
+
const vars = new Map();
|
|
612
|
+
const n = cmd.length;
|
|
613
|
+
let words = [], redirs = [], pending = [], expect = null, fd = null, piped = null, parens = 0, i = start;
|
|
614
|
+
|
|
615
|
+
// A word's value, when it is known enough to read a report path off: its
|
|
616
|
+
// text when it has no expansion, the value of an assigned NAME when it is
|
|
617
|
+
// exactly `$NAME` / `${NAME}` (quoted or not), its text when its report
|
|
618
|
+
// tail is literal, else null.
|
|
619
|
+
const resolve = (w) => {
|
|
620
|
+
if (!w.dynamic) return w.text;
|
|
621
|
+
const m = WHOLE_PARAM_RE.exec(w.raw);
|
|
622
|
+
if (m) return vars.get(m[1] ?? m[2]) ?? null;
|
|
623
|
+
return REPORT_PATH_RE.test(w.text.slice(w.litFrom)) ? w.text : null;
|
|
624
|
+
};
|
|
625
|
+
const assign = (w) => {
|
|
626
|
+
const name = w.raw.slice(0, w.raw.indexOf("="));
|
|
627
|
+
if (!w.dynamic || REPORT_PATH_RE.test(w.text.slice(w.litFrom))) vars.set(name, w.text.slice(name.length + 1));
|
|
628
|
+
else vars.delete(name);
|
|
629
|
+
};
|
|
630
|
+
const teeTargets = (args) => {
|
|
631
|
+
if (args.some((w) => w.text === "--append" || /^-[^-]*a/.test(w.text))) return [];
|
|
632
|
+
return args.filter((w) => !w.text.startsWith("-")).map(resolve).filter(isReportPath);
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
// One finished simple command, ended by `sep`: apply its assignments, scan
|
|
636
|
+
// its `-c` string, and queue its heredocs with the report paths the last of
|
|
637
|
+
// them writes. A plain `cat <<TAG` piped on hands its heredoc to a `tee`.
|
|
638
|
+
const flush = (sep) => {
|
|
639
|
+
expect = null;
|
|
640
|
+
const hd = redirs.filter((r) => r.op === "<<" || r.op === "<<-");
|
|
641
|
+
const stdin = redirs.findLast(isStdin);
|
|
642
|
+
const feeds = hd.length > 0 && stdin === hd.at(-1);
|
|
643
|
+
let k = 0;
|
|
644
|
+
while (k < words.length && ASSIGN_RE.test(words[k].raw)) k++;
|
|
645
|
+
const leading = k;
|
|
646
|
+
k = commandIndex(words, k);
|
|
647
|
+
const cmdWord = words[k];
|
|
648
|
+
const name = cmdWord && !cmdWord.dynamic ? cmdWord.text.split("/").pop() : null;
|
|
649
|
+
if (!cmdWord) {
|
|
650
|
+
for (const w of words.slice(0, leading)) assign(w);
|
|
651
|
+
} else if (DECLARE_COMMANDS.has(name)) {
|
|
652
|
+
for (const w of words.slice(k + 1)) if (ASSIGN_RE.test(w.raw)) assign(w);
|
|
653
|
+
} else if (SHELLS.has(name) && depth < MAX_NESTING) {
|
|
654
|
+
for (let x = k + 1; x < words.length && words[x].text.startsWith("-"); x++) {
|
|
655
|
+
if (!/^-[A-Za-z]*c[A-Za-z]*$/.test(words[x].text)) continue;
|
|
656
|
+
if (words[x + 1]) for (const w of scan(words[x + 1].text, 0, false, depth + 1).writes) writes.push(w);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
const plainCat = name === "cat" && words.length === k + 1;
|
|
661
|
+
const outs = redirs.filter((r) => (r.op === ">" || r.op === ">|" || r.op === "&>") && (r.fd === null || r.fd === "1"));
|
|
662
|
+
let targets = [];
|
|
663
|
+
if (feeds && plainCat && outs.length) targets = [resolve(outs.at(-1).word)].filter(isReportPath);
|
|
664
|
+
else if (feeds && name === "tee") targets = teeTargets(words.slice(k + 1));
|
|
665
|
+
else if (piped && name === "tee" && !stdin) piped.targets = teeTargets(words.slice(k + 1));
|
|
666
|
+
hd.forEach((r, idx) => pending.push({
|
|
667
|
+
tag: r.word.text, dash: r.op === "<<-", targets: idx === hd.length - 1 ? targets : [],
|
|
668
|
+
}));
|
|
669
|
+
piped = sep === "|" && feeds && plainCat && !outs.length ? pending.at(-1) : null;
|
|
670
|
+
words = []; redirs = [];
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
// Once a command line ends, read each queued heredoc's body in turn. False
|
|
674
|
+
// when one has no terminator.
|
|
675
|
+
const consume = () => {
|
|
676
|
+
for (const h of pending) {
|
|
677
|
+
const lines = [];
|
|
678
|
+
let found = false;
|
|
679
|
+
while (i < n && !found) {
|
|
680
|
+
const e = cmd.indexOf("\n", i);
|
|
681
|
+
const end = e < 0 ? n : e;
|
|
682
|
+
const line = h.dash ? stripLeadingTabs(cmd.slice(i, end)) : cmd.slice(i, end);
|
|
683
|
+
i = e < 0 ? n : e + 1;
|
|
684
|
+
if (stripTrailingBlanks(line) === h.tag) found = true;
|
|
685
|
+
else lines.push(line);
|
|
686
|
+
}
|
|
687
|
+
if (!found) return false;
|
|
688
|
+
const content = lines.join("\n");
|
|
689
|
+
for (const t of h.targets) writes.push({ file: basename(t), content });
|
|
690
|
+
}
|
|
691
|
+
pending = [];
|
|
692
|
+
return true;
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
while (i < n) {
|
|
696
|
+
const ch = cmd[i];
|
|
697
|
+
if (ch === "\n") { i++; flush(ch); if (!consume()) return { writes, end: n }; continue; }
|
|
698
|
+
if (ch === " " || ch === "\t" || ch === "\r") { i++; continue; }
|
|
699
|
+
if (ch === "\\" && cmd[i + 1] === "\n") { i += 2; continue; }
|
|
700
|
+
if (ch === "#") { const e = cmd.indexOf("\n", i); i = e < 0 ? n : e; continue; }
|
|
701
|
+
REDIR_RE.lastIndex = i;
|
|
702
|
+
const op = REDIR_RE.exec(cmd);
|
|
703
|
+
if (op) { expect = { op: op[0], fd }; fd = null; i += op[0].length; continue; }
|
|
704
|
+
if (ch === ")" && nested && parens === 0) { flush(ch); return { writes, end: i + 1 }; }
|
|
705
|
+
if (";&|()".includes(ch)) {
|
|
706
|
+
if (ch === "(") parens++;
|
|
707
|
+
else if (ch === ")" && parens > 0) parens--;
|
|
708
|
+
flush(ch); i++; continue;
|
|
709
|
+
}
|
|
710
|
+
const w = scanWord(cmd, i, writes, depth);
|
|
711
|
+
i = w.end;
|
|
712
|
+
// A file-descriptor prefix (`2>`), which the redirect that follows takes.
|
|
713
|
+
if (/^[0-9]{1,2}$/.test(w.raw) && (cmd[i] === "<" || cmd[i] === ">")) { fd = w.raw; continue; }
|
|
714
|
+
if (expect) { redirs.push({ ...expect, word: w }); expect = null; }
|
|
715
|
+
else words.push(w);
|
|
716
|
+
}
|
|
717
|
+
flush(null);
|
|
718
|
+
return { writes, end: n };
|
|
482
719
|
}
|
|
483
720
|
|
|
484
721
|
// The text a transcript message carries: a bare string, or its text blocks.
|