@omercnet/paseo-omp 0.4.0-next.119.1 → 0.4.0-next.120.1
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/client/omp-config-styles.ts +284 -0
- package/client/omp-config-surface.tsx +1 -279
- package/client/omp-routing-editor.tsx +1 -1
- package/package.json +1 -1
- package/server/package-version.ts +1 -1
- package/server/provider/config-normalization.ts +1 -1
- package/server/provider/mcp-transport.ts +1 -1
- package/server/provider/omp-rpc-environment.ts +368 -0
- package/server/provider/omp-rpc-process.ts +119 -0
- package/server/provider/omp-rpc-values.ts +31 -0
- package/server/provider/omp-rpc.ts +22 -492
- package/server/provider/profile-providers.ts +2 -1
- package/server/provider/prompt-payload.ts +247 -0
- package/server/provider/session.ts +11 -243
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { ProviderContent, ProviderInput } from "@getpaseo/plugin/server/provider";
|
|
2
|
+
import { getForgeDefinitionOrNeutral } from "@getpaseo/protocol/forge-manifest";
|
|
3
|
+
import { isValidImagePayload } from "./image";
|
|
4
|
+
import type { OmpImage } from "./omp-rpc";
|
|
5
|
+
import { OmpPublicError, utf8Bytes } from "./security";
|
|
6
|
+
|
|
7
|
+
type SessionPromptInput = Extract<ProviderInput, { type: "session.prompt" }>;
|
|
8
|
+
const MAX_PROMPT_PARTS = 64;
|
|
9
|
+
export const MAX_PROMPT_TEXT_LENGTH = 1024 * 1024;
|
|
10
|
+
const RPC_REQUEST_ID_BYTES = 36;
|
|
11
|
+
|
|
12
|
+
export function isSafeCommandName(name: string): boolean {
|
|
13
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]*(?::[A-Za-z0-9][A-Za-z0-9._-]*)*$/u.test(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type OmpPromptPayload = { text: string; images: OmpImage[]; commandName?: string };
|
|
17
|
+
|
|
18
|
+
const REVIEW_LINE_MARKERS = { add: "+", remove: "-", context: " " } as const;
|
|
19
|
+
|
|
20
|
+
function renderPromptAttachmentAsText(part: Exclude<ProviderContent, { type: "image" }>): string {
|
|
21
|
+
switch (part.type) {
|
|
22
|
+
case "forge_change_request": {
|
|
23
|
+
return renderChangeRequestAttachment({
|
|
24
|
+
forge: part.forge ?? "github",
|
|
25
|
+
number: part.number,
|
|
26
|
+
title: part.title,
|
|
27
|
+
url: part.url,
|
|
28
|
+
body: part.body,
|
|
29
|
+
projectPath: part.projectPath,
|
|
30
|
+
baseRefName: part.baseRefName,
|
|
31
|
+
headRefName: part.headRefName,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
case "github_pr": {
|
|
35
|
+
return renderChangeRequestAttachment({
|
|
36
|
+
forge: "github",
|
|
37
|
+
number: part.number,
|
|
38
|
+
title: part.title,
|
|
39
|
+
url: part.url,
|
|
40
|
+
body: part.body,
|
|
41
|
+
baseRefName: part.baseRefName,
|
|
42
|
+
headRefName: part.headRefName,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
case "forge_issue": {
|
|
46
|
+
return renderIssueAttachment({
|
|
47
|
+
forge: part.forge ?? "github",
|
|
48
|
+
number: part.number,
|
|
49
|
+
title: part.title,
|
|
50
|
+
url: part.url,
|
|
51
|
+
body: part.body,
|
|
52
|
+
projectPath: part.projectPath,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
case "github_issue": {
|
|
56
|
+
return renderIssueAttachment({
|
|
57
|
+
forge: "github",
|
|
58
|
+
number: part.number,
|
|
59
|
+
title: part.title,
|
|
60
|
+
url: part.url,
|
|
61
|
+
body: part.body,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
case "text": {
|
|
65
|
+
return part.text;
|
|
66
|
+
}
|
|
67
|
+
case "review": {
|
|
68
|
+
const lines = [`Paseo review attachment (${part.mode})`, `CWD: ${part.cwd}`];
|
|
69
|
+
if (part.baseRef) {
|
|
70
|
+
lines.push(`Base: ${part.baseRef}`);
|
|
71
|
+
}
|
|
72
|
+
part.comments.forEach((comment, index) => {
|
|
73
|
+
lines.push(
|
|
74
|
+
"",
|
|
75
|
+
`Comment ${index + 1}: ${comment.filePath}:${comment.side}:${comment.lineNumber}`,
|
|
76
|
+
comment.body,
|
|
77
|
+
comment.context.hunkHeader,
|
|
78
|
+
);
|
|
79
|
+
const target = comment.context.targetLine;
|
|
80
|
+
for (const line of comment.context.lines) {
|
|
81
|
+
const isTarget =
|
|
82
|
+
line.oldLineNumber === target.oldLineNumber &&
|
|
83
|
+
line.newLineNumber === target.newLineNumber &&
|
|
84
|
+
line.type === target.type &&
|
|
85
|
+
line.content === target.content;
|
|
86
|
+
const prefix = isTarget ? "> " : " ";
|
|
87
|
+
const oldLn = padLineNumber(line.oldLineNumber);
|
|
88
|
+
const newLn = padLineNumber(line.newLineNumber);
|
|
89
|
+
lines.push(`${prefix}${oldLn} ${newLn} ${REVIEW_LINE_MARKERS[line.type]}${line.content}`);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
case "uploaded_file": {
|
|
95
|
+
return [
|
|
96
|
+
`Uploaded file: ${part.fileName}`,
|
|
97
|
+
`Path: ${part.path}`,
|
|
98
|
+
`MIME: ${part.mimeType}`,
|
|
99
|
+
`Size: ${part.size} bytes`,
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
default:
|
|
103
|
+
throw new Error("unreachable");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderChangeRequestAttachment(input: {
|
|
108
|
+
forge: string;
|
|
109
|
+
number: number;
|
|
110
|
+
title: string;
|
|
111
|
+
url: string;
|
|
112
|
+
body?: string | null;
|
|
113
|
+
projectPath?: string;
|
|
114
|
+
baseRefName?: string | null;
|
|
115
|
+
headRefName?: string | null;
|
|
116
|
+
}): string {
|
|
117
|
+
const lines = [
|
|
118
|
+
`${formatForgeLabel(input.forge)} ${formatChangeRequestAbbrev(input.forge)} ${formatChangeRequestNumber(input.forge, input.number)}: ${input.title}`,
|
|
119
|
+
input.url,
|
|
120
|
+
];
|
|
121
|
+
if (input.projectPath) {
|
|
122
|
+
lines.push(`Project: ${input.projectPath}`);
|
|
123
|
+
}
|
|
124
|
+
if (input.baseRefName) {
|
|
125
|
+
lines.push(`Base: ${input.baseRefName}`);
|
|
126
|
+
}
|
|
127
|
+
if (input.headRefName) {
|
|
128
|
+
lines.push(`Head: ${input.headRefName}`);
|
|
129
|
+
}
|
|
130
|
+
if (input.body) {
|
|
131
|
+
lines.push("", input.body);
|
|
132
|
+
}
|
|
133
|
+
return lines.join("\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderIssueAttachment(input: {
|
|
137
|
+
forge: string;
|
|
138
|
+
number: number;
|
|
139
|
+
title: string;
|
|
140
|
+
url: string;
|
|
141
|
+
body?: string | null;
|
|
142
|
+
projectPath?: string;
|
|
143
|
+
}): string {
|
|
144
|
+
const lines = [
|
|
145
|
+
`${formatForgeLabel(input.forge)} Issue ${formatIssueNumber(input.forge, input.number)}: ${input.title}`,
|
|
146
|
+
input.url,
|
|
147
|
+
];
|
|
148
|
+
if (input.projectPath) {
|
|
149
|
+
lines.push(`Project: ${input.projectPath}`);
|
|
150
|
+
}
|
|
151
|
+
if (input.body) {
|
|
152
|
+
lines.push("", input.body);
|
|
153
|
+
}
|
|
154
|
+
return lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function formatForgeLabel(forge: string): string {
|
|
158
|
+
return getForgeDefinitionOrNeutral(forge).displayName;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function formatChangeRequestAbbrev(forge: string): string {
|
|
162
|
+
return getForgeDefinitionOrNeutral(forge).changeRequestAbbrev;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function formatChangeRequestNumber(forge: string, number: number): string {
|
|
166
|
+
return `${getForgeDefinitionOrNeutral(forge).changeRequestNumberPrefix}${number}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatIssueNumber(forge: string, number: number): string {
|
|
170
|
+
return `${getForgeDefinitionOrNeutral(forge).issueNumberPrefix}${number}`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function padLineNumber(lineNumber: number | null): string {
|
|
174
|
+
return (lineNumber?.toString() ?? "-").padStart(2);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function promptPayload(input: SessionPromptInput): OmpPromptPayload {
|
|
178
|
+
if (input.prompt.outputSchema !== undefined || input.prompt.clearPendingPermissions) {
|
|
179
|
+
throw new OmpPublicError("OMP does not support structured output or permission controls");
|
|
180
|
+
}
|
|
181
|
+
if (input.prompt.input.type === "command") {
|
|
182
|
+
const name = input.prompt.input.name.trim();
|
|
183
|
+
if (!isSafeCommandName(name)) throw new OmpPublicError("Invalid OMP command name");
|
|
184
|
+
const argumentsText = input.prompt.input.arguments.trim();
|
|
185
|
+
const text = `/${name}${argumentsText ? ` ${argumentsText}` : ""}`;
|
|
186
|
+
if (utf8Bytes(text) > MAX_PROMPT_TEXT_LENGTH)
|
|
187
|
+
throw new OmpPublicError("OMP command is too large");
|
|
188
|
+
return { text, images: [], commandName: name };
|
|
189
|
+
}
|
|
190
|
+
if (input.prompt.input.content.length > MAX_PROMPT_PARTS) {
|
|
191
|
+
throw new OmpPublicError("OMP prompt has too many content parts");
|
|
192
|
+
}
|
|
193
|
+
const parts: string[] = [];
|
|
194
|
+
const images: OmpImage[] = [];
|
|
195
|
+
let length = 0;
|
|
196
|
+
const appendText = (text: string) => {
|
|
197
|
+
length += utf8Bytes(text) + (parts.length > 0 ? 2 : 0);
|
|
198
|
+
if (length > MAX_PROMPT_TEXT_LENGTH) throw new OmpPublicError("OMP prompt is too large");
|
|
199
|
+
parts.push(text);
|
|
200
|
+
};
|
|
201
|
+
for (const part of input.prompt.input.content) {
|
|
202
|
+
if (part.type === "text") {
|
|
203
|
+
appendText(part.text);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (part.type === "image") {
|
|
207
|
+
if (!isValidImagePayload(part.data, part.mimeType, 8 * 1024 * 1024)) {
|
|
208
|
+
throw new OmpPublicError("OMP prompt image is invalid");
|
|
209
|
+
}
|
|
210
|
+
images.push({ type: "image", data: part.data, mimeType: part.mimeType });
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
appendText(renderPromptAttachmentAsText(part));
|
|
214
|
+
}
|
|
215
|
+
const text = parts.join("\n\n").trim();
|
|
216
|
+
if (!text && images.length === 0) throw new OmpPublicError("OMP prompt cannot be empty");
|
|
217
|
+
return { text, images };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function inlinePromptFrameBytes(
|
|
221
|
+
payload: OmpPromptPayload,
|
|
222
|
+
delivery: "prompt" | "steer",
|
|
223
|
+
): number {
|
|
224
|
+
const images = payload.images.map(({ type, mimeType }) => ({ type, data: "", mimeType }));
|
|
225
|
+
const frame = {
|
|
226
|
+
type: delivery,
|
|
227
|
+
message: payload.text,
|
|
228
|
+
...(images.length > 0 ? { images } : {}),
|
|
229
|
+
...(delivery === "prompt" ? { id: "" } : {}),
|
|
230
|
+
};
|
|
231
|
+
const requestIdBytes = delivery === "prompt" ? RPC_REQUEST_ID_BYTES : 0;
|
|
232
|
+
return (
|
|
233
|
+
utf8Bytes(JSON.stringify(frame)) +
|
|
234
|
+
requestIdBytes +
|
|
235
|
+
payload.images.reduce((total, image) => total + image.data.length, 0) +
|
|
236
|
+
1
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function slashCommandName(text: string): string | undefined {
|
|
241
|
+
if (!text.startsWith("/")) return undefined;
|
|
242
|
+
const body = text.slice(1);
|
|
243
|
+
if (!body) return undefined;
|
|
244
|
+
const firstWhitespace = body.search(/\s/u);
|
|
245
|
+
const name = firstWhitespace === -1 ? body : body.slice(0, firstWhitespace);
|
|
246
|
+
return name || undefined;
|
|
247
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import type {
|
|
3
3
|
ProviderConfigState,
|
|
4
|
-
ProviderContent,
|
|
5
4
|
ProviderError,
|
|
6
5
|
ProviderEvent,
|
|
7
6
|
ProviderInput,
|
|
@@ -10,7 +9,6 @@ import type {
|
|
|
10
9
|
ProviderToolCallDetail,
|
|
11
10
|
ProviderUsage,
|
|
12
11
|
} from "@getpaseo/plugin/server/provider";
|
|
13
|
-
import { getForgeDefinitionOrNeutral } from "@getpaseo/protocol/forge-manifest";
|
|
14
12
|
import type {
|
|
15
13
|
OmpOperationalFailure,
|
|
16
14
|
OmpOperationalFailureReporter,
|
|
@@ -30,13 +28,12 @@ import {
|
|
|
30
28
|
withCommittedOmpSelection,
|
|
31
29
|
} from "./config-normalization";
|
|
32
30
|
import { OmpHostToolsBridge, type OmpMcpConnector, validateOmpHostToolConfig } from "./host-tools";
|
|
33
|
-
import { isOmpImageMimeType,
|
|
31
|
+
import { isOmpImageMimeType, OmpImageMaterializer } from "./image";
|
|
34
32
|
import type {
|
|
35
33
|
OmpAvailableCommand,
|
|
36
34
|
OmpBranchResult,
|
|
37
35
|
OmpCompactionResult,
|
|
38
36
|
OmpExtensionUiResponse,
|
|
39
|
-
OmpImage,
|
|
40
37
|
OmpMessage,
|
|
41
38
|
OmpModel,
|
|
42
39
|
OmpRpcEvent,
|
|
@@ -44,11 +41,19 @@ import type {
|
|
|
44
41
|
OmpRuntimeSession,
|
|
45
42
|
OmpSessionState,
|
|
46
43
|
OmpSessionStats,
|
|
47
|
-
OmpStartOptions,
|
|
48
44
|
OmpToolApprovalCancel,
|
|
49
45
|
OmpToolApprovalRequest,
|
|
50
46
|
} from "./omp-rpc";
|
|
51
|
-
import {
|
|
47
|
+
import { OmpRpcRequestRejectedError } from "./omp-rpc";
|
|
48
|
+
import { buildOmpSpawnRequest, type OmpStartOptions } from "./omp-rpc-environment";
|
|
49
|
+
import {
|
|
50
|
+
inlinePromptFrameBytes,
|
|
51
|
+
isSafeCommandName,
|
|
52
|
+
MAX_PROMPT_TEXT_LENGTH,
|
|
53
|
+
type OmpPromptPayload,
|
|
54
|
+
promptPayload,
|
|
55
|
+
slashCommandName,
|
|
56
|
+
} from "./prompt-payload";
|
|
52
57
|
import {
|
|
53
58
|
BoundedStringSet,
|
|
54
59
|
boundedJsonBytes,
|
|
@@ -94,9 +99,6 @@ const USAGE_REFRESH_MS = 100;
|
|
|
94
99
|
const FINAL_USAGE_WAIT_MS = 250;
|
|
95
100
|
const COMPACTION_MAX_WAIT_MS = 5 * 60_000;
|
|
96
101
|
const AGENT_END_SETTLE_MS = 5_000;
|
|
97
|
-
const MAX_PROMPT_PARTS = 64;
|
|
98
|
-
const MAX_PROMPT_TEXT_LENGTH = 1024 * 1024;
|
|
99
|
-
const RPC_REQUEST_ID_BYTES = 36;
|
|
100
102
|
const MAX_AGENT_END_CORRELATION_MESSAGES = 512;
|
|
101
103
|
const MAX_TRACKED_ENTRY_IDS = 1_024;
|
|
102
104
|
const MAX_UNCLAIMED_BRANCH_ENTRIES = 1_024;
|
|
@@ -407,240 +409,6 @@ async function settleSessionCleanup(promises: readonly Promise<void>[]): Promise
|
|
|
407
409
|
throw new AggregateError(failures, "OMP session initialization cleanup failed");
|
|
408
410
|
}
|
|
409
411
|
}
|
|
410
|
-
function isSafeCommandName(name: string): boolean {
|
|
411
|
-
return /^[A-Za-z0-9][A-Za-z0-9._-]*(?::[A-Za-z0-9][A-Za-z0-9._-]*)*$/u.test(name);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
type OmpPromptPayload = { text: string; images: OmpImage[]; commandName?: string };
|
|
415
|
-
|
|
416
|
-
const REVIEW_LINE_MARKERS = { add: "+", remove: "-", context: " " } as const;
|
|
417
|
-
|
|
418
|
-
function renderPromptAttachmentAsText(part: Exclude<ProviderContent, { type: "image" }>): string {
|
|
419
|
-
switch (part.type) {
|
|
420
|
-
case "forge_change_request": {
|
|
421
|
-
return renderChangeRequestAttachment({
|
|
422
|
-
forge: part.forge ?? "github",
|
|
423
|
-
number: part.number,
|
|
424
|
-
title: part.title,
|
|
425
|
-
url: part.url,
|
|
426
|
-
body: part.body,
|
|
427
|
-
projectPath: part.projectPath,
|
|
428
|
-
baseRefName: part.baseRefName,
|
|
429
|
-
headRefName: part.headRefName,
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
case "github_pr": {
|
|
433
|
-
return renderChangeRequestAttachment({
|
|
434
|
-
forge: "github",
|
|
435
|
-
number: part.number,
|
|
436
|
-
title: part.title,
|
|
437
|
-
url: part.url,
|
|
438
|
-
body: part.body,
|
|
439
|
-
baseRefName: part.baseRefName,
|
|
440
|
-
headRefName: part.headRefName,
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
case "forge_issue": {
|
|
444
|
-
return renderIssueAttachment({
|
|
445
|
-
forge: part.forge ?? "github",
|
|
446
|
-
number: part.number,
|
|
447
|
-
title: part.title,
|
|
448
|
-
url: part.url,
|
|
449
|
-
body: part.body,
|
|
450
|
-
projectPath: part.projectPath,
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
case "github_issue": {
|
|
454
|
-
return renderIssueAttachment({
|
|
455
|
-
forge: "github",
|
|
456
|
-
number: part.number,
|
|
457
|
-
title: part.title,
|
|
458
|
-
url: part.url,
|
|
459
|
-
body: part.body,
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
case "text": {
|
|
463
|
-
return part.text;
|
|
464
|
-
}
|
|
465
|
-
case "review": {
|
|
466
|
-
const lines = [`Paseo review attachment (${part.mode})`, `CWD: ${part.cwd}`];
|
|
467
|
-
if (part.baseRef) {
|
|
468
|
-
lines.push(`Base: ${part.baseRef}`);
|
|
469
|
-
}
|
|
470
|
-
part.comments.forEach((comment, index) => {
|
|
471
|
-
lines.push(
|
|
472
|
-
"",
|
|
473
|
-
`Comment ${index + 1}: ${comment.filePath}:${comment.side}:${comment.lineNumber}`,
|
|
474
|
-
comment.body,
|
|
475
|
-
comment.context.hunkHeader,
|
|
476
|
-
);
|
|
477
|
-
const target = comment.context.targetLine;
|
|
478
|
-
for (const line of comment.context.lines) {
|
|
479
|
-
const isTarget =
|
|
480
|
-
line.oldLineNumber === target.oldLineNumber &&
|
|
481
|
-
line.newLineNumber === target.newLineNumber &&
|
|
482
|
-
line.type === target.type &&
|
|
483
|
-
line.content === target.content;
|
|
484
|
-
const prefix = isTarget ? "> " : " ";
|
|
485
|
-
const oldLn = padLineNumber(line.oldLineNumber);
|
|
486
|
-
const newLn = padLineNumber(line.newLineNumber);
|
|
487
|
-
lines.push(`${prefix}${oldLn} ${newLn} ${REVIEW_LINE_MARKERS[line.type]}${line.content}`);
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
return lines.join("\n");
|
|
491
|
-
}
|
|
492
|
-
case "uploaded_file": {
|
|
493
|
-
return [
|
|
494
|
-
`Uploaded file: ${part.fileName}`,
|
|
495
|
-
`Path: ${part.path}`,
|
|
496
|
-
`MIME: ${part.mimeType}`,
|
|
497
|
-
`Size: ${part.size} bytes`,
|
|
498
|
-
].join("\n");
|
|
499
|
-
}
|
|
500
|
-
default:
|
|
501
|
-
throw new Error("unreachable");
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function renderChangeRequestAttachment(input: {
|
|
506
|
-
forge: string;
|
|
507
|
-
number: number;
|
|
508
|
-
title: string;
|
|
509
|
-
url: string;
|
|
510
|
-
body?: string | null;
|
|
511
|
-
projectPath?: string;
|
|
512
|
-
baseRefName?: string | null;
|
|
513
|
-
headRefName?: string | null;
|
|
514
|
-
}): string {
|
|
515
|
-
const lines = [
|
|
516
|
-
`${formatForgeLabel(input.forge)} ${formatChangeRequestAbbrev(input.forge)} ${formatChangeRequestNumber(input.forge, input.number)}: ${input.title}`,
|
|
517
|
-
input.url,
|
|
518
|
-
];
|
|
519
|
-
if (input.projectPath) {
|
|
520
|
-
lines.push(`Project: ${input.projectPath}`);
|
|
521
|
-
}
|
|
522
|
-
if (input.baseRefName) {
|
|
523
|
-
lines.push(`Base: ${input.baseRefName}`);
|
|
524
|
-
}
|
|
525
|
-
if (input.headRefName) {
|
|
526
|
-
lines.push(`Head: ${input.headRefName}`);
|
|
527
|
-
}
|
|
528
|
-
if (input.body) {
|
|
529
|
-
lines.push("", input.body);
|
|
530
|
-
}
|
|
531
|
-
return lines.join("\n");
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function renderIssueAttachment(input: {
|
|
535
|
-
forge: string;
|
|
536
|
-
number: number;
|
|
537
|
-
title: string;
|
|
538
|
-
url: string;
|
|
539
|
-
body?: string | null;
|
|
540
|
-
projectPath?: string;
|
|
541
|
-
}): string {
|
|
542
|
-
const lines = [
|
|
543
|
-
`${formatForgeLabel(input.forge)} Issue ${formatIssueNumber(input.forge, input.number)}: ${input.title}`,
|
|
544
|
-
input.url,
|
|
545
|
-
];
|
|
546
|
-
if (input.projectPath) {
|
|
547
|
-
lines.push(`Project: ${input.projectPath}`);
|
|
548
|
-
}
|
|
549
|
-
if (input.body) {
|
|
550
|
-
lines.push("", input.body);
|
|
551
|
-
}
|
|
552
|
-
return lines.join("\n");
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
function formatForgeLabel(forge: string): string {
|
|
556
|
-
return getForgeDefinitionOrNeutral(forge).displayName;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function formatChangeRequestAbbrev(forge: string): string {
|
|
560
|
-
return getForgeDefinitionOrNeutral(forge).changeRequestAbbrev;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function formatChangeRequestNumber(forge: string, number: number): string {
|
|
564
|
-
return `${getForgeDefinitionOrNeutral(forge).changeRequestNumberPrefix}${number}`;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function formatIssueNumber(forge: string, number: number): string {
|
|
568
|
-
return `${getForgeDefinitionOrNeutral(forge).issueNumberPrefix}${number}`;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function padLineNumber(lineNumber: number | null): string {
|
|
572
|
-
return (lineNumber?.toString() ?? "-").padStart(2);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
function promptPayload(input: SessionPromptInput): OmpPromptPayload {
|
|
576
|
-
if (input.prompt.outputSchema !== undefined || input.prompt.clearPendingPermissions) {
|
|
577
|
-
throw new OmpPublicError("OMP does not support structured output or permission controls");
|
|
578
|
-
}
|
|
579
|
-
if (input.prompt.input.type === "command") {
|
|
580
|
-
const name = input.prompt.input.name.trim();
|
|
581
|
-
if (!isSafeCommandName(name)) throw new OmpPublicError("Invalid OMP command name");
|
|
582
|
-
const argumentsText = input.prompt.input.arguments.trim();
|
|
583
|
-
const text = `/${name}${argumentsText ? ` ${argumentsText}` : ""}`;
|
|
584
|
-
if (utf8Bytes(text) > MAX_PROMPT_TEXT_LENGTH)
|
|
585
|
-
throw new OmpPublicError("OMP command is too large");
|
|
586
|
-
return { text, images: [], commandName: name };
|
|
587
|
-
}
|
|
588
|
-
if (input.prompt.input.content.length > MAX_PROMPT_PARTS) {
|
|
589
|
-
throw new OmpPublicError("OMP prompt has too many content parts");
|
|
590
|
-
}
|
|
591
|
-
const parts: string[] = [];
|
|
592
|
-
const images: OmpImage[] = [];
|
|
593
|
-
let length = 0;
|
|
594
|
-
const appendText = (text: string) => {
|
|
595
|
-
length += utf8Bytes(text) + (parts.length > 0 ? 2 : 0);
|
|
596
|
-
if (length > MAX_PROMPT_TEXT_LENGTH) throw new OmpPublicError("OMP prompt is too large");
|
|
597
|
-
parts.push(text);
|
|
598
|
-
};
|
|
599
|
-
for (const part of input.prompt.input.content) {
|
|
600
|
-
if (part.type === "text") {
|
|
601
|
-
appendText(part.text);
|
|
602
|
-
continue;
|
|
603
|
-
}
|
|
604
|
-
if (part.type === "image") {
|
|
605
|
-
if (!isValidImagePayload(part.data, part.mimeType, 8 * 1024 * 1024)) {
|
|
606
|
-
throw new OmpPublicError("OMP prompt image is invalid");
|
|
607
|
-
}
|
|
608
|
-
images.push({ type: "image", data: part.data, mimeType: part.mimeType });
|
|
609
|
-
continue;
|
|
610
|
-
}
|
|
611
|
-
appendText(renderPromptAttachmentAsText(part));
|
|
612
|
-
}
|
|
613
|
-
const text = parts.join("\n\n").trim();
|
|
614
|
-
if (!text && images.length === 0) throw new OmpPublicError("OMP prompt cannot be empty");
|
|
615
|
-
return { text, images };
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function inlinePromptFrameBytes(payload: OmpPromptPayload, delivery: "prompt" | "steer"): number {
|
|
619
|
-
const images = payload.images.map(({ type, mimeType }) => ({ type, data: "", mimeType }));
|
|
620
|
-
const frame = {
|
|
621
|
-
type: delivery,
|
|
622
|
-
message: payload.text,
|
|
623
|
-
...(images.length > 0 ? { images } : {}),
|
|
624
|
-
...(delivery === "prompt" ? { id: "" } : {}),
|
|
625
|
-
};
|
|
626
|
-
const requestIdBytes = delivery === "prompt" ? RPC_REQUEST_ID_BYTES : 0;
|
|
627
|
-
return (
|
|
628
|
-
utf8Bytes(JSON.stringify(frame)) +
|
|
629
|
-
requestIdBytes +
|
|
630
|
-
payload.images.reduce((total, image) => total + image.data.length, 0) +
|
|
631
|
-
1
|
|
632
|
-
);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function slashCommandName(text: string): string | undefined {
|
|
636
|
-
if (!text.startsWith("/")) return undefined;
|
|
637
|
-
const body = text.slice(1);
|
|
638
|
-
if (!body) return undefined;
|
|
639
|
-
const firstWhitespace = body.search(/\s/u);
|
|
640
|
-
const name = firstWhitespace === -1 ? body : body.slice(0, firstWhitespace);
|
|
641
|
-
return name || undefined;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
412
|
function nativeEntryId(message: OmpMessage): string | undefined {
|
|
645
413
|
return message.entryId;
|
|
646
414
|
}
|