@oh-my-pi/pi-coding-agent 15.9.1 → 15.9.5
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 +68 -2
- package/dist/types/cli/classify-install-target.d.ts +5 -1
- package/dist/types/cli/dry-balance-cli.d.ts +104 -0
- package/dist/types/commands/dry-balance.d.ts +31 -0
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +13 -4
- package/dist/types/config/settings.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -3
- package/dist/types/hindsight/bank.d.ts +17 -9
- package/dist/types/hindsight/mental-models.d.ts +1 -1
- package/dist/types/hindsight/state.d.ts +9 -3
- package/dist/types/mcp/manager.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +11 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/error-banner.d.ts +11 -0
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -2
- package/dist/types/modes/components/user-message.d.ts +1 -1
- package/dist/types/modes/image-references.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/auth-storage.d.ts +2 -2
- package/dist/types/session/blob-store.d.ts +12 -11
- package/dist/types/session/session-manager.d.ts +5 -3
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +16 -1
- package/dist/types/tool-discovery/mode.d.ts +8 -0
- package/dist/types/tools/archive-reader.d.ts +5 -1
- package/dist/types/tools/index.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +11 -0
- package/dist/types/tui/hyperlink.d.ts +12 -0
- package/dist/types/web/search/render.d.ts +1 -2
- package/package.json +9 -9
- package/src/cli/classify-install-target.ts +31 -5
- package/src/cli/dry-balance-cli.ts +823 -0
- package/src/cli/plugin-cli.ts +45 -0
- package/src/cli/web-search-cli.ts +0 -1
- package/src/cli-commands.ts +1 -0
- package/src/commands/dry-balance.ts +43 -0
- package/src/config/model-registry.ts +60 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +14 -4
- package/src/config/settings.ts +38 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -0
- package/src/discovery/github.ts +37 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/eval/__tests__/agent-bridge.test.ts +72 -0
- package/src/eval/py/tool-bridge.ts +43 -5
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +31 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +245 -25
- package/src/hindsight/backend.ts +184 -35
- package/src/hindsight/bank.ts +32 -22
- package/src/hindsight/mental-models.ts +1 -1
- package/src/hindsight/state.ts +21 -7
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/omp-protocol.ts +8 -2
- package/src/main.ts +7 -1
- package/src/mcp/manager.ts +40 -21
- package/src/modes/components/assistant-message.ts +22 -0
- package/src/modes/components/custom-editor.ts +14 -2
- package/src/modes/components/error-banner.ts +33 -0
- package/src/modes/components/tool-execution.ts +44 -0
- package/src/modes/components/transcript-container.ts +102 -30
- package/src/modes/components/tree-selector.ts +29 -2
- package/src/modes/components/user-message.ts +9 -2
- package/src/modes/controllers/event-controller.ts +42 -3
- package/src/modes/controllers/input-controller.ts +41 -3
- package/src/modes/image-references.ts +111 -0
- package/src/modes/interactive-mode.ts +48 -13
- package/src/modes/setup-wizard/scenes/sign-in.ts +27 -7
- package/src/modes/types.ts +10 -1
- package/src/modes/utils/ui-helpers.ts +23 -2
- package/src/prompts/agents/explore.md +1 -0
- package/src/prompts/agents/librarian.md +1 -0
- package/src/prompts/ci-green-request.md +5 -3
- package/src/prompts/dry-balance-bench.md +8 -0
- package/src/prompts/system/project-prompt.md +1 -0
- package/src/sdk.ts +99 -18
- package/src/session/agent-session.ts +103 -19
- package/src/session/auth-storage.ts +4 -0
- package/src/session/blob-store.ts +96 -9
- package/src/session/session-manager.ts +19 -10
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +6 -2
- package/src/task/index.ts +8 -7
- package/src/task/types.ts +2 -0
- package/src/tiny/title-client.ts +7 -1
- package/src/tool-discovery/mode.ts +24 -0
- package/src/tools/archive-reader.ts +339 -31
- package/src/tools/bash.ts +3 -4
- package/src/tools/fetch.ts +29 -9
- package/src/tools/gh.ts +65 -11
- package/src/tools/index.ts +22 -8
- package/src/tools/job.ts +3 -3
- package/src/tools/memory-reflect.ts +2 -2
- package/src/tools/path-utils.ts +21 -0
- package/src/tools/read.ts +58 -12
- package/src/tools/search-tool-bm25.ts +4 -6
- package/src/tools/search.ts +78 -12
- package/src/tui/hyperlink.ts +42 -7
- package/src/utils/file-mentions.ts +7 -107
- package/src/utils/title-generator.ts +58 -37
- package/src/web/search/index.ts +2 -2
- package/src/web/search/render.ts +20 -52
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { inflateSync, strFromU8 } from "fflate";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
async function loadFflate(): Promise<typeof import("fflate")> {
|
|
5
|
-
if (!fflateModulePromise) fflateModulePromise = import("fflate");
|
|
6
|
-
return fflateModulePromise;
|
|
7
|
-
}
|
|
3
|
+
import { ToolError } from "./tool-errors";
|
|
8
4
|
|
|
9
5
|
export type ArchiveFormat = "zip" | "tar" | "tar.gz";
|
|
10
6
|
|
|
@@ -35,7 +31,11 @@ interface TarStorage {
|
|
|
35
31
|
|
|
36
32
|
interface ZipStorage {
|
|
37
33
|
type: "zip";
|
|
38
|
-
|
|
34
|
+
archivePath: string;
|
|
35
|
+
compressedSize: number;
|
|
36
|
+
compression: number;
|
|
37
|
+
flags: number;
|
|
38
|
+
localHeaderOffset: number;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
type EntryStorage = TarStorage | ZipStorage;
|
|
@@ -123,6 +123,321 @@ function getArchiveFormatFromPath(filePath: string): ArchiveFormat | undefined {
|
|
|
123
123
|
return undefined;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
const ZIP_LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50;
|
|
127
|
+
const ZIP_CENTRAL_DIRECTORY_HEADER_SIGNATURE = 0x02014b50;
|
|
128
|
+
const ZIP64_EOCD_SIGNATURE = 0x06064b50;
|
|
129
|
+
const ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;
|
|
130
|
+
const ZIP_EOCD_SIGNATURE = 0x06054b50;
|
|
131
|
+
const ZIP_EOCD_MIN_LENGTH = 22;
|
|
132
|
+
const ZIP_EOCD_MAX_COMMENT_LENGTH = 0xffff;
|
|
133
|
+
const ZIP64_EOCD_LOCATOR_LENGTH = 20;
|
|
134
|
+
const ZIP_STORED_COMPRESSION = 0;
|
|
135
|
+
const ZIP_DEFLATE_COMPRESSION = 8;
|
|
136
|
+
const ZIP_UTF8_FLAG = 0x0800;
|
|
137
|
+
const ZIP_ENCRYPTED_FLAG = 0x0001;
|
|
138
|
+
const ZIP_UINT16_MAX = 0xffff;
|
|
139
|
+
const ZIP_UINT32_MAX = 0xffffffff;
|
|
140
|
+
const ZIP_UINT32_RANGE = 0x100000000;
|
|
141
|
+
|
|
142
|
+
interface ZipCentralDirectoryInfo {
|
|
143
|
+
entries: number;
|
|
144
|
+
offset: number;
|
|
145
|
+
size: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface Zip64EntryValues {
|
|
149
|
+
compressedSize: number;
|
|
150
|
+
uncompressedSize: number;
|
|
151
|
+
localHeaderOffset: number;
|
|
152
|
+
diskStart: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface Zip64EntryPlaceholders {
|
|
156
|
+
compressedSize: boolean;
|
|
157
|
+
uncompressedSize: boolean;
|
|
158
|
+
localHeaderOffset: boolean;
|
|
159
|
+
diskStart: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readUInt16LE(bytes: Uint8Array, offset: number): number {
|
|
163
|
+
return bytes[offset]! | (bytes[offset + 1]! << 8);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function readUInt32LE(bytes: Uint8Array, offset: number): number {
|
|
167
|
+
return (bytes[offset]! | (bytes[offset + 1]! << 8) | (bytes[offset + 2]! << 16) | (bytes[offset + 3]! << 24)) >>> 0;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readUInt64LEAsNumber(bytes: Uint8Array, offset: number): number {
|
|
171
|
+
const value = readUInt32LE(bytes, offset) + readUInt32LE(bytes, offset + 4) * ZIP_UINT32_RANGE;
|
|
172
|
+
if (!Number.isSafeInteger(value)) {
|
|
173
|
+
throw new ToolError("ZIP archive uses offsets or sizes too large to read safely");
|
|
174
|
+
}
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function readZipRange(filePath: string, start: number, end: number): Promise<Uint8Array> {
|
|
179
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start) {
|
|
180
|
+
throw new ToolError("Invalid ZIP archive range");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const bytes = await Bun.file(filePath).slice(start, end).bytes();
|
|
184
|
+
if (bytes.byteLength !== end - start) {
|
|
185
|
+
throw new ToolError("Invalid ZIP archive: truncated data");
|
|
186
|
+
}
|
|
187
|
+
return bytes;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function findEndOfCentralDirectory(tail: Uint8Array): number {
|
|
191
|
+
for (let offset = tail.byteLength - ZIP_EOCD_MIN_LENGTH; offset >= 0; offset--) {
|
|
192
|
+
if (readUInt32LE(tail, offset) !== ZIP_EOCD_SIGNATURE) continue;
|
|
193
|
+
const commentLength = readUInt16LE(tail, offset + 20);
|
|
194
|
+
if (offset + ZIP_EOCD_MIN_LENGTH + commentLength === tail.byteLength) return offset;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
throw new ToolError("Invalid ZIP archive: missing end of central directory");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function readZip64CentralDirectoryInfo(
|
|
201
|
+
filePath: string,
|
|
202
|
+
tail: Uint8Array,
|
|
203
|
+
tailStart: number,
|
|
204
|
+
eocdOffset: number,
|
|
205
|
+
): Promise<ZipCentralDirectoryInfo | undefined> {
|
|
206
|
+
const locatorOffset = eocdOffset - ZIP64_EOCD_LOCATOR_LENGTH;
|
|
207
|
+
if (locatorOffset < 0) return undefined;
|
|
208
|
+
|
|
209
|
+
const locator =
|
|
210
|
+
locatorOffset >= tailStart
|
|
211
|
+
? tail.subarray(locatorOffset - tailStart, locatorOffset - tailStart + ZIP64_EOCD_LOCATOR_LENGTH)
|
|
212
|
+
: await readZipRange(filePath, locatorOffset, eocdOffset);
|
|
213
|
+
if (readUInt32LE(locator, 0) !== ZIP64_EOCD_LOCATOR_SIGNATURE) return undefined;
|
|
214
|
+
|
|
215
|
+
const zip64EocdDisk = readUInt32LE(locator, 4);
|
|
216
|
+
const zip64EocdOffset = readUInt64LEAsNumber(locator, 8);
|
|
217
|
+
const totalDisks = readUInt32LE(locator, 16);
|
|
218
|
+
if (zip64EocdDisk !== 0 || totalDisks > 1) {
|
|
219
|
+
throw new ToolError("Multi-disk ZIP archives are not supported");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const record = await readZipRange(filePath, zip64EocdOffset, zip64EocdOffset + 56);
|
|
223
|
+
if (readUInt32LE(record, 0) !== ZIP64_EOCD_SIGNATURE) {
|
|
224
|
+
throw new ToolError("Invalid ZIP archive: missing ZIP64 end of central directory");
|
|
225
|
+
}
|
|
226
|
+
if (readUInt32LE(record, 16) !== 0 || readUInt32LE(record, 20) !== 0) {
|
|
227
|
+
throw new ToolError("Multi-disk ZIP archives are not supported");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
entries: readUInt64LEAsNumber(record, 32),
|
|
232
|
+
size: readUInt64LEAsNumber(record, 40),
|
|
233
|
+
offset: readUInt64LEAsNumber(record, 48),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function readZipCentralDirectoryInfo(filePath: string, fileSize: number): Promise<ZipCentralDirectoryInfo> {
|
|
238
|
+
if (fileSize < ZIP_EOCD_MIN_LENGTH) {
|
|
239
|
+
throw new ToolError("Invalid ZIP archive: missing end of central directory");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const tailLength = Math.min(fileSize, ZIP_EOCD_MIN_LENGTH + ZIP_EOCD_MAX_COMMENT_LENGTH);
|
|
243
|
+
const tailStart = fileSize - tailLength;
|
|
244
|
+
const tail = await readZipRange(filePath, tailStart, fileSize);
|
|
245
|
+
const eocdIndex = findEndOfCentralDirectory(tail);
|
|
246
|
+
const eocdOffset = tailStart + eocdIndex;
|
|
247
|
+
|
|
248
|
+
if (readUInt16LE(tail, eocdIndex + 4) !== 0 || readUInt16LE(tail, eocdIndex + 6) !== 0) {
|
|
249
|
+
throw new ToolError("Multi-disk ZIP archives are not supported");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let entries = readUInt16LE(tail, eocdIndex + 10);
|
|
253
|
+
let size = readUInt32LE(tail, eocdIndex + 12);
|
|
254
|
+
let offset = readUInt32LE(tail, eocdIndex + 16);
|
|
255
|
+
const needsZip64 = entries === ZIP_UINT16_MAX || size === ZIP_UINT32_MAX || offset === ZIP_UINT32_MAX;
|
|
256
|
+
const zip64Info = await readZip64CentralDirectoryInfo(filePath, tail, tailStart, eocdOffset);
|
|
257
|
+
if (zip64Info) {
|
|
258
|
+
({ entries, size, offset } = zip64Info);
|
|
259
|
+
} else if (needsZip64) {
|
|
260
|
+
throw new ToolError("Invalid ZIP archive: missing ZIP64 central directory metadata");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (offset + size > fileSize) {
|
|
264
|
+
throw new ToolError("Invalid ZIP archive: central directory exceeds file size");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return { entries, offset, size };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function readZip64EntryValues(
|
|
271
|
+
extra: Uint8Array,
|
|
272
|
+
placeholders: Zip64EntryPlaceholders,
|
|
273
|
+
current: Zip64EntryValues,
|
|
274
|
+
): Zip64EntryValues {
|
|
275
|
+
if (
|
|
276
|
+
!placeholders.compressedSize &&
|
|
277
|
+
!placeholders.uncompressedSize &&
|
|
278
|
+
!placeholders.localHeaderOffset &&
|
|
279
|
+
!placeholders.diskStart
|
|
280
|
+
) {
|
|
281
|
+
return current;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let offset = 0;
|
|
285
|
+
while (offset + 4 <= extra.byteLength) {
|
|
286
|
+
const headerId = readUInt16LE(extra, offset);
|
|
287
|
+
const dataSize = readUInt16LE(extra, offset + 2);
|
|
288
|
+
const dataStart = offset + 4;
|
|
289
|
+
const dataEnd = dataStart + dataSize;
|
|
290
|
+
if (dataEnd > extra.byteLength) {
|
|
291
|
+
throw new ToolError("Invalid ZIP archive: malformed extra field");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (headerId === 0x0001) {
|
|
295
|
+
let cursor = dataStart;
|
|
296
|
+
let uncompressedSize = current.uncompressedSize;
|
|
297
|
+
let compressedSize = current.compressedSize;
|
|
298
|
+
let localHeaderOffset = current.localHeaderOffset;
|
|
299
|
+
let diskStart = current.diskStart;
|
|
300
|
+
|
|
301
|
+
if (placeholders.uncompressedSize) {
|
|
302
|
+
if (cursor + 8 > dataEnd) throw new ToolError("Invalid ZIP archive: malformed ZIP64 extra field");
|
|
303
|
+
uncompressedSize = readUInt64LEAsNumber(extra, cursor);
|
|
304
|
+
cursor += 8;
|
|
305
|
+
}
|
|
306
|
+
if (placeholders.compressedSize) {
|
|
307
|
+
if (cursor + 8 > dataEnd) throw new ToolError("Invalid ZIP archive: malformed ZIP64 extra field");
|
|
308
|
+
compressedSize = readUInt64LEAsNumber(extra, cursor);
|
|
309
|
+
cursor += 8;
|
|
310
|
+
}
|
|
311
|
+
if (placeholders.localHeaderOffset) {
|
|
312
|
+
if (cursor + 8 > dataEnd) throw new ToolError("Invalid ZIP archive: malformed ZIP64 extra field");
|
|
313
|
+
localHeaderOffset = readUInt64LEAsNumber(extra, cursor);
|
|
314
|
+
cursor += 8;
|
|
315
|
+
}
|
|
316
|
+
if (placeholders.diskStart) {
|
|
317
|
+
if (cursor + 4 > dataEnd) throw new ToolError("Invalid ZIP archive: malformed ZIP64 extra field");
|
|
318
|
+
diskStart = readUInt32LE(extra, cursor);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return { compressedSize, uncompressedSize, localHeaderOffset, diskStart };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
offset = dataEnd;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
throw new ToolError("Invalid ZIP archive: missing ZIP64 extra field");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function parseZipCentralDirectory(
|
|
331
|
+
filePath: string,
|
|
332
|
+
centralDirectory: Uint8Array,
|
|
333
|
+
expectedEntries: number,
|
|
334
|
+
): ArchiveIndexEntry[] {
|
|
335
|
+
const entries: ArchiveIndexEntry[] = [];
|
|
336
|
+
let offset = 0;
|
|
337
|
+
|
|
338
|
+
for (let index = 0; index < expectedEntries; index++) {
|
|
339
|
+
if (offset + 46 > centralDirectory.byteLength) {
|
|
340
|
+
throw new ToolError("Invalid ZIP archive: truncated central directory");
|
|
341
|
+
}
|
|
342
|
+
if (readUInt32LE(centralDirectory, offset) !== ZIP_CENTRAL_DIRECTORY_HEADER_SIGNATURE) {
|
|
343
|
+
throw new ToolError("Invalid ZIP archive: malformed central directory");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const flags = readUInt16LE(centralDirectory, offset + 8);
|
|
347
|
+
const compression = readUInt16LE(centralDirectory, offset + 10);
|
|
348
|
+
const compressedSizeRaw = readUInt32LE(centralDirectory, offset + 20);
|
|
349
|
+
const uncompressedSizeRaw = readUInt32LE(centralDirectory, offset + 24);
|
|
350
|
+
const fileNameLength = readUInt16LE(centralDirectory, offset + 28);
|
|
351
|
+
const extraLength = readUInt16LE(centralDirectory, offset + 30);
|
|
352
|
+
const commentLength = readUInt16LE(centralDirectory, offset + 32);
|
|
353
|
+
const diskStartRaw = readUInt16LE(centralDirectory, offset + 34);
|
|
354
|
+
const localHeaderOffsetRaw = readUInt32LE(centralDirectory, offset + 42);
|
|
355
|
+
const nameStart = offset + 46;
|
|
356
|
+
const extraStart = nameStart + fileNameLength;
|
|
357
|
+
const entryEnd = extraStart + extraLength + commentLength;
|
|
358
|
+
if (entryEnd > centralDirectory.byteLength) {
|
|
359
|
+
throw new ToolError("Invalid ZIP archive: truncated central directory entry");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const rawPath = strFromU8(centralDirectory.subarray(nameStart, extraStart), (flags & ZIP_UTF8_FLAG) === 0);
|
|
363
|
+
const normalizedPath = normalizeArchiveEntryPath(rawPath);
|
|
364
|
+
if (normalizedPath) {
|
|
365
|
+
const values = readZip64EntryValues(
|
|
366
|
+
centralDirectory.subarray(extraStart, extraStart + extraLength),
|
|
367
|
+
{
|
|
368
|
+
compressedSize: compressedSizeRaw === ZIP_UINT32_MAX,
|
|
369
|
+
uncompressedSize: uncompressedSizeRaw === ZIP_UINT32_MAX,
|
|
370
|
+
localHeaderOffset: localHeaderOffsetRaw === ZIP_UINT32_MAX,
|
|
371
|
+
diskStart: diskStartRaw === ZIP_UINT16_MAX,
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
compressedSize: compressedSizeRaw,
|
|
375
|
+
uncompressedSize: uncompressedSizeRaw,
|
|
376
|
+
localHeaderOffset: localHeaderOffsetRaw,
|
|
377
|
+
diskStart: diskStartRaw,
|
|
378
|
+
},
|
|
379
|
+
);
|
|
380
|
+
if (values.diskStart !== 0) {
|
|
381
|
+
throw new ToolError("Multi-disk ZIP archives are not supported");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const isDirectory = isArchiveDirectoryName(rawPath);
|
|
385
|
+
entries.push({
|
|
386
|
+
path: normalizedPath,
|
|
387
|
+
isDirectory,
|
|
388
|
+
size: isDirectory ? 0 : values.uncompressedSize,
|
|
389
|
+
storage: isDirectory
|
|
390
|
+
? undefined
|
|
391
|
+
: {
|
|
392
|
+
type: "zip",
|
|
393
|
+
archivePath: filePath,
|
|
394
|
+
compressedSize: values.compressedSize,
|
|
395
|
+
compression,
|
|
396
|
+
flags,
|
|
397
|
+
localHeaderOffset: values.localHeaderOffset,
|
|
398
|
+
},
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
offset = entryEnd;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return entries;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function readZipFileBytes(storage: ZipStorage, uncompressedSize: number): Promise<Uint8Array> {
|
|
409
|
+
if ((storage.flags & ZIP_ENCRYPTED_FLAG) !== 0) {
|
|
410
|
+
throw new ToolError("Encrypted ZIP entries are not supported");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const localHeader = await readZipRange(
|
|
414
|
+
storage.archivePath,
|
|
415
|
+
storage.localHeaderOffset,
|
|
416
|
+
storage.localHeaderOffset + 30,
|
|
417
|
+
);
|
|
418
|
+
if (readUInt32LE(localHeader, 0) !== ZIP_LOCAL_FILE_HEADER_SIGNATURE) {
|
|
419
|
+
throw new ToolError("Invalid ZIP archive: malformed local file header");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const fileNameLength = readUInt16LE(localHeader, 26);
|
|
423
|
+
const extraLength = readUInt16LE(localHeader, 28);
|
|
424
|
+
const dataStart = storage.localHeaderOffset + 30 + fileNameLength + extraLength;
|
|
425
|
+
const compressedBytes = await readZipRange(storage.archivePath, dataStart, dataStart + storage.compressedSize);
|
|
426
|
+
|
|
427
|
+
if (storage.compression === ZIP_STORED_COMPRESSION) {
|
|
428
|
+
return compressedBytes;
|
|
429
|
+
}
|
|
430
|
+
if (storage.compression !== ZIP_DEFLATE_COMPRESSION) {
|
|
431
|
+
throw new ToolError(`Unsupported ZIP compression method: ${storage.compression}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
try {
|
|
435
|
+
return inflateSync(compressedBytes, { out: new Uint8Array(uncompressedSize) });
|
|
436
|
+
} catch (error) {
|
|
437
|
+
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
126
441
|
async function readTarEntries(bytes: Uint8Array): Promise<ArchiveIndexEntry[]> {
|
|
127
442
|
let archive: Bun.Archive;
|
|
128
443
|
try {
|
|
@@ -155,29 +470,19 @@ async function readTarEntries(bytes: Uint8Array): Promise<ArchiveIndexEntry[]> {
|
|
|
155
470
|
return entries;
|
|
156
471
|
}
|
|
157
472
|
|
|
158
|
-
async function readZipEntries(
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
files = unzipSync(bytes);
|
|
163
|
-
} catch (error) {
|
|
164
|
-
throw new ToolError(error instanceof Error ? error.message : String(error));
|
|
473
|
+
async function readZipEntries(filePath: string): Promise<ArchiveIndexEntry[]> {
|
|
474
|
+
const fileSize = Bun.file(filePath).size;
|
|
475
|
+
if (!Number.isSafeInteger(fileSize)) {
|
|
476
|
+
throw new ToolError("ZIP archive is too large to read safely");
|
|
165
477
|
}
|
|
166
478
|
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
isDirectory,
|
|
175
|
-
size: isDirectory ? 0 : fileBytes.byteLength,
|
|
176
|
-
storage: isDirectory ? undefined : { type: "zip", bytes: fileBytes },
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return entries;
|
|
479
|
+
const directoryInfo = await readZipCentralDirectoryInfo(filePath, fileSize);
|
|
480
|
+
const centralDirectory = await readZipRange(
|
|
481
|
+
filePath,
|
|
482
|
+
directoryInfo.offset,
|
|
483
|
+
directoryInfo.offset + directoryInfo.size,
|
|
484
|
+
);
|
|
485
|
+
return parseZipCentralDirectory(filePath, centralDirectory, directoryInfo.entries);
|
|
181
486
|
}
|
|
182
487
|
|
|
183
488
|
export function parseArchivePathCandidates(filePath: string): ArchivePathCandidate[] {
|
|
@@ -297,7 +602,10 @@ export class ArchiveReader {
|
|
|
297
602
|
throw new ToolError(`Archive file '${normalizedPath}' has no readable storage`);
|
|
298
603
|
}
|
|
299
604
|
|
|
300
|
-
const bytes =
|
|
605
|
+
const bytes =
|
|
606
|
+
entry.storage.type === "tar"
|
|
607
|
+
? await entry.storage.file.bytes()
|
|
608
|
+
: await readZipFileBytes(entry.storage, entry.size);
|
|
301
609
|
|
|
302
610
|
return {
|
|
303
611
|
path: entry.path,
|
|
@@ -315,7 +623,7 @@ export async function openArchive(filePath: string): Promise<ArchiveReader> {
|
|
|
315
623
|
throw new ToolError(`Unsupported archive format: ${filePath}`);
|
|
316
624
|
}
|
|
317
625
|
|
|
318
|
-
const
|
|
319
|
-
|
|
626
|
+
const entries =
|
|
627
|
+
format === "zip" ? await readZipEntries(filePath) : await readTarEntries(await Bun.file(filePath).bytes());
|
|
320
628
|
return new ArchiveReader(format, entries);
|
|
321
629
|
}
|
package/src/tools/bash.ts
CHANGED
|
@@ -10,7 +10,6 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
10
10
|
import { ImageProtocol, TERMINAL } from "@oh-my-pi/pi-tui";
|
|
11
11
|
import { getProjectDir, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import * as z from "zod/v4";
|
|
13
|
-
import { AsyncJobManager } from "../async";
|
|
14
13
|
import { type BashResult, executeBash } from "../exec/bash-executor";
|
|
15
14
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
16
15
|
import { InternalUrlRouter } from "../internal-urls";
|
|
@@ -489,7 +488,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
489
488
|
onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
|
|
490
489
|
startBackgrounded: boolean;
|
|
491
490
|
}): ManagedBashJobHandle {
|
|
492
|
-
const manager =
|
|
491
|
+
const manager = this.session.asyncJobManager;
|
|
493
492
|
if (!manager) {
|
|
494
493
|
throw new ToolError("Background job manager unavailable for this session.");
|
|
495
494
|
}
|
|
@@ -716,7 +715,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
716
715
|
if (timeoutClampNotice) pendingNotices.push(timeoutClampNotice);
|
|
717
716
|
|
|
718
717
|
if (asyncRequested) {
|
|
719
|
-
if (!
|
|
718
|
+
if (!this.session.asyncJobManager) {
|
|
720
719
|
throw new ToolError("Async job manager unavailable for this session.");
|
|
721
720
|
}
|
|
722
721
|
const job = this.#startManagedBashJob({
|
|
@@ -737,7 +736,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
737
736
|
});
|
|
738
737
|
}
|
|
739
738
|
|
|
740
|
-
const autoBgManager =
|
|
739
|
+
const autoBgManager = this.session.asyncJobManager;
|
|
741
740
|
if (this.#autoBackgroundEnabled && !pty && autoBgManager) {
|
|
742
741
|
const autoBackgroundWaitMs = this.#resolveAutoBackgroundWaitMs(timeoutMs);
|
|
743
742
|
const startBackgrounded = autoBackgroundWaitMs === 0;
|
package/src/tools/fetch.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { type Theme, theme } from "../modes/theme/theme";
|
|
|
13
13
|
import type { ToolSession } from "../sdk";
|
|
14
14
|
import type { AgentStorage } from "../session/agent-storage";
|
|
15
15
|
import { DEFAULT_MAX_BYTES, truncateHead } from "../session/streaming-output";
|
|
16
|
-
import { renderStatusLine } from "../tui";
|
|
16
|
+
import { renderStatusLine, urlHyperlink } from "../tui";
|
|
17
17
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
18
18
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
19
19
|
import { ensureTool } from "../utils/tools-manager";
|
|
@@ -1437,6 +1437,27 @@ function countNonEmptyLines(text: string): number {
|
|
|
1437
1437
|
return text.split("\n").filter(l => l.trim()).length;
|
|
1438
1438
|
}
|
|
1439
1439
|
|
|
1440
|
+
function readUrlLinkTarget(input: string): string {
|
|
1441
|
+
try {
|
|
1442
|
+
return parseReadUrlTarget(input)?.path ?? input;
|
|
1443
|
+
} catch {
|
|
1444
|
+
return input;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
function formatReadUrlDescription(input: string): string {
|
|
1449
|
+
const target = readUrlLinkTarget(input);
|
|
1450
|
+
const displayUrl = target.match(/^www\./i) ? `https://${target}` : target;
|
|
1451
|
+
const domain = getDomain(displayUrl);
|
|
1452
|
+
const urlPath = truncate(displayUrl.replace(/^https?:\/\/[^/]+/, ""), 50, "…");
|
|
1453
|
+
const label = `${domain}${urlPath ? ` ${urlPath}` : ""}`.trim();
|
|
1454
|
+
return urlHyperlink(target, label);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
function formatReadUrlMetadataValue(url: string, uiTheme: Theme): string {
|
|
1458
|
+
return urlHyperlink(url, uiTheme.fg("mdLinkUrl", url));
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1440
1461
|
/** Render URL read call (URL preview) */
|
|
1441
1462
|
export function renderReadUrlCall(
|
|
1442
1463
|
args: { path?: string; url?: string; raw?: boolean },
|
|
@@ -1444,9 +1465,7 @@ export function renderReadUrlCall(
|
|
|
1444
1465
|
uiTheme: Theme = theme,
|
|
1445
1466
|
): Component {
|
|
1446
1467
|
const url = args.path ?? args.url ?? "";
|
|
1447
|
-
const
|
|
1448
|
-
const path = truncate(url.replace(/^https?:\/\/[^/]+/, ""), 50, "…");
|
|
1449
|
-
const description = `${domain}${path ? ` ${path}` : ""}`.trim();
|
|
1468
|
+
const description = formatReadUrlDescription(url);
|
|
1450
1469
|
const meta: string[] = [];
|
|
1451
1470
|
if (args.raw) meta.push("raw");
|
|
1452
1471
|
const text = renderStatusLine({ icon: "pending", title: "Read", description, meta }, uiTheme);
|
|
@@ -1465,7 +1484,7 @@ export function renderReadUrlResult(
|
|
|
1465
1484
|
const rawErrorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1466
1485
|
const errorText = (rawErrorText || "No response data").replace(/^Error:\s*/, "");
|
|
1467
1486
|
const urlText = details?.finalUrl ?? details?.url ?? "";
|
|
1468
|
-
const description = urlText ?
|
|
1487
|
+
const description = urlText ? formatReadUrlDescription(urlText) : undefined;
|
|
1469
1488
|
const header = renderStatusLine({ icon: "error", title: "Read", description }, uiTheme);
|
|
1470
1489
|
const errorLines = errorText.split("\n").map(line => uiTheme.fg("error", replaceTabs(line)));
|
|
1471
1490
|
const outputBlock = new CachedOutputBlock();
|
|
@@ -1476,8 +1495,7 @@ export function renderReadUrlResult(
|
|
|
1476
1495
|
};
|
|
1477
1496
|
}
|
|
1478
1497
|
|
|
1479
|
-
const
|
|
1480
|
-
const path = truncate(details.finalUrl.replace(/^https?:\/\/[^/]+/, ""), 50, "…");
|
|
1498
|
+
const description = formatReadUrlDescription(details.finalUrl);
|
|
1481
1499
|
const hasRedirect = details.url !== details.finalUrl;
|
|
1482
1500
|
const hasNotes = details.notes.length > 0;
|
|
1483
1501
|
const truncation = details.meta?.truncation;
|
|
@@ -1487,7 +1505,7 @@ export function renderReadUrlResult(
|
|
|
1487
1505
|
{
|
|
1488
1506
|
icon: truncated ? "warning" : "success",
|
|
1489
1507
|
title: "Read",
|
|
1490
|
-
description
|
|
1508
|
+
description,
|
|
1491
1509
|
},
|
|
1492
1510
|
uiTheme,
|
|
1493
1511
|
);
|
|
@@ -1505,7 +1523,9 @@ export function renderReadUrlResult(
|
|
|
1505
1523
|
`${uiTheme.fg("muted", "Method:")} ${details.method}`,
|
|
1506
1524
|
];
|
|
1507
1525
|
if (hasRedirect) {
|
|
1508
|
-
metadataLines.push(
|
|
1526
|
+
metadataLines.push(
|
|
1527
|
+
`${uiTheme.fg("muted", "Final URL:")} ${formatReadUrlMetadataValue(details.finalUrl, uiTheme)}`,
|
|
1528
|
+
);
|
|
1509
1529
|
}
|
|
1510
1530
|
const lineLabel = `${lineCount} line${lineCount === 1 ? "" : "s"}`;
|
|
1511
1531
|
metadataLines.push(`${uiTheme.fg("muted", "Lines:")} ${lineLabel}`);
|
package/src/tools/gh.ts
CHANGED
|
@@ -792,6 +792,18 @@ function repoFromRepositoryUrl(value: string | undefined): string | undefined {
|
|
|
792
792
|
return value.slice(REPO_API_URL_PREFIX.length);
|
|
793
793
|
}
|
|
794
794
|
|
|
795
|
+
function githubRepoSlugEquals(left: string | undefined, right: string): boolean {
|
|
796
|
+
if (left === undefined || left.length !== right.length) return false;
|
|
797
|
+
for (let idx = 0; idx < left.length; idx += 1) {
|
|
798
|
+
let leftCode = left.charCodeAt(idx);
|
|
799
|
+
let rightCode = right.charCodeAt(idx);
|
|
800
|
+
if (leftCode >= 65 && leftCode <= 90) leftCode += 32;
|
|
801
|
+
if (rightCode >= 65 && rightCode <= 90) rightCode += 32;
|
|
802
|
+
if (leftCode !== rightCode) return false;
|
|
803
|
+
}
|
|
804
|
+
return true;
|
|
805
|
+
}
|
|
806
|
+
|
|
795
807
|
function apiUserToGhUser(user: GhApiUser | null | undefined): GhUser | undefined {
|
|
796
808
|
if (!user) return undefined;
|
|
797
809
|
const login = user.login ?? undefined;
|
|
@@ -1646,7 +1658,7 @@ async function resolveGitHubRepo(
|
|
|
1646
1658
|
runRepo: string | undefined,
|
|
1647
1659
|
signal?: AbortSignal,
|
|
1648
1660
|
): Promise<string> {
|
|
1649
|
-
if (repo && runRepo && repo
|
|
1661
|
+
if (repo && runRepo && !githubRepoSlugEquals(repo, runRepo)) {
|
|
1650
1662
|
throw new ToolError("run URL repository does not match the provided repo");
|
|
1651
1663
|
}
|
|
1652
1664
|
|
|
@@ -1712,6 +1724,33 @@ export async function resolveDefaultRepoMemoized(cwd: string, signal?: AbortSign
|
|
|
1712
1724
|
return untilAborted(signal, pending);
|
|
1713
1725
|
}
|
|
1714
1726
|
|
|
1727
|
+
/**
|
|
1728
|
+
* Best-effort cached cwd → `owner/repo` resolution that swallows any failure
|
|
1729
|
+
* (not a git checkout, no GitHub remote, `gh` unauthenticated, …) into
|
|
1730
|
+
* `undefined`. Use where the cwd repo is a convenience fallback, not a safety
|
|
1731
|
+
* check.
|
|
1732
|
+
*/
|
|
1733
|
+
async function tryResolveCurrentRepo(cwd: string, signal: AbortSignal | undefined): Promise<string | undefined> {
|
|
1734
|
+
try {
|
|
1735
|
+
return await resolveDefaultRepoMemoized(cwd, signal);
|
|
1736
|
+
} catch {
|
|
1737
|
+
return undefined;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/**
|
|
1742
|
+
* Best-effort fresh cwd → `owner/repo` resolution for safety checks that must
|
|
1743
|
+
* reflect the repository currently mounted at `cwd`, not the process-lifetime
|
|
1744
|
+
* default-repo cache.
|
|
1745
|
+
*/
|
|
1746
|
+
async function tryResolveCurrentRepoFresh(cwd: string, signal: AbortSignal | undefined): Promise<string | undefined> {
|
|
1747
|
+
try {
|
|
1748
|
+
return await resolveGitHubRepo(cwd, undefined, undefined, signal);
|
|
1749
|
+
} catch {
|
|
1750
|
+
return undefined;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1715
1754
|
/**
|
|
1716
1755
|
* Matches search-query qualifiers that already scope to a repository, org, or
|
|
1717
1756
|
* user. When present, callers should avoid layering a default `repo:<current>`
|
|
@@ -1738,11 +1777,7 @@ async function resolveSearchRepoScope(
|
|
|
1738
1777
|
): Promise<string | undefined> {
|
|
1739
1778
|
if (repo) return repo;
|
|
1740
1779
|
if (query && REPO_SCOPE_QUALIFIER_PATTERN.test(query)) return undefined;
|
|
1741
|
-
|
|
1742
|
-
return await resolveDefaultRepoMemoized(cwd, signal);
|
|
1743
|
-
} catch {
|
|
1744
|
-
return undefined;
|
|
1745
|
-
}
|
|
1780
|
+
return tryResolveCurrentRepo(cwd, signal);
|
|
1746
1781
|
}
|
|
1747
1782
|
|
|
1748
1783
|
async function resolveGitHubBranchHead(
|
|
@@ -3338,8 +3373,9 @@ async function executeRunWatch(
|
|
|
3338
3373
|
onUpdate: AgentToolUpdateCallback<GhToolDetails> | undefined,
|
|
3339
3374
|
): Promise<AgentToolResult<GhToolDetails>> {
|
|
3340
3375
|
const branchInput = normalizeOptionalString(params.branch);
|
|
3376
|
+
const explicitRepo = normalizeOptionalString(params.repo);
|
|
3341
3377
|
const runReference = parseRunReference(params.run);
|
|
3342
|
-
const repo = await resolveGitHubRepo(session.cwd,
|
|
3378
|
+
const repo = await resolveGitHubRepo(session.cwd, explicitRepo, runReference.repo, signal);
|
|
3343
3379
|
const intervalSeconds = RUN_WATCH_INTERVAL_DEFAULT;
|
|
3344
3380
|
const graceSeconds = RUN_WATCH_GRACE_DEFAULT;
|
|
3345
3381
|
const tail = resolveTailLimit(params.tail);
|
|
@@ -3419,10 +3455,28 @@ async function executeRunWatch(
|
|
|
3419
3455
|
}
|
|
3420
3456
|
}
|
|
3421
3457
|
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3458
|
+
let branch: string;
|
|
3459
|
+
let headSha: string;
|
|
3460
|
+
if (branchInput) {
|
|
3461
|
+
branch = branchInput;
|
|
3462
|
+
headSha = await resolveGitHubBranchHead(session.cwd, repo, branch, signal);
|
|
3463
|
+
} else {
|
|
3464
|
+
// No branch/run selector — derive the commit from the current checkout,
|
|
3465
|
+
// but only when cwd actually points at `repo`. Otherwise we'd watch an
|
|
3466
|
+
// unrelated commit SHA against the explicit repo and silently stream a
|
|
3467
|
+
// confident wrong-repo status (issue #1949). GitHub `owner/repo` slugs
|
|
3468
|
+
// are case-insensitive — `gh repo view` returns the canonical casing
|
|
3469
|
+
// while callers may pass any casing — so the equality check normalizes
|
|
3470
|
+
// both sides before deciding the cwd is a different repo (PR #1951).
|
|
3471
|
+
const cwdRepo = await tryResolveCurrentRepoFresh(session.cwd, signal);
|
|
3472
|
+
if (!githubRepoSlugEquals(cwdRepo, repo)) {
|
|
3473
|
+
throw new ToolError(
|
|
3474
|
+
`Cannot infer the watched commit for ${repo}: current checkout is ${cwdRepo ?? "not a GitHub repository"}. Pass \`branch\` or \`run\` to scope the watch.`,
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
branch = await requireCurrentGitBranch(session.cwd, signal);
|
|
3478
|
+
headSha = await requireCurrentGitHead(session.cwd, signal);
|
|
3479
|
+
}
|
|
3426
3480
|
let pollCount = 0;
|
|
3427
3481
|
let settledSuccessSignature: string | undefined;
|
|
3428
3482
|
|