@oh-my-pi/pi-coding-agent 16.0.9 → 16.0.10
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 +23 -0
- package/dist/cli.js +2822 -2872
- package/dist/types/collab/host.d.ts +2 -2
- package/dist/types/collab/protocol.d.ts +4 -5
- package/dist/types/config/model-resolver.d.ts +11 -2
- package/dist/types/config/settings-schema.d.ts +12 -2
- package/dist/types/session/agent-session.d.ts +13 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/slash-commands/helpers/collab-qrcode.d.ts +13 -0
- package/dist/types/tools/index.d.ts +9 -1
- package/dist/types/utils/image-loading.d.ts +12 -0
- package/dist/types/utils/qrcode.d.ts +48 -0
- package/package.json +12 -12
- package/src/cli/args.ts +7 -1
- package/src/collab/host.ts +4 -4
- package/src/collab/protocol.ts +48 -15
- package/src/config/config-file.ts +1 -1
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-registry.ts +16 -4
- package/src/config/model-resolver.ts +193 -35
- package/src/config/settings-schema.ts +14 -2
- package/src/config/settings.ts +3 -3
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/main.ts +2 -2
- package/src/modes/components/oauth-selector.ts +31 -2
- package/src/prompts/tools/inspect-image.md +1 -1
- package/src/sdk.ts +26 -7
- package/src/session/agent-session.ts +93 -14
- package/src/slash-commands/builtin-registry.ts +29 -11
- package/src/slash-commands/helpers/collab-qrcode.ts +28 -0
- package/src/thinking.ts +25 -5
- package/src/tools/index.ts +10 -1
- package/src/tools/inspect-image.ts +72 -9
- package/src/utils/file-mentions.ts +5 -2
- package/src/utils/image-loading.ts +58 -0
- package/src/utils/qrcode.ts +535 -0
package/src/tools/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
2
2
|
import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
import type { FetchImpl, Model, ToolChoice } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import type { FetchImpl, ImageContent, Model, ToolChoice } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import type { AsyncJobManager } from "../async/job-manager";
|
|
6
6
|
import type { Rule } from "../capability/rule";
|
|
@@ -113,6 +113,13 @@ export type ContextFileEntry = {
|
|
|
113
113
|
depth?: number;
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
+
/** Image attachment handle exposed to tools for user-facing labels such as `Image #1`. */
|
|
117
|
+
export type ImageAttachmentEntry = {
|
|
118
|
+
label: string;
|
|
119
|
+
uri: string;
|
|
120
|
+
image: ImageContent;
|
|
121
|
+
};
|
|
122
|
+
|
|
116
123
|
export type {
|
|
117
124
|
DiscoverableTool,
|
|
118
125
|
DiscoverableToolSearchIndex,
|
|
@@ -353,6 +360,8 @@ export interface ToolSession {
|
|
|
353
360
|
/** Get the active OpenTelemetry config so subagent dispatch can forward
|
|
354
361
|
* the parent's tracer/hooks with the subagent's own identity stamped. */
|
|
355
362
|
getTelemetry?: () => AgentTelemetryConfig | undefined;
|
|
363
|
+
/** Return image attachments visible to tools for resolving labels such as `Image #1`. */
|
|
364
|
+
getImageAttachments?: () => ImageAttachmentEntry[];
|
|
356
365
|
}
|
|
357
366
|
|
|
358
367
|
export type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { instrumentedCompleteSimple, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
import { type Api, completeSimple, type Model, type ToolExample } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { type Api, completeSimple, type ImageContent, type Model, type ToolExample } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { type } from "arktype";
|
|
6
6
|
import { extractTextContent } from "../commit/utils";
|
|
@@ -11,6 +11,7 @@ import inspectImageSystemPromptTemplate from "../prompts/tools/inspect-image-sys
|
|
|
11
11
|
import {
|
|
12
12
|
ImageInputTooLargeError,
|
|
13
13
|
type LoadedImageInput,
|
|
14
|
+
loadImageAttachmentInput,
|
|
14
15
|
loadImageInput,
|
|
15
16
|
MAX_IMAGE_INPUT_BYTES,
|
|
16
17
|
webpExclusionForModel,
|
|
@@ -19,13 +20,62 @@ import type { ToolSession } from "./index";
|
|
|
19
20
|
import { ToolError } from "./tool-errors";
|
|
20
21
|
|
|
21
22
|
const inspectImageSchema = type({
|
|
22
|
-
path: type("string").describe("image path"),
|
|
23
|
+
path: type("string").describe("image file path, Image #N label, or attachment://N URI"),
|
|
23
24
|
question: type("string").describe("question about image"),
|
|
24
25
|
"+": "reject",
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
export type InspectImageParams = typeof inspectImageSchema.infer;
|
|
28
29
|
|
|
30
|
+
interface ImageAttachmentReference {
|
|
31
|
+
index: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const IMAGE_ATTACHMENT_REFERENCE_REGEX =
|
|
35
|
+
/^\s*(?:\[?Image #([1-9]\d*)(?:,[^\]\n]*)?\]?|(?:attachment|image):\/\/([1-9]\d*))\s*$/i;
|
|
36
|
+
|
|
37
|
+
function parseImageAttachmentReference(path: string): ImageAttachmentReference | null {
|
|
38
|
+
const match = IMAGE_ATTACHMENT_REFERENCE_REGEX.exec(path);
|
|
39
|
+
if (!match) return null;
|
|
40
|
+
const rawIndex = match[1] ?? match[2];
|
|
41
|
+
if (!rawIndex) return null;
|
|
42
|
+
return { index: Number(rawIndex) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatAvailableImageAttachments(attachments: readonly { label: string; uri: string }[]): string {
|
|
46
|
+
if (attachments.length === 0) return "none";
|
|
47
|
+
return attachments.map(attachment => `${attachment.label} -> ${attachment.uri}`).join(", ");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function loadAttachmentReferenceInput(options: {
|
|
51
|
+
path: string;
|
|
52
|
+
reference: ImageAttachmentReference;
|
|
53
|
+
attachments: readonly { label: string; uri: string; image: ImageContent }[];
|
|
54
|
+
autoResize: boolean;
|
|
55
|
+
excludeWebP: boolean | undefined;
|
|
56
|
+
}): Promise<LoadedImageInput | null> {
|
|
57
|
+
const attachment = options.attachments[options.reference.index - 1];
|
|
58
|
+
if (!attachment) {
|
|
59
|
+
const available = formatAvailableImageAttachments(options.attachments);
|
|
60
|
+
if (options.attachments.length === 0) {
|
|
61
|
+
throw new ToolError(
|
|
62
|
+
`No image attachments are available in this turn. path="${options.path}" must be a readable file path or attachment URI.`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
throw new ToolError(
|
|
66
|
+
`Could not resolve image attachment '${options.path}'. Available image attachments: ${available}. Pass an attachment URI or a readable filesystem path.`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return loadImageAttachmentInput({
|
|
70
|
+
image: attachment.image,
|
|
71
|
+
label: attachment.label,
|
|
72
|
+
uri: attachment.uri,
|
|
73
|
+
autoResize: options.autoResize,
|
|
74
|
+
maxBytes: MAX_IMAGE_INPUT_BYTES,
|
|
75
|
+
excludeWebP: options.excludeWebP,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
29
79
|
export interface InspectImageToolDetails {
|
|
30
80
|
model: string;
|
|
31
81
|
imagePath: string;
|
|
@@ -129,14 +179,27 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
|
|
|
129
179
|
}
|
|
130
180
|
|
|
131
181
|
let imageInput: LoadedImageInput | null;
|
|
182
|
+
const autoResize = this.session.settings.get("images.autoResize");
|
|
183
|
+
const excludeWebP = webpExclusionForModel(model);
|
|
184
|
+
const attachmentReference = parseImageAttachmentReference(params.path);
|
|
132
185
|
try {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
186
|
+
if (attachmentReference) {
|
|
187
|
+
imageInput = await loadAttachmentReferenceInput({
|
|
188
|
+
path: params.path,
|
|
189
|
+
reference: attachmentReference,
|
|
190
|
+
attachments: this.session.getImageAttachments?.() ?? [],
|
|
191
|
+
autoResize,
|
|
192
|
+
excludeWebP,
|
|
193
|
+
});
|
|
194
|
+
} else {
|
|
195
|
+
imageInput = await loadImageInput({
|
|
196
|
+
path: params.path,
|
|
197
|
+
cwd: this.session.cwd,
|
|
198
|
+
autoResize,
|
|
199
|
+
maxBytes: MAX_IMAGE_INPUT_BYTES,
|
|
200
|
+
excludeWebP,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
140
203
|
} catch (error) {
|
|
141
204
|
if (error instanceof ImageInputTooLargeError) {
|
|
142
205
|
throw new ToolError(error.message);
|
|
@@ -24,7 +24,7 @@ import { resolveReadPath } from "../tools/path-utils";
|
|
|
24
24
|
import { formatDimensionNote, resizeImage } from "./image-resize";
|
|
25
25
|
|
|
26
26
|
/** Regex to match @filepath patterns in text */
|
|
27
|
-
const FILE_MENTION_REGEX = /@([^\s@]+)/g;
|
|
27
|
+
const FILE_MENTION_REGEX = /@(?:"([^"]+)"|'([^']+)'|([^\s@]+))/g;
|
|
28
28
|
const LEADING_PUNCTUATION_REGEX = /^[`"'([{<]+/;
|
|
29
29
|
const TRAILING_PUNCTUATION_REGEX = /[)\]}>.,;:!?"'`]+$/;
|
|
30
30
|
const MENTION_BOUNDARY_REGEX = /[\s([{<"'`]/;
|
|
@@ -168,7 +168,10 @@ export function extractFileMentions(text: string): string[] {
|
|
|
168
168
|
const index = match.index ?? 0;
|
|
169
169
|
if (!isMentionBoundary(text, index)) continue;
|
|
170
170
|
|
|
171
|
-
const
|
|
171
|
+
const rawPath = match[1] ?? match[2] ?? match[3];
|
|
172
|
+
if (!rawPath) continue;
|
|
173
|
+
|
|
174
|
+
const cleaned = match[1] !== undefined || match[2] !== undefined ? rawPath.trim() : sanitizeMentionPath(rawPath);
|
|
172
175
|
if (!cleaned) continue;
|
|
173
176
|
|
|
174
177
|
mentions.push(cleaned);
|
|
@@ -38,6 +38,17 @@ export interface LoadImageInputOptions {
|
|
|
38
38
|
excludeWebP?: boolean;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** Options for loading an in-memory chat image attachment as a vision-model input. */
|
|
42
|
+
export interface LoadImageAttachmentInputOptions {
|
|
43
|
+
image: ImageContent;
|
|
44
|
+
label: string;
|
|
45
|
+
uri: string;
|
|
46
|
+
autoResize: boolean;
|
|
47
|
+
maxBytes?: number;
|
|
48
|
+
/** Force non-WebP output (e.g. for Ollama). Leave unset to honor `OMP_NO_WEBP`. */
|
|
49
|
+
excludeWebP?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
export interface LoadedImageInput {
|
|
42
53
|
resolvedPath: string;
|
|
43
54
|
mimeType: string;
|
|
@@ -161,3 +172,50 @@ export async function loadImageInput(options: LoadImageInputOptions): Promise<Lo
|
|
|
161
172
|
bytes: outputBytes,
|
|
162
173
|
};
|
|
163
174
|
}
|
|
175
|
+
|
|
176
|
+
/** Loads a chat attachment image through the same size and encoder policy as file-backed image inputs. */
|
|
177
|
+
export async function loadImageAttachmentInput(
|
|
178
|
+
options: LoadImageAttachmentInputOptions,
|
|
179
|
+
): Promise<LoadedImageInput | null> {
|
|
180
|
+
const maxBytes = options.maxBytes ?? MAX_IMAGE_INPUT_BYTES;
|
|
181
|
+
if (!SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(options.image.mimeType)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const inputBytes = Buffer.byteLength(options.image.data, "base64");
|
|
186
|
+
if (inputBytes > maxBytes) {
|
|
187
|
+
throw new ImageInputTooLargeError(inputBytes, maxBytes);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let outputData = options.image.data;
|
|
191
|
+
let outputMimeType = options.image.mimeType;
|
|
192
|
+
let outputBytes = inputBytes;
|
|
193
|
+
let dimensionNote: string | undefined;
|
|
194
|
+
|
|
195
|
+
const shouldReencodeWebP = options.excludeWebP === true && options.image.mimeType === "image/webp";
|
|
196
|
+
if (options.autoResize || shouldReencodeWebP) {
|
|
197
|
+
try {
|
|
198
|
+
const resized = await resizeImage(options.image, { excludeWebP: options.excludeWebP });
|
|
199
|
+
outputData = resized.data;
|
|
200
|
+
outputMimeType = resized.mimeType;
|
|
201
|
+
outputBytes = resized.buffer.byteLength;
|
|
202
|
+
dimensionNote = formatDimensionNote(resized);
|
|
203
|
+
} catch {
|
|
204
|
+
// keep original image when resize fails
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let textNote = `Read image attachment ${options.label} [${outputMimeType}]`;
|
|
209
|
+
if (dimensionNote) {
|
|
210
|
+
textNote += `\n${dimensionNote}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
resolvedPath: options.uri,
|
|
215
|
+
mimeType: outputMimeType,
|
|
216
|
+
data: outputData,
|
|
217
|
+
textNote,
|
|
218
|
+
dimensionNote,
|
|
219
|
+
bytes: outputBytes,
|
|
220
|
+
};
|
|
221
|
+
}
|