ompclaw 0.3.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 +34 -0
- package/LICENSE +21 -0
- package/NOTICE +10 -0
- package/README.md +152 -0
- package/SECURITY.md +61 -0
- package/config.example.json +45 -0
- package/docs/guide.md +360 -0
- package/docs/rpc-service.md +240 -0
- package/package.json +93 -0
- package/src/api.ts +556 -0
- package/src/gateway-app.ts +393 -0
- package/src/gateway-config.ts +379 -0
- package/src/gateway-core.ts +410 -0
- package/src/gateway-scheduler.ts +425 -0
- package/src/gateway-store.ts +947 -0
- package/src/gateway-tools.ts +443 -0
- package/src/gateway-types.ts +290 -0
- package/src/inbox.ts +77 -0
- package/src/index.ts +13 -0
- package/src/markdown.ts +156 -0
- package/src/outbound.ts +353 -0
- package/src/rpc-cli.ts +408 -0
- package/src/rpc-client.ts +308 -0
- package/src/rpc-config.ts +70 -0
- package/src/rpc-profile.ts +215 -0
- package/src/rpc-protocol.ts +326 -0
- package/src/rpc-runtime.ts +875 -0
- package/src/rpc-service.ts +191 -0
- package/src/rpc-ui.ts +218 -0
- package/src/transports/telegram/adapter.ts +829 -0
- package/src/transports/websocket/adapter.ts +704 -0
- package/src/transports/websocket/protocol.ts +256 -0
- package/src/type-guards.ts +4 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { isRecord } from "./type-guards";
|
|
2
|
+
|
|
3
|
+
export type RpcRecord = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
export interface RpcCommand extends RpcRecord {
|
|
6
|
+
type: string;
|
|
7
|
+
id?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RpcResponse extends RpcRecord {
|
|
11
|
+
type: "response";
|
|
12
|
+
id?: string;
|
|
13
|
+
command: string;
|
|
14
|
+
success: boolean;
|
|
15
|
+
data?: unknown;
|
|
16
|
+
error?: string;
|
|
17
|
+
code?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RpcReadyFrame extends RpcRecord {
|
|
21
|
+
type: "ready";
|
|
22
|
+
protocolVersion: number;
|
|
23
|
+
supportedProtocolVersions?: number[];
|
|
24
|
+
maxFrameBytes?: number;
|
|
25
|
+
maxReassembledFrameBytes?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RpcChunkFrame extends RpcRecord {
|
|
29
|
+
type: "rpc_chunk";
|
|
30
|
+
chunkId: string;
|
|
31
|
+
index: number;
|
|
32
|
+
count: number;
|
|
33
|
+
byteLength: number;
|
|
34
|
+
data: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RpcImageContent extends RpcRecord {
|
|
38
|
+
type: "image";
|
|
39
|
+
data: string;
|
|
40
|
+
mimeType: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RpcSessionState extends RpcRecord {
|
|
44
|
+
model?: { provider?: string; id?: string };
|
|
45
|
+
thinkingLevel?: string;
|
|
46
|
+
isStreaming: boolean;
|
|
47
|
+
isCompacting: boolean;
|
|
48
|
+
sessionFile?: string;
|
|
49
|
+
sessionId: string;
|
|
50
|
+
sessionName?: string;
|
|
51
|
+
fastModeEnabled?: boolean;
|
|
52
|
+
fastModeActive?: boolean;
|
|
53
|
+
tokensPerSecond?: number | null;
|
|
54
|
+
autoCompactionEnabled?: boolean;
|
|
55
|
+
messageCount?: number;
|
|
56
|
+
queuedMessageCount?: number;
|
|
57
|
+
contextUsage?: { tokens?: number; contextWindow?: number; percent?: number };
|
|
58
|
+
todoPhases?: unknown[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type RpcExtensionUiRequest =
|
|
62
|
+
| {
|
|
63
|
+
type: "extension_ui_request";
|
|
64
|
+
id: string;
|
|
65
|
+
method: "select";
|
|
66
|
+
title: string;
|
|
67
|
+
options: string[];
|
|
68
|
+
optionDetails?: Array<{ description?: string }>;
|
|
69
|
+
timeout?: number;
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
type: "extension_ui_request";
|
|
73
|
+
id: string;
|
|
74
|
+
method: "confirm";
|
|
75
|
+
title: string;
|
|
76
|
+
message: string;
|
|
77
|
+
timeout?: number;
|
|
78
|
+
}
|
|
79
|
+
| {
|
|
80
|
+
type: "extension_ui_request";
|
|
81
|
+
id: string;
|
|
82
|
+
method: "input";
|
|
83
|
+
title: string;
|
|
84
|
+
placeholder?: string;
|
|
85
|
+
timeout?: number;
|
|
86
|
+
}
|
|
87
|
+
| {
|
|
88
|
+
type: "extension_ui_request";
|
|
89
|
+
id: string;
|
|
90
|
+
method: "editor";
|
|
91
|
+
title: string;
|
|
92
|
+
prefill?: string;
|
|
93
|
+
promptStyle?: boolean;
|
|
94
|
+
}
|
|
95
|
+
| { type: "extension_ui_request"; id: string; method: "cancel"; targetId: string }
|
|
96
|
+
| {
|
|
97
|
+
type: "extension_ui_request";
|
|
98
|
+
id: string;
|
|
99
|
+
method: "notify";
|
|
100
|
+
message: string;
|
|
101
|
+
notifyType?: "info" | "warning" | "error";
|
|
102
|
+
}
|
|
103
|
+
| {
|
|
104
|
+
type: "extension_ui_request";
|
|
105
|
+
id: string;
|
|
106
|
+
method: "setStatus";
|
|
107
|
+
statusKey: string;
|
|
108
|
+
statusText?: string;
|
|
109
|
+
}
|
|
110
|
+
| {
|
|
111
|
+
type: "extension_ui_request";
|
|
112
|
+
id: string;
|
|
113
|
+
method: "setWidget";
|
|
114
|
+
widgetKey: string;
|
|
115
|
+
widgetLines?: string[];
|
|
116
|
+
widgetPlacement?: "aboveEditor" | "belowEditor";
|
|
117
|
+
}
|
|
118
|
+
| { type: "extension_ui_request"; id: string; method: "setTitle"; title: string }
|
|
119
|
+
| { type: "extension_ui_request"; id: string; method: "set_editor_text"; text: string }
|
|
120
|
+
| {
|
|
121
|
+
type: "extension_ui_request";
|
|
122
|
+
id: string;
|
|
123
|
+
method: "open_url";
|
|
124
|
+
url: string;
|
|
125
|
+
launchUrl?: string;
|
|
126
|
+
instructions?: string;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type RpcExtensionUiResponse =
|
|
130
|
+
| { type: "extension_ui_response"; id: string; value: string }
|
|
131
|
+
| { type: "extension_ui_response"; id: string; confirmed: boolean }
|
|
132
|
+
| { type: "extension_ui_response"; id: string; cancelled: true; timedOut?: boolean };
|
|
133
|
+
|
|
134
|
+
export interface RpcHostToolCall extends RpcRecord {
|
|
135
|
+
type: "host_tool_call";
|
|
136
|
+
id: string;
|
|
137
|
+
toolCallId: string;
|
|
138
|
+
toolName: string;
|
|
139
|
+
arguments: RpcRecord;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface RpcHostToolCancel extends RpcRecord {
|
|
143
|
+
type: "host_tool_cancel";
|
|
144
|
+
id: string;
|
|
145
|
+
targetId: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface RpcHostToolDefinition extends RpcRecord {
|
|
149
|
+
name: string;
|
|
150
|
+
label?: string;
|
|
151
|
+
description: string;
|
|
152
|
+
parameters: RpcRecord;
|
|
153
|
+
hidden?: boolean;
|
|
154
|
+
loadMode?: "essential" | "discoverable";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type RpcInboundFrame =
|
|
158
|
+
| RpcCommand
|
|
159
|
+
| RpcExtensionUiResponse
|
|
160
|
+
| { type: "host_tool_update"; id: string; partialResult: unknown }
|
|
161
|
+
| { type: "host_tool_result"; id: string; result: unknown; isError?: boolean }
|
|
162
|
+
| { type: "host_uri_result"; id: string; [key: string]: unknown };
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
export function isRpcResponse(value: unknown): value is RpcResponse {
|
|
166
|
+
return (
|
|
167
|
+
isRecord(value) &&
|
|
168
|
+
value.type === "response" &&
|
|
169
|
+
typeof value.command === "string" &&
|
|
170
|
+
typeof value.success === "boolean" &&
|
|
171
|
+
(value.id === undefined || typeof value.id === "string")
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function isRpcReady(value: unknown): value is RpcReadyFrame {
|
|
176
|
+
return isRecord(value) && value.type === "ready" && typeof value.protocolVersion === "number";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function isRpcExtensionUiRequest(value: unknown): value is RpcExtensionUiRequest {
|
|
180
|
+
return (
|
|
181
|
+
isRecord(value) &&
|
|
182
|
+
value.type === "extension_ui_request" &&
|
|
183
|
+
typeof value.id === "string" &&
|
|
184
|
+
typeof value.method === "string"
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function isRpcHostToolCall(value: unknown): value is RpcHostToolCall {
|
|
189
|
+
return (
|
|
190
|
+
isRecord(value) &&
|
|
191
|
+
value.type === "host_tool_call" &&
|
|
192
|
+
typeof value.id === "string" &&
|
|
193
|
+
typeof value.toolCallId === "string" &&
|
|
194
|
+
typeof value.toolName === "string" &&
|
|
195
|
+
isRecord(value.arguments)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function isRpcHostToolCancel(value: unknown): value is RpcHostToolCancel {
|
|
200
|
+
return isRecord(value) && value.type === "host_tool_cancel" && typeof value.targetId === "string";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function decodeBase64Strict(value: string): Uint8Array {
|
|
204
|
+
if (value.length === 0) return new Uint8Array();
|
|
205
|
+
if (value.length % 4 !== 0 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
206
|
+
throw new Error("Invalid RPC chunk base64");
|
|
207
|
+
}
|
|
208
|
+
return Buffer.from(value, "base64");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface ChunkSequence {
|
|
212
|
+
chunkId: string;
|
|
213
|
+
count: number;
|
|
214
|
+
byteLength: number;
|
|
215
|
+
nextIndex: number;
|
|
216
|
+
chunks: Uint8Array[];
|
|
217
|
+
receivedBytes: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Lossless protocol-v2 chunk reassembly with the invariants required by OMP RPC. */
|
|
221
|
+
export class RpcFrameDecoder {
|
|
222
|
+
#sequence: ChunkSequence | undefined;
|
|
223
|
+
#maxBytes: number;
|
|
224
|
+
|
|
225
|
+
constructor(maxBytes = 64 * 1024 * 1024) {
|
|
226
|
+
this.#maxBytes = maxBytes;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
setMaxBytes(maxBytes: number): void {
|
|
230
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) throw new Error("Invalid RPC reassembly limit");
|
|
231
|
+
this.#maxBytes = maxBytes;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
reset(): void {
|
|
235
|
+
this.#sequence = undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
push(frame: unknown): unknown | undefined {
|
|
239
|
+
if (!isRecord(frame) || frame.type !== "rpc_chunk") {
|
|
240
|
+
if (this.#sequence) throw new Error("RPC chunk sequence was interrupted");
|
|
241
|
+
return frame;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const chunk = frame as unknown as RpcChunkFrame;
|
|
245
|
+
if (
|
|
246
|
+
typeof chunk.chunkId !== "string" ||
|
|
247
|
+
chunk.chunkId.length === 0 ||
|
|
248
|
+
!Number.isSafeInteger(chunk.index) ||
|
|
249
|
+
!Number.isSafeInteger(chunk.count) ||
|
|
250
|
+
!Number.isSafeInteger(chunk.byteLength) ||
|
|
251
|
+
chunk.index < 0 ||
|
|
252
|
+
chunk.count <= 0 ||
|
|
253
|
+
chunk.index >= chunk.count ||
|
|
254
|
+
chunk.byteLength < 0 ||
|
|
255
|
+
chunk.byteLength > this.#maxBytes ||
|
|
256
|
+
typeof chunk.data !== "string"
|
|
257
|
+
) {
|
|
258
|
+
throw new Error("Invalid RPC chunk metadata");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!this.#sequence) {
|
|
262
|
+
if (chunk.index !== 0) throw new Error("RPC chunk sequence did not start at index 0");
|
|
263
|
+
this.#sequence = {
|
|
264
|
+
chunkId: chunk.chunkId,
|
|
265
|
+
count: chunk.count,
|
|
266
|
+
byteLength: chunk.byteLength,
|
|
267
|
+
nextIndex: 0,
|
|
268
|
+
chunks: [],
|
|
269
|
+
receivedBytes: 0,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const sequence = this.#sequence;
|
|
274
|
+
if (
|
|
275
|
+
sequence.chunkId !== chunk.chunkId ||
|
|
276
|
+
sequence.count !== chunk.count ||
|
|
277
|
+
sequence.byteLength !== chunk.byteLength ||
|
|
278
|
+
sequence.nextIndex !== chunk.index
|
|
279
|
+
) {
|
|
280
|
+
this.#sequence = undefined;
|
|
281
|
+
throw new Error("RPC chunk sequence was interleaved or out of order");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const bytes = decodeBase64Strict(chunk.data);
|
|
285
|
+
sequence.receivedBytes += bytes.byteLength;
|
|
286
|
+
if (sequence.receivedBytes > sequence.byteLength || sequence.receivedBytes > this.#maxBytes) {
|
|
287
|
+
this.#sequence = undefined;
|
|
288
|
+
throw new Error("RPC chunk sequence exceeded its declared size");
|
|
289
|
+
}
|
|
290
|
+
sequence.chunks.push(bytes);
|
|
291
|
+
sequence.nextIndex += 1;
|
|
292
|
+
if (sequence.nextIndex !== sequence.count) return undefined;
|
|
293
|
+
|
|
294
|
+
this.#sequence = undefined;
|
|
295
|
+
if (sequence.receivedBytes !== sequence.byteLength) throw new Error("RPC chunk sequence length mismatch");
|
|
296
|
+
const joined = Buffer.concat(sequence.chunks.map((part) => Buffer.from(part)), sequence.receivedBytes);
|
|
297
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(joined);
|
|
298
|
+
const decoded: unknown = JSON.parse(text);
|
|
299
|
+
if (!isRecord(decoded)) throw new Error("Reassembled RPC frame is not an object");
|
|
300
|
+
return decoded;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function textFromContent(content: unknown): string {
|
|
305
|
+
if (typeof content === "string") return content;
|
|
306
|
+
if (!Array.isArray(content)) return "";
|
|
307
|
+
return content
|
|
308
|
+
.filter((block): block is RpcRecord => isRecord(block) && block.type === "text" && typeof block.text === "string")
|
|
309
|
+
.map((block) => String(block.text))
|
|
310
|
+
.join("");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Visible assistant text only. Thinking and tool-call blocks never reach Telegram. */
|
|
314
|
+
export function assistantText(message: unknown): string {
|
|
315
|
+
if (!isRecord(message) || message.role !== "assistant") return "";
|
|
316
|
+
return textFromContent(message.content);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function finalAssistantText(messages: unknown): string {
|
|
320
|
+
if (!Array.isArray(messages)) return "";
|
|
321
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
322
|
+
const text = assistantText(messages[i]);
|
|
323
|
+
if (text.trim().length > 0) return text;
|
|
324
|
+
}
|
|
325
|
+
return "";
|
|
326
|
+
}
|