@trim21/personal-pi-extensions 0.1.546 → 0.1.548
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/src/bwrap/runtime.ts +1 -1
- package/src/claude-code/shell.ts +1 -1
- package/src/gh-readonly.ts +171 -89
- package/src/opencode/bash.ts +1 -1
package/package.json
CHANGED
package/src/bwrap/runtime.ts
CHANGED
|
@@ -440,7 +440,7 @@ export class BwrapRuntime {
|
|
|
440
440
|
// 兼容 pi local ops 抛的 new Error("aborted")
|
|
441
441
|
if (error instanceof Error && (error.name === "AbortError" || error.message === "aborted")) {
|
|
442
442
|
const partial = await this.finalizeOutput(output);
|
|
443
|
-
throw new BashInterruptedError("aborted", "Command aborted", partial, error);
|
|
443
|
+
throw new BashInterruptedError("aborted", "Command aborted by user", partial, error);
|
|
444
444
|
}
|
|
445
445
|
throw error;
|
|
446
446
|
} finally {
|
package/src/claude-code/shell.ts
CHANGED
|
@@ -148,7 +148,7 @@ export function registerShellTools(
|
|
|
148
148
|
);
|
|
149
149
|
if (error.kind === "aborted") {
|
|
150
150
|
// 用户取消:直接返回已捕获的输出,不抛错
|
|
151
|
-
const full = text ? `${text}\n\nCommand aborted` : "Command aborted";
|
|
151
|
+
const full = text ? `${text}\n\nCommand aborted by user` : "Command aborted by user";
|
|
152
152
|
return { content: [{ type: "text", text: full }], details: undefined };
|
|
153
153
|
}
|
|
154
154
|
const full = text
|
package/src/gh-readonly.ts
CHANGED
|
@@ -262,6 +262,7 @@ const stepSchema = Type.Object({
|
|
|
262
262
|
number: Type.Number(),
|
|
263
263
|
status: Type.String(),
|
|
264
264
|
conclusion: Type.Union([Type.String(), Type.Null()]),
|
|
265
|
+
started_at: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
265
266
|
});
|
|
266
267
|
|
|
267
268
|
const jobRunSchema = Type.Object({
|
|
@@ -401,6 +402,8 @@ export interface StepInfo {
|
|
|
401
402
|
number: number;
|
|
402
403
|
status: string;
|
|
403
404
|
conclusion: string | null;
|
|
405
|
+
/** ISO-8601 UTC, second precision. Null for steps that never started. */
|
|
406
|
+
started_at?: string | null;
|
|
404
407
|
}
|
|
405
408
|
|
|
406
409
|
// 模块级串行状态:同一资源(如 CI 日志)的请求排队执行,配合函数内部的
|
|
@@ -529,115 +532,194 @@ export interface StepSpan {
|
|
|
529
532
|
end: number;
|
|
530
533
|
}
|
|
531
534
|
|
|
535
|
+
/** The part of a step the log index needs. `StepInfo` satisfies it. */
|
|
536
|
+
export interface StepRef {
|
|
537
|
+
number: number;
|
|
538
|
+
name: string;
|
|
539
|
+
conclusion?: string | null;
|
|
540
|
+
started_at?: string | null;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** A `##[group]Run …` / `##[group]Post Run …` line: the header of one executed step. */
|
|
544
|
+
interface StepHeader {
|
|
545
|
+
/** 0-based index of the `##[group]` line. */
|
|
546
|
+
line: number;
|
|
547
|
+
/** Header text with the `Run ` / `Post Run ` prefix stripped. */
|
|
548
|
+
action: string;
|
|
549
|
+
/** Runner timestamp on that line (epoch ms), null when unparsable. */
|
|
550
|
+
timestamp: number | null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Runner timestamp every log line starts with: `2026-08-05T16:36:08.1842645Z `. */
|
|
554
|
+
const LOG_TIMESTAMP_RE = /^\uFEFF?(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z) /;
|
|
555
|
+
const HEADER_PREFIX_RE = /^(Run |Post Run )/;
|
|
556
|
+
|
|
532
557
|
/**
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
* marker at depth 1. Composite actions emit their internal steps as *additional*
|
|
537
|
-
* depth-1 groups *after* the composite's own `##[endgroup]` (e.g. the internal
|
|
538
|
-
* `Run actions/setup-python@…` groups inside `Run pypa/cibuildwheel@…`), so the
|
|
539
|
-
* log's "Run " groups are NOT one-per-step.
|
|
540
|
-
*
|
|
541
|
-
* To handle that we treat a group as an *anchor* only when its action name
|
|
542
|
-
* (after stripping the "Run "/"Post Run " prefix) matches a top-level API step
|
|
543
|
-
* name. Composite-action internals match no API step and are absorbed into the
|
|
544
|
-
* span of the enclosing step instead of truncating it.
|
|
545
|
-
*
|
|
546
|
-
* Step 1 ("Set up job") maps to everything before the first anchor group.
|
|
547
|
-
* Steps with an anchor map to the span from their anchor to the next anchor.
|
|
548
|
-
* Explicitly named steps that lack a "Run " prefix (e.g. a step named
|
|
549
|
-
* "Setup node" running actions/setup-node) are located between the previous
|
|
550
|
-
* and next anchor's groups.
|
|
551
|
-
* Steps that were skipped and never executed get no span.
|
|
558
|
+
* The runner writes a step's header within ~1.6s of the step's API `started_at`
|
|
559
|
+
* (the API truncates its timestamps to whole seconds), so a header inside this
|
|
560
|
+
* window is evidence of the step it belongs to.
|
|
552
561
|
*/
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
562
|
+
const HEADER_WINDOW_MS = 5_000;
|
|
563
|
+
/** An exact name match proves the header belongs to that step. */
|
|
564
|
+
const NAME_MATCH_SCORE = 4;
|
|
565
|
+
/** Weight of a header inside the step's start window. */
|
|
566
|
+
const TIME_MATCH_SCORE = 2;
|
|
567
|
+
|
|
568
|
+
function headerTimestamp(line: string): number | null {
|
|
569
|
+
const match = LOG_TIMESTAMP_RE.exec(line);
|
|
570
|
+
if (match === null) return null;
|
|
571
|
+
const ms = Date.parse(match[1]);
|
|
572
|
+
return Number.isNaN(ms) ? null : ms;
|
|
573
|
+
}
|
|
559
574
|
|
|
560
|
-
|
|
561
|
-
|
|
575
|
+
/** Collect the depth-1 `Run ` / `Post Run ` headers, in log order. */
|
|
576
|
+
function stepHeaders(lines: string[]): StepHeader[] {
|
|
577
|
+
const headers: StepHeader[] = [];
|
|
562
578
|
let depth = 0;
|
|
563
579
|
for (const [i, line] of lines.entries()) {
|
|
564
580
|
if (line.includes("##[endgroup]")) {
|
|
565
581
|
if (depth > 0) depth--;
|
|
566
582
|
continue;
|
|
567
583
|
}
|
|
568
|
-
if (line.includes("##[group]"))
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
584
|
+
if (!line.includes("##[group]")) continue;
|
|
585
|
+
depth++;
|
|
586
|
+
if (depth !== 1) continue;
|
|
587
|
+
const name = /##\[group\](.*)/.exec(line)?.[1].trim() ?? "";
|
|
588
|
+
if (HEADER_PREFIX_RE.test(name)) {
|
|
589
|
+
headers.push({
|
|
590
|
+
line: i,
|
|
591
|
+
action: name.replace(HEADER_PREFIX_RE, "").trim(),
|
|
592
|
+
timestamp: headerTimestamp(line),
|
|
593
|
+
});
|
|
577
594
|
}
|
|
578
595
|
}
|
|
596
|
+
return headers;
|
|
597
|
+
}
|
|
579
598
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
if (gi !== -1) {
|
|
593
|
-
used.add(gi);
|
|
594
|
-
stepToGroup.set(rs.number, gi);
|
|
599
|
+
/** How well a step explains a header — 0 means "no evidence, don't guess". */
|
|
600
|
+
function headerScore(step: StepRef, header: StepHeader): number {
|
|
601
|
+
let score = 0;
|
|
602
|
+
const named = step.name.replace(HEADER_PREFIX_RE, "").trim();
|
|
603
|
+
if (HEADER_PREFIX_RE.test(step.name) && named === header.action) {
|
|
604
|
+
score += NAME_MATCH_SCORE;
|
|
605
|
+
}
|
|
606
|
+
const started = step.started_at == null ? NaN : Date.parse(step.started_at);
|
|
607
|
+
if (!Number.isNaN(started) && header.timestamp !== null) {
|
|
608
|
+
const delta = header.timestamp - started;
|
|
609
|
+
if (delta >= 0 && delta <= HEADER_WINDOW_MS) {
|
|
610
|
+
score += TIME_MATCH_SCORE * (1 - delta / HEADER_WINDOW_MS);
|
|
595
611
|
}
|
|
596
612
|
}
|
|
613
|
+
return score;
|
|
614
|
+
}
|
|
597
615
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
616
|
+
/**
|
|
617
|
+
* Align steps with headers: an order-preserving best-scoring matching, where
|
|
618
|
+
* either side may be left unmatched. Only pairs with real evidence are matched,
|
|
619
|
+
* so a step whose block cannot be identified gets no span instead of a guess.
|
|
620
|
+
*
|
|
621
|
+
* Name evidence disappears as soon as the workflow names a step with `name:`
|
|
622
|
+
* (the API name is then the custom one, while the log header carries the action
|
|
623
|
+
* or command), which is why the timestamps matter too. Headers belonging to a
|
|
624
|
+
* composite action's *internal* steps carry no evidence for any API step, so
|
|
625
|
+
* they stay unmatched and are absorbed into the enclosing step's span.
|
|
626
|
+
*/
|
|
627
|
+
function alignStepsToHeaders(steps: StepRef[], headers: StepHeader[]): Map<number, number> {
|
|
628
|
+
const n = steps.length;
|
|
629
|
+
const m = headers.length;
|
|
630
|
+
// Equal-scoring alignments are decided in favour of the earlier step: a step
|
|
631
|
+
// whose output the runner never logged (post steps, "Complete job") comes last
|
|
632
|
+
// in step order, so a header claimed by both belongs to the earlier one. A
|
|
633
|
+
// step that ran always emits its header before the next step starts, which
|
|
634
|
+
// leaves several steps competing for one header whenever the API timestamps
|
|
635
|
+
// (whole seconds) collapse them into the same second.
|
|
636
|
+
const TIE_BREAK = 1e-6;
|
|
637
|
+
const scores = steps.map((step, i) =>
|
|
638
|
+
headers.map((header) => {
|
|
639
|
+
const score = headerScore(step, header);
|
|
640
|
+
return score > 0 ? score + TIE_BREAK * (n - i) : 0;
|
|
641
|
+
}),
|
|
642
|
+
);
|
|
643
|
+
// best[i][j]: score of aligning the first i steps with the first j headers.
|
|
644
|
+
const best: number[][] = Array.from({ length: n + 1 }, () =>
|
|
645
|
+
Array.from({ length: m + 1 }, () => 0),
|
|
646
|
+
);
|
|
647
|
+
const paired: boolean[][] = Array.from({ length: n + 1 }, () =>
|
|
648
|
+
Array.from({ length: m + 1 }, () => false),
|
|
649
|
+
);
|
|
624
650
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
651
|
+
for (let i = 1; i <= n; i++) {
|
|
652
|
+
for (let j = 1; j <= m; j++) {
|
|
653
|
+
const pairing = scores[i - 1][j - 1];
|
|
654
|
+
const withPairing = pairing > 0 ? best[i - 1][j - 1] + pairing : -Infinity;
|
|
655
|
+
if (withPairing >= best[i - 1][j] && withPairing >= best[i][j - 1]) {
|
|
656
|
+
best[i][j] = withPairing;
|
|
657
|
+
paired[i][j] = true;
|
|
631
658
|
} else {
|
|
632
|
-
|
|
633
|
-
start = anchors[anchorIdx].line;
|
|
634
|
-
end = anchorIdx + 1 < anchors.length ? anchors[anchorIdx + 1].line : lines.length;
|
|
659
|
+
best[i][j] = Math.max(best[i - 1][j], best[i][j - 1]);
|
|
635
660
|
}
|
|
636
661
|
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const assignment = new Map<number, number>(); // step number -> header index
|
|
665
|
+
for (let i = n, j = m; i > 0 && j > 0;) {
|
|
666
|
+
if (paired[i][j]) {
|
|
667
|
+
assignment.set(steps[i - 1].number, j - 1);
|
|
668
|
+
i--;
|
|
669
|
+
j--;
|
|
670
|
+
} else if (best[i - 1][j] >= best[i][j - 1]) {
|
|
671
|
+
i--;
|
|
672
|
+
} else {
|
|
673
|
+
j--;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return assignment;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Exclusive end index with trailing blank lines dropped, so a span slices to real text. */
|
|
680
|
+
function trimTrailingBlankLines(lines: string[], start: number, end: number): number {
|
|
681
|
+
while (end > start && (lines[end - 1] ?? "").trim() === "") end--;
|
|
682
|
+
return end;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Locate a job's steps in its raw log: each executed step emits a depth-1
|
|
687
|
+
* `##[group]Run <x>` / `##[group]Post Run <x>` header, and its block runs from
|
|
688
|
+
* that header up to the next executed step's header. Everything in between —
|
|
689
|
+
* the action's own `::group::` output, a composite action's internal step
|
|
690
|
+
* headers — belongs to the enclosing step.
|
|
691
|
+
*
|
|
692
|
+
* "Set up job" emits no header: it owns the runner preamble before the first
|
|
693
|
+
* header. Steps with no header of their own (skipped steps, post steps the
|
|
694
|
+
* runner never logged, "Complete job") get no span.
|
|
695
|
+
*/
|
|
696
|
+
export function stepLineSpans(log: string, apiSteps: StepRef[]): Map<number, StepSpan> {
|
|
697
|
+
const lines = log.split("\n");
|
|
698
|
+
const headers = stepHeaders(lines);
|
|
699
|
+
const spans = new Map<number, StepSpan>();
|
|
700
|
+
|
|
701
|
+
// "Set up job" is the runner's own preamble and never takes part in matching:
|
|
702
|
+
// its start window overlaps the first real step's header.
|
|
703
|
+
const preamble = apiSteps.find((s) => s.number === 1 && s.name === "Set up job");
|
|
704
|
+
if (preamble !== undefined) {
|
|
705
|
+
const end = trimTrailingBlankLines(lines, 0, headers[0]?.line ?? lines.length);
|
|
706
|
+
spans.set(preamble.number, { start: 0, end });
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// A skipped step never started, so it emitted no header — and its name often
|
|
710
|
+
// repeats another step's ("Clear build" twice, "Post Run <action>" next to
|
|
711
|
+
// its "Run <action>"), which would let it steal that step's block.
|
|
712
|
+
const assignment = alignStepsToHeaders(
|
|
713
|
+
apiSteps.filter((s) => s !== preamble && s.conclusion !== "skipped"),
|
|
714
|
+
headers,
|
|
715
|
+
);
|
|
716
|
+
const placed = [...assignment]
|
|
717
|
+
.map(([number, headerIndex]) => ({ number, line: headers[headerIndex].line }))
|
|
718
|
+
.toSorted((a, b) => a.line - b.line);
|
|
637
719
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
spans.set(
|
|
720
|
+
for (const [index, step] of placed.entries()) {
|
|
721
|
+
const end = trimTrailingBlankLines(lines, step.line, placed[index + 1]?.line ?? lines.length);
|
|
722
|
+
spans.set(step.number, { start: step.line, end });
|
|
641
723
|
}
|
|
642
724
|
|
|
643
725
|
return spans;
|
package/src/opencode/bash.ts
CHANGED
|
@@ -109,7 +109,7 @@ export default function opencodeBash(
|
|
|
109
109
|
const status =
|
|
110
110
|
error.kind === "timeout"
|
|
111
111
|
? `Command exceeded timeout of ${timeout} ms. Retry with a larger timeout if the command is expected to take longer.`
|
|
112
|
-
: "Command aborted";
|
|
112
|
+
: "Command aborted by user";
|
|
113
113
|
const full = text ? `${text}\n\n${status}` : status;
|
|
114
114
|
// 对齐上游 opencode:超时与中断都不抛错,输出与状态文本一起返回
|
|
115
115
|
return {
|