@trim21/personal-pi-extensions 0.1.544 → 0.1.546
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/gh-readonly.ts +155 -631
package/package.json
CHANGED
package/src/gh-readonly.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { spawn } from "node:child_process";
|
|
|
33
33
|
import { existsSync } from "node:fs";
|
|
34
34
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
35
35
|
import { homedir } from "node:os";
|
|
36
|
-
import { delimiter, dirname, join
|
|
36
|
+
import { delimiter, dirname, join } from "node:path";
|
|
37
37
|
|
|
38
38
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
39
39
|
import { Type } from "typebox";
|
|
@@ -266,6 +266,8 @@ const stepSchema = Type.Object({
|
|
|
266
266
|
|
|
267
267
|
const jobRunSchema = Type.Object({
|
|
268
268
|
id: Type.Number(),
|
|
269
|
+
run_id: Type.Number(),
|
|
270
|
+
run_url: Type.String(),
|
|
269
271
|
name: Type.String(),
|
|
270
272
|
status: Type.String(),
|
|
271
273
|
conclusion: Type.Union([Type.String(), Type.Null()]),
|
|
@@ -401,49 +403,48 @@ export interface StepInfo {
|
|
|
401
403
|
conclusion: string | null;
|
|
402
404
|
}
|
|
403
405
|
|
|
404
|
-
export interface JobInfo {
|
|
405
|
-
id: number;
|
|
406
|
-
name: string;
|
|
407
|
-
conclusion: string | null;
|
|
408
|
-
steps: StepInfo[];
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/** Build GitHub-UI-style step list for details, marking expanded steps. */
|
|
412
|
-
export function stepsDetail(
|
|
413
|
-
job: JobInfo,
|
|
414
|
-
expandedSteps?: Set<number>,
|
|
415
|
-
): { number: number; name: string; conclusion: string | null; expanded?: boolean }[] {
|
|
416
|
-
return job.steps.map((s) => ({
|
|
417
|
-
number: s.number,
|
|
418
|
-
name: s.name,
|
|
419
|
-
conclusion: s.conclusion,
|
|
420
|
-
...(expandedSteps?.has(s.number) && { expanded: true }),
|
|
421
|
-
}));
|
|
422
|
-
}
|
|
423
|
-
|
|
424
406
|
// 模块级串行状态:同一资源(如 CI 日志)的请求排队执行,配合函数内部的
|
|
425
407
|
// 缓存检查避免重复网络请求。闭包状态不与其他扩展共享,key 无需全局前缀。
|
|
426
408
|
const seq = createSeqState();
|
|
427
409
|
|
|
410
|
+
/**
|
|
411
|
+
* `OWNER/REPO` from a job's `run_url`
|
|
412
|
+
* (`https://api.github.com/repos/OWNER/REPO/actions/runs/123`). The path is
|
|
413
|
+
* parsed as a URL rather than pattern-matched, and GitHub canonicalizes the
|
|
414
|
+
* owner/repo casing in these fields — so this is the spelling to key the log
|
|
415
|
+
* cache on, independent of whatever `repo` the caller passed.
|
|
416
|
+
*/
|
|
417
|
+
export function repoFromRunUrl(runUrl: string): string {
|
|
418
|
+
const segments = new URL(runUrl).pathname.split("/").filter(Boolean);
|
|
419
|
+
const reposAt = segments.indexOf("repos");
|
|
420
|
+
const ownerAndRepo = reposAt === -1 ? [] : segments.slice(reposAt + 1, reposAt + 3);
|
|
421
|
+
if (ownerAndRepo.length !== 2) {
|
|
422
|
+
throw new Error(`unexpected run_url (expected /repos/<owner>/<repo>/...): ${runUrl}`);
|
|
423
|
+
}
|
|
424
|
+
return ownerAndRepo.join("/");
|
|
425
|
+
}
|
|
426
|
+
|
|
428
427
|
/** Absolute path of the raw job log cache file written by `getJobLog`. */
|
|
429
|
-
export function jobLogPath(runId: string, jobId: number): string {
|
|
430
|
-
|
|
428
|
+
export function jobLogPath(repo: string, runId: string, jobId: number): string {
|
|
429
|
+
const { owner, repo: name } = splitRepo(repo);
|
|
430
|
+
return join(homedir(), ".cache", "pi", "github", "ci-logs", owner, name, runId, `${jobId}.log`);
|
|
431
431
|
}
|
|
432
432
|
|
|
433
433
|
async function getJobLog(
|
|
434
|
-
|
|
435
|
-
jobId: number,
|
|
436
|
-
effectiveRepo: string,
|
|
434
|
+
job: CiLogsJob,
|
|
437
435
|
signal: AbortSignal | undefined,
|
|
438
436
|
cwd: string | undefined,
|
|
439
437
|
input?: unknown,
|
|
440
438
|
): Promise<string> {
|
|
441
|
-
|
|
439
|
+
// The log download only accepts a job id, and the cache is keyed on the
|
|
440
|
+
// canonical repo/run from the job itself, not on the caller's `repo` string.
|
|
441
|
+
const repo = repoFromRunUrl(job.run_url);
|
|
442
|
+
const cacheFile = jobLogPath(repo, String(job.run_id), job.id);
|
|
442
443
|
const cacheDir = dirname(cacheFile);
|
|
443
444
|
|
|
444
|
-
// 同一
|
|
445
|
+
// 同一 cache 文件的请求串行执行:后一个进入时缓存已写入,直接命中缓存,
|
|
445
446
|
// 不会重复发网络请求;串行也保证不会有两个并发写同一 cache 文件。
|
|
446
|
-
return seq.execute(
|
|
447
|
+
return seq.execute(cacheFile, async () => {
|
|
447
448
|
// Check file cache
|
|
448
449
|
try {
|
|
449
450
|
return await readFile(cacheFile, "utf8");
|
|
@@ -452,13 +453,14 @@ async function getJobLog(
|
|
|
452
453
|
}
|
|
453
454
|
|
|
454
455
|
// `gh api` refuses to print responses that contain terminal escape
|
|
455
|
-
// sequences unless `--allow-escape-sequences` is passed. Job logs
|
|
456
|
-
//
|
|
456
|
+
// sequences unless `--allow-escape-sequences` is passed. Job logs carry
|
|
457
|
+
// ANSI color codes, so without this flag the download always fails with
|
|
457
458
|
// "the response contains terminal escape sequences; pass
|
|
458
|
-
// --allow-escape-sequences to output it anyway". The
|
|
459
|
-
//
|
|
459
|
+
// --allow-escape-sequences to output it anyway". The raw bytes are kept
|
|
460
|
+
// as-is (the file is the log exactly as GitHub delivers it); the tool never
|
|
461
|
+
// echoes them, and the TUI strips ANSI when rendering tool results.
|
|
460
462
|
const log = await ghExec(
|
|
461
|
-
["api", "--allow-escape-sequences", `/repos/${
|
|
463
|
+
["api", "--allow-escape-sequences", `/repos/${repo}/actions/jobs/${job.id}/logs`],
|
|
462
464
|
{
|
|
463
465
|
cwd,
|
|
464
466
|
signal,
|
|
@@ -521,8 +523,14 @@ export function statusIcon(conclusion: string | null): string {
|
|
|
521
523
|
}
|
|
522
524
|
}
|
|
523
525
|
|
|
526
|
+
/** A step's line span in the raw job log: 0-based `start`, exclusive `end`. */
|
|
527
|
+
export interface StepSpan {
|
|
528
|
+
start: number;
|
|
529
|
+
end: number;
|
|
530
|
+
}
|
|
531
|
+
|
|
524
532
|
/**
|
|
525
|
-
*
|
|
533
|
+
* Locate a job's steps in its raw log by matching step names to "Run " groups.
|
|
526
534
|
*
|
|
527
535
|
* Each top-level step emits a `##[group]Run <name>` / `##[group]Post Run <name>`
|
|
528
536
|
* marker at depth 1. Composite actions emit their internal steps as *additional*
|
|
@@ -540,18 +548,14 @@ export function statusIcon(conclusion: string | null): string {
|
|
|
540
548
|
* Explicitly named steps that lack a "Run " prefix (e.g. a step named
|
|
541
549
|
* "Setup node" running actions/setup-node) are located between the previous
|
|
542
550
|
* and next anchor's groups.
|
|
543
|
-
* Steps that were skipped and never executed
|
|
544
|
-
*
|
|
545
|
-
* Returns null if no matching group is found.
|
|
551
|
+
* Steps that were skipped and never executed get no span.
|
|
546
552
|
*/
|
|
547
|
-
export function
|
|
553
|
+
export function stepLineSpans(
|
|
548
554
|
log: string,
|
|
549
|
-
stepNumber: number,
|
|
550
555
|
apiSteps: { number: number; name: string }[],
|
|
551
|
-
):
|
|
552
|
-
if (apiSteps.every((s) => s.number !== stepNumber)) return null;
|
|
553
|
-
|
|
556
|
+
): Map<number, StepSpan> {
|
|
554
557
|
const lines = log.split("\n");
|
|
558
|
+
const spans = new Map<number, StepSpan>();
|
|
555
559
|
|
|
556
560
|
// Collect depth-1 "Run "/"Post Run " groups in log order.
|
|
557
561
|
const groups: { line: number; action: string }[] = [];
|
|
@@ -573,14 +577,6 @@ export function extractStepFromLog(
|
|
|
573
577
|
}
|
|
574
578
|
}
|
|
575
579
|
|
|
576
|
-
// Step 1 ("Set up job"): everything before the first "Run "/"Post Run " group.
|
|
577
|
-
if (stepNumber === 1) {
|
|
578
|
-
return lines
|
|
579
|
-
.slice(0, groups[0]?.line ?? lines.length)
|
|
580
|
-
.join("\n")
|
|
581
|
-
.trimEnd();
|
|
582
|
-
}
|
|
583
|
-
|
|
584
580
|
// API steps that produce a "Run "/"Post Run " log group, in step order.
|
|
585
581
|
const runSteps = apiSteps
|
|
586
582
|
.filter((s) => /^(Run |Post Run )/.test(s.name))
|
|
@@ -604,40 +600,67 @@ export function extractStepFromLog(
|
|
|
604
600
|
.map(([stepNum, gi]) => ({ stepNum, line: groups[gi].line }))
|
|
605
601
|
.toSorted((a, b) => a.line - b.line);
|
|
606
602
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const start = anchors[anchorIdx].line;
|
|
611
|
-
const end = anchorIdx + 1 < anchors.length ? anchors[anchorIdx + 1].line : lines.length;
|
|
612
|
-
return lines.slice(start, end).join("\n").trimEnd();
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Non-anchor step (explicitly named, e.g. "Setup node"): its group sits in
|
|
616
|
-
// the gap between the previous and next anchors' groups. Take the first
|
|
617
|
-
// unclaimed group in that span.
|
|
618
|
-
const prevAnchor = anchors.reduce<{ stepNum: number; line: number } | undefined>(
|
|
619
|
-
(acc, a) => (a.stepNum < stepNumber ? a : acc),
|
|
620
|
-
undefined,
|
|
621
|
-
);
|
|
622
|
-
const nextAnchor = anchors.find((a) => a.stepNum > stepNumber);
|
|
603
|
+
for (const s of apiSteps) {
|
|
604
|
+
let start: number;
|
|
605
|
+
let end: number;
|
|
623
606
|
|
|
624
|
-
|
|
625
|
-
|
|
607
|
+
if (s.number === 1) {
|
|
608
|
+
// Step 1 ("Set up job"): everything before the first "Run " group.
|
|
609
|
+
start = 0;
|
|
610
|
+
end = groups[0]?.line ?? lines.length;
|
|
611
|
+
} else {
|
|
612
|
+
const anchorIdx = anchors.findIndex((a) => a.stepNum === s.number);
|
|
613
|
+
if (anchorIdx === -1) {
|
|
614
|
+
// Non-anchor step (explicitly named, e.g. "Setup node"): its group sits
|
|
615
|
+
// in the gap between the previous and next anchors' groups. Take the
|
|
616
|
+
// first unclaimed group in that span.
|
|
617
|
+
const prevAnchor = anchors.reduce<{ stepNum: number; line: number } | undefined>(
|
|
618
|
+
(acc, a) => (a.stepNum < s.number ? a : acc),
|
|
619
|
+
undefined,
|
|
620
|
+
);
|
|
621
|
+
const nextAnchor = anchors.find((a) => a.stepNum > s.number);
|
|
622
|
+
const gapStart = prevAnchor ? prevAnchor.line + 1 : 0;
|
|
623
|
+
const gapEnd = nextAnchor ? nextAnchor.line : lines.length;
|
|
626
624
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
625
|
+
const gi = groups.findIndex(
|
|
626
|
+
(g, idx) => !used.has(idx) && g.line >= gapStart && g.line < gapEnd,
|
|
627
|
+
);
|
|
628
|
+
if (gi === -1) continue; // step never ran → no block in the log
|
|
629
|
+
start = groups[gi].line;
|
|
630
|
+
end = gapEnd;
|
|
631
|
+
} else {
|
|
632
|
+
// Direct anchor hit: span from this anchor to the next one.
|
|
633
|
+
start = anchors[anchorIdx].line;
|
|
634
|
+
end = anchorIdx + 1 < anchors.length ? anchors[anchorIdx + 1].line : lines.length;
|
|
635
|
+
}
|
|
631
636
|
}
|
|
637
|
+
|
|
638
|
+
// Drop trailing blank lines so the span slices to the step's own text.
|
|
639
|
+
while (end > start && lines[end - 1].trim() === "") end--;
|
|
640
|
+
spans.set(s.number, { start, end });
|
|
632
641
|
}
|
|
633
642
|
|
|
634
|
-
return
|
|
643
|
+
return spans;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** Text of `stepNumber` in its raw log, or null when the step never ran. */
|
|
647
|
+
export function extractStepFromLog(
|
|
648
|
+
log: string,
|
|
649
|
+
stepNumber: number,
|
|
650
|
+
apiSteps: { number: number; name: string }[],
|
|
651
|
+
): string | null {
|
|
652
|
+
const span = stepLineSpans(log, apiSteps).get(stepNumber);
|
|
653
|
+
if (span === undefined) return null;
|
|
654
|
+
return log.split("\n").slice(span.start, span.end).join("\n").trimEnd();
|
|
635
655
|
}
|
|
636
656
|
|
|
637
657
|
// ── ci-logs rendering (pure, testable) ──────────────────────────────────────
|
|
638
658
|
|
|
639
659
|
export interface CiLogsJob {
|
|
640
660
|
id: number;
|
|
661
|
+
run_id: number;
|
|
662
|
+
/** Canonical `api.github.com/repos/<owner>/<repo>/actions/runs/<id>`. */
|
|
663
|
+
run_url: string;
|
|
641
664
|
name: string;
|
|
642
665
|
status: string;
|
|
643
666
|
conclusion: string | null;
|
|
@@ -649,500 +672,50 @@ export interface CiLogsResult {
|
|
|
649
672
|
details: Record<string, unknown>;
|
|
650
673
|
}
|
|
651
674
|
|
|
652
|
-
/**
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
// eslint-disable-next-line no-control-regex -- intentional: matching raw ESC sequences in runner logs
|
|
656
|
-
const ANSI_RE = /\u001B\[[0-9;]*m/g;
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* Strip the runner framing from a step's raw log, leaving the command's own
|
|
660
|
-
* output as plain text: removes the per-line timestamp prefix, ANSI color
|
|
661
|
-
* escapes and `##[group]` / `##[endgroup]` marker lines. `##[error]` /
|
|
662
|
-
* `##[warning]` lines are kept — their message is part of the output.
|
|
663
|
-
*/
|
|
664
|
-
export function cleanStepOutput(stepLog: string): string {
|
|
665
|
-
return stepLog
|
|
666
|
-
.split("\n")
|
|
667
|
-
.map((line) =>
|
|
668
|
-
line
|
|
669
|
-
.replace(/^\uFEFF/, "") // UTF-8 BOM on the first line
|
|
670
|
-
.replace(RUNNER_TIMESTAMP_RE, "")
|
|
671
|
-
.replaceAll(ANSI_RE, "")
|
|
672
|
-
.replace(/\r$/, "")
|
|
673
|
-
.trimEnd(),
|
|
674
|
-
)
|
|
675
|
-
.filter((line) => !line.startsWith("##[group]") && !line.startsWith("##[endgroup]"))
|
|
676
|
-
.join("\n")
|
|
677
|
-
.trim();
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* Strip terminal escape sequences and a leading UTF-8 BOM from a raw job log,
|
|
682
|
-
* keeping everything else — timestamps, `##[group]` markers, blank lines —
|
|
683
|
-
* intact. Used when writing a job's complete log to a file: complete, but
|
|
684
|
-
* readable without ANSI garbage.
|
|
685
|
-
*/
|
|
686
|
-
export function stripAnsi(text: string): string {
|
|
687
|
-
return text.replace(/^\uFEFF/, "").replaceAll(ANSI_RE, "");
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Appended to `read-github-ci-logs` content so the raw log file path reaches
|
|
692
|
-
* the model: tool `details` is not part of the LLM context, so the path has to
|
|
693
|
-
* live in the returned text.
|
|
694
|
-
*/
|
|
695
|
-
function rawLogNotice(path: string): string {
|
|
696
|
-
return `[Raw job log (whole job): ${path}]`;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
export interface StepLogParams {
|
|
700
|
-
runId: string;
|
|
701
|
-
job?: string;
|
|
702
|
-
step: string;
|
|
703
|
-
offset?: number;
|
|
704
|
-
limit?: number;
|
|
705
|
-
/** Return the complete, untruncated step output (ignores `offset`/`limit`). */
|
|
706
|
-
full?: boolean;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
* Render the result of `read-github-ci-logs` for a single step: the step's
|
|
711
|
-
* complete log as plain text (no runner framing). `job` is required — a step
|
|
712
|
-
* only exists inside a specific job. `offset`/`limit` control the returned
|
|
713
|
-
* text. Pure — no network, no `gh`.
|
|
714
|
-
*/
|
|
715
|
-
export async function renderStepLog(
|
|
716
|
-
params: StepLogParams,
|
|
717
|
-
jobs: CiLogsJob[],
|
|
718
|
-
fetchJobLog: (jobId: number) => Promise<string>,
|
|
719
|
-
onUpdate?: (msg: CiLogsResult) => void,
|
|
720
|
-
): Promise<CiLogsResult> {
|
|
721
|
-
const { job, step, offset, limit, full } = params;
|
|
722
|
-
|
|
723
|
-
if (!job) {
|
|
724
|
-
return {
|
|
725
|
-
content: [{ type: "text", text: "`job` is required when fetching a step's logs." }],
|
|
726
|
-
details: {},
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
const isNumeric = /^\d+$/.test(job);
|
|
731
|
-
const targetJob = jobs.find((j) => (isNumeric ? String(j.id) : j.name) === job);
|
|
732
|
-
if (!targetJob) {
|
|
733
|
-
return {
|
|
734
|
-
content: [
|
|
735
|
-
{
|
|
736
|
-
type: "text",
|
|
737
|
-
text: `Job "${job}" not found. Available: ${jobs.map((j) => `${j.name} (id: ${j.id})`).join(", ")}`,
|
|
738
|
-
},
|
|
739
|
-
],
|
|
740
|
-
details: {},
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
if (targetJob.status === "queued") {
|
|
745
|
-
return {
|
|
746
|
-
content: [
|
|
747
|
-
{
|
|
748
|
-
type: "text",
|
|
749
|
-
text: `Job "${targetJob.name}" is still queued — no logs available yet. Use \`watch-github-run\` to wait for it to start, then retry.`,
|
|
750
|
-
},
|
|
751
|
-
],
|
|
752
|
-
details: {},
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Resolve step name → number
|
|
757
|
-
const found = targetJob.steps.find((s) => s.name.toLowerCase() === step.toLowerCase());
|
|
758
|
-
if (!found) {
|
|
759
|
-
return {
|
|
760
|
-
content: [
|
|
761
|
-
{
|
|
762
|
-
type: "text",
|
|
763
|
-
text: `Step "${step}" not found. Available: ${targetJob.steps.map((s) => `${s.name} (${s.number})`).join(", ")}`,
|
|
764
|
-
},
|
|
765
|
-
],
|
|
766
|
-
details: {},
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
const stepNum = found.number;
|
|
770
|
-
|
|
771
|
-
if (stepNum < 1 || stepNum > targetJob.steps.length) {
|
|
772
|
-
return {
|
|
773
|
-
content: [
|
|
774
|
-
{
|
|
775
|
-
type: "text",
|
|
776
|
-
text: `Step ${stepNum} out of range. Job "${targetJob.name}" has ${targetJob.steps.length} steps (1-${targetJob.steps.length}).`,
|
|
777
|
-
},
|
|
778
|
-
],
|
|
779
|
-
details: {},
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
onUpdate?.({
|
|
784
|
-
content: [{ type: "text", text: `Fetching logs for step ${stepNum}...` }],
|
|
785
|
-
details: {},
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
const rawLog = await fetchJobLog(targetJob.id);
|
|
789
|
-
const rawLogFile = jobLogPath(params.runId, targetJob.id);
|
|
790
|
-
|
|
791
|
-
const stepLog = extractStepFromLog(rawLog, stepNum, targetJob.steps);
|
|
792
|
-
if (stepLog === null) {
|
|
793
|
-
return {
|
|
794
|
-
content: [
|
|
795
|
-
{
|
|
796
|
-
type: "text",
|
|
797
|
-
text: `Could not extract step ${stepNum} from job "${targetJob.name}" logs. The log may be malformed or empty — read the raw file instead.\n\n${rawLogNotice(rawLogFile)}`,
|
|
798
|
-
},
|
|
799
|
-
],
|
|
800
|
-
details: { rawLogFile },
|
|
801
|
-
};
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const clean = cleanStepOutput(stepLog);
|
|
805
|
-
|
|
806
|
-
// `full`: return the complete output, no truncation and no offset.
|
|
807
|
-
if (full) {
|
|
808
|
-
const fullLines = clean.split("\n").length;
|
|
809
|
-
return {
|
|
810
|
-
content: [{ type: "text", text: `${clean}\n\n${rawLogNotice(rawLogFile)}` }],
|
|
811
|
-
details: {
|
|
812
|
-
summary: `Step ${stepNum} — ${targetJob.name} / ${found.name}: complete output (${fullLines} lines)`,
|
|
813
|
-
truncated: false,
|
|
814
|
-
full: true,
|
|
815
|
-
rawLogFile,
|
|
816
|
-
job: {
|
|
817
|
-
name: targetJob.name,
|
|
818
|
-
conclusion: targetJob.conclusion,
|
|
819
|
-
steps: stepsDetail(targetJob, new Set([stepNum])),
|
|
820
|
-
},
|
|
821
|
-
totalLines: fullLines,
|
|
822
|
-
shownLines: fullLines,
|
|
823
|
-
},
|
|
824
|
-
};
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// Apply offset on the cleaned text, then truncate.
|
|
828
|
-
const totalLines = clean.split("\n").length;
|
|
829
|
-
let logToShow = clean;
|
|
830
|
-
let appliedOffset = false;
|
|
831
|
-
if (offset !== undefined && offset > 1) {
|
|
832
|
-
if (offset > totalLines) {
|
|
833
|
-
return {
|
|
834
|
-
content: [
|
|
835
|
-
{
|
|
836
|
-
type: "text",
|
|
837
|
-
text: `Offset ${offset} exceeds step log length (${totalLines} lines).\n\n${rawLogNotice(rawLogFile)}`,
|
|
838
|
-
},
|
|
839
|
-
],
|
|
840
|
-
details: { rawLogFile },
|
|
841
|
-
};
|
|
842
|
-
}
|
|
843
|
-
logToShow = clean
|
|
844
|
-
.split("\n")
|
|
845
|
-
.slice(offset - 1)
|
|
846
|
-
.join("\n");
|
|
847
|
-
appliedOffset = true;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
const maxLines = limit ?? 500;
|
|
851
|
-
const maxBytes = 60 * 1024;
|
|
852
|
-
const { text, truncated: tr } = truncate(logToShow, maxLines, maxBytes);
|
|
853
|
-
const shownLines = text.split("\n").length;
|
|
854
|
-
|
|
855
|
-
return {
|
|
856
|
-
content: [{ type: "text", text: `${text}\n\n${rawLogNotice(rawLogFile)}` }],
|
|
857
|
-
details: {
|
|
858
|
-
summary: `Step ${stepNum} — ${targetJob.name} / ${found.name}: ${shownLines} of ${totalLines} lines${tr ? " (truncated)" : ""}`,
|
|
859
|
-
truncated: tr,
|
|
860
|
-
rawLogFile,
|
|
861
|
-
job: {
|
|
862
|
-
name: targetJob.name,
|
|
863
|
-
conclusion: targetJob.conclusion,
|
|
864
|
-
steps: stepsDetail(targetJob, new Set([stepNum])),
|
|
865
|
-
},
|
|
866
|
-
totalLines,
|
|
867
|
-
shownLines,
|
|
868
|
-
offset: appliedOffset ? offset : undefined,
|
|
869
|
-
},
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
export interface JobLogsParams {
|
|
874
|
-
runId: string;
|
|
875
|
-
job?: string;
|
|
876
|
-
offset?: number;
|
|
877
|
-
limit?: number;
|
|
878
|
-
/** Expand every step's complete output (default: only failed steps, truncated). */
|
|
879
|
-
full?: boolean;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
export interface JobLogsStep {
|
|
675
|
+
/** One step of a job, indexed into the job's raw log file. */
|
|
676
|
+
export interface CiLogsStepIndex {
|
|
677
|
+
number: number;
|
|
883
678
|
name: string;
|
|
884
|
-
|
|
679
|
+
conclusion: string | null;
|
|
680
|
+
/** 1-based inclusive line range of this step's block in `log_file`. */
|
|
681
|
+
start_line?: number;
|
|
682
|
+
end_line?: number;
|
|
885
683
|
}
|
|
886
684
|
|
|
887
|
-
|
|
685
|
+
/** A job's steps plus the raw log file holding their output. */
|
|
686
|
+
export interface CiLogsJobIndex {
|
|
888
687
|
name: string;
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
688
|
+
id: number;
|
|
689
|
+
status: string;
|
|
690
|
+
conclusion: string | null;
|
|
691
|
+
log_file: string;
|
|
692
|
+
steps: CiLogsStepIndex[];
|
|
892
693
|
}
|
|
893
694
|
|
|
894
695
|
/**
|
|
895
|
-
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
* text. Pure — no network, no `gh`.
|
|
696
|
+
* Index a job's steps into its raw log: every step that produced a log block
|
|
697
|
+
* gets the `[start_line, end_line]` range (1-based, inclusive) of that block in
|
|
698
|
+
* `log_file`; steps that never ran (skipped, or absent from the log) carry no
|
|
699
|
+
* range. The step content itself is not returned — the model reads it out of
|
|
700
|
+
* the file.
|
|
901
701
|
*/
|
|
902
|
-
export
|
|
903
|
-
|
|
904
|
-
jobs: CiLogsJob[],
|
|
905
|
-
fetchJobLog: (jobId: number) => Promise<string>,
|
|
906
|
-
): Promise<CiLogsResult> {
|
|
907
|
-
const { job, offset, limit, full } = params;
|
|
908
|
-
|
|
909
|
-
if (jobs.length === 0) {
|
|
910
|
-
return {
|
|
911
|
-
content: [{ type: "text", text: `No jobs found for run ${params.runId}` }],
|
|
912
|
-
details: {},
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
let targetJobs = jobs;
|
|
917
|
-
if (job) {
|
|
918
|
-
const isNumeric = /^\d+$/.test(job);
|
|
919
|
-
targetJobs = jobs.filter((j) => (isNumeric ? String(j.id) : j.name) === job);
|
|
920
|
-
if (targetJobs.length === 0) {
|
|
921
|
-
return {
|
|
922
|
-
content: [
|
|
923
|
-
{
|
|
924
|
-
type: "text",
|
|
925
|
-
text: `Job "${job}" not found. Available: ${jobs.map((j) => `${j.name} (id: ${j.id})`).join(", ")}`,
|
|
926
|
-
},
|
|
927
|
-
],
|
|
928
|
-
details: {},
|
|
929
|
-
};
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
const output: JobLogsOutput[] = [];
|
|
934
|
-
|
|
935
|
-
for (const j of targetJobs) {
|
|
936
|
-
const steps: JobLogsStep[] = [];
|
|
937
|
-
let rawLog: string | null = null;
|
|
938
|
-
|
|
939
|
-
for (const s of j.steps) {
|
|
940
|
-
if (!full && s.conclusion !== "failure") {
|
|
941
|
-
steps.push({ name: s.name });
|
|
942
|
-
continue;
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
try {
|
|
946
|
-
rawLog ??= await fetchJobLog(j.id);
|
|
947
|
-
const stepLog = extractStepFromLog(rawLog, s.number, j.steps);
|
|
948
|
-
if (!stepLog) {
|
|
949
|
-
steps.push({ name: s.name });
|
|
950
|
-
continue;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
const clean = cleanStepOutput(stepLog);
|
|
954
|
-
|
|
955
|
-
// `full`: every step carries its complete, untruncated output.
|
|
956
|
-
if (full) {
|
|
957
|
-
steps.push({ name: s.name, output: clean });
|
|
958
|
-
continue;
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
const totalLines = clean.split("\n").length;
|
|
962
|
-
|
|
963
|
-
// Apply offset on the cleaned text, then truncate.
|
|
964
|
-
let logToShow = clean;
|
|
965
|
-
if (offset !== undefined && offset > 1) {
|
|
966
|
-
if (offset > totalLines) {
|
|
967
|
-
steps.push({ name: s.name });
|
|
968
|
-
continue;
|
|
969
|
-
}
|
|
970
|
-
logToShow = clean
|
|
971
|
-
.split("\n")
|
|
972
|
-
.slice(offset - 1)
|
|
973
|
-
.join("\n");
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
const { text } = truncate(logToShow, limit ?? 500, 60 * 1024);
|
|
977
|
-
steps.push({ name: s.name, ...(text && { output: text }) });
|
|
978
|
-
} catch {
|
|
979
|
-
// Log fetch failed — list the step without an output.
|
|
980
|
-
steps.push({ name: s.name });
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
output.push({
|
|
985
|
-
name: j.name,
|
|
986
|
-
...(rawLog !== null && { log_file: jobLogPath(params.runId, j.id) }),
|
|
987
|
-
steps,
|
|
988
|
-
});
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
const totalJobs = output.length;
|
|
992
|
-
const failedJobs = output.filter((j) => j.steps.some((s) => s.output !== undefined)).length;
|
|
993
|
-
const expandedSteps = output.reduce(
|
|
994
|
-
(acc, j) => acc + j.steps.filter((s) => s.output !== undefined).length,
|
|
995
|
-
0,
|
|
996
|
-
);
|
|
997
|
-
|
|
702
|
+
export function jobLogIndex(job: CiLogsJob, rawLog: string): CiLogsJobIndex {
|
|
703
|
+
const spans = stepLineSpans(rawLog, job.steps);
|
|
998
704
|
return {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
jobs: targetJobs.map((j) => ({
|
|
1007
|
-
name: j.name,
|
|
1008
|
-
conclusion: j.conclusion,
|
|
1009
|
-
steps: stepsDetail(j, undefined),
|
|
1010
|
-
})),
|
|
1011
|
-
},
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
// ── writing complete logs to a file ─────────────────────────────────────────
|
|
1016
|
-
|
|
1017
|
-
export interface WriteLogFileParams {
|
|
1018
|
-
runId: string;
|
|
1019
|
-
job?: string;
|
|
1020
|
-
step?: string;
|
|
1021
|
-
outputFile: string;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
/**
|
|
1025
|
-
* Write the complete log to a file and return metadata (path, line/byte
|
|
1026
|
-
* counts) instead of the log content itself. With `step`: the step's cleaned
|
|
1027
|
-
* output. Without `step`: the whole job's log, timestamps and `##[group]`
|
|
1028
|
-
* markers kept but ANSI escapes stripped. `job` is required when the run has
|
|
1029
|
-
* more than one job (a single-job run is used implicitly). Relative
|
|
1030
|
-
* `outputFile` paths resolve against `cwd`.
|
|
1031
|
-
*/
|
|
1032
|
-
export async function writeLogFile(
|
|
1033
|
-
params: WriteLogFileParams,
|
|
1034
|
-
jobs: CiLogsJob[],
|
|
1035
|
-
fetchJobLog: (jobId: number) => Promise<string>,
|
|
1036
|
-
cwd: string | undefined,
|
|
1037
|
-
input: unknown,
|
|
1038
|
-
): Promise<CiLogsResult> {
|
|
1039
|
-
const { job, step, outputFile } = params;
|
|
1040
|
-
|
|
1041
|
-
const isNumeric = /^\d+$/.test(job ?? "");
|
|
1042
|
-
const targetJobs = job ? jobs.filter((j) => (isNumeric ? String(j.id) : j.name) === job) : jobs;
|
|
1043
|
-
if (targetJobs.length === 0) {
|
|
1044
|
-
return {
|
|
1045
|
-
content: [
|
|
1046
|
-
{
|
|
1047
|
-
type: "text",
|
|
1048
|
-
text: `Job "${job}" not found. Available: ${jobs.map((j) => `${j.name} (id: ${j.id})`).join(", ")}`,
|
|
1049
|
-
},
|
|
1050
|
-
],
|
|
1051
|
-
details: { input },
|
|
1052
|
-
};
|
|
1053
|
-
}
|
|
1054
|
-
if (targetJobs.length > 1) {
|
|
1055
|
-
return {
|
|
1056
|
-
content: [
|
|
1057
|
-
{
|
|
1058
|
-
type: "text",
|
|
1059
|
-
text: `Job "${job}" matches ${targetJobs.length} jobs. Specify a unique job name or id. Available: ${jobs.map((j) => `${j.name} (id: ${j.id})`).join(", ")}`,
|
|
1060
|
-
},
|
|
1061
|
-
],
|
|
1062
|
-
details: { input },
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
const targetJob = targetJobs[0];
|
|
1066
|
-
|
|
1067
|
-
if (targetJob.status === "queued") {
|
|
1068
|
-
return {
|
|
1069
|
-
content: [
|
|
1070
|
-
{
|
|
1071
|
-
type: "text",
|
|
1072
|
-
text: `Job "${targetJob.name}" is still queued — no logs available yet. Use \`watch-github-run\` to wait for it to start, then retry.`,
|
|
1073
|
-
},
|
|
1074
|
-
],
|
|
1075
|
-
details: { input },
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
const rawLog = await fetchJobLog(targetJob.id);
|
|
1080
|
-
|
|
1081
|
-
let content: string;
|
|
1082
|
-
let what: string;
|
|
1083
|
-
if (step === undefined) {
|
|
1084
|
-
content = stripAnsi(rawLog);
|
|
1085
|
-
what = `job "${targetJob.name}" (id: ${targetJob.id})`;
|
|
1086
|
-
} else {
|
|
1087
|
-
const found = targetJob.steps.find((s) => s.name.toLowerCase() === step.toLowerCase());
|
|
1088
|
-
if (found === undefined) {
|
|
1089
|
-
return {
|
|
1090
|
-
content: [
|
|
1091
|
-
{
|
|
1092
|
-
type: "text",
|
|
1093
|
-
text: `Step "${step}" not found. Available: ${targetJob.steps.map((s) => `${s.name} (${s.number})`).join(", ")}`,
|
|
1094
|
-
},
|
|
1095
|
-
],
|
|
1096
|
-
details: { input },
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
const stepLog = extractStepFromLog(rawLog, found.number, targetJob.steps);
|
|
1100
|
-
if (stepLog === null) {
|
|
705
|
+
name: job.name,
|
|
706
|
+
id: job.id,
|
|
707
|
+
status: job.status,
|
|
708
|
+
conclusion: job.conclusion,
|
|
709
|
+
log_file: jobLogPath(repoFromRunUrl(job.run_url), String(job.run_id), job.id),
|
|
710
|
+
steps: job.steps.map((s) => {
|
|
711
|
+
const span = spans.get(s.number);
|
|
1101
712
|
return {
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
},
|
|
1107
|
-
],
|
|
1108
|
-
details: { input },
|
|
713
|
+
number: s.number,
|
|
714
|
+
name: s.name,
|
|
715
|
+
conclusion: s.conclusion,
|
|
716
|
+
...(span && span.end > span.start && { start_line: span.start + 1, end_line: span.end }),
|
|
1109
717
|
};
|
|
1110
|
-
}
|
|
1111
|
-
content = cleanStepOutput(stepLog);
|
|
1112
|
-
what = `step ${found.number} ("${found.name}") of job "${targetJob.name}"`;
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
const target = resolve(cwd ?? process.cwd(), outputFile);
|
|
1116
|
-
await mkdir(dirname(target), { recursive: true });
|
|
1117
|
-
await writeFile(target, content);
|
|
1118
|
-
|
|
1119
|
-
const lines = content.split("\n").length;
|
|
1120
|
-
const bytes = Buffer.byteLength(content, "utf8");
|
|
1121
|
-
|
|
1122
|
-
return {
|
|
1123
|
-
content: [
|
|
1124
|
-
{
|
|
1125
|
-
type: "text",
|
|
1126
|
-
text:
|
|
1127
|
-
`## CI log written to \`${target}\`\n\n` +
|
|
1128
|
-
`- content: ${what}\n` +
|
|
1129
|
-
`- ${lines} lines, ${bytes} bytes\n` +
|
|
1130
|
-
`- run: ${params.runId}\n\n` +
|
|
1131
|
-
`Read it with the \`read\` tool (use \`offset\`/\`limit\` for large files).`,
|
|
1132
|
-
},
|
|
1133
|
-
],
|
|
1134
|
-
details: {
|
|
1135
|
-
outputFile: target,
|
|
1136
|
-
lines,
|
|
1137
|
-
bytes,
|
|
1138
|
-
runId: params.runId,
|
|
1139
|
-
job: {
|
|
1140
|
-
name: targetJob.name,
|
|
1141
|
-
id: targetJob.id,
|
|
1142
|
-
conclusion: targetJob.conclusion,
|
|
1143
|
-
},
|
|
1144
|
-
input,
|
|
1145
|
-
},
|
|
718
|
+
}),
|
|
1146
719
|
};
|
|
1147
720
|
}
|
|
1148
721
|
|
|
@@ -1880,49 +1453,17 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1880
1453
|
name: "read-github-ci-logs",
|
|
1881
1454
|
label: "GitHub CI Logs",
|
|
1882
1455
|
description:
|
|
1883
|
-
"
|
|
1456
|
+
"Download one GitHub Actions job's CI log and index its steps. Returns JSON {name, id, status, conclusion, log_file, steps:[{number, name, conclusion, start_line?, end_line?}]}: `log_file` is the job's complete raw log on disk (runner timestamps and ANSI kept, exactly as GitHub delivers it) and each step carries the 1-based inclusive line range of its block inside that file. Read the content out of the file yourself (read/grep with offset/limit) — it is not echoed back. Get `job` names/ids from read-github-workflow-jobs." +
|
|
1457
|
+
" Note: queued jobs have no logs yet; use watch-github-run to wait for completion.",
|
|
1884
1458
|
promptSnippet: "Read GitHub CI logs",
|
|
1885
1459
|
parameters: Type.Object({
|
|
1886
1460
|
run_id: Type.Union([Type.Number(), Type.String()], { description: "Workflow run ID" }),
|
|
1887
1461
|
repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
|
|
1888
|
-
job: Type.
|
|
1889
|
-
Type.String({
|
|
1890
|
-
description:
|
|
1891
|
-
"Job name or ID. Optional filter when listing jobs; required when fetching a specific step's logs.",
|
|
1892
|
-
}),
|
|
1893
|
-
),
|
|
1894
|
-
step: Type.Optional(
|
|
1895
|
-
Type.String({
|
|
1896
|
-
description:
|
|
1897
|
-
"Step name to fetch the complete log for. Requires `job`. Omit to list jobs/steps with failed step logs expanded.",
|
|
1898
|
-
}),
|
|
1899
|
-
),
|
|
1900
|
-
offset: Type.Optional(
|
|
1901
|
-
Type.Number({
|
|
1902
|
-
description:
|
|
1903
|
-
"Line number to start each output text from (1-indexed). Useful for long outputs where the error is at the end.",
|
|
1904
|
-
}),
|
|
1905
|
-
),
|
|
1906
|
-
limit: Type.Optional(
|
|
1907
|
-
Type.Number({
|
|
1908
|
-
description: "Maximum number of lines per output text (default 500).",
|
|
1909
|
-
}),
|
|
1910
|
-
),
|
|
1911
|
-
full: Type.Optional(
|
|
1912
|
-
Type.Boolean({
|
|
1913
|
-
description:
|
|
1914
|
-
"Return complete, untruncated output instead of the default 500-line/60KB cap. With `step`: that step's full output. Without `step`: every step's full output (not just failed ones). Ignored when `output_file` is set. Caution: very large outputs consume a lot of LLM context — prefer `output_file` for big logs.",
|
|
1915
|
-
}),
|
|
1916
|
-
),
|
|
1917
|
-
output_file: Type.Optional(
|
|
1918
|
-
Type.String({
|
|
1919
|
-
description:
|
|
1920
|
-
"Write the complete log to this file instead of returning it (relative paths resolve against the working directory). With `step` (requires `job`): the step's cleaned output. Without `step`: requires `job` (or a run with a single job) and writes that job's full log — timestamps and group markers kept, ANSI escapes stripped. Returns the file path; read it with the `read` tool.",
|
|
1921
|
-
}),
|
|
1922
|
-
),
|
|
1462
|
+
job: Type.String({ description: "Job name or job ID, from read-github-workflow-jobs." }),
|
|
1923
1463
|
}),
|
|
1924
1464
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
1925
|
-
const { run_id, repo, job
|
|
1465
|
+
const { run_id, repo, job } = params;
|
|
1466
|
+
const runId = String(run_id);
|
|
1926
1467
|
|
|
1927
1468
|
const pendant = subtitlePendant(params, "run_id");
|
|
1928
1469
|
const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
|
|
@@ -1933,52 +1474,35 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1933
1474
|
});
|
|
1934
1475
|
const { jobs } = Value.Parse(jobsResponseSchema, JSON.parse(jobsOut));
|
|
1935
1476
|
|
|
1936
|
-
const
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
params,
|
|
1477
|
+
const failure = (text: string): CiLogsResult => ({
|
|
1478
|
+
content: [{ type: "text", text }],
|
|
1479
|
+
details: { input: params, ...(pendant && { pendant }) },
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
const isNumeric = /^\d+$/.test(job);
|
|
1483
|
+
const target = jobs.find((j) => (isNumeric ? String(j.id) : j.name) === job);
|
|
1484
|
+
if (target === undefined) {
|
|
1485
|
+
return failure(
|
|
1486
|
+
`Job "${job}" not found in run ${runId}. Available: ${jobs.map((j) => `${j.name} (id: ${j.id})`).join(", ")}`,
|
|
1947
1487
|
);
|
|
1948
|
-
return { ...result, details: { ...result.details, ...(pendant && { pendant }) } };
|
|
1949
1488
|
}
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
onUpdate?.({
|
|
1954
|
-
content: [{ type: "text", text: `Fetching job list...` }],
|
|
1955
|
-
details: {},
|
|
1956
|
-
});
|
|
1957
|
-
const stepResult = await renderStepLog(
|
|
1958
|
-
{ runId: String(run_id), job, step, offset, limit, full },
|
|
1959
|
-
jobs,
|
|
1960
|
-
fetchJobLog,
|
|
1961
|
-
onUpdate,
|
|
1489
|
+
if (target.status === "queued") {
|
|
1490
|
+
return failure(
|
|
1491
|
+
`Job "${target.name}" is still queued — no logs available yet. Use \`watch-github-run\` to wait for it to start, then retry.`,
|
|
1962
1492
|
);
|
|
1963
|
-
return {
|
|
1964
|
-
...stepResult,
|
|
1965
|
-
details: { ...stepResult.details, input: params, ...(pendant && { pendant }) },
|
|
1966
|
-
};
|
|
1967
1493
|
}
|
|
1968
1494
|
|
|
1969
|
-
// ── List jobs/steps, with failed step logs expanded ────────────────
|
|
1970
1495
|
onUpdate?.({
|
|
1971
|
-
content: [{ type: "text", text: `Fetching job
|
|
1496
|
+
content: [{ type: "text", text: `Fetching log of job "${target.name}"...` }],
|
|
1972
1497
|
details: {},
|
|
1973
1498
|
});
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
);
|
|
1499
|
+
|
|
1500
|
+
const rawLog = await getJobLog(target, signal, ctx.cwd, params);
|
|
1501
|
+
const index = jobLogIndex(target, rawLog);
|
|
1502
|
+
|
|
1979
1503
|
return {
|
|
1980
|
-
|
|
1981
|
-
details: { ...
|
|
1504
|
+
content: [{ type: "text", text: JSON.stringify(index, null, 2) }],
|
|
1505
|
+
details: { ...index, input: params, ...(pendant && { pendant }) },
|
|
1982
1506
|
};
|
|
1983
1507
|
},
|
|
1984
1508
|
});
|