@oh-my-pi/pi-coding-agent 13.17.1 → 13.17.6

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 (148) hide show
  1. package/CHANGELOG.md +93 -3
  2. package/package.json +10 -8
  3. package/scripts/format-prompts.ts +3 -3
  4. package/src/cli/plugin-cli.ts +114 -25
  5. package/src/commands/plugin.ts +5 -0
  6. package/src/commit/agentic/prompts/analyze-file.md +1 -1
  7. package/src/commit/agentic/prompts/session-user.md +1 -1
  8. package/src/commit/agentic/prompts/split-confirm.md +1 -1
  9. package/src/commit/agentic/prompts/system.md +1 -1
  10. package/src/commit/prompts/analysis-system.md +1 -1
  11. package/src/commit/prompts/analysis-user.md +1 -1
  12. package/src/commit/prompts/changelog-system.md +1 -1
  13. package/src/commit/prompts/changelog-user.md +1 -1
  14. package/src/commit/prompts/file-observer-system.md +1 -1
  15. package/src/commit/prompts/file-observer-user.md +1 -1
  16. package/src/commit/prompts/reduce-system.md +1 -1
  17. package/src/commit/prompts/reduce-user.md +1 -1
  18. package/src/commit/prompts/summary-retry.md +1 -1
  19. package/src/commit/prompts/summary-system.md +1 -1
  20. package/src/commit/prompts/summary-user.md +1 -1
  21. package/src/commit/prompts/types-description.md +1 -1
  22. package/src/config/settings-schema.ts +16 -0
  23. package/src/discovery/claude-plugins.ts +5 -5
  24. package/src/discovery/helpers.ts +144 -24
  25. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +37 -0
  26. package/src/extensibility/custom-commands/loader.ts +7 -0
  27. package/src/extensibility/plugins/marketplace/fetcher.ts +55 -37
  28. package/src/extensibility/plugins/marketplace/manager.ts +296 -73
  29. package/src/extensibility/plugins/marketplace/registry.ts +15 -0
  30. package/src/extensibility/plugins/marketplace/types.ts +17 -3
  31. package/src/internal-urls/docs-index.generated.ts +2 -1
  32. package/src/main.ts +13 -4
  33. package/src/modes/components/assistant-message.ts +2 -1
  34. package/src/modes/components/plugin-selector.ts +20 -11
  35. package/src/modes/components/tool-execution.ts +2 -3
  36. package/src/modes/controllers/command-controller.ts +19 -7
  37. package/src/modes/controllers/selector-controller.ts +7 -4
  38. package/src/modes/interactive-mode.ts +4 -0
  39. package/src/modes/types.ts +1 -0
  40. package/src/modes/utils/tools-markdown.ts +27 -0
  41. package/src/prompts/agents/designer.md +1 -1
  42. package/src/prompts/agents/explore.md +1 -1
  43. package/src/prompts/agents/frontmatter.md +1 -1
  44. package/src/prompts/agents/init.md +1 -1
  45. package/src/prompts/agents/librarian.md +1 -1
  46. package/src/prompts/agents/oracle.md +1 -1
  47. package/src/prompts/agents/plan.md +1 -1
  48. package/src/prompts/agents/reviewer.md +1 -1
  49. package/src/prompts/agents/task.md +1 -1
  50. package/src/prompts/ci-green-request.md +36 -0
  51. package/src/prompts/compaction/branch-summary-context.md +1 -1
  52. package/src/prompts/compaction/branch-summary-preamble.md +1 -1
  53. package/src/prompts/compaction/branch-summary.md +1 -1
  54. package/src/prompts/compaction/compaction-short-summary.md +1 -1
  55. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  56. package/src/prompts/compaction/compaction-summary.md +1 -1
  57. package/src/prompts/compaction/compaction-turn-prefix.md +1 -1
  58. package/src/prompts/compaction/compaction-update-summary.md +1 -1
  59. package/src/prompts/memories/consolidation.md +1 -1
  60. package/src/prompts/memories/read-path.md +1 -1
  61. package/src/prompts/memories/stage_one_input.md +1 -1
  62. package/src/prompts/memories/stage_one_system.md +1 -1
  63. package/src/prompts/review-request.md +1 -1
  64. package/src/prompts/system/agent-creation-architect.md +1 -1
  65. package/src/prompts/system/agent-creation-user.md +1 -1
  66. package/src/prompts/system/auto-handoff-threshold-focus.md +1 -1
  67. package/src/prompts/system/btw-user.md +1 -1
  68. package/src/prompts/system/commit-message-system.md +1 -1
  69. package/src/prompts/system/custom-system-prompt.md +1 -1
  70. package/src/prompts/system/eager-todo.md +1 -1
  71. package/src/prompts/system/file-operations.md +1 -1
  72. package/src/prompts/system/handoff-document.md +1 -1
  73. package/src/prompts/system/plan-mode-active.md +1 -1
  74. package/src/prompts/system/plan-mode-approved.md +1 -1
  75. package/src/prompts/system/plan-mode-reference.md +1 -1
  76. package/src/prompts/system/plan-mode-subagent.md +1 -1
  77. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  78. package/src/prompts/system/subagent-submit-reminder.md +1 -1
  79. package/src/prompts/system/subagent-system-prompt.md +1 -1
  80. package/src/prompts/system/subagent-user-prompt.md +1 -1
  81. package/src/prompts/system/summarization-system.md +1 -1
  82. package/src/prompts/system/system-prompt.md +1 -1
  83. package/src/prompts/system/title-system.md +1 -1
  84. package/src/prompts/system/ttsr-interrupt.md +1 -1
  85. package/src/prompts/system/web-search.md +1 -1
  86. package/src/prompts/tools/ask.md +1 -1
  87. package/src/prompts/tools/ast-edit.md +1 -1
  88. package/src/prompts/tools/ast-grep.md +1 -1
  89. package/src/prompts/tools/async-result.md +1 -1
  90. package/src/prompts/tools/await.md +1 -1
  91. package/src/prompts/tools/bash.md +1 -1
  92. package/src/prompts/tools/browser.md +1 -1
  93. package/src/prompts/tools/calculator.md +1 -1
  94. package/src/prompts/tools/cancel-job.md +1 -1
  95. package/src/prompts/tools/checkpoint.md +1 -1
  96. package/src/prompts/tools/exit-plan-mode.md +1 -1
  97. package/src/prompts/tools/fetch.md +1 -1
  98. package/src/prompts/tools/find.md +1 -1
  99. package/src/prompts/tools/gemini-image.md +1 -1
  100. package/src/prompts/tools/gh-issue-view.md +11 -0
  101. package/src/prompts/tools/gh-pr-checkout.md +12 -0
  102. package/src/prompts/tools/gh-pr-diff.md +12 -0
  103. package/src/prompts/tools/gh-pr-push.md +11 -0
  104. package/src/prompts/tools/gh-pr-view.md +11 -0
  105. package/src/prompts/tools/gh-repo-view.md +11 -0
  106. package/src/prompts/tools/gh-run-watch.md +12 -0
  107. package/src/prompts/tools/gh-search-issues.md +11 -0
  108. package/src/prompts/tools/gh-search-prs.md +11 -0
  109. package/src/prompts/tools/grep.md +1 -1
  110. package/src/prompts/tools/hashline.md +1 -1
  111. package/src/prompts/tools/inspect-image-system.md +1 -1
  112. package/src/prompts/tools/inspect-image.md +1 -1
  113. package/src/prompts/tools/lsp.md +1 -1
  114. package/src/prompts/tools/patch.md +1 -1
  115. package/src/prompts/tools/python.md +1 -1
  116. package/src/prompts/tools/read.md +6 -3
  117. package/src/prompts/tools/render-mermaid.md +1 -1
  118. package/src/prompts/tools/replace.md +1 -1
  119. package/src/prompts/tools/resolve.md +1 -1
  120. package/src/prompts/tools/rewind.md +1 -1
  121. package/src/prompts/tools/search-tool-bm25.md +1 -1
  122. package/src/prompts/tools/ssh.md +1 -1
  123. package/src/prompts/tools/task-summary.md +1 -1
  124. package/src/prompts/tools/task.md +1 -1
  125. package/src/prompts/tools/todo-write.md +1 -1
  126. package/src/prompts/tools/web-search.md +1 -1
  127. package/src/prompts/tools/write.md +2 -1
  128. package/src/sdk.ts +3 -1
  129. package/src/session/messages.ts +11 -7
  130. package/src/session/session-manager.ts +13 -3
  131. package/src/slash-commands/builtin-registry.ts +109 -37
  132. package/src/slash-commands/marketplace-install-parser.ts +99 -0
  133. package/src/task/discovery.ts +1 -1
  134. package/src/tools/archive-reader.ts +315 -0
  135. package/src/tools/fetch.ts +21 -19
  136. package/src/tools/gh-cli.ts +125 -0
  137. package/src/tools/gh-renderer.ts +305 -0
  138. package/src/tools/gh.ts +2719 -0
  139. package/src/tools/index.ts +22 -0
  140. package/src/tools/read.ts +286 -34
  141. package/src/tools/render-utils.ts +20 -0
  142. package/src/tools/renderers.ts +2 -0
  143. package/src/tools/write.ts +175 -4
  144. package/src/utils/markit.ts +81 -0
  145. package/src/utils/tools-manager.ts +1 -6
  146. package/src/web/scrapers/arxiv.ts +3 -3
  147. package/src/web/scrapers/iacr.ts +3 -3
  148. package/src/web/scrapers/utils.ts +6 -34
@@ -0,0 +1,315 @@
1
+ import { unzipSync } from "fflate";
2
+ import { ToolError } from "./tool-errors";
3
+
4
+ export type ArchiveFormat = "zip" | "tar" | "tar.gz";
5
+
6
+ export interface ArchivePathCandidate {
7
+ archivePath: string;
8
+ subPath: string;
9
+ }
10
+
11
+ export interface ArchiveNode {
12
+ path: string;
13
+ isDirectory: boolean;
14
+ size: number;
15
+ mtimeMs?: number;
16
+ }
17
+
18
+ export interface ArchiveDirectoryEntry extends ArchiveNode {
19
+ name: string;
20
+ }
21
+
22
+ export interface ExtractedArchiveFile extends ArchiveNode {
23
+ bytes: Uint8Array;
24
+ }
25
+
26
+ interface TarStorage {
27
+ type: "tar";
28
+ file: File;
29
+ }
30
+
31
+ interface ZipStorage {
32
+ type: "zip";
33
+ bytes: Uint8Array;
34
+ }
35
+
36
+ type EntryStorage = TarStorage | ZipStorage;
37
+
38
+ interface ArchiveIndexEntry extends ArchiveNode {
39
+ storage?: EntryStorage;
40
+ }
41
+
42
+ function normalizeArchiveLookupPath(rawPath?: string): string | undefined {
43
+ if (!rawPath) return "";
44
+
45
+ const parts = rawPath.replace(/\\/g, "/").split("/");
46
+ const normalizedParts: string[] = [];
47
+ for (const part of parts) {
48
+ if (!part || part === ".") continue;
49
+ if (part === "..") return undefined;
50
+ normalizedParts.push(part);
51
+ }
52
+
53
+ return normalizedParts.join("/");
54
+ }
55
+
56
+ function normalizeArchiveEntryPath(rawPath: string): string | undefined {
57
+ const parts = rawPath.replace(/\\/g, "/").split("/");
58
+ const normalizedParts: string[] = [];
59
+ for (const part of parts) {
60
+ if (!part || part === ".") continue;
61
+ if (part === "..") return undefined;
62
+ normalizedParts.push(part);
63
+ }
64
+
65
+ if (normalizedParts.length === 0) return undefined;
66
+ return normalizedParts.join("/");
67
+ }
68
+
69
+ function isArchiveDirectoryName(rawPath: string): boolean {
70
+ return rawPath.endsWith("/") || rawPath.endsWith("\\");
71
+ }
72
+
73
+ function upsertArchiveEntry(map: Map<string, ArchiveIndexEntry>, entry: ArchiveIndexEntry): void {
74
+ const existing = map.get(entry.path);
75
+ if (!existing) {
76
+ map.set(entry.path, entry);
77
+ return;
78
+ }
79
+
80
+ if (existing.isDirectory && !entry.isDirectory) {
81
+ map.set(entry.path, entry);
82
+ return;
83
+ }
84
+
85
+ if (!existing.isDirectory && entry.isDirectory) {
86
+ return;
87
+ }
88
+
89
+ map.set(entry.path, {
90
+ ...existing,
91
+ size: existing.size || entry.size,
92
+ mtimeMs: existing.mtimeMs ?? entry.mtimeMs,
93
+ storage: existing.storage ?? entry.storage,
94
+ });
95
+ }
96
+
97
+ function ensureParentDirectories(map: Map<string, ArchiveIndexEntry>): void {
98
+ for (const entry of [...map.values()]) {
99
+ const parts = entry.path.split("/");
100
+ const stop = parts.length - 1;
101
+ for (let index = 1; index <= stop; index++) {
102
+ const dirPath = parts.slice(0, index).join("/");
103
+ if (!dirPath || map.has(dirPath)) continue;
104
+ map.set(dirPath, {
105
+ path: dirPath,
106
+ isDirectory: true,
107
+ size: 0,
108
+ });
109
+ }
110
+ }
111
+ }
112
+
113
+ function getArchiveFormatFromPath(filePath: string): ArchiveFormat | undefined {
114
+ const normalized = filePath.toLowerCase();
115
+ if (normalized.endsWith(".tar.gz") || normalized.endsWith(".tgz")) return "tar.gz";
116
+ if (normalized.endsWith(".tar")) return "tar";
117
+ if (normalized.endsWith(".zip")) return "zip";
118
+ return undefined;
119
+ }
120
+
121
+ async function readTarEntries(bytes: Uint8Array): Promise<ArchiveIndexEntry[]> {
122
+ let archive: Bun.Archive;
123
+ try {
124
+ archive = new Bun.Archive(bytes);
125
+ } catch (error) {
126
+ throw new ToolError(error instanceof Error ? error.message : String(error));
127
+ }
128
+
129
+ let files: Map<string, File>;
130
+ try {
131
+ files = await archive.files();
132
+ } catch (error) {
133
+ throw new ToolError(error instanceof Error ? error.message : String(error));
134
+ }
135
+
136
+ const entries: ArchiveIndexEntry[] = [];
137
+ for (const [rawPath, file] of files) {
138
+ const normalizedPath = normalizeArchiveEntryPath(rawPath);
139
+ if (!normalizedPath) continue;
140
+ const mtimeMs = file.lastModified > 0 ? file.lastModified : undefined;
141
+ entries.push({
142
+ path: normalizedPath,
143
+ isDirectory: false,
144
+ size: file.size,
145
+ mtimeMs,
146
+ storage: { type: "tar", file },
147
+ });
148
+ }
149
+
150
+ return entries;
151
+ }
152
+
153
+ function readZipEntries(bytes: Uint8Array): ArchiveIndexEntry[] {
154
+ let files: Record<string, Uint8Array>;
155
+ try {
156
+ files = unzipSync(bytes);
157
+ } catch (error) {
158
+ throw new ToolError(error instanceof Error ? error.message : String(error));
159
+ }
160
+
161
+ const entries: ArchiveIndexEntry[] = [];
162
+ for (const [rawPath, fileBytes] of Object.entries(files)) {
163
+ const normalizedPath = normalizeArchiveEntryPath(rawPath);
164
+ if (!normalizedPath) continue;
165
+ const isDirectory = isArchiveDirectoryName(rawPath);
166
+ entries.push({
167
+ path: normalizedPath,
168
+ isDirectory,
169
+ size: isDirectory ? 0 : fileBytes.byteLength,
170
+ storage: isDirectory ? undefined : { type: "zip", bytes: fileBytes },
171
+ });
172
+ }
173
+
174
+ return entries;
175
+ }
176
+
177
+ export function parseArchivePathCandidates(filePath: string): ArchivePathCandidate[] {
178
+ const normalized = filePath.replace(/\\/g, "/");
179
+ const pattern = /\.(?:tar\.gz|tgz|zip|tar)(?=(?::|$))/gi;
180
+ const seen = new Set<string>();
181
+ const candidates: ArchivePathCandidate[] = [];
182
+
183
+ let match: RegExpExecArray | null;
184
+ while (true) {
185
+ match = pattern.exec(normalized);
186
+ if (match === null) {
187
+ break;
188
+ }
189
+ const end = match.index + match[0].length;
190
+ const archivePath = filePath.slice(0, end);
191
+ const subPath = normalized.slice(end).replace(/^:+/, "");
192
+ const key = `${archivePath}\0${subPath}`;
193
+ if (seen.has(key)) continue;
194
+ seen.add(key);
195
+ candidates.push({ archivePath, subPath });
196
+ }
197
+
198
+ return candidates.sort((left, right) => right.archivePath.length - left.archivePath.length);
199
+ }
200
+
201
+ export class ArchiveReader {
202
+ readonly format: ArchiveFormat;
203
+ #entries = new Map<string, ArchiveIndexEntry>();
204
+
205
+ constructor(format: ArchiveFormat, entries: ArchiveIndexEntry[]) {
206
+ this.format = format;
207
+ for (const entry of entries) {
208
+ upsertArchiveEntry(this.#entries, entry);
209
+ }
210
+ ensureParentDirectories(this.#entries);
211
+ }
212
+
213
+ getNode(subPath?: string): ArchiveNode | undefined {
214
+ const normalizedPath = normalizeArchiveLookupPath(subPath);
215
+ if (normalizedPath === undefined) return undefined;
216
+ if (normalizedPath === "") {
217
+ return { path: "", isDirectory: true, size: 0 };
218
+ }
219
+
220
+ const entry = this.#entries.get(normalizedPath);
221
+ if (!entry) return undefined;
222
+ return {
223
+ path: entry.path,
224
+ isDirectory: entry.isDirectory,
225
+ size: entry.size,
226
+ mtimeMs: entry.mtimeMs,
227
+ };
228
+ }
229
+
230
+ listDirectory(subPath?: string): ArchiveDirectoryEntry[] {
231
+ const normalizedPath = normalizeArchiveLookupPath(subPath);
232
+ if (normalizedPath === undefined) {
233
+ throw new ToolError("Archive path cannot contain '..'");
234
+ }
235
+
236
+ if (normalizedPath) {
237
+ const entry = this.#entries.get(normalizedPath);
238
+ if (!entry) {
239
+ throw new ToolError(`Archive path '${normalizedPath}' not found`);
240
+ }
241
+ if (!entry.isDirectory) {
242
+ throw new ToolError(`Archive path '${normalizedPath}' is not a directory`);
243
+ }
244
+ }
245
+
246
+ const prefix = normalizedPath ? `${normalizedPath}/` : "";
247
+ const children = new Map<string, ArchiveDirectoryEntry>();
248
+
249
+ for (const entry of this.#entries.values()) {
250
+ if (normalizedPath) {
251
+ if (!entry.path.startsWith(prefix) || entry.path === normalizedPath) continue;
252
+ }
253
+
254
+ const relativePath = normalizedPath ? entry.path.slice(prefix.length) : entry.path;
255
+ const nextSegment = relativePath.split("/")[0];
256
+ if (!nextSegment) continue;
257
+
258
+ const childPath = normalizedPath ? `${normalizedPath}/${nextSegment}` : nextSegment;
259
+ if (children.has(childPath)) continue;
260
+
261
+ const childEntry = this.#entries.get(childPath);
262
+ const isDirectory = childEntry?.isDirectory ?? relativePath.includes("/");
263
+ children.set(childPath, {
264
+ name: nextSegment,
265
+ path: childPath,
266
+ isDirectory,
267
+ size: isDirectory ? 0 : (childEntry?.size ?? entry.size),
268
+ mtimeMs: childEntry?.mtimeMs ?? entry.mtimeMs,
269
+ });
270
+ }
271
+
272
+ return [...children.values()].sort((left, right) =>
273
+ left.name.toLowerCase().localeCompare(right.name.toLowerCase()),
274
+ );
275
+ }
276
+
277
+ async readFile(subPath: string): Promise<ExtractedArchiveFile> {
278
+ const normalizedPath = normalizeArchiveLookupPath(subPath);
279
+ if (!normalizedPath) {
280
+ throw new ToolError("Archive file path is required");
281
+ }
282
+
283
+ const entry = this.#entries.get(normalizedPath);
284
+ if (!entry) {
285
+ throw new ToolError(`Archive file '${normalizedPath}' not found`);
286
+ }
287
+ if (entry.isDirectory) {
288
+ throw new ToolError(`Archive path '${normalizedPath}' is a directory`);
289
+ }
290
+ if (!entry.storage) {
291
+ throw new ToolError(`Archive file '${normalizedPath}' has no readable storage`);
292
+ }
293
+
294
+ const bytes = entry.storage.type === "tar" ? await entry.storage.file.bytes() : entry.storage.bytes;
295
+
296
+ return {
297
+ path: entry.path,
298
+ isDirectory: false,
299
+ size: entry.size,
300
+ mtimeMs: entry.mtimeMs,
301
+ bytes,
302
+ };
303
+ }
304
+ }
305
+
306
+ export async function openArchive(filePath: string): Promise<ArchiveReader> {
307
+ const format = getArchiveFormatFromPath(filePath);
308
+ if (!format) {
309
+ throw new ToolError(`Unsupported archive format: ${filePath}`);
310
+ }
311
+
312
+ const bytes = await Bun.file(filePath).bytes();
313
+ const entries = format === "zip" ? readZipEntries(bytes) : await readTarEntries(bytes);
314
+ return new ArchiveReader(format, entries);
315
+ }
@@ -20,7 +20,7 @@ import { extractWithParallel, findParallelApiKey, getParallelExtractContent } fr
20
20
  import { specialHandlers } from "../web/scrapers";
21
21
  import type { RenderResult } from "../web/scrapers/types";
22
22
  import { finalizeOutput, loadPage, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
23
- import { convertWithMarkitdown, fetchBinary } from "../web/scrapers/utils";
23
+ import { convertWithMarkit, fetchBinary } from "../web/scrapers/utils";
24
24
  import type { ToolSession } from ".";
25
25
  import { applyListLimit } from "./list-limit";
26
26
  import { formatStyledArtifactReference, type OutputMeta } from "./output-meta";
@@ -34,7 +34,7 @@ import { clampTimeout } from "./tool-timeouts";
34
34
  // =============================================================================
35
35
 
36
36
  const FETCH_DEFAULT_MAX_LINES = 300;
37
- // Convertible document types (markitdown supported)
37
+ // Convertible document types handled by markit.
38
38
  const CONVERTIBLE_MIMES = new Set([
39
39
  "application/pdf",
40
40
  "application/msword",
@@ -45,6 +45,7 @@ const CONVERTIBLE_MIMES = new Set([
45
45
  "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
46
46
  "application/rtf",
47
47
  "application/epub+zip",
48
+ "application/x-ipynb+json",
48
49
  "application/zip",
49
50
  "image/png",
50
51
  "image/jpeg",
@@ -65,6 +66,7 @@ const CONVERTIBLE_EXTENSIONS = new Set([
65
66
  ".xlsx",
66
67
  ".rtf",
67
68
  ".epub",
69
+ ".ipynb",
68
70
  ".png",
69
71
  ".jpg",
70
72
  ".jpeg",
@@ -164,7 +166,7 @@ function getExtensionHint(url: string, contentDisposition?: string): string {
164
166
  }
165
167
 
166
168
  /**
167
- * Check if content type is convertible via markitdown
169
+ * Check if content type is convertible via markit.
168
170
  */
169
171
  function isConvertible(mime: string, extensionHint: string): boolean {
170
172
  if (CONVERTIBLE_MIMES.has(mime)) return true;
@@ -711,18 +713,18 @@ async function renderUrl(
711
713
  notes.push("Fetched image binary");
712
714
  const conversionExtension = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
713
715
  let convertedText: string | null = null;
714
- const converted = await convertWithMarkitdown(binary.buffer, conversionExtension, timeout, signal);
716
+ const converted = await convertWithMarkit(binary.buffer, conversionExtension, timeout, signal);
715
717
  if (converted.ok) {
716
718
  if (converted.content.trim().length > 50) {
717
- notes.push("Converted with markitdown");
719
+ notes.push("Converted with markit");
718
720
  convertedText = converted.content;
719
721
  } else {
720
- notes.push("markitdown conversion produced no usable output");
722
+ notes.push("markit conversion produced no usable output");
721
723
  }
722
724
  } else if (converted.error) {
723
- notes.push(`markitdown conversion failed: ${converted.error}`);
725
+ notes.push(`markit conversion failed: ${converted.error}`);
724
726
  } else {
725
- notes.push("markitdown conversion failed");
727
+ notes.push("markit conversion failed");
726
728
  }
727
729
 
728
730
  if (binary.buffer.byteLength > MAX_INLINE_IMAGE_SOURCE_BYTES) {
@@ -736,7 +738,7 @@ async function renderUrl(
736
738
  url,
737
739
  finalUrl,
738
740
  contentType: imageMimeType,
739
- method: convertedText ? "markitdown" : "image-too-large",
741
+ method: convertedText ? "markit" : "image-too-large",
740
742
  content: output.content,
741
743
  fetchedAt,
742
744
  truncated: output.truncated,
@@ -761,7 +763,7 @@ async function renderUrl(
761
763
  url,
762
764
  finalUrl,
763
765
  contentType: imageMimeType,
764
- method: convertedText ? "markitdown" : "image-invalid",
766
+ method: convertedText ? "markit" : "image-invalid",
765
767
  content: output.content,
766
768
  fetchedAt,
767
769
  truncated: output.truncated,
@@ -779,7 +781,7 @@ async function renderUrl(
779
781
  url,
780
782
  finalUrl,
781
783
  contentType: imageMimeType,
782
- method: convertedText ? "markitdown" : "image-too-large",
784
+ method: convertedText ? "markit" : "image-too-large",
783
785
  content: output.content,
784
786
  fetchedAt,
785
787
  truncated: output.truncated,
@@ -819,27 +821,27 @@ async function renderUrl(
819
821
  const binary = await fetchBinary(finalUrl, timeout, signal);
820
822
  if (binary.ok) {
821
823
  const ext = getExtensionHint(finalUrl, binary.contentDisposition) || extHint;
822
- const converted = await convertWithMarkitdown(binary.buffer, ext, timeout, signal);
824
+ const converted = await convertWithMarkit(binary.buffer, ext, timeout, signal);
823
825
  if (converted.ok) {
824
826
  if (converted.content.trim().length > 50) {
825
- notes.push("Converted with markitdown");
827
+ notes.push("Converted with markit");
826
828
  const output = finalizeOutput(converted.content);
827
829
  return {
828
830
  url,
829
831
  finalUrl,
830
832
  contentType: mime,
831
- method: "markitdown",
833
+ method: "markit",
832
834
  content: output.content,
833
835
  fetchedAt,
834
836
  truncated: output.truncated,
835
837
  notes,
836
838
  };
837
839
  }
838
- notes.push("markitdown conversion produced no usable output");
840
+ notes.push("markit conversion produced no usable output");
839
841
  } else if (converted.error) {
840
- notes.push(`markitdown conversion failed: ${converted.error}`);
842
+ notes.push(`markit conversion failed: ${converted.error}`);
841
843
  } else {
842
- notes.push("markitdown conversion failed");
844
+ notes.push("markit conversion failed");
843
845
  }
844
846
  } else if (binary.error) {
845
847
  notes.push(`Binary fetch failed: ${binary.error}`);
@@ -1007,7 +1009,7 @@ async function renderUrl(
1007
1009
  const binary = await fetchBinary(docUrl, timeout, signal);
1008
1010
  if (binary.ok) {
1009
1011
  const ext = getExtensionHint(docUrl, binary.contentDisposition);
1010
- const converted = await convertWithMarkitdown(binary.buffer, ext, timeout, signal);
1012
+ const converted = await convertWithMarkit(binary.buffer, ext, timeout, signal);
1011
1013
  if (converted.ok && converted.content.trim().length > htmlResult.content.length) {
1012
1014
  notes.push(`Extracted and converted document: ${docUrl}`);
1013
1015
  const output = finalizeOutput(converted.content);
@@ -1023,7 +1025,7 @@ async function renderUrl(
1023
1025
  };
1024
1026
  }
1025
1027
  if (!converted.ok && converted.error) {
1026
- notes.push(`markitdown conversion failed: ${converted.error}`);
1028
+ notes.push(`markit conversion failed: ${converted.error}`);
1027
1029
  }
1028
1030
  } else if (binary.error) {
1029
1031
  notes.push(`Binary fetch failed: ${binary.error}`);
@@ -0,0 +1,125 @@
1
+ import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
2
+
3
+ export interface GhCommandResult {
4
+ exitCode: number;
5
+ stdout: string;
6
+ stderr: string;
7
+ }
8
+
9
+ export interface GhCommandOptions {
10
+ repoProvided?: boolean;
11
+ trimOutput?: boolean;
12
+ }
13
+
14
+ export function isGhAvailable(): boolean {
15
+ return Boolean(Bun.which("gh"));
16
+ }
17
+
18
+ function formatGhFailure(args: string[], stdout: string, stderr: string, options?: GhCommandOptions): string {
19
+ const output = stderr || stdout;
20
+ const message = output.trim();
21
+
22
+ if (message.includes("gh auth login") || message.includes("not logged into any GitHub hosts")) {
23
+ return "GitHub CLI is not authenticated. Run `gh auth login`.";
24
+ }
25
+
26
+ if (
27
+ !options?.repoProvided &&
28
+ (message.includes("not a git repository") ||
29
+ message.includes("no git remotes found") ||
30
+ message.includes("unable to determine current repository"))
31
+ ) {
32
+ return "GitHub repository context is unavailable. Pass `repo` explicitly or run the tool inside a GitHub checkout.";
33
+ }
34
+
35
+ if (message.length > 0) {
36
+ return message;
37
+ }
38
+
39
+ return `GitHub CLI command failed: gh ${args.join(" ")}`;
40
+ }
41
+
42
+ export async function runGhCommand(
43
+ cwd: string,
44
+ args: string[],
45
+ signal?: AbortSignal,
46
+ options?: GhCommandOptions,
47
+ ): Promise<GhCommandResult> {
48
+ throwIfAborted(signal);
49
+
50
+ if (!isGhAvailable()) {
51
+ throw new ToolError("GitHub CLI (gh) is not installed. Install it from https://cli.github.com/.");
52
+ }
53
+
54
+ try {
55
+ const child = Bun.spawn(["gh", ...args], {
56
+ cwd,
57
+ stdin: "ignore",
58
+ stdout: "pipe",
59
+ stderr: "pipe",
60
+ windowsHide: true,
61
+ signal,
62
+ });
63
+
64
+ if (!child.stdout || !child.stderr) {
65
+ throw new ToolError("Failed to capture GitHub CLI output.");
66
+ }
67
+
68
+ const [stdout, stderr, exitCode] = await Promise.all([
69
+ new Response(child.stdout).text(),
70
+ new Response(child.stderr).text(),
71
+ child.exited,
72
+ ]);
73
+
74
+ throwIfAborted(signal);
75
+
76
+ return {
77
+ exitCode: exitCode ?? 0,
78
+ stdout: options?.trimOutput === false ? stdout : stdout.trim(),
79
+ stderr: options?.trimOutput === false ? stderr : stderr.trim(),
80
+ };
81
+ } catch (error) {
82
+ if (signal?.aborted) {
83
+ throw new ToolAbortError();
84
+ }
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ export async function runGhJson<T>(
90
+ cwd: string,
91
+ args: string[],
92
+ signal?: AbortSignal,
93
+ options?: GhCommandOptions,
94
+ ): Promise<T> {
95
+ const result = await runGhCommand(cwd, args, signal, options);
96
+
97
+ if (result.exitCode !== 0) {
98
+ throw new ToolError(formatGhFailure(args, result.stdout, result.stderr, options));
99
+ }
100
+
101
+ if (!result.stdout) {
102
+ throw new ToolError("GitHub CLI returned empty output.");
103
+ }
104
+
105
+ try {
106
+ return JSON.parse(result.stdout) as T;
107
+ } catch {
108
+ throw new ToolError("GitHub CLI returned invalid JSON output.");
109
+ }
110
+ }
111
+
112
+ export async function runGhText(
113
+ cwd: string,
114
+ args: string[],
115
+ signal?: AbortSignal,
116
+ options?: GhCommandOptions,
117
+ ): Promise<string> {
118
+ const result = await runGhCommand(cwd, args, signal, options);
119
+
120
+ if (result.exitCode !== 0) {
121
+ throw new ToolError(formatGhFailure(args, result.stdout, result.stderr, options));
122
+ }
123
+
124
+ return result.stdout;
125
+ }