@trim21/personal-pi-extensions 0.1.521 → 0.1.523

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.521",
3
+ "version": "0.1.523",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
package/src/aft/bridge.ts CHANGED
@@ -59,7 +59,7 @@ export async function createAftState(
59
59
  }
60
60
 
61
61
  /** semantic_search 等待索引构建完成的上限(与 Rust 侧 AFT_WAIT_FOR_SEMANTIC_READY_MS 一致)。 */
62
- export const SEMANTIC_INDEX_WAIT_TIMEOUT_MS = 600_000;
62
+ export const SEMANTIC_INDEX_WAIT_TIMEOUT_MS = 3_600_000;
63
63
 
64
64
  /**
65
65
  * 调用图存储未就绪时查询侧的内联等待窗口。Rust 侧默认 0(纯异步:立即返回
@@ -19,6 +19,7 @@
19
19
  * - list-github-releases: List releases
20
20
  * - read-github-release: Get release details
21
21
  * - wait-github-pr-checks: Watch PR CI checks
22
+ * - wait-github-commit-checks: Watch CI checks of a commit (no PR required)
22
23
  * - watch-github-run: Watch a workflow run
23
24
  *
24
25
  * Install:
@@ -243,6 +244,15 @@ function repoArgs(repo?: string): string[] {
243
244
  return repo ? ["--repo", repo] : [];
244
245
  }
245
246
 
247
+ /** Split `OWNER/REPO`; throws when the name doesn't have exactly one slash. */
248
+ function splitRepo(nameWithOwner: string): { owner: string; repo: string } {
249
+ const slash = nameWithOwner.indexOf("/");
250
+ if (slash <= 0 || slash === nameWithOwner.length - 1 || nameWithOwner.includes("/", slash + 1)) {
251
+ throw new Error(`invalid repository: ${nameWithOwner} (expected OWNER/REPO)`);
252
+ }
253
+ return { owner: nameWithOwner.slice(0, slash), repo: nameWithOwner.slice(slash + 1) };
254
+ }
255
+
246
256
  // ── runtime validation schemas for JSON.parse results ───────────────────────
247
257
 
248
258
  const repoViewSchema = Type.Object({ nameWithOwner: Type.String() });
@@ -1207,11 +1217,12 @@ export function checkDisplayName(check: MergedCheck): string {
1207
1217
  * Pure — no network.
1208
1218
  */
1209
1219
  export function renderPrChecksList(options: {
1210
- prNumber: number | string;
1220
+ /** Report subject, e.g. `PR #7` or `commit 5a7c407`. */
1221
+ subject: string;
1211
1222
  round: number;
1212
1223
  checks: readonly MergedCheck[];
1213
1224
  }): string {
1214
- const { prNumber, round, checks } = options;
1225
+ const { subject, round, checks } = options;
1215
1226
  const completed = checks.filter((c) => c.bucket !== "pending").length;
1216
1227
 
1217
1228
  const pending = checks.filter((c) => c.bucket === "pending");
@@ -1225,7 +1236,7 @@ export function renderPrChecksList(options: {
1225
1236
  const body =
1226
1237
  checks.length === 0 ? "- _no checks reported_" : lines.length > 0 ? lines.join("\n") : "";
1227
1238
 
1228
- return `PR #${prNumber} checks — round ${round}: ${completed}/${checks.length} complete${body ? `\n\n${body}` : ""}`;
1239
+ return `${subject} checks — round ${round}: ${completed}/${checks.length} complete${body ? `\n\n${body}` : ""}`;
1229
1240
  }
1230
1241
 
1231
1242
  function sleepInterruptibly(ms: number, signal: AbortSignal | undefined): Promise<void> {
@@ -1255,12 +1266,19 @@ export interface ChecksPollResult {
1255
1266
  }
1256
1267
 
1257
1268
  export interface PollPrChecksOptions {
1258
- prNumber: number | string;
1269
+ /** Report subject for progress lines, e.g. `PR #7` or `commit 5a7c407`. */
1270
+ subject: string;
1259
1271
  owner: string;
1260
1272
  repo: string;
1261
1273
  headSha: string;
1262
1274
  failFast: boolean;
1263
1275
  checks: GithubChecksClient;
1276
+ /**
1277
+ * When set, only check runs triggered by this workflow event (e.g. push)
1278
+ * are judged; commit statuses have an unknown trigger event and are
1279
+ * excluded. Unset means all checks of the commit.
1280
+ */
1281
+ event?: string;
1264
1282
  /** Owned by the caller; the poll loop observes it but never aborts it. */
1265
1283
  signal: AbortSignal;
1266
1284
  /** Test overrides. */
@@ -1281,7 +1299,7 @@ export interface PollPrChecksOptions {
1281
1299
  * when no round ever succeeded by the deadline is the last error thrown.
1282
1300
  */
1283
1301
  export async function pollPrChecks(options: PollPrChecksOptions): Promise<ChecksPollResult> {
1284
- const { prNumber, owner, repo, headSha, failFast, checks, signal, onUpdate } = options;
1302
+ const { subject, owner, repo, headSha, failFast, checks, signal, onUpdate } = options;
1285
1303
  const intervalMs = options.intervalMs ?? CHECKS_POLL_INTERVAL_MS;
1286
1304
  const deadlineMs = options.deadlineMs ?? CHECKS_WATCH_DEADLINE_MS;
1287
1305
 
@@ -1299,9 +1317,12 @@ export async function pollPrChecks(options: PollPrChecksOptions): Promise<Checks
1299
1317
  ]);
1300
1318
  everSucceeded = true;
1301
1319
  lastChecks = mergeChecks(statuses, runs);
1320
+ if (options.event) {
1321
+ lastChecks = lastChecks.filter((c) => c.event === options.event);
1322
+ }
1302
1323
  onUpdate?.({
1303
1324
  content: [
1304
- { type: "text", text: renderPrChecksList({ prNumber, round, checks: lastChecks }) },
1325
+ { type: "text", text: renderPrChecksList({ subject, round, checks: lastChecks }) },
1305
1326
  ],
1306
1327
  details: {},
1307
1328
  });
@@ -1351,14 +1372,15 @@ export interface ChecksVerdict {
1351
1372
  * display-only enrichment. Pure — no network.
1352
1373
  */
1353
1374
  export function renderChecksVerdict(options: {
1354
- prNumber: number | string;
1375
+ /** Report subject, e.g. `PR #123` or `commit 5a7c407`. */
1376
+ subject: string;
1355
1377
  poll: ChecksPollResult;
1356
1378
  /** All Actions jobs of the head commit; failed/incomplete ones are listed. */
1357
1379
  actionJobs?: readonly ActionJob[];
1358
1380
  /** Set when the Actions job fetch failed; the verdict stays untouched. */
1359
1381
  enrichmentError?: string;
1360
1382
  }): ChecksVerdict {
1361
- const { prNumber, poll, actionJobs, enrichmentError } = options;
1383
+ const { subject, poll, actionJobs, enrichmentError } = options;
1362
1384
  const totalChecks = poll.checks.length;
1363
1385
  const failed = poll.checks.filter((c) => c.bucket === "fail");
1364
1386
  const pending = poll.checks.filter((c) => c.bucket === "pending");
@@ -1366,7 +1388,7 @@ export function renderChecksVerdict(options: {
1366
1388
  if (failed.length === 0 && poll.outcome === "completed") {
1367
1389
  return {
1368
1390
  status: "success",
1369
- text: `## PR #${prNumber} CI Checks - PASSED\n\nAll ${totalChecks} check(s) passed.`,
1391
+ text: `## ${subject} CI Checks - PASSED\n\nAll ${totalChecks} check(s) passed.`,
1370
1392
  failedJobs: [],
1371
1393
  };
1372
1394
  }
@@ -1400,7 +1422,7 @@ export function renderChecksVerdict(options: {
1400
1422
  return {
1401
1423
  status: "failure",
1402
1424
  text:
1403
- `## PR #${prNumber} CI Checks - FAILED\n\n` +
1425
+ `## ${subject} CI Checks - FAILED\n\n` +
1404
1426
  `${failed.length} of ${totalChecks} check(s) failed:\n\n${lines.join("\n")}`,
1405
1427
  failedJobs,
1406
1428
  };
@@ -1413,7 +1435,7 @@ export function renderChecksVerdict(options: {
1413
1435
  return {
1414
1436
  status: "pending",
1415
1437
  text:
1416
- `## PR #${prNumber} CI Checks - STILL IN FLIGHT\n\n` +
1438
+ `## ${subject} CI Checks - STILL IN FLIGHT\n\n` +
1417
1439
  `${pending.length} of ${totalChecks} check(s) still incomplete after ~${waitedMinutes}m:\n\n` +
1418
1440
  (pendingLines.length > 0 ? pendingLines.join("\n") : "- _no checks reported_"),
1419
1441
  failedJobs: [],
@@ -1448,6 +1470,69 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
1448
1470
  const githubSearch = createGithubSearch();
1449
1471
  const githubChecks = createGithubChecks();
1450
1472
 
1473
+ /**
1474
+ * Shared wait core of `wait-github-pr-checks` and
1475
+ * `wait-github-commit-checks`: poll the commit's checks, enrich FAILED
1476
+ * reports with Actions job details (display-only), render the verdict.
1477
+ * The caller resolves repo/headSha; `subject` formats the report header.
1478
+ */
1479
+ async function waitChecksReport(options: {
1480
+ subject: string;
1481
+ owner: string;
1482
+ repo: string;
1483
+ headSha: string;
1484
+ failFast: boolean;
1485
+ event?: string;
1486
+ signal: AbortSignal | undefined;
1487
+ onUpdate: ((msg: CiLogsResult) => void) | undefined;
1488
+ params: unknown;
1489
+ pendant?: ToolPendant;
1490
+ }) {
1491
+ const { subject, owner, repo, headSha, failFast, event, signal, onUpdate, params, pendant } =
1492
+ options;
1493
+
1494
+ // 轮询层要求非空 signal;框架可能不给时构造一个占位的(从不取消)。
1495
+ const pollSignal = signal ?? new AbortController().signal;
1496
+
1497
+ const poll = await pollPrChecks({
1498
+ subject,
1499
+ owner,
1500
+ repo,
1501
+ headSha,
1502
+ failFast,
1503
+ event,
1504
+ checks: githubChecks,
1505
+ signal: pollSignal,
1506
+ onUpdate,
1507
+ });
1508
+
1509
+ // Actions job 详情只做展示补充,不影响判定(判定来自 checks bucket,
1510
+ // 覆盖 Azure 等外部 CI)。抓取失败时降级为提示,不推翻结论。
1511
+ let actionJobs: readonly ActionJob[] | undefined;
1512
+ let enrichmentError: string | undefined;
1513
+ if (poll.checks.some((c) => c.bucket === "fail")) {
1514
+ try {
1515
+ actionJobs = await githubChecks.actionJobs(owner, repo, headSha, pollSignal);
1516
+ } catch (error) {
1517
+ enrichmentError =
1518
+ error instanceof Error ? error.message : "Actions job details unavailable";
1519
+ }
1520
+ }
1521
+
1522
+ const verdict = renderChecksVerdict({ subject, poll, actionJobs, enrichmentError });
1523
+ return {
1524
+ content: [{ type: "text" as const, text: verdict.text }],
1525
+ details: {
1526
+ status: verdict.status,
1527
+ totalChecks: poll.checks.length,
1528
+ checks: poll.checks,
1529
+ failedJobs: verdict.failedJobs,
1530
+ input: params,
1531
+ ...(pendant && { pendant }),
1532
+ },
1533
+ };
1534
+ }
1535
+
1451
1536
  // ── read-github-issue ──────────────────────────────────────────────────────
1452
1537
  pi.registerTool({
1453
1538
  name: "read-github-issue",
@@ -1999,12 +2084,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
1999
2084
  });
2000
2085
 
2001
2086
  const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
2002
- const slash = effectiveRepo.indexOf("/");
2003
- if (slash <= 0 || slash === effectiveRepo.length - 1) {
2004
- throw new Error(`invalid repository: ${effectiveRepo} (expected OWNER/REPO)`);
2005
- }
2006
- const owner = effectiveRepo.slice(0, slash);
2007
- const repoName = effectiveRepo.slice(slash + 1);
2087
+ const { owner, repo: repoName } = splitRepo(effectiveRepo);
2008
2088
 
2009
2089
  const prOut = await ghExec(
2010
2090
  ["pr", "view", String(number), "--repo", effectiveRepo, "--json", "headRefOid"],
@@ -2012,45 +2092,75 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
2012
2092
  );
2013
2093
  const { headRefOid } = Value.Parse(prHeadSchema, JSON.parse(prOut));
2014
2094
 
2015
- // 轮询层要求非空 signal;框架可能不给时构造一个占位的(从不取消)。
2016
- const pollSignal = signal ?? new AbortController().signal;
2017
-
2018
- const poll = await pollPrChecks({
2019
- prNumber: number,
2095
+ return waitChecksReport({
2096
+ subject: `PR #${number}`,
2020
2097
  owner,
2021
2098
  repo: repoName,
2022
2099
  headSha: headRefOid,
2023
2100
  failFast: fail_fast === true,
2024
- checks: githubChecks,
2025
- signal: pollSignal,
2101
+ signal,
2026
2102
  onUpdate,
2103
+ params,
2104
+ pendant,
2027
2105
  });
2106
+ },
2107
+ });
2028
2108
 
2029
- // Actions job 详情只做展示补充,不影响判定(判定来自 checks bucket,
2030
- // 覆盖 Azure 等外部 CI)。抓取失败时降级为提示,不推翻结论。
2031
- let actionJobs: readonly ActionJob[] | undefined;
2032
- let enrichmentError: string | undefined;
2033
- if (poll.checks.some((c) => c.bucket === "fail")) {
2034
- try {
2035
- actionJobs = await githubChecks.actionJobs(owner, repoName, headRefOid, pollSignal);
2036
- } catch (error) {
2037
- enrichmentError =
2038
- error instanceof Error ? error.message : "Actions job details unavailable";
2039
- }
2040
- }
2109
+ // ── wait-github-commit-checks ──────────────────────────────────────────────
2110
+ pi.registerTool({
2111
+ name: "wait-github-commit-checks",
2112
+ label: "Watch GitHub Commit Checks",
2113
+ description:
2114
+ "Watch CI status checks for a commit until they complete — no pull request required. " +
2115
+ "Same semantics as wait-github-pr-checks: returns when any check fails (immediately under fail_fast) " +
2116
+ "or all checks pass/skip; on timeout the still-in-flight snapshot is returned. " +
2117
+ "With `event`, only check runs triggered by that workflow event (e.g. push) are judged; " +
2118
+ "commit statuses have an unknown trigger event and are excluded under a filter. " +
2119
+ "Use this to wait for the runs a commit's push triggered, or for checks on an arbitrary ref.",
2120
+ promptSnippet: "Watch and wait for GitHub commit CI checks to complete",
2121
+ parameters: Type.Object({
2122
+ commit: Type.Union([Type.Number(), Type.String()], {
2123
+ description:
2124
+ "Commit to wait for: full or partial SHA, branch name, or tag name (resolved to the commit's SHA)",
2125
+ }),
2126
+ repo: Type.Optional(Type.String({ description: "OWNER/REPO" })),
2127
+ event: Type.Optional(
2128
+ Type.String({
2129
+ description:
2130
+ "Only judge check runs triggered by this workflow event (e.g. push, pull_request)",
2131
+ }),
2132
+ ),
2133
+ fail_fast: Type.Optional(
2134
+ Type.Boolean({ description: "Exit immediately when any check fails (default: false)" }),
2135
+ ),
2136
+ }),
2137
+ async execute(_id, params, signal, onUpdate, ctx) {
2138
+ const { commit, repo, event, fail_fast } = params;
2041
2139
 
2042
- const verdict = renderChecksVerdict({ prNumber: number, poll, actionJobs, enrichmentError });
2043
- return {
2044
- content: [{ type: "text", text: verdict.text }],
2045
- details: {
2046
- status: verdict.status,
2047
- totalChecks: poll.checks.length,
2048
- checks: poll.checks,
2049
- failedJobs: verdict.failedJobs,
2050
- input: params,
2051
- ...(pendant && { pendant }),
2052
- },
2053
- };
2140
+ const pendant = subtitlePendant(params, "commit");
2141
+ onUpdate?.({
2142
+ content: [{ type: "text", text: `Watching CI checks for commit ${commit}...` }],
2143
+ details: {},
2144
+ });
2145
+
2146
+ const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
2147
+ const { owner, repo: repoName } = splitRepo(effectiveRepo);
2148
+
2149
+ const pollSignal = signal ?? new AbortController().signal;
2150
+ const sha = await githubChecks.headSha(owner, repoName, String(commit), pollSignal);
2151
+
2152
+ return waitChecksReport({
2153
+ subject: `commit ${sha.slice(0, 7)}`,
2154
+ owner,
2155
+ repo: repoName,
2156
+ headSha: sha,
2157
+ failFast: fail_fast === true,
2158
+ event,
2159
+ signal,
2160
+ onUpdate,
2161
+ params,
2162
+ pendant,
2163
+ });
2054
2164
  },
2055
2165
  });
2056
2166
 
package/src/lib/github.ts CHANGED
@@ -367,6 +367,8 @@ export interface GithubChecksClient {
367
367
  headSha: string,
368
368
  signal: AbortSignal,
369
369
  ): Promise<readonly ActionJob[]>;
370
+ /** Resolve a SHA, branch name, or tag name to the commit's full SHA. */
371
+ headSha(owner: string, repo: string, ref: string, signal: AbortSignal): Promise<string>;
370
372
  }
371
373
 
372
374
  /**
@@ -471,5 +473,12 @@ export function createGithubChecks(): GithubChecksClient {
471
473
  }
472
474
  return jobs;
473
475
  },
476
+
477
+ async headSha(owner, repo, ref, signal) {
478
+ const { data } = await api.call((octokit) =>
479
+ octokit.rest.repos.getCommit({ owner, repo, ref, request: { signal } }),
480
+ );
481
+ return data.sha;
482
+ },
474
483
  };
475
484
  }