@oh-my-pi/pi-coding-agent 8.12.1 → 8.12.4
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 +5 -0
- package/package.json +7 -10
- package/src/config/settings-manager.ts +36 -0
- package/src/config.ts +0 -5
- package/src/exec/bash-executor.ts +4 -4
- package/src/exec/exec.ts +9 -12
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/plugins/doctor.ts +0 -2
- package/src/ipy/kernel.ts +11 -13
- package/src/migrations.ts +1 -46
- package/src/modes/components/hook-selector.ts +55 -7
- package/src/modes/components/settings-defs.ts +23 -0
- package/src/modes/controllers/extension-ui-controller.ts +32 -16
- package/src/modes/rpc/rpc-client.ts +4 -4
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/session/compaction/compaction.ts +37 -3
- package/src/ssh/ssh-executor.ts +3 -5
- package/src/system-prompt.ts +25 -98
- package/src/tools/ask.ts +55 -8
- package/src/tools/fetch.ts +9 -61
- package/src/tools/find.ts +28 -49
- package/src/tools/grep.ts +110 -550
- package/src/tools/read.ts +41 -102
- package/src/utils/image-convert.ts +7 -11
- package/src/utils/image-resize.ts +15 -25
- package/src/utils/tools-manager.ts +3 -43
- package/src/web/scrapers/utils.ts +11 -6
- package/src/web/scrapers/youtube.ts +21 -49
- package/src/utils/utils.ts +0 -1
- package/src/vendor/photon/LICENSE.md +0 -201
- package/src/vendor/photon/README.md +0 -158
- package/src/vendor/photon/index.d.ts +0 -3013
- package/src/vendor/photon/index.js +0 -4521
- package/src/vendor/photon/photon_rs_bg.wasm +0 -0
- package/src/vendor/photon/photon_rs_bg.wasm.d.ts +0 -193
package/src/tools/read.ts
CHANGED
|
@@ -2,9 +2,10 @@ import * as os from "node:os";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
4
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
5
|
+
import { find as wasmFind } from "@oh-my-pi/pi-natives";
|
|
5
6
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
6
7
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
|
-
import { ptree } from "@oh-my-pi/pi-utils";
|
|
8
|
+
import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
|
|
8
9
|
import { Type } from "@sinclair/typebox";
|
|
9
10
|
import { CONFIG_DIR_NAME } from "../config";
|
|
10
11
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
@@ -16,7 +17,6 @@ import { renderCodeCell, renderOutputBlock, renderStatusLine } from "../tui";
|
|
|
16
17
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
17
18
|
import { detectSupportedImageMimeTypeFromFile } from "../utils/mime";
|
|
18
19
|
import { ensureTool } from "../utils/tools-manager";
|
|
19
|
-
import { runRg } from "./grep";
|
|
20
20
|
import { applyListLimit } from "./list-limit";
|
|
21
21
|
import { LsTool } from "./ls";
|
|
22
22
|
import type { OutputMeta } from "./output-meta";
|
|
@@ -49,6 +49,7 @@ const MAX_FUZZY_RESULTS = 5;
|
|
|
49
49
|
const MAX_FUZZY_CANDIDATES = 20000;
|
|
50
50
|
const MIN_BASE_SIMILARITY = 0.5;
|
|
51
51
|
const MIN_FULL_SIMILARITY = 0.6;
|
|
52
|
+
const GLOB_TIMEOUT_MS = 5000;
|
|
52
53
|
|
|
53
54
|
function normalizePathForMatch(value: string): string {
|
|
54
55
|
return value
|
|
@@ -162,95 +163,36 @@ function similarityScore(a: string, b: string): number {
|
|
|
162
163
|
async function listCandidateFiles(
|
|
163
164
|
searchRoot: string,
|
|
164
165
|
signal?: AbortSignal,
|
|
165
|
-
|
|
166
|
+
_notify?: (message: string) => void,
|
|
166
167
|
): Promise<{ files: string[]; truncated: boolean; error?: string }> {
|
|
167
|
-
let
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
} catch {
|
|
171
|
-
return { files: [], truncated: false, error: "rg not available" };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (!rgPath) {
|
|
175
|
-
return { files: [], truncated: false, error: "rg not available" };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const args: string[] = [
|
|
179
|
-
"--files",
|
|
180
|
-
"--color=never",
|
|
181
|
-
"--hidden",
|
|
182
|
-
"--glob",
|
|
183
|
-
"!**/.git/**",
|
|
184
|
-
"--glob",
|
|
185
|
-
"!**/node_modules/**",
|
|
186
|
-
];
|
|
187
|
-
|
|
188
|
-
const gitignoreFiles = new Set<string>();
|
|
189
|
-
const rootGitignore = path.join(searchRoot, ".gitignore");
|
|
190
|
-
if (await Bun.file(rootGitignore).exists()) {
|
|
191
|
-
gitignoreFiles.add(rootGitignore);
|
|
192
|
-
}
|
|
193
|
-
|
|
168
|
+
let files: string[];
|
|
169
|
+
const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
|
|
170
|
+
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
194
171
|
try {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
"--glob",
|
|
205
|
-
".gitignore",
|
|
206
|
-
searchRoot,
|
|
207
|
-
];
|
|
208
|
-
const { stdout } = await runRg(rgPath, gitignoreArgs, signal);
|
|
209
|
-
const output = stdout.trim();
|
|
210
|
-
if (output) {
|
|
211
|
-
const nestedGitignores = output
|
|
212
|
-
.split("\n")
|
|
213
|
-
.map(line => line.replace(/\r$/, "").trim())
|
|
214
|
-
.filter(line => line.length > 0);
|
|
215
|
-
for (const file of nestedGitignores) {
|
|
216
|
-
const normalized = file.replace(/\\/g, "/");
|
|
217
|
-
if (normalized.includes("/node_modules/") || normalized.includes("/.git/")) {
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
gitignoreFiles.add(file);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
172
|
+
const result = await untilAborted(combinedSignal, () =>
|
|
173
|
+
wasmFind({
|
|
174
|
+
pattern: "**/*",
|
|
175
|
+
path: searchRoot,
|
|
176
|
+
fileType: "file",
|
|
177
|
+
hidden: true,
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
files = result.matches.map(match => match.path);
|
|
223
181
|
} catch (error) {
|
|
224
|
-
if (error instanceof
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
for (const gitignorePath of gitignoreFiles) {
|
|
231
|
-
args.push("--ignore-file", gitignorePath);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
args.push(searchRoot);
|
|
235
|
-
|
|
236
|
-
const { stdout, stderr, exitCode } = await runRg(rgPath, args, signal);
|
|
237
|
-
const output = stdout.trim();
|
|
238
|
-
|
|
239
|
-
if (!output) {
|
|
240
|
-
// rg exit codes: 0 = ok, 1 = no matches, other = error
|
|
241
|
-
if (exitCode !== 0 && exitCode !== 1) {
|
|
242
|
-
return { files: [], truncated: false, error: stderr.trim() || `rg failed (exit ${exitCode})` };
|
|
182
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
183
|
+
if (timeoutSignal.aborted && !signal?.aborted) {
|
|
184
|
+
const timeoutSeconds = Math.max(1, Math.round(GLOB_TIMEOUT_MS / 1000));
|
|
185
|
+
return { files: [], truncated: false, error: `find timed out after ${timeoutSeconds}s` };
|
|
186
|
+
}
|
|
187
|
+
throw new ToolAbortError();
|
|
243
188
|
}
|
|
244
|
-
|
|
189
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
190
|
+
return { files: [], truncated: false, error: message };
|
|
245
191
|
}
|
|
246
192
|
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
.filter(line => line.length > 0);
|
|
251
|
-
|
|
252
|
-
const truncated = files.length > MAX_FUZZY_CANDIDATES;
|
|
253
|
-
const limited = truncated ? files.slice(0, MAX_FUZZY_CANDIDATES) : files;
|
|
193
|
+
const normalizedFiles = files.filter(line => line.length > 0);
|
|
194
|
+
const truncated = normalizedFiles.length > MAX_FUZZY_CANDIDATES;
|
|
195
|
+
const limited = truncated ? normalizedFiles.slice(0, MAX_FUZZY_CANDIDATES) : normalizedFiles;
|
|
254
196
|
|
|
255
197
|
return { files: limited, truncated };
|
|
256
198
|
}
|
|
@@ -304,11 +246,7 @@ async function findReadPathSuggestions(
|
|
|
304
246
|
const cleaned = file.replace(/\r$/, "").trim();
|
|
305
247
|
if (!cleaned) continue;
|
|
306
248
|
|
|
307
|
-
const relativePath =
|
|
308
|
-
? cleaned.startsWith(searchRoot)
|
|
309
|
-
? cleaned.slice(searchRoot.length + 1)
|
|
310
|
-
: path.relative(searchRoot, cleaned)
|
|
311
|
-
: cleaned;
|
|
249
|
+
const relativePath = cleaned;
|
|
312
250
|
|
|
313
251
|
if (!relativePath || relativePath.startsWith("..")) {
|
|
314
252
|
continue;
|
|
@@ -363,22 +301,23 @@ async function convertWithMarkitdown(
|
|
|
363
301
|
return { content: "", ok: false, error: "markitdown not found (uv/pip unavailable)" };
|
|
364
302
|
}
|
|
365
303
|
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
304
|
+
const result = await ptree.exec([cmd, filePath], {
|
|
305
|
+
signal,
|
|
306
|
+
allowNonZero: true,
|
|
307
|
+
allowAbort: true,
|
|
308
|
+
stderr: "buffer",
|
|
309
|
+
detached: true,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (result.exitError?.aborted) {
|
|
313
|
+
throw new ToolAbortError();
|
|
375
314
|
}
|
|
376
315
|
|
|
377
|
-
if (
|
|
378
|
-
return { content: stdout, ok: true };
|
|
316
|
+
if (result.exitCode === 0 && result.stdout.length > 0) {
|
|
317
|
+
return { content: result.stdout, ok: true };
|
|
379
318
|
}
|
|
380
319
|
|
|
381
|
-
return { content: "", ok: false, error:
|
|
320
|
+
return { content: "", ok: false, error: result.stderr.trim() || "Conversion failed" };
|
|
382
321
|
}
|
|
383
322
|
|
|
384
323
|
const readSchema = Type.Object({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { PhotonImage } from "@oh-my-pi/pi-natives";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Convert image to PNG format for terminal display.
|
|
@@ -14,16 +14,12 @@ export async function convertToPng(
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
try {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
};
|
|
24
|
-
} finally {
|
|
25
|
-
image.free();
|
|
26
|
-
}
|
|
17
|
+
using image = await PhotonImage.new_from_byteslice(new Uint8Array(Buffer.from(base64Data, "base64")));
|
|
18
|
+
const pngBuffer = await image.get_bytes();
|
|
19
|
+
return {
|
|
20
|
+
data: Buffer.from(pngBuffer).toString("base64"),
|
|
21
|
+
mimeType: "image/png",
|
|
22
|
+
};
|
|
27
23
|
} catch {
|
|
28
24
|
// Conversion failed
|
|
29
25
|
return null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import
|
|
2
|
+
import { PhotonImage, resize, SamplingFilter } from "@oh-my-pi/pi-natives";
|
|
3
3
|
|
|
4
4
|
export interface ImageResizeOptions {
|
|
5
5
|
maxWidth?: number; // Default: 2000
|
|
@@ -53,9 +53,8 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
53
53
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
54
54
|
const inputBuffer = Buffer.from(img.data, "base64");
|
|
55
55
|
|
|
56
|
-
let image: ReturnType<typeof photon.PhotonImage.new_from_byteslice> | undefined;
|
|
57
56
|
try {
|
|
58
|
-
image =
|
|
57
|
+
using image = await PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer));
|
|
59
58
|
|
|
60
59
|
const originalWidth = image.get_width();
|
|
61
60
|
const originalHeight = image.get_height();
|
|
@@ -89,24 +88,19 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
// Helper to resize and encode in both formats, returning the smaller one
|
|
92
|
-
function tryBothFormats(
|
|
91
|
+
async function tryBothFormats(
|
|
93
92
|
width: number,
|
|
94
93
|
height: number,
|
|
95
94
|
jpegQuality: number,
|
|
96
|
-
): { buffer: Uint8Array; mimeType: string } {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
|
106
|
-
);
|
|
107
|
-
} finally {
|
|
108
|
-
resized.free();
|
|
109
|
-
}
|
|
95
|
+
): Promise<{ buffer: Uint8Array; mimeType: string }> {
|
|
96
|
+
using resized = await resize(image!, width, height, SamplingFilter.Lanczos3);
|
|
97
|
+
|
|
98
|
+
const [pngBuffer, jpegBuffer] = await Promise.all([resized.get_bytes(), resized.get_bytes_jpeg(jpegQuality)]);
|
|
99
|
+
|
|
100
|
+
return pickSmaller(
|
|
101
|
+
{ buffer: pngBuffer, mimeType: "image/png" },
|
|
102
|
+
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
|
103
|
+
);
|
|
110
104
|
}
|
|
111
105
|
|
|
112
106
|
// Try to produce an image under maxBytes
|
|
@@ -118,7 +112,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
118
112
|
let finalHeight = targetHeight;
|
|
119
113
|
|
|
120
114
|
// First attempt: resize to target dimensions, try both formats
|
|
121
|
-
best = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
|
115
|
+
best = await tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
|
122
116
|
|
|
123
117
|
if (best.buffer.length <= opts.maxBytes) {
|
|
124
118
|
return {
|
|
@@ -134,7 +128,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
134
128
|
|
|
135
129
|
// Still too large - try JPEG with decreasing quality
|
|
136
130
|
for (const quality of qualitySteps) {
|
|
137
|
-
best = tryBothFormats(targetWidth, targetHeight, quality);
|
|
131
|
+
best = await tryBothFormats(targetWidth, targetHeight, quality);
|
|
138
132
|
|
|
139
133
|
if (best.buffer.length <= opts.maxBytes) {
|
|
140
134
|
return {
|
|
@@ -159,7 +153,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
159
153
|
}
|
|
160
154
|
|
|
161
155
|
for (const quality of qualitySteps) {
|
|
162
|
-
best = tryBothFormats(finalWidth, finalHeight, quality);
|
|
156
|
+
best = await tryBothFormats(finalWidth, finalHeight, quality);
|
|
163
157
|
|
|
164
158
|
if (best.buffer.length <= opts.maxBytes) {
|
|
165
159
|
return {
|
|
@@ -196,10 +190,6 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
196
190
|
height: 0,
|
|
197
191
|
wasResized: false,
|
|
198
192
|
};
|
|
199
|
-
} finally {
|
|
200
|
-
if (image) {
|
|
201
|
-
image.free();
|
|
202
|
-
}
|
|
203
193
|
}
|
|
204
194
|
}
|
|
205
195
|
|
|
@@ -3,9 +3,9 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { logger, TempDir } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { $ } from "bun";
|
|
6
|
-
import { APP_NAME,
|
|
6
|
+
import { APP_NAME, getToolsDir } from "../config";
|
|
7
7
|
|
|
8
|
-
const TOOLS_DIR =
|
|
8
|
+
const TOOLS_DIR = getToolsDir();
|
|
9
9
|
const TOOL_DOWNLOAD_TIMEOUT_MS = 15000;
|
|
10
10
|
|
|
11
11
|
interface ToolConfig {
|
|
@@ -18,46 +18,6 @@ interface ToolConfig {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const TOOLS: Record<string, ToolConfig> = {
|
|
21
|
-
fd: {
|
|
22
|
-
name: "fd",
|
|
23
|
-
repo: "sharkdp/fd",
|
|
24
|
-
binaryName: "fd",
|
|
25
|
-
tagPrefix: "v",
|
|
26
|
-
getAssetName: (version, plat, architecture) => {
|
|
27
|
-
if (plat === "darwin") {
|
|
28
|
-
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
29
|
-
return `fd-v${version}-${archStr}-apple-darwin.tar.gz`;
|
|
30
|
-
} else if (plat === "linux") {
|
|
31
|
-
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
32
|
-
return `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`;
|
|
33
|
-
} else if (plat === "win32") {
|
|
34
|
-
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
35
|
-
return `fd-v${version}-${archStr}-pc-windows-msvc.zip`;
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
rg: {
|
|
41
|
-
name: "ripgrep",
|
|
42
|
-
repo: "BurntSushi/ripgrep",
|
|
43
|
-
binaryName: "rg",
|
|
44
|
-
tagPrefix: "",
|
|
45
|
-
getAssetName: (version, plat, architecture) => {
|
|
46
|
-
if (plat === "darwin") {
|
|
47
|
-
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
48
|
-
return `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`;
|
|
49
|
-
} else if (plat === "linux") {
|
|
50
|
-
if (architecture === "arm64") {
|
|
51
|
-
return `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`;
|
|
52
|
-
}
|
|
53
|
-
return `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`;
|
|
54
|
-
} else if (plat === "win32") {
|
|
55
|
-
const archStr = architecture === "arm64" ? "aarch64" : "x86_64";
|
|
56
|
-
return `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`;
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
21
|
sd: {
|
|
62
22
|
name: "sd",
|
|
63
23
|
repo: "chmln/sd",
|
|
@@ -135,7 +95,7 @@ const PYTHON_TOOLS: Record<string, PythonToolConfig> = {
|
|
|
135
95
|
},
|
|
136
96
|
};
|
|
137
97
|
|
|
138
|
-
export type ToolName = "
|
|
98
|
+
export type ToolName = "sd" | "sg" | "yt-dlp" | "markitdown" | "html2text";
|
|
139
99
|
|
|
140
100
|
// Get the path to a tool (system-wide or in our tools dir)
|
|
141
101
|
export async function getToolPath(tool: ToolName): Promise<string | null> {
|
|
@@ -49,16 +49,21 @@ export async function convertWithMarkitdown(
|
|
|
49
49
|
|
|
50
50
|
try {
|
|
51
51
|
await Bun.write(tmpFile, content);
|
|
52
|
-
const result = await ptree.
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const result = await ptree.exec([markitdown, tmpFile], {
|
|
53
|
+
timeout,
|
|
54
|
+
allowNonZero: true,
|
|
55
|
+
stderr: "full",
|
|
56
|
+
detached: true,
|
|
57
|
+
});
|
|
58
|
+
if (!result.ok) {
|
|
55
59
|
return {
|
|
56
|
-
content: stdout,
|
|
60
|
+
content: result.stdout,
|
|
57
61
|
ok: false,
|
|
58
|
-
error:
|
|
62
|
+
error:
|
|
63
|
+
result.stderr.length > 0 ? result.stderr : `markitdown failed (exit ${result.exitCode ?? "unknown"})`,
|
|
59
64
|
};
|
|
60
65
|
}
|
|
61
|
-
return { content: stdout, ok: true };
|
|
66
|
+
return { content: result.stdout, ok: true };
|
|
62
67
|
} finally {
|
|
63
68
|
try {
|
|
64
69
|
await fs.rm(tmpFile, { force: true });
|
|
@@ -1,41 +1,13 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { ptree } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { nanoid } from "nanoid";
|
|
6
6
|
import { throwIfAborted } from "../../tools/tool-errors";
|
|
7
7
|
import { ensureTool } from "../../utils/tools-manager";
|
|
8
8
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
9
9
|
import { finalizeOutput } from "./types";
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Execute a command and return stdout
|
|
13
|
-
*/
|
|
14
|
-
async function exec(
|
|
15
|
-
cmd: string,
|
|
16
|
-
args: string[],
|
|
17
|
-
options?: { timeout?: number; input?: string | Buffer; signal?: AbortSignal },
|
|
18
|
-
): Promise<{ stdout: string; stderr: string; ok: boolean; exitCode: number | null }> {
|
|
19
|
-
const proc = cspawn([cmd, ...args], {
|
|
20
|
-
signal: options?.signal,
|
|
21
|
-
timeout: options?.timeout,
|
|
22
|
-
stdin: options?.input ? Buffer.from(options.input) : undefined,
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
const [stdout, stderr, exitResult] = await Promise.all([
|
|
26
|
-
proc.stdout.text(),
|
|
27
|
-
proc.stderr.text(),
|
|
28
|
-
proc.exited.then(() => proc.exitCode ?? 0),
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
stdout,
|
|
33
|
-
stderr,
|
|
34
|
-
ok: exitResult === 0,
|
|
35
|
-
exitCode: exitResult,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
11
|
interface YouTubeUrl {
|
|
40
12
|
videoId: string;
|
|
41
13
|
playlistId?: string;
|
|
@@ -163,16 +135,20 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
163
135
|
const fetchedAt = new Date().toISOString();
|
|
164
136
|
const notes: string[] = [];
|
|
165
137
|
const videoUrl = `https://www.youtube.com/watch?v=${yt.videoId}`;
|
|
138
|
+
const execOptions = {
|
|
139
|
+
mode: "group" as const,
|
|
140
|
+
signal,
|
|
141
|
+
timeout: timeout * 1000,
|
|
142
|
+
allowNonZero: true,
|
|
143
|
+
allowAbort: true,
|
|
144
|
+
stderr: "full" as const,
|
|
145
|
+
};
|
|
166
146
|
|
|
167
147
|
// Fetch video metadata
|
|
168
148
|
throwIfAborted(signal);
|
|
169
|
-
const metaResult = await exec(
|
|
170
|
-
ytdlp,
|
|
171
|
-
|
|
172
|
-
{
|
|
173
|
-
timeout: timeout * 1000,
|
|
174
|
-
signal,
|
|
175
|
-
},
|
|
149
|
+
const metaResult = await ptree.exec(
|
|
150
|
+
[ytdlp, "--dump-json", "--no-warnings", "--no-playlist", "--skip-download", videoUrl],
|
|
151
|
+
execOptions,
|
|
176
152
|
);
|
|
177
153
|
throwIfAborted(signal);
|
|
178
154
|
|
|
@@ -215,13 +191,9 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
215
191
|
|
|
216
192
|
// First, list available subtitles
|
|
217
193
|
throwIfAborted(signal);
|
|
218
|
-
const listResult = await exec(
|
|
219
|
-
ytdlp,
|
|
220
|
-
|
|
221
|
-
{
|
|
222
|
-
timeout: timeout * 1000,
|
|
223
|
-
signal,
|
|
224
|
-
},
|
|
194
|
+
const listResult = await ptree.exec(
|
|
195
|
+
[ytdlp, "--list-subs", "--no-warnings", "--no-playlist", "--skip-download", videoUrl],
|
|
196
|
+
execOptions,
|
|
225
197
|
);
|
|
226
198
|
throwIfAborted(signal);
|
|
227
199
|
|
|
@@ -236,9 +208,9 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
236
208
|
// Try manual subtitles first (English preferred)
|
|
237
209
|
if (hasManualSubs) {
|
|
238
210
|
throwIfAborted(signal);
|
|
239
|
-
const subResult = await exec(
|
|
240
|
-
ytdlp,
|
|
211
|
+
const subResult = await ptree.exec(
|
|
241
212
|
[
|
|
213
|
+
ytdlp,
|
|
242
214
|
"--write-sub",
|
|
243
215
|
"--sub-lang",
|
|
244
216
|
"en,en-US,en-GB",
|
|
@@ -251,7 +223,7 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
251
223
|
tmpBase,
|
|
252
224
|
videoUrl,
|
|
253
225
|
],
|
|
254
|
-
|
|
226
|
+
execOptions,
|
|
255
227
|
);
|
|
256
228
|
|
|
257
229
|
if (subResult.ok) {
|
|
@@ -271,9 +243,9 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
271
243
|
// Fall back to auto-generated captions
|
|
272
244
|
if (!transcript && hasAutoSubs) {
|
|
273
245
|
throwIfAborted(signal);
|
|
274
|
-
const autoResult = await exec(
|
|
275
|
-
ytdlp,
|
|
246
|
+
const autoResult = await ptree.exec(
|
|
276
247
|
[
|
|
248
|
+
ytdlp,
|
|
277
249
|
"--write-auto-sub",
|
|
278
250
|
"--sub-lang",
|
|
279
251
|
"en,en-US,en-GB",
|
|
@@ -286,7 +258,7 @@ export const handleYouTube: SpecialHandler = async (
|
|
|
286
258
|
tmpBase,
|
|
287
259
|
videoUrl,
|
|
288
260
|
],
|
|
289
|
-
|
|
261
|
+
execOptions,
|
|
290
262
|
);
|
|
291
263
|
|
|
292
264
|
if (autoResult.ok) {
|
package/src/utils/utils.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { abortableSleep, once, untilAborted } from "@oh-my-pi/pi-utils";
|