@oh-my-pi/pi-coding-agent 3.25.0 → 3.31.0
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 +90 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +369 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/complete.ts +2 -4
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +6 -8
- package/src/core/tools/jtd-to-json-schema.ts +174 -196
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +58 -9
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +55 -32
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +152 -76
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/executor.ts +204 -67
- package/src/core/tools/task/index.ts +129 -92
- package/src/core/tools/task/name-generator.ts +1544 -214
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +34 -11
- package/src/core/tools/task/worker.ts +152 -27
- package/src/core/tools/web-fetch.ts +220 -1657
- package/src/core/tools/web-scrapers/academic.test.ts +239 -0
- package/src/core/tools/web-scrapers/artifacthub.ts +215 -0
- package/src/core/tools/web-scrapers/arxiv.ts +88 -0
- package/src/core/tools/web-scrapers/aur.ts +175 -0
- package/src/core/tools/web-scrapers/biorxiv.ts +141 -0
- package/src/core/tools/web-scrapers/bluesky.ts +284 -0
- package/src/core/tools/web-scrapers/brew.ts +177 -0
- package/src/core/tools/web-scrapers/business.test.ts +82 -0
- package/src/core/tools/web-scrapers/cheatsh.ts +78 -0
- package/src/core/tools/web-scrapers/chocolatey.ts +158 -0
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/web-scrapers/coingecko.ts +184 -0
- package/src/core/tools/web-scrapers/crates-io.ts +128 -0
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/web-scrapers/dev-platforms.test.ts +254 -0
- package/src/core/tools/web-scrapers/devto.ts +177 -0
- package/src/core/tools/web-scrapers/discogs.ts +308 -0
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/web-scrapers/dockerhub.ts +160 -0
- package/src/core/tools/web-scrapers/documentation.test.ts +85 -0
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/finance-media.test.ts +144 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/web-scrapers/git-hosting.test.ts +272 -0
- package/src/core/tools/web-scrapers/github-gist.ts +68 -0
- package/src/core/tools/web-scrapers/github.ts +455 -0
- package/src/core/tools/web-scrapers/gitlab.ts +456 -0
- package/src/core/tools/web-scrapers/go-pkg.ts +275 -0
- package/src/core/tools/web-scrapers/hackage.ts +94 -0
- package/src/core/tools/web-scrapers/hackernews.ts +208 -0
- package/src/core/tools/web-scrapers/hex.ts +121 -0
- package/src/core/tools/web-scrapers/huggingface.ts +385 -0
- package/src/core/tools/web-scrapers/iacr.ts +86 -0
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/web-scrapers/lobsters.ts +186 -0
- package/src/core/tools/web-scrapers/mastodon.ts +310 -0
- package/src/core/tools/web-scrapers/maven.ts +152 -0
- package/src/core/tools/web-scrapers/mdn.ts +174 -0
- package/src/core/tools/web-scrapers/media.test.ts +138 -0
- package/src/core/tools/web-scrapers/metacpan.ts +253 -0
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/web-scrapers/npm.ts +114 -0
- package/src/core/tools/web-scrapers/nuget.ts +205 -0
- package/src/core/tools/web-scrapers/nvd.ts +243 -0
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/web-scrapers/opencorporates.ts +275 -0
- package/src/core/tools/web-scrapers/openlibrary.ts +319 -0
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/web-scrapers/osv.ts +189 -0
- package/src/core/tools/web-scrapers/package-managers-2.test.ts +199 -0
- package/src/core/tools/web-scrapers/package-managers.test.ts +171 -0
- package/src/core/tools/web-scrapers/package-registries.test.ts +259 -0
- package/src/core/tools/web-scrapers/packagist.ts +174 -0
- package/src/core/tools/web-scrapers/pub-dev.ts +185 -0
- package/src/core/tools/web-scrapers/pubmed.ts +178 -0
- package/src/core/tools/web-scrapers/pypi.ts +129 -0
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/web-scrapers/readthedocs.ts +126 -0
- package/src/core/tools/web-scrapers/reddit.ts +104 -0
- package/src/core/tools/web-scrapers/repology.ts +262 -0
- package/src/core/tools/web-scrapers/research.test.ts +107 -0
- package/src/core/tools/web-scrapers/rfc.ts +209 -0
- package/src/core/tools/web-scrapers/rubygems.ts +117 -0
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/web-scrapers/sec-edgar.ts +274 -0
- package/src/core/tools/web-scrapers/security.test.ts +103 -0
- package/src/core/tools/web-scrapers/semantic-scholar.ts +190 -0
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/social-extended.test.ts +192 -0
- package/src/core/tools/web-scrapers/social.test.ts +259 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/web-scrapers/spotify.ts +218 -0
- package/src/core/tools/web-scrapers/stackexchange.test.ts +120 -0
- package/src/core/tools/web-scrapers/stackoverflow.ts +124 -0
- package/src/core/tools/web-scrapers/standards.test.ts +122 -0
- package/src/core/tools/web-scrapers/terraform.ts +304 -0
- package/src/core/tools/web-scrapers/tldr.ts +51 -0
- package/src/core/tools/web-scrapers/twitter.ts +96 -0
- package/src/core/tools/web-scrapers/types.ts +234 -0
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/web-scrapers/vimeo.ts +152 -0
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/web-scrapers/wikidata.ts +357 -0
- package/src/core/tools/web-scrapers/wikipedia.test.ts +73 -0
- package/src/core/tools/web-scrapers/wikipedia.ts +95 -0
- package/src/core/tools/web-scrapers/youtube.test.ts +198 -0
- package/src/core/tools/web-scrapers/youtube.ts +371 -0
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -38
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/src/utils/tools-manager.ts +110 -8
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { unlinkSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { FileSink } from "bun";
|
|
5
|
+
import { nanoid } from "nanoid";
|
|
6
|
+
import { ensureTool } from "../../../utils/tools-manager";
|
|
7
|
+
import type { RenderResult, SpecialHandler } from "./types";
|
|
8
|
+
import { finalizeOutput } from "./types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Execute a command and return stdout
|
|
12
|
+
*/
|
|
13
|
+
async function exec(
|
|
14
|
+
cmd: string,
|
|
15
|
+
args: string[],
|
|
16
|
+
options?: { timeout?: number; input?: string | Buffer; signal?: AbortSignal },
|
|
17
|
+
): Promise<{ stdout: string; stderr: string; ok: boolean; exitCode: number | null }> {
|
|
18
|
+
const proc = Bun.spawn([cmd, ...args], {
|
|
19
|
+
stdin: options?.input ? "pipe" : "ignore",
|
|
20
|
+
stdout: "pipe",
|
|
21
|
+
stderr: "pipe",
|
|
22
|
+
timeout: options?.timeout,
|
|
23
|
+
signal: options?.signal,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (options?.input && proc.stdin) {
|
|
27
|
+
const stdin = proc.stdin as FileSink;
|
|
28
|
+
const payload = typeof options.input === "string" ? new TextEncoder().encode(options.input) : options.input;
|
|
29
|
+
stdin.write(payload);
|
|
30
|
+
const flushed = stdin.flush();
|
|
31
|
+
if (flushed instanceof Promise) {
|
|
32
|
+
await flushed;
|
|
33
|
+
}
|
|
34
|
+
const ended = stdin.end();
|
|
35
|
+
if (ended instanceof Promise) {
|
|
36
|
+
await ended;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const [stdout, stderr] = await Promise.all([
|
|
41
|
+
(proc.stdout as ReadableStream<Uint8Array>).text(),
|
|
42
|
+
(proc.stderr as ReadableStream<Uint8Array>).text(),
|
|
43
|
+
]);
|
|
44
|
+
const exitCode = await proc.exited;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
stdout,
|
|
48
|
+
stderr,
|
|
49
|
+
ok: exitCode === 0,
|
|
50
|
+
exitCode,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface YouTubeUrl {
|
|
55
|
+
videoId: string;
|
|
56
|
+
playlistId?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse YouTube URL into components
|
|
61
|
+
*/
|
|
62
|
+
function parseYouTubeUrl(url: string): YouTubeUrl | null {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = new URL(url);
|
|
65
|
+
const hostname = parsed.hostname.replace(/^www\./, "");
|
|
66
|
+
|
|
67
|
+
// youtube.com/watch?v=VIDEO_ID
|
|
68
|
+
if ((hostname === "youtube.com" || hostname === "m.youtube.com") && parsed.pathname === "/watch") {
|
|
69
|
+
const videoId = parsed.searchParams.get("v");
|
|
70
|
+
const playlistId = parsed.searchParams.get("list") || undefined;
|
|
71
|
+
if (videoId) return { videoId, playlistId };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// youtube.com/v/VIDEO_ID or youtube.com/embed/VIDEO_ID
|
|
75
|
+
if (hostname === "youtube.com" || hostname === "m.youtube.com") {
|
|
76
|
+
const match = parsed.pathname.match(/^\/(v|embed)\/([a-zA-Z0-9_-]{11})/);
|
|
77
|
+
if (match) return { videoId: match[2] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// youtu.be/VIDEO_ID
|
|
81
|
+
if (hostname === "youtu.be") {
|
|
82
|
+
const videoId = parsed.pathname.slice(1).split("/")[0];
|
|
83
|
+
if (videoId && /^[a-zA-Z0-9_-]{11}$/.test(videoId)) {
|
|
84
|
+
return { videoId };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// youtube.com/shorts/VIDEO_ID
|
|
89
|
+
if (hostname === "youtube.com" && parsed.pathname.startsWith("/shorts/")) {
|
|
90
|
+
const videoId = parsed.pathname.replace("/shorts/", "").split("/")[0];
|
|
91
|
+
if (videoId && /^[a-zA-Z0-9_-]{11}$/.test(videoId)) {
|
|
92
|
+
return { videoId };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch {}
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Clean VTT subtitle content to plain text
|
|
102
|
+
*/
|
|
103
|
+
function cleanVttToText(vtt: string): string {
|
|
104
|
+
const lines = vtt.split("\n");
|
|
105
|
+
const textLines: string[] = [];
|
|
106
|
+
let lastLine = "";
|
|
107
|
+
|
|
108
|
+
for (const line of lines) {
|
|
109
|
+
// Skip WEBVTT header, timestamps, and metadata
|
|
110
|
+
if (
|
|
111
|
+
line.startsWith("WEBVTT") ||
|
|
112
|
+
line.startsWith("Kind:") ||
|
|
113
|
+
line.startsWith("Language:") ||
|
|
114
|
+
line.match(/^\d{2}:\d{2}/) || // Timestamp lines
|
|
115
|
+
line.match(/^[a-f0-9-]{36}$/) || // UUID cue identifiers
|
|
116
|
+
line.match(/^\d+$/) || // Numeric cue identifiers
|
|
117
|
+
line.includes("-->") ||
|
|
118
|
+
line.trim() === ""
|
|
119
|
+
) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Remove inline timestamp tags like <00:00:01.520>
|
|
124
|
+
let cleaned = line.replace(/<\d{2}:\d{2}:\d{2}\.\d{3}>/g, "");
|
|
125
|
+
// Remove other VTT tags like <c> </c>
|
|
126
|
+
cleaned = cleaned.replace(/<\/?[^>]+>/g, "");
|
|
127
|
+
cleaned = cleaned.trim();
|
|
128
|
+
|
|
129
|
+
// Skip duplicates (auto-generated captions often repeat)
|
|
130
|
+
if (cleaned && cleaned !== lastLine) {
|
|
131
|
+
textLines.push(cleaned);
|
|
132
|
+
lastLine = cleaned;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return textLines.join(" ").replace(/\s+/g, " ").trim();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Format duration from seconds to human readable
|
|
141
|
+
*/
|
|
142
|
+
function formatDuration(seconds: number): string {
|
|
143
|
+
const h = Math.floor(seconds / 3600);
|
|
144
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
145
|
+
const s = Math.floor(seconds % 60);
|
|
146
|
+
if (h > 0) return `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
|
|
147
|
+
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Handle YouTube URLs - fetch metadata and transcript
|
|
152
|
+
*/
|
|
153
|
+
export const handleYouTube: SpecialHandler = async (
|
|
154
|
+
url: string,
|
|
155
|
+
timeout: number,
|
|
156
|
+
signal?: AbortSignal,
|
|
157
|
+
): Promise<RenderResult | null> => {
|
|
158
|
+
signal?.throwIfAborted();
|
|
159
|
+
const yt = parseYouTubeUrl(url);
|
|
160
|
+
if (!yt) return null;
|
|
161
|
+
|
|
162
|
+
// Ensure yt-dlp is available (auto-download if missing)
|
|
163
|
+
const ytdlp = await ensureTool("yt-dlp", true);
|
|
164
|
+
signal?.throwIfAborted();
|
|
165
|
+
if (!ytdlp) {
|
|
166
|
+
return {
|
|
167
|
+
url,
|
|
168
|
+
finalUrl: url,
|
|
169
|
+
contentType: "text/plain",
|
|
170
|
+
method: "youtube-no-ytdlp",
|
|
171
|
+
content: "YouTube video detected but yt-dlp could not be installed.",
|
|
172
|
+
fetchedAt: new Date().toISOString(),
|
|
173
|
+
truncated: false,
|
|
174
|
+
notes: ["yt-dlp installation failed"],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const fetchedAt = new Date().toISOString();
|
|
179
|
+
const notes: string[] = [];
|
|
180
|
+
const videoUrl = `https://www.youtube.com/watch?v=${yt.videoId}`;
|
|
181
|
+
|
|
182
|
+
// Fetch video metadata
|
|
183
|
+
signal?.throwIfAborted();
|
|
184
|
+
const metaResult = await exec(
|
|
185
|
+
ytdlp,
|
|
186
|
+
["--dump-json", "--no-warnings", "--no-playlist", "--skip-download", videoUrl],
|
|
187
|
+
{
|
|
188
|
+
timeout: timeout * 1000,
|
|
189
|
+
signal,
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
signal?.throwIfAborted();
|
|
193
|
+
|
|
194
|
+
let title = "YouTube Video";
|
|
195
|
+
let channel = "";
|
|
196
|
+
let description = "";
|
|
197
|
+
let duration = 0;
|
|
198
|
+
let uploadDate = "";
|
|
199
|
+
let viewCount = 0;
|
|
200
|
+
|
|
201
|
+
if (metaResult.ok && metaResult.stdout.trim()) {
|
|
202
|
+
try {
|
|
203
|
+
const meta = JSON.parse(metaResult.stdout) as {
|
|
204
|
+
title?: string;
|
|
205
|
+
channel?: string;
|
|
206
|
+
uploader?: string;
|
|
207
|
+
description?: string;
|
|
208
|
+
duration?: number;
|
|
209
|
+
upload_date?: string;
|
|
210
|
+
view_count?: number;
|
|
211
|
+
};
|
|
212
|
+
title = meta.title || title;
|
|
213
|
+
channel = meta.channel || meta.uploader || "";
|
|
214
|
+
description = meta.description || "";
|
|
215
|
+
duration = meta.duration || 0;
|
|
216
|
+
uploadDate = meta.upload_date || "";
|
|
217
|
+
viewCount = meta.view_count || 0;
|
|
218
|
+
} catch {}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Format upload date
|
|
222
|
+
let formattedDate = "";
|
|
223
|
+
if (uploadDate && uploadDate.length === 8) {
|
|
224
|
+
formattedDate = `${uploadDate.slice(0, 4)}-${uploadDate.slice(4, 6)}-${uploadDate.slice(6, 8)}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Try to fetch subtitles
|
|
228
|
+
let transcript = "";
|
|
229
|
+
let transcriptSource = "";
|
|
230
|
+
|
|
231
|
+
// First, list available subtitles
|
|
232
|
+
signal?.throwIfAborted();
|
|
233
|
+
const listResult = await exec(
|
|
234
|
+
ytdlp,
|
|
235
|
+
["--list-subs", "--no-warnings", "--no-playlist", "--skip-download", videoUrl],
|
|
236
|
+
{
|
|
237
|
+
timeout: timeout * 1000,
|
|
238
|
+
signal,
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
signal?.throwIfAborted();
|
|
242
|
+
|
|
243
|
+
const hasManualSubs = listResult.stdout.includes("[info] Available subtitles");
|
|
244
|
+
const hasAutoSubs = listResult.stdout.includes("[info] Available automatic captions");
|
|
245
|
+
|
|
246
|
+
// Create temp directory for subtitle download
|
|
247
|
+
const tmpDir = tmpdir();
|
|
248
|
+
const tmpBase = path.join(tmpDir, `yt-${yt.videoId}-${nanoid()}`);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
// Try manual subtitles first (English preferred)
|
|
252
|
+
if (hasManualSubs) {
|
|
253
|
+
signal?.throwIfAborted();
|
|
254
|
+
const subResult = await exec(
|
|
255
|
+
ytdlp,
|
|
256
|
+
[
|
|
257
|
+
"--write-sub",
|
|
258
|
+
"--sub-lang",
|
|
259
|
+
"en,en-US,en-GB",
|
|
260
|
+
"--sub-format",
|
|
261
|
+
"vtt",
|
|
262
|
+
"--skip-download",
|
|
263
|
+
"--no-warnings",
|
|
264
|
+
"--no-playlist",
|
|
265
|
+
"-o",
|
|
266
|
+
tmpBase,
|
|
267
|
+
videoUrl,
|
|
268
|
+
],
|
|
269
|
+
{ timeout: timeout * 1000, signal },
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
if (subResult.ok) {
|
|
273
|
+
// Find the downloaded subtitle file using glob
|
|
274
|
+
signal?.throwIfAborted();
|
|
275
|
+
const subFiles = await Array.fromAsync(new Bun.Glob(`${tmpBase}*.vtt`).scan({ absolute: true }));
|
|
276
|
+
if (subFiles.length > 0) {
|
|
277
|
+
signal?.throwIfAborted();
|
|
278
|
+
const vttContent = await Bun.file(subFiles[0]).text();
|
|
279
|
+
transcript = cleanVttToText(vttContent);
|
|
280
|
+
transcriptSource = "manual";
|
|
281
|
+
notes.push("Using manual subtitles");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Fall back to auto-generated captions
|
|
287
|
+
if (!transcript && hasAutoSubs) {
|
|
288
|
+
signal?.throwIfAborted();
|
|
289
|
+
const autoResult = await exec(
|
|
290
|
+
ytdlp,
|
|
291
|
+
[
|
|
292
|
+
"--write-auto-sub",
|
|
293
|
+
"--sub-lang",
|
|
294
|
+
"en,en-US,en-GB",
|
|
295
|
+
"--sub-format",
|
|
296
|
+
"vtt",
|
|
297
|
+
"--skip-download",
|
|
298
|
+
"--no-warnings",
|
|
299
|
+
"--no-playlist",
|
|
300
|
+
"-o",
|
|
301
|
+
tmpBase,
|
|
302
|
+
videoUrl,
|
|
303
|
+
],
|
|
304
|
+
{ timeout: timeout * 1000, signal },
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (autoResult.ok) {
|
|
308
|
+
signal?.throwIfAborted();
|
|
309
|
+
const subFiles = await Array.fromAsync(new Bun.Glob(`${tmpBase}*.vtt`).scan({ absolute: true }));
|
|
310
|
+
if (subFiles.length > 0) {
|
|
311
|
+
signal?.throwIfAborted();
|
|
312
|
+
const vttContent = await Bun.file(subFiles[0]).text();
|
|
313
|
+
transcript = cleanVttToText(vttContent);
|
|
314
|
+
transcriptSource = "auto-generated";
|
|
315
|
+
notes.push("Using auto-generated captions");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} finally {
|
|
320
|
+
// Cleanup temp files using sync unlink to avoid leaving handles open
|
|
321
|
+
try {
|
|
322
|
+
const tmpFiles = await Array.fromAsync(new Bun.Glob(`${tmpBase}*`).scan({ absolute: true }));
|
|
323
|
+
for (const f of tmpFiles) {
|
|
324
|
+
try {
|
|
325
|
+
unlinkSync(f);
|
|
326
|
+
} catch {}
|
|
327
|
+
}
|
|
328
|
+
} catch {}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Build markdown output
|
|
332
|
+
let md = `# ${title}\n\n`;
|
|
333
|
+
if (channel) md += `**Channel:** ${channel}\n`;
|
|
334
|
+
if (formattedDate) md += `**Uploaded:** ${formattedDate}\n`;
|
|
335
|
+
if (duration > 0) md += `**Duration:** ${formatDuration(duration)}\n`;
|
|
336
|
+
if (viewCount > 0) {
|
|
337
|
+
const formatted =
|
|
338
|
+
viewCount >= 1_000_000
|
|
339
|
+
? `${(viewCount / 1_000_000).toFixed(1)}M`
|
|
340
|
+
: viewCount >= 1_000
|
|
341
|
+
? `${(viewCount / 1_000).toFixed(1)}K`
|
|
342
|
+
: String(viewCount);
|
|
343
|
+
md += `**Views:** ${formatted}\n`;
|
|
344
|
+
}
|
|
345
|
+
md += `**Video ID:** ${yt.videoId}\n\n`;
|
|
346
|
+
|
|
347
|
+
if (description) {
|
|
348
|
+
// Truncate long descriptions
|
|
349
|
+
const descPreview = description.length > 1000 ? `${description.slice(0, 1000)}...` : description;
|
|
350
|
+
md += `---\n\n## Description\n\n${descPreview}\n\n`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (transcript) {
|
|
354
|
+
md += `---\n\n## Transcript (${transcriptSource})\n\n${transcript}\n`;
|
|
355
|
+
} else {
|
|
356
|
+
notes.push("No subtitles/captions available");
|
|
357
|
+
md += `---\n\n*No transcript available for this video.*\n`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const output = finalizeOutput(md);
|
|
361
|
+
return {
|
|
362
|
+
url,
|
|
363
|
+
finalUrl: videoUrl,
|
|
364
|
+
contentType: "text/markdown",
|
|
365
|
+
method: "youtube",
|
|
366
|
+
content: output.content,
|
|
367
|
+
fetchedAt,
|
|
368
|
+
truncated: output.truncated,
|
|
369
|
+
notes,
|
|
370
|
+
};
|
|
371
|
+
};
|
package/src/core/tools/write.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/inte
|
|
|
6
6
|
import writeDescription from "../../prompts/tools/write.md" with { type: "text" };
|
|
7
7
|
import type { RenderResultOptions } from "../custom-tools/types";
|
|
8
8
|
import type { ToolSession } from "../sdk";
|
|
9
|
+
import { untilAborted } from "../utils";
|
|
9
10
|
import { createLspWritethrough, type FileDiagnosticsResult } from "./lsp/index";
|
|
10
11
|
import { resolveToCwd } from "./path-utils";
|
|
11
12
|
import { formatDiagnostics, replaceTabs, shortenPath } from "./render-utils";
|
|
@@ -34,27 +35,29 @@ export function createWriteTool(session: ToolSession): AgentTool<typeof writeSch
|
|
|
34
35
|
{ path, content }: { path: string; content: string },
|
|
35
36
|
signal?: AbortSignal,
|
|
36
37
|
) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
return untilAborted(signal, async () => {
|
|
39
|
+
const absolutePath = resolveToCwd(path, session.cwd);
|
|
40
|
+
|
|
41
|
+
const diagnostics = await writethrough(absolutePath, content, signal);
|
|
42
|
+
|
|
43
|
+
let resultText = `Successfully wrote ${content.length} bytes to ${path}`;
|
|
44
|
+
if (!diagnostics) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: "text", text: resultText }],
|
|
47
|
+
details: {},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const messages = diagnostics?.messages;
|
|
52
|
+
if (messages && messages.length > 0) {
|
|
53
|
+
resultText += `\n\nLSP Diagnostics (${diagnostics.summary}):\n`;
|
|
54
|
+
resultText += messages.map((d) => ` ${d}`).join("\n");
|
|
55
|
+
}
|
|
43
56
|
return {
|
|
44
57
|
content: [{ type: "text", text: resultText }],
|
|
45
|
-
details: {},
|
|
58
|
+
details: { diagnostics },
|
|
46
59
|
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const messages = diagnostics?.messages;
|
|
50
|
-
if (messages && messages.length > 0) {
|
|
51
|
-
resultText += `\n\nLSP Diagnostics (${diagnostics.summary}):\n`;
|
|
52
|
-
resultText += messages.map((d) => ` ${d}`).join("\n");
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
content: [{ type: "text", text: resultText }],
|
|
56
|
-
details: { diagnostics },
|
|
57
|
-
};
|
|
60
|
+
});
|
|
58
61
|
},
|
|
59
62
|
};
|
|
60
63
|
}
|
package/src/core/voice.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { unlinkSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { completeSimple, type Model } from "@mariozechner/pi-ai";
|
|
5
|
+
import { nanoid } from "nanoid";
|
|
5
6
|
import voiceSummaryPrompt from "../prompts/voice-summary.md" with { type: "text" };
|
|
6
7
|
import { logger } from "./logger";
|
|
7
8
|
import type { ModelRegistry } from "./model-registry";
|
|
@@ -99,7 +100,7 @@ function buildRecordingCommand(filePath: string, sampleRate: number, channels: n
|
|
|
99
100
|
export async function startVoiceRecording(_settings: VoiceSettings): Promise<VoiceRecordingHandle> {
|
|
100
101
|
const sampleRate = DEFAULT_SAMPLE_RATE;
|
|
101
102
|
const channels = DEFAULT_CHANNELS;
|
|
102
|
-
const filePath = join(tmpdir(), `omp-voice-${
|
|
103
|
+
const filePath = join(tmpdir(), `omp-voice-${nanoid()}.wav`);
|
|
103
104
|
const command = buildRecordingCommand(filePath, sampleRate, channels);
|
|
104
105
|
if (!command) {
|
|
105
106
|
throw new Error("No audio recorder found (install sox, arecord, or ffmpeg).");
|
|
@@ -233,7 +234,7 @@ function getPlayerCommand(filePath: string, format: VoiceSynthesisResult["format
|
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
export async function playAudio(audio: Uint8Array, format: VoiceSynthesisResult["format"]): Promise<void> {
|
|
236
|
-
const filePath = join(tmpdir(), `omp-
|
|
237
|
+
const filePath = join(tmpdir(), `omp-tts-${nanoid()}.${format}`);
|
|
237
238
|
await Bun.write(filePath, audio);
|
|
238
239
|
|
|
239
240
|
const command = getPlayerCommand(filePath, format);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
1
2
|
import { WorktreeError, WorktreeErrorCode } from "./errors";
|
|
2
3
|
import { git, gitWithStdin } from "./git";
|
|
3
4
|
import { find, remove, type Worktree } from "./operations";
|
|
@@ -89,7 +90,7 @@ async function collapseRebase(src: Worktree, dst: Worktree): Promise<string> {
|
|
|
89
90
|
throw new WorktreeError("Failed to resolve HEAD", WorktreeErrorCode.COLLAPSE_FAILED);
|
|
90
91
|
}
|
|
91
92
|
const originalHead = headResult.stdout.trim();
|
|
92
|
-
const tempBranch = `wt-collapse-${
|
|
93
|
+
const tempBranch = `wt-collapse-${nanoid()}`;
|
|
93
94
|
|
|
94
95
|
await requireGitSuccess(await git(["checkout", "-b", tempBranch], src.path), "Failed to create temp branch");
|
|
95
96
|
|
package/src/lib/worktree/git.ts
CHANGED
|
@@ -17,22 +17,6 @@ type WritableLike = {
|
|
|
17
17
|
|
|
18
18
|
const textEncoder = new TextEncoder();
|
|
19
19
|
|
|
20
|
-
async function readStream(stream: ReadableStream<Uint8Array> | undefined): Promise<string> {
|
|
21
|
-
if (!stream) return "";
|
|
22
|
-
const reader = stream.getReader();
|
|
23
|
-
const chunks: Uint8Array[] = [];
|
|
24
|
-
try {
|
|
25
|
-
while (true) {
|
|
26
|
-
const { done, value } = await reader.read();
|
|
27
|
-
if (done) break;
|
|
28
|
-
chunks.push(value);
|
|
29
|
-
}
|
|
30
|
-
} finally {
|
|
31
|
-
reader.releaseLock();
|
|
32
|
-
}
|
|
33
|
-
return Buffer.concat(chunks).toString();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
20
|
async function writeStdin(handle: unknown, stdin: string): Promise<void> {
|
|
37
21
|
if (!handle || typeof handle === "number") return;
|
|
38
22
|
if (typeof (handle as WritableStream<Uint8Array>).getWriter === "function") {
|
|
@@ -77,8 +61,8 @@ export async function gitWithStdin(args: string[], stdin: string, cwd?: string):
|
|
|
77
61
|
await writeStdin(proc.stdin, stdin);
|
|
78
62
|
|
|
79
63
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
80
|
-
|
|
81
|
-
|
|
64
|
+
(proc.stdout as ReadableStream<Uint8Array>).text(),
|
|
65
|
+
(proc.stderr as ReadableStream<Uint8Array>).text(),
|
|
82
66
|
proc.exited,
|
|
83
67
|
]);
|
|
84
68
|
|
package/src/main.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import { type ImageContent, supportsXhigh } from "@mariozechner/pi-ai";
|
|
9
9
|
import chalk from "chalk";
|
|
10
|
+
import { homedir, tmpdir } from "node:os";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
10
12
|
import { type Args, parseArgs, printHelp } from "./cli/args";
|
|
11
13
|
import { processFileArguments } from "./cli/file-processor";
|
|
12
14
|
import { listModels } from "./cli/list-models";
|
|
@@ -187,6 +189,59 @@ async function createSessionManager(parsed: Args, cwd: string): Promise<SessionM
|
|
|
187
189
|
return undefined;
|
|
188
190
|
}
|
|
189
191
|
|
|
192
|
+
async function maybeAutoChdir(parsed: Args): Promise<void> {
|
|
193
|
+
if (parsed.allowHome || parsed.cwd) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const home = homedir();
|
|
198
|
+
if (!home) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const normalizePath = (value: string) => {
|
|
203
|
+
const resolved = resolve(value);
|
|
204
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const cwd = normalizePath(process.cwd());
|
|
208
|
+
const normalizedHome = normalizePath(home);
|
|
209
|
+
if (cwd !== normalizedHome) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const isDirectory = async (path: string) => {
|
|
214
|
+
try {
|
|
215
|
+
const stat = await Bun.file(path).stat();
|
|
216
|
+
return stat.isDirectory();
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const candidates = [join(home, "tmp"), "/tmp", "/var/tmp"];
|
|
223
|
+
for (const candidate of candidates) {
|
|
224
|
+
try {
|
|
225
|
+
if (!(await isDirectory(candidate))) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
process.chdir(candidate);
|
|
229
|
+
return;
|
|
230
|
+
} catch {
|
|
231
|
+
// Try next candidate.
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
const fallback = tmpdir();
|
|
237
|
+
if (fallback && normalizePath(fallback) !== cwd && (await isDirectory(fallback))) {
|
|
238
|
+
process.chdir(fallback);
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
// Ignore fallback errors.
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
190
245
|
/** Discover SYSTEM.md file if no CLI system prompt was provided */
|
|
191
246
|
function discoverSystemPromptFile(): string | undefined {
|
|
192
247
|
// Check project-local first (.omp/SYSTEM.md, .pi/SYSTEM.md legacy)
|
|
@@ -318,6 +373,10 @@ export async function main(args: string[]) {
|
|
|
318
373
|
return;
|
|
319
374
|
}
|
|
320
375
|
|
|
376
|
+
const parsed = parseArgs(args);
|
|
377
|
+
time("parseArgs");
|
|
378
|
+
await maybeAutoChdir(parsed);
|
|
379
|
+
|
|
321
380
|
// Run migrations (pass cwd for project-local migrations)
|
|
322
381
|
const { migratedAuthProviders: migratedProviders, deprecationWarnings } = await runMigrations(process.cwd());
|
|
323
382
|
|
|
@@ -326,9 +385,6 @@ export async function main(args: string[]) {
|
|
|
326
385
|
const modelRegistry = await discoverModels(authStorage);
|
|
327
386
|
time("discoverModels");
|
|
328
387
|
|
|
329
|
-
const parsed = parseArgs(args);
|
|
330
|
-
time("parseArgs");
|
|
331
|
-
|
|
332
388
|
if (parsed.version) {
|
|
333
389
|
console.log(VERSION);
|
|
334
390
|
return;
|
|
@@ -38,30 +38,40 @@ export class ExtensionDashboard extends Container {
|
|
|
38
38
|
private inspector: InspectorPanel;
|
|
39
39
|
private settingsManager: SettingsManager | null;
|
|
40
40
|
private cwd: string;
|
|
41
|
+
private terminalHeight: number;
|
|
41
42
|
|
|
42
43
|
public onClose?: () => void;
|
|
43
44
|
|
|
44
|
-
constructor(cwd: string, settingsManager: SettingsManager | null = null) {
|
|
45
|
+
constructor(cwd: string, settingsManager: SettingsManager | null = null, terminalHeight?: number) {
|
|
45
46
|
super();
|
|
46
47
|
this.cwd = cwd;
|
|
47
48
|
this.settingsManager = settingsManager;
|
|
49
|
+
this.terminalHeight = terminalHeight ?? process.stdout.rows ?? 24;
|
|
48
50
|
const disabledIds = settingsManager?.getDisabledExtensions() ?? [];
|
|
49
51
|
this.state = createInitialState(cwd, disabledIds);
|
|
50
52
|
|
|
53
|
+
// Calculate max visible items based on terminal height
|
|
54
|
+
// Reserve ~10 lines for header, tabs, help text, borders
|
|
55
|
+
const maxVisible = Math.max(5, Math.floor((this.terminalHeight - 10) / 2));
|
|
56
|
+
|
|
51
57
|
// Create main list - always focused
|
|
52
|
-
this.mainList = new ExtensionList(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
this.mainList = new ExtensionList(
|
|
59
|
+
this.state.searchFiltered,
|
|
60
|
+
{
|
|
61
|
+
onSelectionChange: (ext) => {
|
|
62
|
+
this.state.selected = ext;
|
|
63
|
+
this.inspector.setExtension(ext);
|
|
64
|
+
},
|
|
65
|
+
onToggle: (extensionId, enabled) => {
|
|
66
|
+
this.handleExtensionToggle(extensionId, enabled);
|
|
67
|
+
},
|
|
68
|
+
onMasterToggle: (providerId) => {
|
|
69
|
+
this.handleProviderToggle(providerId);
|
|
70
|
+
},
|
|
71
|
+
masterSwitchProvider: this.getActiveProviderId(),
|
|
62
72
|
},
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
maxVisible,
|
|
74
|
+
);
|
|
65
75
|
this.mainList.setFocused(true);
|
|
66
76
|
|
|
67
77
|
// Create inspector
|
|
@@ -91,9 +101,10 @@ export class ExtensionDashboard extends Container {
|
|
|
91
101
|
this.addChild(new Text(this.renderTabBar(), 0, 0));
|
|
92
102
|
this.addChild(new Spacer(1));
|
|
93
103
|
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
104
|
+
// 2-column body with height limit
|
|
105
|
+
// Reserve ~8 lines for header, tabs, help text, borders
|
|
106
|
+
const bodyMaxHeight = Math.max(5, this.terminalHeight - 8);
|
|
107
|
+
this.addChild(new TwoColumnBody(this.mainList, this.inspector, bodyMaxHeight));
|
|
97
108
|
|
|
98
109
|
this.addChild(new Spacer(1));
|
|
99
110
|
this.addChild(new Text(theme.fg("dim", " ↑/↓: navigate Space: toggle Tab: next provider Esc: close"), 0, 0));
|
|
@@ -262,10 +273,12 @@ export class ExtensionDashboard extends Container {
|
|
|
262
273
|
class TwoColumnBody implements Component {
|
|
263
274
|
private leftPane: ExtensionList;
|
|
264
275
|
private rightPane: InspectorPanel;
|
|
276
|
+
private maxHeight: number;
|
|
265
277
|
|
|
266
|
-
constructor(left: ExtensionList, right: InspectorPanel) {
|
|
278
|
+
constructor(left: ExtensionList, right: InspectorPanel, maxHeight: number) {
|
|
267
279
|
this.leftPane = left;
|
|
268
280
|
this.rightPane = right;
|
|
281
|
+
this.maxHeight = maxHeight;
|
|
269
282
|
}
|
|
270
283
|
|
|
271
284
|
render(width: number): string[] {
|
|
@@ -275,11 +288,12 @@ class TwoColumnBody implements Component {
|
|
|
275
288
|
const leftLines = this.leftPane.render(leftWidth);
|
|
276
289
|
const rightLines = this.rightPane.render(rightWidth);
|
|
277
290
|
|
|
278
|
-
|
|
291
|
+
// Limit to maxHeight lines
|
|
292
|
+
const numLines = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
|
|
279
293
|
const combined: string[] = [];
|
|
280
294
|
const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
|
|
281
295
|
|
|
282
|
-
for (let i = 0; i <
|
|
296
|
+
for (let i = 0; i < numLines; i++) {
|
|
283
297
|
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
|
|
284
298
|
const leftPadded = left + " ".repeat(Math.max(0, leftWidth - visibleWidth(left)));
|
|
285
299
|
const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
|