@oh-my-pi/pi-coding-agent 17.2.10 → 17.2.12

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.
Files changed (247) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-k9ghy5sn.md} +53 -0
  3. package/dist/cli.js +10775 -10762
  4. package/dist/types/advisor/runtime.d.ts +1 -1
  5. package/dist/types/capability/mcp.d.ts +11 -0
  6. package/dist/types/capability/skill.d.ts +6 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/custom-models.d.ts +31 -0
  14. package/dist/types/config/model-config-values.d.ts +19 -0
  15. package/dist/types/config/model-patch.d.ts +93 -0
  16. package/dist/types/config/model-provider-discovery.d.ts +51 -0
  17. package/dist/types/config/model-registry.d.ts +9 -52
  18. package/dist/types/config/settings-schema.d.ts +1 -34
  19. package/dist/types/config/settings.d.ts +5 -3
  20. package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
  21. package/dist/types/discovery/agent-plugin-format.d.ts +136 -0
  22. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  23. package/dist/types/discovery/contained-path.d.ts +33 -0
  24. package/dist/types/discovery/index.d.ts +1 -0
  25. package/dist/types/eval/executor-base.d.ts +8 -2
  26. package/dist/types/eval/kernel-session-registry.d.ts +60 -0
  27. package/dist/types/export/share.d.ts +1 -1
  28. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  29. package/dist/types/extensibility/extensions/loader.d.ts +7 -0
  30. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  31. package/dist/types/extensibility/shared-events.d.ts +3 -2
  32. package/dist/types/extensibility/skills.d.ts +5 -0
  33. package/dist/types/lsp/diagnostics.d.ts +96 -0
  34. package/dist/types/lsp/index.d.ts +7 -128
  35. package/dist/types/lsp/servers.d.ts +72 -0
  36. package/dist/types/lsp/tool.d.ts +42 -0
  37. package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
  38. package/dist/types/lsp/writethrough.d.ts +33 -0
  39. package/dist/types/mcp/transports/header-policy.d.ts +46 -0
  40. package/dist/types/mcp/types.d.ts +15 -0
  41. package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
  42. package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
  43. package/dist/types/modes/components/agent-hub.d.ts +2 -0
  44. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  45. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  46. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  47. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  48. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  49. package/dist/types/modes/theme/color.d.ts +19 -0
  50. package/dist/types/modes/theme/loader.d.ts +19 -0
  51. package/dist/types/modes/theme/schema.d.ts +175 -0
  52. package/dist/types/modes/theme/symbols.d.ts +28 -0
  53. package/dist/types/modes/theme/theme-class.d.ts +244 -0
  54. package/dist/types/modes/theme/theme.d.ts +9 -280
  55. package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
  56. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  57. package/dist/types/registry/agent-registry.d.ts +1 -3
  58. package/dist/types/secrets/index.d.ts +26 -2
  59. package/dist/types/secrets/message-transform.d.ts +40 -0
  60. package/dist/types/secrets/obfuscator.d.ts +0 -108
  61. package/dist/types/secrets/placeholder-scan.d.ts +95 -0
  62. package/dist/types/secrets/placeholder.d.ts +94 -0
  63. package/dist/types/secrets/replacement.d.ts +82 -0
  64. package/dist/types/session/agent-session-events.d.ts +2 -2
  65. package/dist/types/session/agent-session-types.d.ts +6 -0
  66. package/dist/types/session/agent-session.d.ts +2 -2
  67. package/dist/types/session/messages.d.ts +1 -0
  68. package/dist/types/session/prewalk.d.ts +3 -1
  69. package/dist/types/session/session-handoff.d.ts +3 -3
  70. package/dist/types/session/session-manager.d.ts +32 -0
  71. package/dist/types/session/session-provider-boundary.d.ts +1 -1
  72. package/dist/types/session/session-tools.d.ts +8 -0
  73. package/dist/types/session/turn-recovery.d.ts +9 -4
  74. package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
  75. package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
  76. package/dist/types/slash-commands/builtin-control.d.ts +2 -0
  77. package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
  78. package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
  79. package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
  80. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  81. package/dist/types/slash-commands/builtin-session.d.ts +2 -0
  82. package/dist/types/task/index.d.ts +2 -0
  83. package/dist/types/tools/gh-common.d.ts +69 -0
  84. package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
  85. package/dist/types/tools/gh-pr-diff.d.ts +102 -0
  86. package/dist/types/tools/gh-run-watch.d.ts +78 -0
  87. package/dist/types/tools/gh-search.d.ts +61 -0
  88. package/dist/types/tools/gh-types.d.ts +340 -0
  89. package/dist/types/tools/gh-view.d.ts +88 -0
  90. package/dist/types/tools/gh.d.ts +4 -201
  91. package/dist/types/tools/read-archive.d.ts +16 -0
  92. package/dist/types/tools/read-format.d.ts +104 -0
  93. package/dist/types/tools/read-path-resolution.d.ts +19 -0
  94. package/dist/types/tools/read-pdf-images.d.ts +12 -0
  95. package/dist/types/tools/read-renderer.d.ts +24 -0
  96. package/dist/types/tools/read-selector.d.ts +27 -0
  97. package/dist/types/tools/read-sqlite.d.ts +16 -0
  98. package/dist/types/tools/read-summary.d.ts +19 -0
  99. package/dist/types/tools/read.d.ts +1 -23
  100. package/dist/types/tools/shell-tokenize.d.ts +18 -1
  101. package/dist/types/utils/changelog.d.ts +0 -5
  102. package/package.json +13 -13
  103. package/scripts/legacy-pi-virtual-module.ts +4 -1
  104. package/src/advisor/runtime.ts +12 -7
  105. package/src/capability/mcp.ts +11 -0
  106. package/src/capability/skill.ts +6 -0
  107. package/src/cli/command-help.ts +4 -0
  108. package/src/cli/gallery-cli.ts +1 -1
  109. package/src/cli-commands.ts +5 -0
  110. package/src/commands/commit.ts +16 -3
  111. package/src/commands/share.ts +71 -0
  112. package/src/commit/agentic/index.ts +39 -21
  113. package/src/commit/execute.ts +56 -0
  114. package/src/commit/index.ts +2 -1
  115. package/src/commit/pipeline.ts +20 -7
  116. package/src/config/custom-models.ts +188 -0
  117. package/src/config/model-config-values.ts +128 -0
  118. package/src/config/model-discovery.ts +1 -1
  119. package/src/config/model-patch.ts +252 -0
  120. package/src/config/model-provider-discovery.ts +132 -0
  121. package/src/config/model-registry.ts +84 -704
  122. package/src/config/settings-schema.ts +2 -33
  123. package/src/config/settings.ts +53 -3
  124. package/src/debug/raw-sse-buffer.ts +20 -0
  125. package/src/discovery/agent-plugin-format.ts +551 -0
  126. package/src/discovery/agent-plugins.ts +341 -0
  127. package/src/discovery/claude-plugins.ts +21 -5
  128. package/src/discovery/contained-path.ts +75 -0
  129. package/src/discovery/helpers.ts +23 -9
  130. package/src/discovery/index.ts +1 -0
  131. package/src/discovery/omp-plugins.ts +20 -7
  132. package/src/edit/hashline/diff.ts +5 -1
  133. package/src/eval/executor-base.ts +39 -6
  134. package/src/eval/jl/executor.ts +82 -371
  135. package/src/eval/kernel-session-registry.ts +398 -0
  136. package/src/eval/py/executor.ts +53 -261
  137. package/src/eval/rb/executor.ts +36 -279
  138. package/src/exec/non-interactive-env.ts +1 -0
  139. package/src/export/share.ts +2 -1
  140. package/src/extensibility/custom-tools/types.ts +2 -2
  141. package/src/extensibility/extensions/loader.ts +78 -14
  142. package/src/extensibility/extensions/runner.ts +6 -0
  143. package/src/extensibility/extensions/types.ts +12 -0
  144. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  145. package/src/extensibility/shared-events.ts +3 -2
  146. package/src/extensibility/skills.ts +9 -0
  147. package/src/internal-urls/skill-protocol.ts +14 -0
  148. package/src/internal-urls/vault-protocol.ts +2 -2
  149. package/src/launch/client.ts +11 -1
  150. package/src/launch/protocol.ts +7 -2
  151. package/src/lsp/diagnostics.ts +516 -0
  152. package/src/lsp/index.ts +20 -2819
  153. package/src/lsp/servers.ts +296 -0
  154. package/src/lsp/tool.ts +1352 -0
  155. package/src/lsp/workspace-diagnostics.ts +170 -0
  156. package/src/lsp/writethrough.ts +561 -0
  157. package/src/main.ts +3 -2
  158. package/src/mcp/config.ts +3 -0
  159. package/src/mcp/manager.ts +12 -9
  160. package/src/mcp/transports/header-policy.ts +95 -0
  161. package/src/mcp/transports/http.ts +35 -52
  162. package/src/mcp/transports/sse.ts +20 -27
  163. package/src/mcp/types.ts +15 -0
  164. package/src/modes/acp/acp-agent.ts +15 -4
  165. package/src/modes/components/agent-dashboard.ts +2 -0
  166. package/src/modes/components/agent-hub-projection.ts +2 -2
  167. package/src/modes/components/agent-hub-renderer.ts +3 -7
  168. package/src/modes/components/agent-hub.ts +5 -0
  169. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  170. package/src/modes/components/chat-transcript-builder.ts +3 -0
  171. package/src/modes/components/custom-editor.ts +0 -9
  172. package/src/modes/components/status-line/component.ts +1 -0
  173. package/src/modes/components/status-line/segments.ts +6 -4
  174. package/src/modes/components/status-line/types.ts +2 -0
  175. package/src/modes/components/tool-execution.ts +13 -16
  176. package/src/modes/components/tree-selector.ts +62 -3
  177. package/src/modes/controllers/command-controller.ts +8 -2
  178. package/src/modes/controllers/event-controller.ts +15 -15
  179. package/src/modes/controllers/input-controller.ts +20 -2
  180. package/src/modes/controllers/selector-controller.ts +1 -0
  181. package/src/modes/theme/color.ts +133 -0
  182. package/src/modes/theme/loader.ts +178 -0
  183. package/src/modes/theme/schema.ts +263 -0
  184. package/src/modes/theme/symbols.ts +1000 -0
  185. package/src/modes/theme/theme-class.ts +611 -0
  186. package/src/modes/theme/theme.ts +27 -2453
  187. package/src/modes/theme/tui-adapters.ts +279 -0
  188. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  189. package/src/modes/utils/ui-helpers.ts +1 -0
  190. package/src/registry/agent-registry.ts +2 -2
  191. package/src/sdk.ts +6 -49
  192. package/src/secrets/index.ts +51 -6
  193. package/src/secrets/message-transform.ts +287 -0
  194. package/src/secrets/obfuscator.ts +39 -1303
  195. package/src/secrets/placeholder-scan.ts +506 -0
  196. package/src/secrets/placeholder.ts +309 -0
  197. package/src/secrets/replacement.ts +216 -0
  198. package/src/session/agent-session-events.ts +2 -2
  199. package/src/session/agent-session-types.ts +6 -0
  200. package/src/session/agent-session.ts +146 -21
  201. package/src/session/indexed-session-storage.ts +7 -2
  202. package/src/session/messages.ts +1 -0
  203. package/src/session/prewalk.ts +57 -17
  204. package/src/session/session-advisors.ts +32 -34
  205. package/src/session/session-context.ts +3 -5
  206. package/src/session/session-handoff.ts +39 -16
  207. package/src/session/session-manager.ts +67 -3
  208. package/src/session/session-provider-boundary.ts +3 -6
  209. package/src/session/session-tools.ts +37 -3
  210. package/src/session/todo-tracker.ts +11 -1
  211. package/src/session/turn-recovery.ts +115 -84
  212. package/src/slash-commands/builtin-collaboration.ts +504 -0
  213. package/src/slash-commands/builtin-completions.ts +291 -0
  214. package/src/slash-commands/builtin-control.ts +78 -0
  215. package/src/slash-commands/builtin-lifecycle.ts +506 -0
  216. package/src/slash-commands/builtin-marketplace.ts +567 -0
  217. package/src/slash-commands/builtin-modes.ts +518 -0
  218. package/src/slash-commands/builtin-registry.ts +21 -2996
  219. package/src/slash-commands/builtin-session.ts +592 -0
  220. package/src/task/executor.ts +17 -14
  221. package/src/task/index.ts +21 -6
  222. package/src/tools/bash-skill-urls.ts +15 -0
  223. package/src/tools/bash.ts +10 -11
  224. package/src/tools/debug.ts +1 -1
  225. package/src/tools/gh-common.ts +288 -0
  226. package/src/tools/gh-pr-checkout.ts +679 -0
  227. package/src/tools/gh-pr-diff.ts +473 -0
  228. package/src/tools/gh-run-watch.ts +1015 -0
  229. package/src/tools/gh-search.ts +500 -0
  230. package/src/tools/gh-types.ts +379 -0
  231. package/src/tools/gh-view.ts +612 -0
  232. package/src/tools/gh.ts +109 -3821
  233. package/src/tools/jtd-to-json-schema.ts +123 -21
  234. package/src/tools/read-archive.ts +209 -0
  235. package/src/tools/read-format.ts +593 -0
  236. package/src/tools/read-path-resolution.ts +36 -0
  237. package/src/tools/read-pdf-images.ts +250 -0
  238. package/src/tools/read-renderer.ts +289 -0
  239. package/src/tools/read-selector.ts +84 -0
  240. package/src/tools/read-sqlite.ts +215 -0
  241. package/src/tools/read-summary.ts +199 -0
  242. package/src/tools/read.ts +82 -1824
  243. package/src/tools/shell-tokenize.ts +99 -1
  244. package/src/utils/changelog.ts +1 -1
  245. package/src/utils/title-generator.ts +3 -1
  246. package/src/web/search/providers/exa.ts +1 -1
  247. package/src/web/search/providers/zai.ts +12 -3
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
+ import { resolveContainedPathSync } from "../discovery/contained-path";
3
4
  import type { Skill } from "../extensibility/skills";
4
5
  import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
5
6
  import { validateRelativePath } from "../internal-urls/skill-protocol";
@@ -89,6 +90,20 @@ export function resolveSkillUrlToPath(url: string, skills: readonly Skill[]): st
89
90
  if (!resolvedPath.startsWith(resolvedBaseDir + path.sep) && resolvedPath !== resolvedBaseDir) {
90
91
  throw new ToolError("Path traversal is not allowed in skill:// URLs");
91
92
  }
93
+ // Agent Plugin skills (§4.1): the resource must canonically resolve within
94
+ // the plugin root. Fail closed: a dangling or unresolvable path is rejected
95
+ // rather than handed to bash, where writing through it could create the
96
+ // outside target. Symlinks may target other files inside the same package.
97
+ if (skill.containRoot) {
98
+ const contained = resolveContainedPathSync(skill.containRoot, resolvedPath);
99
+ if (contained.status === "outside") {
100
+ throw new ToolError(`skill:// path resolves outside the plugin root: ${url}`);
101
+ }
102
+ if (contained.status === "missing") {
103
+ throw new ToolError(`skill:// path does not exist: ${url}`);
104
+ }
105
+ return contained.realPath;
106
+ }
92
107
 
93
108
  return resolvedPath;
94
109
  }
package/src/tools/bash.ts CHANGED
@@ -49,7 +49,7 @@ import {
49
49
  previewWindowRows,
50
50
  replaceTabs,
51
51
  } from "./render-utils";
52
- import { tokenizeShellSegments } from "./shell-tokenize";
52
+ import { extractLeadingCdTarget, tokenizeShellSegments } from "./shell-tokenize";
53
53
  import { ToolAbortError, ToolError } from "./tool-errors";
54
54
  import { toolResult } from "./tool-result";
55
55
  import { clampTimeout, TOOL_TIMEOUTS } from "./tool-timeouts";
@@ -960,17 +960,16 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
960
960
  let command = rawCommand;
961
961
  const env = normalizeBashEnv(rawEnv);
962
962
 
963
- // Extract leading `cd <path> && ...` into cwd when the model ignores the cwd parameter.
964
- // Constrained to a single line so a `&&` that sits on a later line of a multiline
965
- // script can't pull the entire script into the "cwd" capture.
963
+ // Extract a leading `cd <path> && ...` into cwd when the model ignores the
964
+ // cwd parameter. The scanner captures only a single path token and defers
965
+ // to the shell for anything else (redirects, extra args, shell expansion),
966
+ // so it never absorbs shell syntax like `cd /tmp 2>/dev/null && ...` into
967
+ // the structured cwd. Constrained to a top-level `&&` on the first line.
966
968
  if (!cwd) {
967
- const cdMatch = command.match(/^cd[ \t]+((?:[^&\\\n\r]|\\.)+?)[ \t]*&&[ \t]*/);
968
- // Skip extraction when the path needs shell expansion ($VAR, $(...),
969
- // backticks) — resolveToCwd only expands `~`, so routing those through
970
- // cwd would reject commands the shell itself handles fine.
971
- if (cdMatch && !/[$`(]/.test(cdMatch[1])) {
972
- cwd = cdMatch[1].trim().replace(/^["']|["']$/g, "");
973
- command = command.slice(cdMatch[0].length);
969
+ const cd = extractLeadingCdTarget(command);
970
+ if (cd) {
971
+ cwd = cd.path;
972
+ command = cd.rest;
974
973
  }
975
974
  }
976
975
  if (asyncRequested && !this.#asyncEnabled) {
@@ -505,7 +505,7 @@ const ADAPTER_UNAVAILABLE_MESSAGES: Readonly<Record<string, string>> = {
505
505
  dlv: "adapter 'dlv' is not available: install with 'go install github.com/go-delve/delve/cmd/dlv@latest'",
506
506
  rdbg: "adapter 'rdbg' is not available: install with 'gem install debug'",
507
507
  "js-debug-adapter":
508
- "adapter 'js-debug-adapter' is not available: install vscode-js-debug with Mason or set JS_DEBUG_DAP_SERVER to dapDebugServer.js",
508
+ "adapter 'js-debug-adapter' is not available: download it from https://github.com/microsoft/vscode-js-debug",
509
509
  };
510
510
 
511
511
  const ADAPTER_CANONICAL_COMMANDS: Readonly<Record<string, string>> = {
@@ -0,0 +1,288 @@
1
+ import * as path from "node:path";
2
+ import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
3
+ import { untilAborted } from "@oh-my-pi/pi-utils";
4
+ import * as git from "../utils/git";
5
+ import type { ToolSession } from ".";
6
+ import type { GhToolDetails } from "./gh";
7
+ import type { GhLabel, GhUser } from "./gh-types";
8
+ import { ToolError } from "./tool-errors";
9
+ import { toolResult } from "./tool-result";
10
+
11
+ export function normalizeText(value: string | null | undefined): string {
12
+ return (value ?? "").replaceAll("\r\n", "\n").replaceAll("\r", "\n").replaceAll("\t", " ").trim();
13
+ }
14
+
15
+ export function normalizeBlock(value: string | null | undefined): string {
16
+ return (value ?? "").replaceAll("\r\n", "\n").replaceAll("\r", "\n").replaceAll("\t", " ").trimEnd();
17
+ }
18
+
19
+ export function looksLikeGitHubUrl(value: string | undefined): boolean {
20
+ return value?.startsWith("https://github.com/") ?? false;
21
+ }
22
+
23
+ export function normalizeOptionalString(value: string | null | undefined): string | undefined {
24
+ const normalized = value?.trim();
25
+ return normalized ? normalized : undefined;
26
+ }
27
+
28
+ export function normalizePrIdentifierList(value: string | string[] | undefined): string[] {
29
+ if (value === undefined) return [];
30
+ const raw = typeof value === "string" ? [value] : value;
31
+ const cleaned: string[] = [];
32
+ for (const entry of raw) {
33
+ const trimmed = entry?.trim();
34
+ if (trimmed) cleaned.push(trimmed);
35
+ }
36
+ return cleaned;
37
+ }
38
+
39
+ export function requireNonEmpty(value: string | null | undefined, label: string): string {
40
+ const normalized = normalizeOptionalString(value);
41
+ if (!normalized) {
42
+ throw new ToolError(`${label} must not be empty`);
43
+ }
44
+ return normalized;
45
+ }
46
+
47
+ export function appendRepoFlag(args: string[], repo: string | undefined, identifier?: string): void {
48
+ if (!repo || looksLikeGitHubUrl(identifier)) {
49
+ return;
50
+ }
51
+
52
+ args.push("--repo", repo);
53
+ }
54
+
55
+ export const REPO_API_URL_PREFIX = "https://api.github.com/repos/";
56
+
57
+ export const PR_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)(?:\/.*)?$/;
58
+ export const ISSUE_URL_PATTERN = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/issues\/(\d+)(?:\/.*)?$/;
59
+
60
+ export async function requireCurrentGitBranch(cwd: string, signal?: AbortSignal): Promise<string> {
61
+ const branch = await git.branch.current(cwd, signal);
62
+ if (!branch) {
63
+ throw new ToolError("Current git branch is unavailable. Pass `branch` or `run` explicitly.");
64
+ }
65
+
66
+ return branch;
67
+ }
68
+
69
+ export async function requireCurrentGitHead(cwd: string, signal?: AbortSignal): Promise<string> {
70
+ const headSha = await git.head.sha(cwd, signal);
71
+ if (!headSha) {
72
+ throw new ToolError("Current git HEAD is unavailable. Pass `run` explicitly.");
73
+ }
74
+
75
+ return headSha;
76
+ }
77
+
78
+ export function formatAuthor(author: GhUser | null | undefined): string | undefined {
79
+ if (!author) return undefined;
80
+ if (author.login) return `@${author.login}`;
81
+ if (author.name) return author.name;
82
+ return undefined;
83
+ }
84
+
85
+ export function formatLabels(labels: GhLabel[] | undefined): string | undefined {
86
+ const names = labels?.map(label => label.name).filter((value): value is string => Boolean(value)) ?? [];
87
+ if (names.length === 0) return undefined;
88
+ return names.join(", ");
89
+ }
90
+
91
+ export function pushLine(lines: string[], label: string, value: string | number | boolean | undefined): void {
92
+ if (value === undefined || value === "") return;
93
+ lines.push(`${label}: ${value}`);
94
+ }
95
+
96
+ export function parsePullRequestUrl(value: string | undefined): { repo?: string; prNumber?: number } {
97
+ const normalized = normalizeOptionalString(value);
98
+ if (!normalized) {
99
+ return {};
100
+ }
101
+
102
+ const match = normalized.match(PR_URL_PATTERN);
103
+ if (!match) {
104
+ return {};
105
+ }
106
+
107
+ return {
108
+ repo: match[1],
109
+ prNumber: Number(match[2]),
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Parse a digit-only decimal positive integer or return undefined. Rejects
115
+ * `1e2`, `0x10`, `12.0`, leading +/-, or any other shape `Number()` would
116
+ * accept — those would otherwise key the cache against the wrong row.
117
+ */
118
+ export function parsePositiveDecimalInt(value: string | undefined): number | undefined {
119
+ if (!value || !/^\d+$/.test(value)) return undefined;
120
+ const num = Number(value);
121
+ if (!Number.isSafeInteger(num) || num <= 0) return undefined;
122
+ return num;
123
+ }
124
+
125
+ export function parseIssueUrl(value: string | undefined): { repo?: string; issueNumber?: number } {
126
+ const normalized = normalizeOptionalString(value);
127
+ if (!normalized) return {};
128
+ const match = normalized.match(ISSUE_URL_PATTERN);
129
+ if (!match) return {};
130
+ return {
131
+ repo: match[1],
132
+ issueNumber: Number(match[2]),
133
+ };
134
+ }
135
+
136
+ export function githubRepoSlugEquals(left: string | undefined, right: string): boolean {
137
+ if (left === undefined || left.length !== right.length) return false;
138
+ for (let idx = 0; idx < left.length; idx += 1) {
139
+ let leftCode = left.charCodeAt(idx);
140
+ let rightCode = right.charCodeAt(idx);
141
+ if (leftCode >= 65 && leftCode <= 90) leftCode += 32;
142
+ if (rightCode >= 65 && rightCode <= 90) rightCode += 32;
143
+ if (leftCode !== rightCode) return false;
144
+ }
145
+ return true;
146
+ }
147
+
148
+ export async function resolveGitHubRepo(
149
+ cwd: string,
150
+ repo: string | undefined,
151
+ runRepo: string | undefined,
152
+ signal?: AbortSignal,
153
+ ): Promise<string> {
154
+ if (repo && runRepo && !githubRepoSlugEquals(repo, runRepo)) {
155
+ throw new ToolError("run URL repository does not match the provided repo");
156
+ }
157
+
158
+ if (repo) {
159
+ return repo;
160
+ }
161
+
162
+ if (runRepo) {
163
+ return runRepo;
164
+ }
165
+
166
+ const resolved = await git.github.text(
167
+ cwd,
168
+ ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"],
169
+ signal,
170
+ );
171
+ return requireNonEmpty(resolved, "repo");
172
+ }
173
+
174
+ /**
175
+ * Process-lifetime cache of `gh repo view --json nameWithOwner` lookups keyed
176
+ * by absolute cwd. Avoids repeated `gh` chatter when the same protocol handler
177
+ * or tool call resolves the default repo many times in a row.
178
+ *
179
+ * The shared lookup is intentionally **not** bound to any caller's
180
+ * AbortSignal. Cancelling one caller would otherwise kill the underlying
181
+ * `gh repo view` for every concurrent waiter on the same cwd. Each caller's
182
+ * signal is honored at the wait point via `untilAborted` instead, so an abort
183
+ * unwinds only that caller.
184
+ */
185
+ export const DEFAULT_REPO_RESOLVED = new Map<string, string>();
186
+ export const DEFAULT_REPO_INFLIGHT = new Map<string, Promise<string>>();
187
+
188
+ export async function resolveDefaultRepoMemoized(cwd: string, signal?: AbortSignal): Promise<string> {
189
+ const key = path.resolve(cwd);
190
+ const ready = DEFAULT_REPO_RESOLVED.get(key);
191
+ if (ready) return ready;
192
+ let pending = DEFAULT_REPO_INFLIGHT.get(key);
193
+ if (!pending) {
194
+ pending = (async () => {
195
+ // No caller signal: this lookup is shared across every concurrent
196
+ // waiter on the same cwd.
197
+ const resolved = await git.github.text(cwd, [
198
+ "repo",
199
+ "view",
200
+ "--json",
201
+ "nameWithOwner",
202
+ "-q",
203
+ ".nameWithOwner",
204
+ ]);
205
+ const value = requireNonEmpty(resolved, "repo");
206
+ DEFAULT_REPO_RESOLVED.set(key, value);
207
+ return value;
208
+ })();
209
+ // Drop the in-flight slot on settle so failures don't poison the cache
210
+ // and so a successful resolution survives only in `DEFAULT_REPO_RESOLVED`.
211
+ void pending.then(
212
+ () => DEFAULT_REPO_INFLIGHT.delete(key),
213
+ () => DEFAULT_REPO_INFLIGHT.delete(key),
214
+ );
215
+ DEFAULT_REPO_INFLIGHT.set(key, pending);
216
+ }
217
+ return untilAborted(signal, pending);
218
+ }
219
+
220
+ /**
221
+ * Best-effort cached cwd → `owner/repo` resolution that swallows any failure
222
+ * (not a git checkout, no GitHub remote, `gh` unauthenticated, …) into
223
+ * `undefined`. Use where the cwd repo is a convenience fallback, not a safety
224
+ * check.
225
+ */
226
+ export async function tryResolveCurrentRepo(cwd: string, signal: AbortSignal | undefined): Promise<string | undefined> {
227
+ try {
228
+ return await resolveDefaultRepoMemoized(cwd, signal);
229
+ } catch {
230
+ return undefined;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Best-effort fresh cwd → `owner/repo` resolution for safety checks that must
236
+ * reflect the repository currently mounted at `cwd`, not the process-lifetime
237
+ * default-repo cache.
238
+ */
239
+ export async function tryResolveCurrentRepoFresh(
240
+ cwd: string,
241
+ signal: AbortSignal | undefined,
242
+ ): Promise<string | undefined> {
243
+ try {
244
+ return await resolveGitHubRepo(cwd, undefined, undefined, signal);
245
+ } catch {
246
+ return undefined;
247
+ }
248
+ }
249
+
250
+ export async function saveArtifactText(
251
+ session: ToolSession,
252
+ toolType: string,
253
+ text: string,
254
+ ): Promise<string | undefined> {
255
+ const { path: artifactPath, id: artifactId } = (await session.allocateOutputArtifact?.(toolType)) ?? {};
256
+ if (!artifactPath || !artifactId) {
257
+ return undefined;
258
+ }
259
+
260
+ await Bun.write(artifactPath, text);
261
+ return artifactId;
262
+ }
263
+
264
+ export function appendArtifactReference(text: string, artifactId: string | undefined, label: string): string {
265
+ if (!artifactId) {
266
+ return text;
267
+ }
268
+
269
+ return `${text}\n\n${label}: artifact://${artifactId}`;
270
+ }
271
+
272
+ export function buildTextResult(
273
+ text: string,
274
+ sourceUrl?: string,
275
+ details?: GhToolDetails,
276
+ options?: { artifactId?: string; artifactLabel?: string; useless?: boolean },
277
+ ): AgentToolResult<GhToolDetails> {
278
+ const builder = toolResult<GhToolDetails>(details).text(
279
+ appendArtifactReference(text, options?.artifactId, options?.artifactLabel ?? "Saved artifact"),
280
+ );
281
+ if (sourceUrl) {
282
+ builder.sourceUrl(sourceUrl);
283
+ }
284
+ if (options?.useless) {
285
+ builder.useless();
286
+ }
287
+ return builder.done();
288
+ }