@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.1
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/CHANGELOG.md +123 -0
- package/examples/extensions/plan-mode.ts +0 -1
- package/package.json +9 -9
- package/scripts/build-binary.ts +5 -0
- package/scripts/format-prompts.ts +1 -1
- package/src/autoresearch/helpers.ts +17 -0
- package/src/autoresearch/tools/log-experiment.ts +9 -17
- package/src/autoresearch/tools/run-experiment.ts +2 -17
- package/src/capability/skill.ts +7 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/list-models.ts +1 -1
- package/src/cli/shell-cli.ts +3 -13
- package/src/cli/update-cli.ts +1 -1
- package/src/cli.ts +11 -29
- package/src/commands/acp.ts +24 -0
- package/src/commands/launch.ts +6 -4
- package/src/commit/agentic/prompts/system.md +1 -1
- package/src/commit/agentic/tools/propose-changelog.ts +8 -1
- package/src/commit/analysis/conventional.ts +8 -66
- package/src/commit/map-reduce/reduce-phase.ts +6 -65
- package/src/commit/pipeline.ts +2 -2
- package/src/commit/shared-llm.ts +89 -0
- package/src/config/config-file.ts +210 -0
- package/src/config/model-equivalence.ts +8 -11
- package/src/config/model-registry.ts +13 -2
- package/src/config/model-resolver.ts +31 -4
- package/src/config/settings-schema.ts +102 -1
- package/src/config/settings.ts +1 -1
- package/src/config.ts +3 -219
- package/src/edit/index.ts +22 -1
- package/src/edit/modes/patch.ts +10 -0
- package/src/edit/modes/replace.ts +3 -0
- package/src/edit/renderer.ts +17 -1
- package/src/eval/js/context-manager.ts +1 -1
- package/src/eval/js/executor.ts +3 -0
- package/src/eval/js/shared/rewrite-imports.ts +122 -50
- package/src/eval/js/shared/runtime.ts +31 -4
- package/src/eval/js/tool-bridge.ts +43 -21
- package/src/eval/py/executor.ts +5 -0
- package/src/exa/factory.ts +2 -2
- package/src/exa/mcp-client.ts +74 -1
- package/src/exec/bash-executor.ts +5 -1
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -11
- package/src/extensibility/extensions/runner.ts +55 -2
- package/src/extensibility/extensions/types.ts +98 -221
- package/src/extensibility/hooks/types.ts +89 -314
- package/src/extensibility/shared-events.ts +343 -0
- package/src/extensibility/skills.ts +42 -1
- package/src/goals/index.ts +3 -0
- package/src/goals/runtime.ts +500 -0
- package/src/goals/state.ts +37 -0
- package/src/goals/tools/goal-tool.ts +237 -0
- package/src/hashline/anchors.ts +2 -2
- package/src/hindsight/mental-models.ts +1 -1
- package/src/internal-urls/agent-protocol.ts +1 -20
- package/src/internal-urls/artifact-protocol.ts +1 -19
- package/src/internal-urls/docs-index.generated.ts +9 -10
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/issue-pr-protocol.ts +577 -0
- package/src/internal-urls/registry-helpers.ts +25 -0
- package/src/internal-urls/router.ts +6 -3
- package/src/internal-urls/types.ts +22 -1
- package/src/main.ts +24 -11
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/modes/acp/acp-agent.ts +412 -71
- package/src/modes/acp/acp-client-bridge.ts +152 -0
- package/src/modes/acp/acp-event-mapper.ts +180 -15
- package/src/modes/acp/terminal-auth.ts +37 -0
- package/src/modes/components/assistant-message.ts +14 -8
- package/src/modes/components/bash-execution.ts +24 -63
- package/src/modes/components/custom-message.ts +14 -40
- package/src/modes/components/eval-execution.ts +27 -57
- package/src/modes/components/execution-shared.ts +102 -0
- package/src/modes/components/hook-message.ts +17 -49
- package/src/modes/components/mcp-add-wizard.ts +26 -5
- package/src/modes/components/message-frame.ts +88 -0
- package/src/modes/components/model-selector.ts +1 -1
- package/src/modes/components/read-tool-group.ts +29 -1
- package/src/modes/components/session-observer-overlay.ts +6 -2
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/status-line/segments.ts +55 -4
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +28 -10
- package/src/modes/components/tool-execution.ts +7 -8
- package/src/modes/controllers/command-controller-shared.ts +108 -0
- package/src/modes/controllers/command-controller.ts +27 -10
- package/src/modes/controllers/event-controller.ts +60 -18
- package/src/modes/controllers/extension-ui-controller.ts +8 -2
- package/src/modes/controllers/input-controller.ts +85 -39
- package/src/modes/controllers/mcp-command-controller.ts +56 -61
- package/src/modes/controllers/ssh-command-controller.ts +18 -57
- package/src/modes/interactive-mode.ts +675 -39
- package/src/modes/print-mode.ts +16 -86
- package/src/modes/rpc/rpc-mode.ts +30 -88
- package/src/modes/runtime-init.ts +115 -0
- package/src/modes/theme/defaults/dark-poimandres.json +2 -0
- package/src/modes/theme/defaults/light-poimandres.json +2 -0
- package/src/modes/theme/theme.ts +18 -6
- package/src/modes/types.ts +20 -5
- package/src/modes/utils/context-usage.ts +13 -13
- package/src/modes/utils/ui-helpers.ts +25 -6
- package/src/plan-mode/approved-plan.ts +35 -1
- package/src/prompts/agents/designer.md +5 -5
- package/src/prompts/agents/explore.md +7 -7
- package/src/prompts/agents/init.md +9 -9
- package/src/prompts/agents/librarian.md +14 -14
- package/src/prompts/agents/plan.md +4 -4
- package/src/prompts/agents/reviewer.md +5 -5
- package/src/prompts/agents/task.md +10 -10
- package/src/prompts/commands/orchestrate.md +2 -2
- package/src/prompts/compaction/branch-summary.md +3 -3
- package/src/prompts/compaction/compaction-short-summary.md +7 -7
- package/src/prompts/compaction/compaction-summary-context.md +1 -1
- package/src/prompts/compaction/compaction-summary.md +5 -5
- package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
- package/src/prompts/compaction/compaction-update-summary.md +11 -11
- package/src/prompts/goals/goal-budget-limit.md +16 -0
- package/src/prompts/goals/goal-continuation.md +28 -0
- package/src/prompts/goals/goal-mode-active.md +23 -0
- package/src/prompts/memories/consolidation.md +2 -2
- package/src/prompts/memories/read-path.md +1 -1
- package/src/prompts/memories/stage_one_input.md +1 -1
- package/src/prompts/memories/stage_one_system.md +5 -5
- package/src/prompts/review-request.md +4 -4
- package/src/prompts/system/agent-creation-architect.md +17 -17
- package/src/prompts/system/agent-creation-user.md +2 -2
- package/src/prompts/system/commit-message-system.md +2 -2
- package/src/prompts/system/custom-system-prompt.md +2 -2
- package/src/prompts/system/eager-todo.md +6 -6
- package/src/prompts/system/handoff-document.md +1 -1
- package/src/prompts/system/plan-mode-active.md +25 -24
- package/src/prompts/system/plan-mode-approved.md +4 -4
- package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
- package/src/prompts/system/plan-mode-reference.md +2 -2
- package/src/prompts/system/plan-mode-subagent.md +8 -8
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
- package/src/prompts/system/project-prompt.md +4 -4
- package/src/prompts/system/subagent-system-prompt.md +7 -7
- package/src/prompts/system/subagent-yield-reminder.md +4 -4
- package/src/prompts/system/system-prompt.md +72 -71
- package/src/prompts/system/ttsr-interrupt.md +1 -1
- package/src/prompts/tools/apply-patch.md +1 -1
- package/src/prompts/tools/ast-edit.md +3 -3
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/bash.md +6 -0
- package/src/prompts/tools/browser.md +3 -3
- package/src/prompts/tools/checkpoint.md +3 -3
- package/src/prompts/tools/find.md +3 -3
- package/src/prompts/tools/github.md +2 -5
- package/src/prompts/tools/goal.md +13 -0
- package/src/prompts/tools/hashline.md +104 -116
- package/src/prompts/tools/image-gen.md +3 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/lsp.md +2 -2
- package/src/prompts/tools/patch.md +6 -6
- package/src/prompts/tools/read.md +8 -7
- package/src/prompts/tools/replace.md +5 -5
- package/src/prompts/tools/resolve.md +6 -5
- package/src/prompts/tools/retain.md +1 -1
- package/src/prompts/tools/rewind.md +2 -2
- package/src/prompts/tools/search.md +2 -2
- package/src/prompts/tools/ssh.md +2 -2
- package/src/prompts/tools/task.md +12 -6
- package/src/prompts/tools/web-search.md +2 -2
- package/src/prompts/tools/write.md +3 -3
- package/src/sdk.ts +81 -17
- package/src/session/agent-session.ts +656 -125
- package/src/session/blob-store.ts +36 -3
- package/src/session/client-bridge.ts +81 -0
- package/src/session/compaction/errors.ts +31 -0
- package/src/session/compaction/index.ts +1 -0
- package/src/session/messages.ts +67 -2
- package/src/session/session-manager.ts +131 -12
- package/src/session/session-storage.ts +33 -15
- package/src/session/streaming-output.ts +309 -13
- package/src/slash-commands/acp-builtins.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +717 -116
- package/src/slash-commands/helpers/context-report.ts +39 -0
- package/src/slash-commands/helpers/format.ts +23 -0
- package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
- package/src/slash-commands/helpers/mcp.ts +532 -0
- package/src/slash-commands/helpers/parse.ts +85 -0
- package/src/slash-commands/helpers/ssh.ts +193 -0
- package/src/slash-commands/helpers/todo.ts +279 -0
- package/src/slash-commands/helpers/usage-report.ts +91 -0
- package/src/slash-commands/types.ts +126 -0
- package/src/ssh/ssh-executor.ts +5 -0
- package/src/system-prompt.ts +4 -2
- package/src/task/executor.ts +27 -10
- package/src/task/index.ts +20 -1
- package/src/task/render.ts +27 -18
- package/src/task/types.ts +4 -0
- package/src/tools/ast-edit.ts +21 -120
- package/src/tools/ast-grep.ts +21 -119
- package/src/tools/bash-interactive.ts +9 -1
- package/src/tools/bash.ts +203 -6
- package/src/tools/browser/attach.ts +3 -3
- package/src/tools/browser/launch.ts +81 -18
- package/src/tools/browser/registry.ts +1 -5
- package/src/tools/browser/tab-supervisor.ts +51 -14
- package/src/tools/conflict-detect.ts +21 -10
- package/src/tools/eval.ts +3 -1
- package/src/tools/fetch.ts +15 -4
- package/src/tools/find.ts +39 -39
- package/src/tools/gh-renderer.ts +0 -12
- package/src/tools/gh.ts +689 -182
- package/src/tools/github-cache.ts +548 -0
- package/src/tools/index.ts +25 -11
- package/src/tools/inspect-image.ts +3 -10
- package/src/tools/output-meta.ts +176 -37
- package/src/tools/path-utils.ts +125 -2
- package/src/tools/read.ts +605 -239
- package/src/tools/render-utils.ts +92 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resolve.ts +72 -44
- package/src/tools/search.ts +120 -186
- package/src/tools/write.ts +67 -10
- package/src/tui/code-cell.ts +70 -2
- package/src/utils/file-mentions.ts +1 -1
- package/src/utils/image-loading.ts +7 -3
- package/src/utils/image-resize.ts +32 -43
- package/src/vim/parser.ts +0 -17
- package/src/vim/render.ts +1 -1
- package/src/vim/types.ts +1 -1
- package/src/web/search/providers/gemini.ts +35 -95
- package/src/prompts/tools/exit-plan-mode.md +0 -6
- package/src/tools/exit-plan-mode.ts +0 -97
- package/src/utils/fuzzy.ts +0 -108
- package/src/utils/image-convert.ts +0 -27
package/src/tools/gh.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { scheduler } from "node:timers/promises";
|
|
4
5
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
5
6
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
6
|
-
import {
|
|
7
|
+
import { getWorktreesDir, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
|
7
8
|
import { type Static, Type } from "@sinclair/typebox";
|
|
9
|
+
import type { Settings } from "../config/settings";
|
|
8
10
|
import githubDescription from "../prompts/tools/github.md" with { type: "text" };
|
|
9
11
|
import * as git from "../utils/git";
|
|
10
12
|
import type { ToolSession } from ".";
|
|
11
13
|
import { formatShortSha } from "./gh-format";
|
|
14
|
+
import { type CacheStatus, getOrFetchView, resolveGithubCacheAuthKey } from "./github-cache";
|
|
12
15
|
import type { OutputMeta } from "./output-meta";
|
|
13
16
|
import { ToolError, throwIfAborted } from "./tool-errors";
|
|
14
17
|
import { toolResult } from "./tool-result";
|
|
@@ -103,35 +106,81 @@ const GH_PR_CHECKOUT_FIELDS = [
|
|
|
103
106
|
"title",
|
|
104
107
|
"url",
|
|
105
108
|
];
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
109
|
+
// /search/<endpoint> API response shapes (subset). Used when projecting raw
|
|
110
|
+
// REST results into the normalized `GhSearch*Result` shapes the formatters
|
|
111
|
+
// consume. We talk to the API directly because `gh search prs`/`issues`
|
|
112
|
+
// quotes multi-token positional queries (`is:"merged is:pr"`) and returns 0
|
|
113
|
+
// hits — see https://github.com/cli/cli for the upstream regression.
|
|
114
|
+
interface GhApiSearchResponse<T> {
|
|
115
|
+
total_count?: number;
|
|
116
|
+
incomplete_results?: boolean;
|
|
117
|
+
items?: T[];
|
|
118
|
+
}
|
|
119
|
+
interface GhApiUser {
|
|
120
|
+
login?: string;
|
|
121
|
+
name?: string | null;
|
|
122
|
+
}
|
|
123
|
+
interface GhApiLabel {
|
|
124
|
+
name?: string;
|
|
125
|
+
}
|
|
126
|
+
interface GhApiPullRequestRef {
|
|
127
|
+
merged_at?: string | null;
|
|
128
|
+
}
|
|
129
|
+
interface GhApiSearchIssueItem {
|
|
130
|
+
number?: number;
|
|
131
|
+
title?: string;
|
|
132
|
+
state?: string;
|
|
133
|
+
state_reason?: string | null;
|
|
134
|
+
user?: GhApiUser | null;
|
|
135
|
+
labels?: GhApiLabel[];
|
|
136
|
+
created_at?: string;
|
|
137
|
+
updated_at?: string;
|
|
138
|
+
html_url?: string;
|
|
139
|
+
repository_url?: string;
|
|
140
|
+
pull_request?: GhApiPullRequestRef | null;
|
|
141
|
+
}
|
|
142
|
+
interface GhApiSearchCodeItem {
|
|
143
|
+
name?: string;
|
|
144
|
+
path?: string;
|
|
145
|
+
sha?: string;
|
|
146
|
+
html_url?: string;
|
|
147
|
+
repository?: { full_name?: string } | null;
|
|
148
|
+
text_matches?: Array<{ fragment?: string; property?: string }>;
|
|
149
|
+
}
|
|
150
|
+
interface GhApiSearchCommitGitActor {
|
|
151
|
+
name?: string;
|
|
152
|
+
email?: string;
|
|
153
|
+
date?: string;
|
|
154
|
+
}
|
|
155
|
+
interface GhApiSearchCommitItem {
|
|
156
|
+
sha?: string;
|
|
157
|
+
node_id?: string;
|
|
158
|
+
html_url?: string;
|
|
159
|
+
author?: GhApiUser | null;
|
|
160
|
+
committer?: GhApiUser | null;
|
|
161
|
+
commit?: {
|
|
162
|
+
author?: GhApiSearchCommitGitActor | null;
|
|
163
|
+
committer?: GhApiSearchCommitGitActor | null;
|
|
164
|
+
message?: string;
|
|
165
|
+
} | null;
|
|
166
|
+
repository?: { full_name?: string } | null;
|
|
167
|
+
}
|
|
168
|
+
interface GhApiSearchRepoItem {
|
|
169
|
+
full_name?: string;
|
|
170
|
+
description?: string | null;
|
|
171
|
+
language?: string | null;
|
|
172
|
+
stargazers_count?: number;
|
|
173
|
+
forks_count?: number;
|
|
174
|
+
open_issues_count?: number;
|
|
175
|
+
archived?: boolean;
|
|
176
|
+
fork?: boolean;
|
|
177
|
+
private?: boolean;
|
|
178
|
+
visibility?: string | null;
|
|
179
|
+
updated_at?: string;
|
|
180
|
+
created_at?: string;
|
|
181
|
+
html_url?: string;
|
|
182
|
+
owner?: GhApiUser | null;
|
|
183
|
+
}
|
|
135
184
|
const SEARCH_LIMIT_DEFAULT = 10;
|
|
136
185
|
const SEARCH_LIMIT_MAX = 50;
|
|
137
186
|
const FILE_PREVIEW_LIMIT = 50;
|
|
@@ -142,6 +191,7 @@ const RUN_WATCH_TAIL_MAX = 200;
|
|
|
142
191
|
const REVIEW_COMMENTS_PAGE_SIZE = 100;
|
|
143
192
|
const RUN_JOBS_PAGE_SIZE = 100;
|
|
144
193
|
const PR_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)(?:\/.*)?$/;
|
|
194
|
+
const ISSUE_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)(?:\/.*)?$/;
|
|
145
195
|
const RUN_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/actions\/runs\/(\d+)(?:\/.*)?$/;
|
|
146
196
|
const RUN_SUCCESS_CONCLUSIONS = new Set(["success", "neutral", "skipped"]);
|
|
147
197
|
const RUN_FAILURE_CONCLUSIONS = new Set(["failure", "timed_out", "cancelled", "action_required", "startup_failure"]);
|
|
@@ -151,10 +201,7 @@ const githubSchema = Type.Object({
|
|
|
151
201
|
op: StringEnum(
|
|
152
202
|
[
|
|
153
203
|
"repo_view",
|
|
154
|
-
"issue_view",
|
|
155
204
|
"pr_create",
|
|
156
|
-
"pr_view",
|
|
157
|
-
"pr_diff",
|
|
158
205
|
"pr_checkout",
|
|
159
206
|
"pr_push",
|
|
160
207
|
"search_issues",
|
|
@@ -178,12 +225,6 @@ const githubSchema = Type.Object({
|
|
|
178
225
|
examples: ["main", "develop"],
|
|
179
226
|
}),
|
|
180
227
|
),
|
|
181
|
-
issue: Type.Optional(
|
|
182
|
-
Type.String({
|
|
183
|
-
description: "issue number or url (issue_view)",
|
|
184
|
-
examples: ["123", "https://github.com/owner/repo/issues/123"],
|
|
185
|
-
}),
|
|
186
|
-
),
|
|
187
228
|
pr: Type.Optional(
|
|
188
229
|
Type.Union(
|
|
189
230
|
[
|
|
@@ -194,17 +235,10 @@ const githubSchema = Type.Object({
|
|
|
194
235
|
],
|
|
195
236
|
{
|
|
196
237
|
description:
|
|
197
|
-
"pr number, url, or branch (
|
|
238
|
+
"pr number, url, or branch (pr_checkout); pass an array to batch-process multiple pull requests in one call",
|
|
198
239
|
},
|
|
199
240
|
),
|
|
200
241
|
),
|
|
201
|
-
comments: Type.Optional(Type.Boolean({ description: "include comments (issue_view, pr_view)", default: true })),
|
|
202
|
-
nameOnly: Type.Optional(Type.Boolean({ description: "return file names only (pr_diff)" })),
|
|
203
|
-
exclude: Type.Optional(
|
|
204
|
-
Type.Array(Type.String({ description: "glob to exclude" }), {
|
|
205
|
-
description: "file globs to exclude (pr_diff)",
|
|
206
|
-
}),
|
|
207
|
-
),
|
|
208
242
|
force: Type.Optional(Type.Boolean({ description: "reset existing local branch (pr_checkout)" })),
|
|
209
243
|
forceWithLease: Type.Optional(Type.Boolean({ description: "force-with-lease push (pr_push)" })),
|
|
210
244
|
title: Type.Optional(
|
|
@@ -699,13 +733,7 @@ function appendRepoFlag(args: string[], repo: string | undefined, identifier?: s
|
|
|
699
733
|
args.push("--repo", repo);
|
|
700
734
|
}
|
|
701
735
|
|
|
702
|
-
const
|
|
703
|
-
issues: GH_SEARCH_FIELDS,
|
|
704
|
-
prs: GH_SEARCH_FIELDS,
|
|
705
|
-
code: GH_SEARCH_CODE_FIELDS,
|
|
706
|
-
commits: GH_SEARCH_COMMITS_FIELDS,
|
|
707
|
-
repos: GH_SEARCH_REPOS_FIELDS,
|
|
708
|
-
};
|
|
736
|
+
const REPO_API_URL_PREFIX = "https://api.github.com/repos/";
|
|
709
737
|
|
|
710
738
|
const RELATIVE_DURATION_PATTERN = /^(\d+)\s*(m|h|d|w|mo|y)$/i;
|
|
711
739
|
const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -811,21 +839,98 @@ function composeSearchQuery(parts: ReadonlyArray<string | undefined>): string {
|
|
|
811
839
|
return cleaned.join(" ");
|
|
812
840
|
}
|
|
813
841
|
|
|
814
|
-
function
|
|
815
|
-
|
|
842
|
+
function buildGhApiSearchArgs(
|
|
843
|
+
endpoint: "issues" | "code" | "commits" | "repositories",
|
|
816
844
|
query: string,
|
|
817
845
|
limit: number,
|
|
818
|
-
|
|
846
|
+
extraHeaders?: ReadonlyArray<string>,
|
|
819
847
|
): string[] {
|
|
820
|
-
const
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
appendRepoFlag(args, repo);
|
|
848
|
+
const args = ["api", "-X", "GET", `/search/${endpoint}`, "-f", `q=${query}`, "-F", `per_page=${limit}`];
|
|
849
|
+
for (const header of extraHeaders ?? []) {
|
|
850
|
+
args.push("-H", header);
|
|
824
851
|
}
|
|
825
|
-
args.push("--", query);
|
|
826
852
|
return args;
|
|
827
853
|
}
|
|
828
854
|
|
|
855
|
+
function repoFromRepositoryUrl(value: string | undefined): string | undefined {
|
|
856
|
+
if (!value?.startsWith(REPO_API_URL_PREFIX)) return undefined;
|
|
857
|
+
return value.slice(REPO_API_URL_PREFIX.length);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function apiUserToGhUser(user: GhApiUser | null | undefined): GhUser | undefined {
|
|
861
|
+
if (!user) return undefined;
|
|
862
|
+
const login = user.login ?? undefined;
|
|
863
|
+
const name = user.name ?? undefined;
|
|
864
|
+
if (login === undefined && name === undefined) return undefined;
|
|
865
|
+
return { login, name };
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function apiLabelsToGhLabels(labels: GhApiLabel[] | undefined): GhLabel[] {
|
|
869
|
+
return labels?.map(label => ({ name: label.name })) ?? [];
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function apiIssueToSearchResult(item: GhApiSearchIssueItem): GhSearchResult {
|
|
873
|
+
const merged = Boolean(item.pull_request?.merged_at);
|
|
874
|
+
return {
|
|
875
|
+
author: apiUserToGhUser(item.user) ?? null,
|
|
876
|
+
createdAt: item.created_at,
|
|
877
|
+
labels: apiLabelsToGhLabels(item.labels),
|
|
878
|
+
number: item.number,
|
|
879
|
+
repository: { nameWithOwner: repoFromRepositoryUrl(item.repository_url) },
|
|
880
|
+
state: merged ? "merged" : item.state,
|
|
881
|
+
title: item.title,
|
|
882
|
+
updatedAt: item.updated_at,
|
|
883
|
+
url: item.html_url,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function apiCodeToSearchResult(item: GhApiSearchCodeItem): GhSearchCodeResult {
|
|
888
|
+
return {
|
|
889
|
+
path: item.path,
|
|
890
|
+
repository: { nameWithOwner: item.repository?.full_name },
|
|
891
|
+
sha: item.sha,
|
|
892
|
+
textMatches: item.text_matches?.map(match => ({ fragment: match.fragment, property: match.property })),
|
|
893
|
+
url: item.html_url,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function apiCommitToSearchResult(item: GhApiSearchCommitItem): GhSearchCommitResult {
|
|
898
|
+
return {
|
|
899
|
+
author: apiUserToGhUser(item.author) ?? null,
|
|
900
|
+
commit: item.commit
|
|
901
|
+
? {
|
|
902
|
+
author: item.commit.author ?? null,
|
|
903
|
+
committer: item.commit.committer ?? null,
|
|
904
|
+
message: item.commit.message,
|
|
905
|
+
}
|
|
906
|
+
: null,
|
|
907
|
+
committer: apiUserToGhUser(item.committer) ?? null,
|
|
908
|
+
id: item.node_id,
|
|
909
|
+
repository: { nameWithOwner: item.repository?.full_name },
|
|
910
|
+
sha: item.sha,
|
|
911
|
+
url: item.html_url,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function apiRepoToSearchResult(item: GhApiSearchRepoItem): GhSearchRepoResult {
|
|
916
|
+
return {
|
|
917
|
+
createdAt: item.created_at,
|
|
918
|
+
description: item.description,
|
|
919
|
+
forksCount: item.forks_count,
|
|
920
|
+
fullName: item.full_name,
|
|
921
|
+
isArchived: item.archived,
|
|
922
|
+
isFork: item.fork,
|
|
923
|
+
isPrivate: item.private,
|
|
924
|
+
language: item.language,
|
|
925
|
+
openIssuesCount: item.open_issues_count,
|
|
926
|
+
owner: apiUserToGhUser(item.owner) ?? null,
|
|
927
|
+
stargazersCount: item.stargazers_count,
|
|
928
|
+
updatedAt: item.updated_at,
|
|
929
|
+
url: item.html_url,
|
|
930
|
+
visibility: item.visibility ?? null,
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
|
|
829
934
|
function sanitizeRemoteName(value: string): string {
|
|
830
935
|
const sanitized = value
|
|
831
936
|
.toLowerCase()
|
|
@@ -1086,6 +1191,29 @@ function parsePullRequestUrl(value: string | undefined): { repo?: string; prNumb
|
|
|
1086
1191
|
};
|
|
1087
1192
|
}
|
|
1088
1193
|
|
|
1194
|
+
/**
|
|
1195
|
+
* Parse a digit-only decimal positive integer or return undefined. Rejects
|
|
1196
|
+
* `1e2`, `0x10`, `12.0`, leading +/-, or any other shape `Number()` would
|
|
1197
|
+
* accept — those would otherwise key the cache against the wrong row.
|
|
1198
|
+
*/
|
|
1199
|
+
export function parsePositiveDecimalInt(value: string | undefined): number | undefined {
|
|
1200
|
+
if (!value || !/^\d+$/.test(value)) return undefined;
|
|
1201
|
+
const num = Number(value);
|
|
1202
|
+
if (!Number.isSafeInteger(num) || num <= 0) return undefined;
|
|
1203
|
+
return num;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function parseIssueUrl(value: string | undefined): { repo?: string; issueNumber?: number } {
|
|
1207
|
+
const normalized = normalizeOptionalString(value);
|
|
1208
|
+
if (!normalized) return {};
|
|
1209
|
+
const match = normalized.match(ISSUE_URL_PATTERN);
|
|
1210
|
+
if (!match) return {};
|
|
1211
|
+
return {
|
|
1212
|
+
repo: match[1],
|
|
1213
|
+
issueNumber: Number(match[2]),
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1089
1217
|
function normalizePrReviewComment(comment: GhPrReviewCommentApi): GhPrReviewComment | null {
|
|
1090
1218
|
if (typeof comment.id !== "number") {
|
|
1091
1219
|
return null;
|
|
@@ -1600,6 +1728,52 @@ async function resolveGitHubRepo(
|
|
|
1600
1728
|
return requireNonEmpty(resolved, "repo");
|
|
1601
1729
|
}
|
|
1602
1730
|
|
|
1731
|
+
/**
|
|
1732
|
+
* Process-lifetime cache of `gh repo view --json nameWithOwner` lookups keyed
|
|
1733
|
+
* by absolute cwd. Avoids repeated `gh` chatter when the same protocol handler
|
|
1734
|
+
* or tool call resolves the default repo many times in a row.
|
|
1735
|
+
*
|
|
1736
|
+
* The shared lookup is intentionally **not** bound to any caller's
|
|
1737
|
+
* AbortSignal. Cancelling one caller would otherwise kill the underlying
|
|
1738
|
+
* `gh repo view` for every concurrent waiter on the same cwd. Each caller's
|
|
1739
|
+
* signal is honored at the wait point via `untilAborted` instead, so an abort
|
|
1740
|
+
* unwinds only that caller.
|
|
1741
|
+
*/
|
|
1742
|
+
const DEFAULT_REPO_RESOLVED = new Map<string, string>();
|
|
1743
|
+
const DEFAULT_REPO_INFLIGHT = new Map<string, Promise<string>>();
|
|
1744
|
+
|
|
1745
|
+
export async function resolveDefaultRepoMemoized(cwd: string, signal?: AbortSignal): Promise<string> {
|
|
1746
|
+
const key = path.resolve(cwd);
|
|
1747
|
+
const ready = DEFAULT_REPO_RESOLVED.get(key);
|
|
1748
|
+
if (ready) return ready;
|
|
1749
|
+
let pending = DEFAULT_REPO_INFLIGHT.get(key);
|
|
1750
|
+
if (!pending) {
|
|
1751
|
+
pending = (async () => {
|
|
1752
|
+
// No caller signal: this lookup is shared across every concurrent
|
|
1753
|
+
// waiter on the same cwd.
|
|
1754
|
+
const resolved = await git.github.text(cwd, [
|
|
1755
|
+
"repo",
|
|
1756
|
+
"view",
|
|
1757
|
+
"--json",
|
|
1758
|
+
"nameWithOwner",
|
|
1759
|
+
"-q",
|
|
1760
|
+
".nameWithOwner",
|
|
1761
|
+
]);
|
|
1762
|
+
const value = requireNonEmpty(resolved, "repo");
|
|
1763
|
+
DEFAULT_REPO_RESOLVED.set(key, value);
|
|
1764
|
+
return value;
|
|
1765
|
+
})();
|
|
1766
|
+
// Drop the in-flight slot on settle so failures don't poison the cache
|
|
1767
|
+
// and so a successful resolution survives only in `DEFAULT_REPO_RESOLVED`.
|
|
1768
|
+
void pending.then(
|
|
1769
|
+
() => DEFAULT_REPO_INFLIGHT.delete(key),
|
|
1770
|
+
() => DEFAULT_REPO_INFLIGHT.delete(key),
|
|
1771
|
+
);
|
|
1772
|
+
DEFAULT_REPO_INFLIGHT.set(key, pending);
|
|
1773
|
+
}
|
|
1774
|
+
return untilAborted(signal, pending);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1603
1777
|
async function resolveGitHubBranchHead(
|
|
1604
1778
|
cwd: string,
|
|
1605
1779
|
repo: string,
|
|
@@ -2237,14 +2411,8 @@ export class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails>
|
|
|
2237
2411
|
switch (params.op) {
|
|
2238
2412
|
case "repo_view":
|
|
2239
2413
|
return executeRepoView(this.session, params, signal);
|
|
2240
|
-
case "issue_view":
|
|
2241
|
-
return executeIssueView(this.session, params, signal);
|
|
2242
2414
|
case "pr_create":
|
|
2243
2415
|
return executePrCreate(this.session, params, signal);
|
|
2244
|
-
case "pr_view":
|
|
2245
|
-
return executePrView(this.session, params, signal);
|
|
2246
|
-
case "pr_diff":
|
|
2247
|
-
return executePrDiff(this.session, params, signal);
|
|
2248
2416
|
case "pr_checkout":
|
|
2249
2417
|
return executePrCheckout(this.session, params, signal);
|
|
2250
2418
|
case "pr_push":
|
|
@@ -2288,111 +2456,449 @@ async function executeRepoView(
|
|
|
2288
2456
|
return buildTextResult(formatRepoView(data, { repo, branch }), data.url);
|
|
2289
2457
|
}
|
|
2290
2458
|
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2459
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2460
|
+
// Cached issue/PR view fetchers
|
|
2461
|
+
//
|
|
2462
|
+
// Used by `executeIssueView`/`executePrView` and by the `issue://` / `pr://`
|
|
2463
|
+
// internal-URL protocol handlers. The cache wrapper lives in `./github-cache`;
|
|
2464
|
+
// the fresh fetchers stay here to share the existing formatter helpers.
|
|
2465
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2466
|
+
|
|
2467
|
+
export interface IssueViewLookupOptions {
|
|
2468
|
+
cwd: string;
|
|
2469
|
+
repo?: string;
|
|
2470
|
+
/** Issue number or GitHub issue URL. */
|
|
2471
|
+
issue: string;
|
|
2472
|
+
includeComments?: boolean;
|
|
2473
|
+
signal?: AbortSignal;
|
|
2474
|
+
settings?: Settings;
|
|
2475
|
+
cacheAuthKey?: string | null;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
export interface PrViewLookupOptions {
|
|
2479
|
+
cwd: string;
|
|
2480
|
+
repo: string;
|
|
2481
|
+
number: number;
|
|
2482
|
+
includeComments?: boolean;
|
|
2483
|
+
signal?: AbortSignal;
|
|
2484
|
+
settings?: Settings;
|
|
2485
|
+
cacheAuthKey?: string | null;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
export interface ViewLookupResult<T> {
|
|
2489
|
+
rendered: string;
|
|
2490
|
+
sourceUrl: string | undefined;
|
|
2491
|
+
payload: T;
|
|
2492
|
+
status: CacheStatus;
|
|
2493
|
+
fetchedAt: number;
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
async function fetchIssueViewFresh(
|
|
2497
|
+
cwd: string,
|
|
2498
|
+
repo: string | undefined,
|
|
2499
|
+
identifier: string,
|
|
2500
|
+
includeComments: boolean,
|
|
2294
2501
|
signal: AbortSignal | undefined,
|
|
2295
|
-
): Promise<
|
|
2296
|
-
const
|
|
2297
|
-
|
|
2298
|
-
const includeComments = params.comments ?? true;
|
|
2299
|
-
const args = ["issue", "view", issue];
|
|
2300
|
-
appendRepoFlag(args, repo, issue);
|
|
2502
|
+
): Promise<{ rendered: string; sourceUrl: string | undefined; payload: GhIssueViewData }> {
|
|
2503
|
+
const args = ["issue", "view", identifier];
|
|
2504
|
+
appendRepoFlag(args, repo, identifier);
|
|
2301
2505
|
args.push("--json", (includeComments ? GH_ISSUE_FIELDS : GH_ISSUE_FIELDS_NO_COMMENTS).join(","));
|
|
2302
|
-
|
|
2303
|
-
const data = await git.github.json<GhIssueViewData>(session.cwd, args, signal, {
|
|
2506
|
+
const data = await git.github.json<GhIssueViewData>(cwd, args, signal, {
|
|
2304
2507
|
repoProvided: Boolean(repo),
|
|
2305
2508
|
});
|
|
2306
|
-
|
|
2509
|
+
const rendered = formatIssueView(data, { issue: identifier, repo, comments: includeComments });
|
|
2510
|
+
return { rendered, sourceUrl: data.url, payload: data };
|
|
2307
2511
|
}
|
|
2308
2512
|
|
|
2309
|
-
async function
|
|
2310
|
-
|
|
2311
|
-
|
|
2513
|
+
async function fetchPrViewFresh(
|
|
2514
|
+
cwd: string,
|
|
2515
|
+
repo: string,
|
|
2516
|
+
number: number,
|
|
2517
|
+
includeComments: boolean,
|
|
2312
2518
|
signal: AbortSignal | undefined,
|
|
2313
|
-
): Promise<
|
|
2314
|
-
const
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
const
|
|
2519
|
+
): Promise<{ rendered: string; sourceUrl: string | undefined; payload: GhPrViewData }> {
|
|
2520
|
+
const args = ["pr", "view", String(number)];
|
|
2521
|
+
appendRepoFlag(args, repo, String(number));
|
|
2522
|
+
args.push("--json", (includeComments ? GH_PR_FIELDS : GH_PR_FIELDS_NO_COMMENTS).join(","));
|
|
2523
|
+
const data = await git.github.json<GhPrViewData>(cwd, args, signal, { repoProvided: true });
|
|
2524
|
+
if (includeComments && typeof data.number === "number") {
|
|
2525
|
+
data.reviewComments = await fetchPrReviewComments(cwd, repo, data.number, signal);
|
|
2526
|
+
}
|
|
2527
|
+
const rendered = formatPrView(data, { pr: String(number), repo, comments: includeComments });
|
|
2528
|
+
return { rendered, sourceUrl: data.url, payload: data };
|
|
2529
|
+
}
|
|
2318
2530
|
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2531
|
+
/**
|
|
2532
|
+
* Cache-aware issue/view fetcher. Used by both the `github` tool op and the
|
|
2533
|
+
* `issue://` protocol handler so a single shared row services both surfaces.
|
|
2534
|
+
*/
|
|
2535
|
+
export async function getOrFetchIssue(options: IssueViewLookupOptions): Promise<ViewLookupResult<GhIssueViewData>> {
|
|
2536
|
+
const identifier = requireNonEmpty(options.issue, "issue");
|
|
2537
|
+
const includeComments = options.includeComments ?? true;
|
|
2538
|
+
const authKey = options.cacheAuthKey === undefined ? (resolveGithubCacheAuthKey() ?? null) : options.cacheAuthKey;
|
|
2539
|
+
const urlParse = parseIssueUrl(identifier);
|
|
2540
|
+
// Prefer the URL's repo when the identifier is a full URL; fall back to the
|
|
2541
|
+
// explicit `repo` option, then to the cwd's default repo.
|
|
2542
|
+
let repo = urlParse.repo ?? normalizeOptionalString(options.repo);
|
|
2543
|
+
let cacheNumber = urlParse.issueNumber;
|
|
2544
|
+
if (cacheNumber === undefined) {
|
|
2545
|
+
cacheNumber = parsePositiveDecimalInt(identifier);
|
|
2546
|
+
}
|
|
2547
|
+
if (cacheNumber !== undefined && !repo) {
|
|
2548
|
+
try {
|
|
2549
|
+
repo = await resolveDefaultRepoMemoized(options.cwd, options.signal);
|
|
2550
|
+
} catch {
|
|
2551
|
+
// Resolution failure leaves `repo` undefined: we'll fall through to a
|
|
2552
|
+
// direct fetch below so gh produces its own error message instead of
|
|
2553
|
+
// us masking it with a friendlier one.
|
|
2554
|
+
repo = undefined;
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2325
2557
|
|
|
2326
|
-
|
|
2327
|
-
repoProvided: Boolean(repo),
|
|
2328
|
-
});
|
|
2329
|
-
const resolvedRepo = repo ?? parsePullRequestUrl(data.url).repo;
|
|
2330
|
-
if (includeComments && resolvedRepo && typeof data.number === "number") {
|
|
2331
|
-
data.reviewComments = await fetchPrReviewComments(session.cwd, resolvedRepo, data.number, signal);
|
|
2332
|
-
}
|
|
2333
|
-
return { prRef, data };
|
|
2334
|
-
}),
|
|
2335
|
-
);
|
|
2558
|
+
const doFetch = () => fetchIssueViewFresh(options.cwd, repo, identifier, includeComments, options.signal);
|
|
2336
2559
|
|
|
2337
|
-
if (
|
|
2338
|
-
const
|
|
2339
|
-
return
|
|
2340
|
-
formatPrView(view.data, { pr: view.prRef, repo, comments: includeComments }),
|
|
2341
|
-
view.data.url,
|
|
2342
|
-
);
|
|
2560
|
+
if (!repo || cacheNumber === undefined) {
|
|
2561
|
+
const fresh = await doFetch();
|
|
2562
|
+
return { ...fresh, status: "miss", fetchedAt: Date.now() };
|
|
2343
2563
|
}
|
|
2344
2564
|
|
|
2345
|
-
const
|
|
2346
|
-
|
|
2347
|
-
|
|
2565
|
+
const lookup = await getOrFetchView<GhIssueViewData>({
|
|
2566
|
+
repo,
|
|
2567
|
+
kind: "issue",
|
|
2568
|
+
number: cacheNumber,
|
|
2569
|
+
includeComments,
|
|
2570
|
+
settings: options.settings,
|
|
2571
|
+
authKey,
|
|
2572
|
+
fetchFresh: doFetch,
|
|
2573
|
+
});
|
|
2574
|
+
return {
|
|
2575
|
+
rendered: lookup.rendered,
|
|
2576
|
+
sourceUrl: lookup.sourceUrl,
|
|
2577
|
+
payload: lookup.payload,
|
|
2578
|
+
status: lookup.status,
|
|
2579
|
+
fetchedAt: lookup.fetchedAt,
|
|
2580
|
+
};
|
|
2348
2581
|
}
|
|
2349
2582
|
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
const
|
|
2357
|
-
const
|
|
2358
|
-
|
|
2359
|
-
const
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2583
|
+
/**
|
|
2584
|
+
* Cache-aware PR view fetcher. Caller must supply a numeric PR number;
|
|
2585
|
+
* branch-name / current-branch lookups bypass the cache entirely upstream
|
|
2586
|
+
* (see `executePrView`).
|
|
2587
|
+
*/
|
|
2588
|
+
export async function getOrFetchPr(options: PrViewLookupOptions): Promise<ViewLookupResult<GhPrViewData>> {
|
|
2589
|
+
const includeComments = options.includeComments ?? true;
|
|
2590
|
+
const authKey = options.cacheAuthKey === undefined ? (resolveGithubCacheAuthKey() ?? null) : options.cacheAuthKey;
|
|
2591
|
+
const doFetch = () => fetchPrViewFresh(options.cwd, options.repo, options.number, includeComments, options.signal);
|
|
2592
|
+
const lookup = await getOrFetchView<GhPrViewData>({
|
|
2593
|
+
repo: options.repo,
|
|
2594
|
+
kind: "pr",
|
|
2595
|
+
number: options.number,
|
|
2596
|
+
includeComments,
|
|
2597
|
+
settings: options.settings,
|
|
2598
|
+
authKey,
|
|
2599
|
+
fetchFresh: doFetch,
|
|
2600
|
+
});
|
|
2601
|
+
return {
|
|
2602
|
+
rendered: lookup.rendered,
|
|
2603
|
+
sourceUrl: lookup.sourceUrl,
|
|
2604
|
+
payload: lookup.payload,
|
|
2605
|
+
status: lookup.status,
|
|
2606
|
+
fetchedAt: lookup.fetchedAt,
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2611
|
+
// PR diff fetcher
|
|
2612
|
+
//
|
|
2613
|
+
// Used by the `pr://<n>/diff[/…]` internal-URL family. Stores the verbatim
|
|
2614
|
+
// `gh pr diff` text plus a parsed file index so the listing, full-diff, and
|
|
2615
|
+
// per-file slice variants all share one cache row.
|
|
2616
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2617
|
+
|
|
2618
|
+
export interface PrDiffFile {
|
|
2619
|
+
/** Display path. Prefers the post-image (`b/<path>`) when present. */
|
|
2620
|
+
path: string;
|
|
2621
|
+
additions: number;
|
|
2622
|
+
deletions: number;
|
|
2623
|
+
changeType: "modified" | "added" | "deleted" | "renamed" | "binary";
|
|
2624
|
+
/** Pre-image path for renames/deletes; same as `path` otherwise. */
|
|
2625
|
+
oldPath?: string;
|
|
2626
|
+
/** Byte offset of the section's `diff --git` line in the unified diff. */
|
|
2627
|
+
startOffset: number;
|
|
2628
|
+
/** Byte offset of the next section (or end-of-text). */
|
|
2629
|
+
endOffset: number;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
export interface PrDiffPayload {
|
|
2633
|
+
/** Full unified diff text as returned by `gh pr diff --color never`. */
|
|
2634
|
+
unified: string;
|
|
2635
|
+
files: PrDiffFile[];
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
export interface PrDiffLookupOptions {
|
|
2639
|
+
cwd: string;
|
|
2640
|
+
repo: string;
|
|
2641
|
+
number: number;
|
|
2642
|
+
signal?: AbortSignal;
|
|
2643
|
+
settings?: Settings;
|
|
2644
|
+
cacheAuthKey?: string | null;
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Split `gh pr diff` output on `^diff --git ` boundaries and parse per-file
|
|
2648
|
+
* metadata. The unified diff is preserved verbatim so callers can slice it by
|
|
2649
|
+
* byte offsets without re-running gh.
|
|
2650
|
+
*/
|
|
2651
|
+
export function parsePrUnifiedDiff(text: string): PrDiffPayload {
|
|
2652
|
+
const files: PrDiffFile[] = [];
|
|
2653
|
+
if (text.length === 0) {
|
|
2654
|
+
return { unified: text, files };
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// Walk match positions manually so we capture each section's byte range.
|
|
2658
|
+
const sectionStarts: number[] = [];
|
|
2659
|
+
const re = /^diff --git /gm;
|
|
2660
|
+
let m: RegExpExecArray | null = re.exec(text);
|
|
2661
|
+
while (m !== null) {
|
|
2662
|
+
sectionStarts.push(m.index);
|
|
2663
|
+
// Avoid zero-length match infinite loop (regex has fixed prefix, but
|
|
2664
|
+
// be explicit).
|
|
2665
|
+
if (re.lastIndex === m.index) re.lastIndex += 1;
|
|
2666
|
+
m = re.exec(text);
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
for (let i = 0; i < sectionStarts.length; i += 1) {
|
|
2670
|
+
const startOffset = sectionStarts[i] ?? 0;
|
|
2671
|
+
const endOffset = sectionStarts[i + 1] ?? text.length;
|
|
2672
|
+
const section = text.slice(startOffset, endOffset);
|
|
2673
|
+
files.push(parsePrDiffSection(section, startOffset, endOffset));
|
|
2674
|
+
}
|
|
2675
|
+
return { unified: text, files };
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
interface ParsedDiffHeaderToken {
|
|
2679
|
+
value: string;
|
|
2680
|
+
nextIndex: number;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
function skipDiffHeaderSpaces(text: string, index: number): number {
|
|
2684
|
+
let i = index;
|
|
2685
|
+
while (text.charAt(i) === " ") i += 1;
|
|
2686
|
+
return i;
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
function parseDiffQuotedEscape(text: string, slashIndex: number): ParsedDiffHeaderToken {
|
|
2690
|
+
const next = text.charAt(slashIndex + 1);
|
|
2691
|
+
if (next === "") return { value: "\\", nextIndex: slashIndex + 1 };
|
|
2692
|
+
|
|
2693
|
+
if (next >= "0" && next <= "7") {
|
|
2694
|
+
let end = slashIndex + 1;
|
|
2695
|
+
while (end < text.length && end < slashIndex + 4) {
|
|
2696
|
+
const digit = text.charAt(end);
|
|
2697
|
+
if (digit < "0" || digit > "7") break;
|
|
2698
|
+
end += 1;
|
|
2699
|
+
}
|
|
2700
|
+
return {
|
|
2701
|
+
value: String.fromCharCode(Number.parseInt(text.slice(slashIndex + 1, end), 8)),
|
|
2702
|
+
nextIndex: end,
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
switch (next) {
|
|
2707
|
+
case "a":
|
|
2708
|
+
return { value: "\x07", nextIndex: slashIndex + 2 };
|
|
2709
|
+
case "b":
|
|
2710
|
+
return { value: "\b", nextIndex: slashIndex + 2 };
|
|
2711
|
+
case "f":
|
|
2712
|
+
return { value: "\f", nextIndex: slashIndex + 2 };
|
|
2713
|
+
case "n":
|
|
2714
|
+
return { value: "\n", nextIndex: slashIndex + 2 };
|
|
2715
|
+
case "r":
|
|
2716
|
+
return { value: "\r", nextIndex: slashIndex + 2 };
|
|
2717
|
+
case "t":
|
|
2718
|
+
return { value: "\t", nextIndex: slashIndex + 2 };
|
|
2719
|
+
case "v":
|
|
2720
|
+
return { value: "\v", nextIndex: slashIndex + 2 };
|
|
2721
|
+
case "\\":
|
|
2722
|
+
case '"':
|
|
2723
|
+
return { value: next, nextIndex: slashIndex + 2 };
|
|
2724
|
+
default:
|
|
2725
|
+
return { value: next, nextIndex: slashIndex + 2 };
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
function parseDiffQuotedToken(text: string, startIndex: number): ParsedDiffHeaderToken | undefined {
|
|
2730
|
+
if (text.charAt(startIndex) !== '"') return undefined;
|
|
2731
|
+
let value = "";
|
|
2732
|
+
for (let i = startIndex + 1; i < text.length; i += 1) {
|
|
2733
|
+
const ch = text.charAt(i);
|
|
2734
|
+
if (ch === '"') return { value, nextIndex: i + 1 };
|
|
2735
|
+
if (ch !== "\\") {
|
|
2736
|
+
value += ch;
|
|
2737
|
+
continue;
|
|
2738
|
+
}
|
|
2739
|
+
const escaped = parseDiffQuotedEscape(text, i);
|
|
2740
|
+
value += escaped.value;
|
|
2741
|
+
i = escaped.nextIndex - 1;
|
|
2742
|
+
}
|
|
2743
|
+
return undefined;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2746
|
+
function parseDiffHeaderToken(text: string, startIndex: number): ParsedDiffHeaderToken | undefined {
|
|
2747
|
+
const start = skipDiffHeaderSpaces(text, startIndex);
|
|
2748
|
+
if (start >= text.length) return undefined;
|
|
2749
|
+
const quoted = parseDiffQuotedToken(text, start);
|
|
2750
|
+
if (quoted) return quoted;
|
|
2751
|
+
const end = text.indexOf(" ", start);
|
|
2752
|
+
if (end === -1) return { value: text.slice(start), nextIndex: text.length };
|
|
2753
|
+
return { value: text.slice(start, end), nextIndex: end };
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
function stripPrDiffPathPrefix(value: string, prefix: "a/" | "b/"): string | undefined {
|
|
2757
|
+
return value.startsWith(prefix) ? value.slice(prefix.length) : undefined;
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
function parsePrDiffHeaderPaths(header: string): { oldPath?: string; newPath?: string } {
|
|
2761
|
+
const trail = header.slice("diff --git ".length);
|
|
2762
|
+
if (trail.startsWith('"')) {
|
|
2763
|
+
const oldToken = parseDiffQuotedToken(trail, 0);
|
|
2764
|
+
if (!oldToken) return {};
|
|
2765
|
+
const newToken = parseDiffHeaderToken(trail, oldToken.nextIndex);
|
|
2766
|
+
if (!newToken) return {};
|
|
2767
|
+
return {
|
|
2768
|
+
oldPath: stripPrDiffPathPrefix(oldToken.value, "a/"),
|
|
2769
|
+
newPath: stripPrDiffPathPrefix(newToken.value, "b/"),
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
const bIdx = trail.indexOf(" b/");
|
|
2774
|
+
if (trail.startsWith("a/") && bIdx > 0) {
|
|
2775
|
+
return {
|
|
2776
|
+
oldPath: trail.slice(2, bIdx),
|
|
2777
|
+
newPath: trail.slice(bIdx + 3),
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
return {};
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
function isPrDiffFileHeaderLine(line: string): boolean {
|
|
2784
|
+
return (
|
|
2785
|
+
line === "--- /dev/null" ||
|
|
2786
|
+
line === "+++ /dev/null" ||
|
|
2787
|
+
line.startsWith("--- a/") ||
|
|
2788
|
+
line.startsWith("+++ b/") ||
|
|
2789
|
+
line.startsWith('--- "a/') ||
|
|
2790
|
+
line.startsWith('+++ "b/')
|
|
2375
2791
|
);
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
function parsePrDiffSection(section: string, startOffset: number, endOffset: number): PrDiffFile {
|
|
2795
|
+
const lines = section.split("\n");
|
|
2796
|
+
const header = lines[0] ?? "";
|
|
2797
|
+
const headerPaths = parsePrDiffHeaderPaths(header);
|
|
2798
|
+
let oldPath = headerPaths.oldPath;
|
|
2799
|
+
let newPath = headerPaths.newPath;
|
|
2800
|
+
|
|
2801
|
+
let changeType: PrDiffFile["changeType"] = "modified";
|
|
2802
|
+
let isBinary = false;
|
|
2803
|
+
let additions = 0;
|
|
2804
|
+
let deletions = 0;
|
|
2805
|
+
|
|
2806
|
+
let inHunk = false;
|
|
2807
|
+
for (let li = 1; li < lines.length; li += 1) {
|
|
2808
|
+
const line = lines[li] ?? "";
|
|
2809
|
+
if (line.startsWith("new file mode")) {
|
|
2810
|
+
changeType = "added";
|
|
2811
|
+
continue;
|
|
2812
|
+
}
|
|
2813
|
+
if (line.startsWith("deleted file mode")) {
|
|
2814
|
+
changeType = "deleted";
|
|
2815
|
+
continue;
|
|
2816
|
+
}
|
|
2817
|
+
if (line.startsWith("rename from ")) {
|
|
2818
|
+
changeType = "renamed";
|
|
2819
|
+
oldPath = line.slice("rename from ".length);
|
|
2820
|
+
continue;
|
|
2821
|
+
}
|
|
2822
|
+
if (line.startsWith("rename to ")) {
|
|
2823
|
+
newPath = line.slice("rename to ".length);
|
|
2824
|
+
continue;
|
|
2825
|
+
}
|
|
2826
|
+
if (line.startsWith("Binary files ") && line.endsWith(" differ")) {
|
|
2827
|
+
isBinary = true;
|
|
2828
|
+
continue;
|
|
2829
|
+
}
|
|
2830
|
+
if (line.startsWith("@@ ")) {
|
|
2831
|
+
inHunk = true;
|
|
2832
|
+
continue;
|
|
2833
|
+
}
|
|
2834
|
+
if (!inHunk && isPrDiffFileHeaderLine(line)) continue;
|
|
2835
|
+
if (line.startsWith("+")) {
|
|
2836
|
+
additions += 1;
|
|
2837
|
+
} else if (line.startsWith("-")) {
|
|
2838
|
+
deletions += 1;
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2376
2841
|
|
|
2377
|
-
|
|
2378
|
-
|
|
2842
|
+
if (isBinary) {
|
|
2843
|
+
if (changeType === "modified") changeType = "binary";
|
|
2844
|
+
additions = 0;
|
|
2845
|
+
deletions = 0;
|
|
2846
|
+
}
|
|
2379
2847
|
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2848
|
+
const displayPath =
|
|
2849
|
+
changeType === "deleted" ? (oldPath ?? newPath ?? "(unknown)") : (newPath ?? oldPath ?? "(unknown)");
|
|
2850
|
+
const file: PrDiffFile = {
|
|
2851
|
+
path: displayPath,
|
|
2852
|
+
additions,
|
|
2853
|
+
deletions,
|
|
2854
|
+
changeType,
|
|
2855
|
+
startOffset,
|
|
2856
|
+
endOffset,
|
|
2857
|
+
};
|
|
2858
|
+
if (oldPath && oldPath !== displayPath) {
|
|
2859
|
+
file.oldPath = oldPath;
|
|
2384
2860
|
}
|
|
2861
|
+
return file;
|
|
2862
|
+
}
|
|
2385
2863
|
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2864
|
+
async function fetchPrDiffFresh(
|
|
2865
|
+
cwd: string,
|
|
2866
|
+
repo: string,
|
|
2867
|
+
number: number,
|
|
2868
|
+
signal: AbortSignal | undefined,
|
|
2869
|
+
): Promise<{ rendered: string; sourceUrl: string | undefined; payload: PrDiffPayload }> {
|
|
2870
|
+
const args = ["pr", "diff", String(number), "--color", "never"];
|
|
2871
|
+
appendRepoFlag(args, repo, String(number));
|
|
2872
|
+
const text = await git.github.text(cwd, args, signal, { repoProvided: true, trimOutput: false });
|
|
2873
|
+
const payload = parsePrUnifiedDiff(text);
|
|
2874
|
+
return { rendered: text, sourceUrl: undefined, payload };
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
/**
|
|
2878
|
+
* Cache-aware PR diff fetcher. Stores the full unified diff plus a parsed
|
|
2879
|
+
* file index in a single `pr-diff` cache row so the listing, full-diff, and
|
|
2880
|
+
* per-file slice variants of `pr://<n>/diff` share one `gh pr diff`
|
|
2881
|
+
* invocation.
|
|
2882
|
+
*/
|
|
2883
|
+
export async function getOrFetchPrDiff(options: PrDiffLookupOptions): Promise<ViewLookupResult<PrDiffPayload>> {
|
|
2884
|
+
const authKey = options.cacheAuthKey === undefined ? (resolveGithubCacheAuthKey() ?? null) : options.cacheAuthKey;
|
|
2885
|
+
const doFetch = () => fetchPrDiffFresh(options.cwd, options.repo, options.number, options.signal);
|
|
2886
|
+
const lookup = await getOrFetchView<PrDiffPayload>({
|
|
2887
|
+
repo: options.repo,
|
|
2888
|
+
kind: "pr-diff",
|
|
2889
|
+
number: options.number,
|
|
2890
|
+
includeComments: false,
|
|
2891
|
+
settings: options.settings,
|
|
2892
|
+
authKey,
|
|
2893
|
+
fetchFresh: doFetch,
|
|
2393
2894
|
});
|
|
2394
|
-
|
|
2395
|
-
|
|
2895
|
+
return {
|
|
2896
|
+
rendered: lookup.rendered,
|
|
2897
|
+
sourceUrl: lookup.sourceUrl,
|
|
2898
|
+
payload: lookup.payload,
|
|
2899
|
+
status: lookup.status,
|
|
2900
|
+
fetchedAt: lookup.fetchedAt,
|
|
2901
|
+
};
|
|
2396
2902
|
}
|
|
2397
2903
|
|
|
2398
2904
|
function joinSections(sections: string[]): string[] {
|
|
@@ -2765,13 +3271,13 @@ async function executeSearchIssues(
|
|
|
2765
3271
|
const limit = resolveSearchLimit(params.limit);
|
|
2766
3272
|
const dateField = resolveSearchDateField("issues", params.dateField);
|
|
2767
3273
|
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
2768
|
-
const
|
|
2769
|
-
const
|
|
3274
|
+
const displayQuery = composeSearchQuery([params.query, dateQualifier]);
|
|
3275
|
+
const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined, "is:issue"]);
|
|
3276
|
+
const args = buildGhApiSearchArgs("issues", apiQuery, limit);
|
|
2770
3277
|
|
|
2771
|
-
const
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
return buildTextResult(formatSearchResults("issues", query, repo, items));
|
|
3278
|
+
const response = await git.github.json<GhApiSearchResponse<GhApiSearchIssueItem>>(session.cwd, args, signal);
|
|
3279
|
+
const items = (response.items ?? []).map(apiIssueToSearchResult);
|
|
3280
|
+
return buildTextResult(formatSearchResults("issues", displayQuery, repo, items));
|
|
2775
3281
|
}
|
|
2776
3282
|
|
|
2777
3283
|
async function executeSearchPrs(
|
|
@@ -2783,13 +3289,13 @@ async function executeSearchPrs(
|
|
|
2783
3289
|
const limit = resolveSearchLimit(params.limit);
|
|
2784
3290
|
const dateField = resolveSearchDateField("prs", params.dateField);
|
|
2785
3291
|
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
2786
|
-
const
|
|
2787
|
-
const
|
|
3292
|
+
const displayQuery = composeSearchQuery([params.query, dateQualifier]);
|
|
3293
|
+
const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined, "is:pr"]);
|
|
3294
|
+
const args = buildGhApiSearchArgs("issues", apiQuery, limit);
|
|
2788
3295
|
|
|
2789
|
-
const
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
return buildTextResult(formatSearchResults("pull requests", query, repo, items));
|
|
3296
|
+
const response = await git.github.json<GhApiSearchResponse<GhApiSearchIssueItem>>(session.cwd, args, signal);
|
|
3297
|
+
const items = (response.items ?? []).map(apiIssueToSearchResult);
|
|
3298
|
+
return buildTextResult(formatSearchResults("pull requests", displayQuery, repo, items));
|
|
2793
3299
|
}
|
|
2794
3300
|
|
|
2795
3301
|
async function executeSearchCode(
|
|
@@ -2803,11 +3309,11 @@ async function executeSearchCode(
|
|
|
2803
3309
|
}
|
|
2804
3310
|
const repo = normalizeOptionalString(params.repo);
|
|
2805
3311
|
const limit = resolveSearchLimit(params.limit);
|
|
2806
|
-
const
|
|
3312
|
+
const apiQuery = composeSearchQuery([query, repo ? `repo:${repo}` : undefined]);
|
|
3313
|
+
const args = buildGhApiSearchArgs("code", apiQuery, limit, ["Accept: application/vnd.github.text-match+json"]);
|
|
2807
3314
|
|
|
2808
|
-
const
|
|
2809
|
-
|
|
2810
|
-
});
|
|
3315
|
+
const response = await git.github.json<GhApiSearchResponse<GhApiSearchCodeItem>>(session.cwd, args, signal);
|
|
3316
|
+
const items = (response.items ?? []).map(apiCodeToSearchResult);
|
|
2811
3317
|
return buildTextResult(formatSearchCodeResults(query, repo, items));
|
|
2812
3318
|
}
|
|
2813
3319
|
|
|
@@ -2820,13 +3326,13 @@ async function executeSearchCommits(
|
|
|
2820
3326
|
const limit = resolveSearchLimit(params.limit);
|
|
2821
3327
|
const dateField = resolveSearchDateField("commits", params.dateField);
|
|
2822
3328
|
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
2823
|
-
const
|
|
2824
|
-
const
|
|
3329
|
+
const displayQuery = composeSearchQuery([params.query, dateQualifier]);
|
|
3330
|
+
const apiQuery = composeSearchQuery([displayQuery, repo ? `repo:${repo}` : undefined]);
|
|
3331
|
+
const args = buildGhApiSearchArgs("commits", apiQuery, limit);
|
|
2825
3332
|
|
|
2826
|
-
const
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
return buildTextResult(formatSearchCommitsResults(query, repo, items));
|
|
3333
|
+
const response = await git.github.json<GhApiSearchResponse<GhApiSearchCommitItem>>(session.cwd, args, signal);
|
|
3334
|
+
const items = (response.items ?? []).map(apiCommitToSearchResult);
|
|
3335
|
+
return buildTextResult(formatSearchCommitsResults(displayQuery, repo, items));
|
|
2830
3336
|
}
|
|
2831
3337
|
|
|
2832
3338
|
async function executeSearchRepos(
|
|
@@ -2838,9 +3344,10 @@ async function executeSearchRepos(
|
|
|
2838
3344
|
const dateField = resolveSearchDateField("repos", params.dateField);
|
|
2839
3345
|
const dateQualifier = buildSearchDateQualifier(dateField, params.since, params.until);
|
|
2840
3346
|
const query = composeSearchQuery([params.query, dateQualifier]);
|
|
2841
|
-
const args =
|
|
3347
|
+
const args = buildGhApiSearchArgs("repositories", query, limit);
|
|
2842
3348
|
|
|
2843
|
-
const
|
|
3349
|
+
const response = await git.github.json<GhApiSearchResponse<GhApiSearchRepoItem>>(session.cwd, args, signal);
|
|
3350
|
+
const items = (response.items ?? []).map(apiRepoToSearchResult);
|
|
2844
3351
|
return buildTextResult(formatSearchReposResults(query, items));
|
|
2845
3352
|
}
|
|
2846
3353
|
|
|
@@ -2894,7 +3401,7 @@ async function executeRunWatch(
|
|
|
2894
3401
|
note,
|
|
2895
3402
|
}),
|
|
2896
3403
|
});
|
|
2897
|
-
await
|
|
3404
|
+
await scheduler.wait(graceSeconds * 1000, { signal });
|
|
2898
3405
|
run = await fetchRunSnapshot(session.cwd, repo, runId, signal);
|
|
2899
3406
|
}
|
|
2900
3407
|
|
|
@@ -2929,7 +3436,7 @@ async function executeRunWatch(
|
|
|
2929
3436
|
return buildTextResult(formatRunWatchResult(repo, run, [], tail), run.url, finalDetails);
|
|
2930
3437
|
}
|
|
2931
3438
|
|
|
2932
|
-
await
|
|
3439
|
+
await scheduler.wait(intervalSeconds * 1000, { signal });
|
|
2933
3440
|
}
|
|
2934
3441
|
}
|
|
2935
3442
|
|
|
@@ -2971,7 +3478,7 @@ async function executeRunWatch(
|
|
|
2971
3478
|
note,
|
|
2972
3479
|
}),
|
|
2973
3480
|
});
|
|
2974
|
-
await
|
|
3481
|
+
await scheduler.wait(graceSeconds * 1000, { signal });
|
|
2975
3482
|
runs = await fetchRunsForCommit(session.cwd, repo, headSha, branch, signal);
|
|
2976
3483
|
}
|
|
2977
3484
|
|
|
@@ -3027,11 +3534,11 @@ async function executeRunWatch(
|
|
|
3027
3534
|
note,
|
|
3028
3535
|
}),
|
|
3029
3536
|
});
|
|
3030
|
-
await
|
|
3537
|
+
await scheduler.wait(intervalSeconds * 1000, { signal });
|
|
3031
3538
|
continue;
|
|
3032
3539
|
}
|
|
3033
3540
|
|
|
3034
3541
|
settledSuccessSignature = undefined;
|
|
3035
|
-
await
|
|
3542
|
+
await scheduler.wait(intervalSeconds * 1000, { signal });
|
|
3036
3543
|
}
|
|
3037
3544
|
}
|