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,443 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import {
|
|
4
|
+
formatScheduledJob,
|
|
5
|
+
type GatewayAutomationControl,
|
|
6
|
+
type ScheduledJobContext,
|
|
7
|
+
} from "./gateway-scheduler";
|
|
8
|
+
import type {
|
|
9
|
+
ConversationAddress,
|
|
10
|
+
DeliveryContext,
|
|
11
|
+
MessageAttachment,
|
|
12
|
+
OutboundContent,
|
|
13
|
+
OutboundReceipt,
|
|
14
|
+
Reaction,
|
|
15
|
+
TransportIdentity,
|
|
16
|
+
UiRequest,
|
|
17
|
+
UiResponseFor,
|
|
18
|
+
} from "./gateway-types";
|
|
19
|
+
import type { RpcHostToolCall, RpcHostToolDefinition, RpcRecord } from "./rpc-protocol";
|
|
20
|
+
|
|
21
|
+
/** Transport-neutral delivery surface used by OMP host tools. */
|
|
22
|
+
export interface GatewayDelivery {
|
|
23
|
+
send(
|
|
24
|
+
address: ConversationAddress,
|
|
25
|
+
content: OutboundContent,
|
|
26
|
+
context: DeliveryContext,
|
|
27
|
+
signal?: AbortSignal,
|
|
28
|
+
): Promise<OutboundReceipt>;
|
|
29
|
+
update(
|
|
30
|
+
address: ConversationAddress,
|
|
31
|
+
receipt: OutboundReceipt,
|
|
32
|
+
content: OutboundContent,
|
|
33
|
+
context: DeliveryContext,
|
|
34
|
+
signal?: AbortSignal,
|
|
35
|
+
): Promise<OutboundReceipt>;
|
|
36
|
+
react(
|
|
37
|
+
address: ConversationAddress,
|
|
38
|
+
receipt: OutboundReceipt,
|
|
39
|
+
reaction: Reaction,
|
|
40
|
+
context: DeliveryContext,
|
|
41
|
+
signal?: AbortSignal,
|
|
42
|
+
): Promise<void>;
|
|
43
|
+
presentUi<Request extends UiRequest>(
|
|
44
|
+
address: ConversationAddress,
|
|
45
|
+
request: Request,
|
|
46
|
+
context: DeliveryContext,
|
|
47
|
+
signal?: AbortSignal,
|
|
48
|
+
): Promise<UiResponseFor<Request>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface GatewayHostToolContext {
|
|
52
|
+
readonly delivery: GatewayDelivery;
|
|
53
|
+
readonly address: ConversationAddress;
|
|
54
|
+
readonly deliveryContext: DeliveryContext;
|
|
55
|
+
readonly identity: TransportIdentity;
|
|
56
|
+
readonly automation?: GatewayAutomationControl;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const gatewayHostTools: readonly RpcHostToolDefinition[] = [
|
|
60
|
+
{
|
|
61
|
+
name: "ompclaw_send",
|
|
62
|
+
label: "Send message",
|
|
63
|
+
description: "Send text and optional absolute local files to the active conversation.",
|
|
64
|
+
loadMode: "essential",
|
|
65
|
+
parameters: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
text: { type: "string", minLength: 1 },
|
|
69
|
+
files: {
|
|
70
|
+
type: "array",
|
|
71
|
+
minItems: 1,
|
|
72
|
+
items: { type: "string", minLength: 1, pattern: "^/" },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
anyOf: [{ required: ["text"] }, { required: ["files"] }],
|
|
76
|
+
additionalProperties: false,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "ompclaw_react",
|
|
81
|
+
label: "React to message",
|
|
82
|
+
description: "React to a message in the active conversation.",
|
|
83
|
+
loadMode: "discoverable",
|
|
84
|
+
parameters: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
message_id: { type: "string", minLength: 1 },
|
|
88
|
+
emoji: { type: "string", minLength: 1 },
|
|
89
|
+
},
|
|
90
|
+
required: ["message_id", "emoji"],
|
|
91
|
+
additionalProperties: false,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "ompclaw_ask",
|
|
96
|
+
label: "Ask operator",
|
|
97
|
+
description: "Ask the active operator a free-text, single-select, or multi-select question.",
|
|
98
|
+
loadMode: "essential",
|
|
99
|
+
parameters: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
title: { type: "string" },
|
|
103
|
+
question: { type: "string", minLength: 1 },
|
|
104
|
+
options: {
|
|
105
|
+
type: "array",
|
|
106
|
+
minItems: 1,
|
|
107
|
+
items: { type: "string", minLength: 1 },
|
|
108
|
+
},
|
|
109
|
+
multi: { type: "boolean" },
|
|
110
|
+
},
|
|
111
|
+
required: ["question"],
|
|
112
|
+
additionalProperties: false,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
const automationHostTools: readonly RpcHostToolDefinition[] = [
|
|
118
|
+
{
|
|
119
|
+
name: "ompclaw_schedule_job",
|
|
120
|
+
label: "Schedule unattended job",
|
|
121
|
+
description: "Create a durable one-shot or cron job that runs in this gateway and reports to the active conversation.",
|
|
122
|
+
loadMode: "essential",
|
|
123
|
+
parameters: {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: {
|
|
126
|
+
name: { type: "string", minLength: 1, maxLength: 120 },
|
|
127
|
+
prompt: { type: "string", minLength: 1, maxLength: 16_000 },
|
|
128
|
+
at: { type: "string", minLength: 1, description: "ISO 8601 date-time for a one-shot job." },
|
|
129
|
+
cron: { type: "string", minLength: 1, maxLength: 256, description: "Cron expression for a recurring job." },
|
|
130
|
+
timezone: { type: "string", minLength: 1, maxLength: 128, description: "IANA timezone for a cron job." },
|
|
131
|
+
},
|
|
132
|
+
required: ["name", "prompt"],
|
|
133
|
+
oneOf: [{ required: ["at"], not: { required: ["cron"] } }, { required: ["cron"], not: { required: ["at"] } }],
|
|
134
|
+
additionalProperties: false,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "ompclaw_update_job",
|
|
139
|
+
label: "Update scheduled job",
|
|
140
|
+
description: "Update a durable job owned by the active principal. Supplying at or cron replaces its schedule.",
|
|
141
|
+
loadMode: "discoverable",
|
|
142
|
+
parameters: {
|
|
143
|
+
type: "object",
|
|
144
|
+
properties: {
|
|
145
|
+
id: { type: "string", minLength: 1, maxLength: 128 },
|
|
146
|
+
name: { type: "string", minLength: 1, maxLength: 120 },
|
|
147
|
+
prompt: { type: "string", minLength: 1, maxLength: 16_000 },
|
|
148
|
+
at: { type: "string", minLength: 1 },
|
|
149
|
+
cron: { type: "string", minLength: 1, maxLength: 256 },
|
|
150
|
+
timezone: { type: "string", minLength: 1, maxLength: 128 },
|
|
151
|
+
},
|
|
152
|
+
required: ["id"],
|
|
153
|
+
additionalProperties: false,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "ompclaw_list_jobs",
|
|
158
|
+
label: "List scheduled jobs",
|
|
159
|
+
description: "List durable jobs owned by the active principal, including schedules, next runs, and failures.",
|
|
160
|
+
loadMode: "essential",
|
|
161
|
+
parameters: { type: "object", properties: {}, additionalProperties: false },
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "ompclaw_set_job_enabled",
|
|
165
|
+
label: "Enable or disable job",
|
|
166
|
+
description: "Enable or disable a durable job owned by the active principal.",
|
|
167
|
+
loadMode: "discoverable",
|
|
168
|
+
parameters: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
id: { type: "string", minLength: 1, maxLength: 128 },
|
|
172
|
+
enabled: { type: "boolean" },
|
|
173
|
+
},
|
|
174
|
+
required: ["id", "enabled"],
|
|
175
|
+
additionalProperties: false,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "ompclaw_delete_job",
|
|
180
|
+
label: "Delete scheduled job",
|
|
181
|
+
description: "Permanently delete a durable job owned by the active principal.",
|
|
182
|
+
loadMode: "discoverable",
|
|
183
|
+
parameters: {
|
|
184
|
+
type: "object",
|
|
185
|
+
properties: { id: { type: "string", minLength: 1, maxLength: 128 } },
|
|
186
|
+
required: ["id"],
|
|
187
|
+
additionalProperties: false,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "ompclaw_run_job",
|
|
192
|
+
label: "Run scheduled job now",
|
|
193
|
+
description: "Queue a durable job owned by the active principal to run as soon as the current turn finishes.",
|
|
194
|
+
loadMode: "discoverable",
|
|
195
|
+
parameters: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: { id: { type: "string", minLength: 1, maxLength: 128 } },
|
|
198
|
+
required: ["id"],
|
|
199
|
+
additionalProperties: false,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
export function gatewayHostToolDefinitions(automation = false): RpcHostToolDefinition[] {
|
|
205
|
+
const tools = automation ? [...gatewayHostTools, ...automationHostTools] : gatewayHostTools;
|
|
206
|
+
return tools.map((tool) => ({ ...tool }));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function executeGatewayHostTool(
|
|
210
|
+
call: RpcHostToolCall,
|
|
211
|
+
context: GatewayHostToolContext,
|
|
212
|
+
signal?: AbortSignal,
|
|
213
|
+
): Promise<unknown> {
|
|
214
|
+
switch (call.toolName) {
|
|
215
|
+
case "ompclaw_send":
|
|
216
|
+
return executeSend(call.arguments, context, signal);
|
|
217
|
+
case "ompclaw_react":
|
|
218
|
+
return executeReact(call.arguments, context, signal);
|
|
219
|
+
case "ompclaw_ask":
|
|
220
|
+
return executeAsk(call.arguments, context, signal);
|
|
221
|
+
case "ompclaw_schedule_job":
|
|
222
|
+
return executeScheduleJob(call.arguments, context);
|
|
223
|
+
case "ompclaw_update_job":
|
|
224
|
+
return executeUpdateJob(call.arguments, context);
|
|
225
|
+
case "ompclaw_list_jobs":
|
|
226
|
+
return executeListJobs(call.arguments, context);
|
|
227
|
+
case "ompclaw_set_job_enabled":
|
|
228
|
+
return executeSetJobEnabled(call.arguments, context);
|
|
229
|
+
case "ompclaw_delete_job":
|
|
230
|
+
return executeDeleteJob(call.arguments, context);
|
|
231
|
+
case "ompclaw_run_job":
|
|
232
|
+
return executeRunJob(call.arguments, context);
|
|
233
|
+
default:
|
|
234
|
+
throw new Error(`Unknown host tool: ${call.toolName}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function executeSend(
|
|
239
|
+
arguments_: unknown,
|
|
240
|
+
context: GatewayHostToolContext,
|
|
241
|
+
signal?: AbortSignal,
|
|
242
|
+
): Promise<OutboundReceipt> {
|
|
243
|
+
const argumentsRecord = parseArguments(arguments_, ["text", "files"]);
|
|
244
|
+
const text = optionalNonEmptyString(argumentsRecord, "text");
|
|
245
|
+
const files = optionalAbsoluteFiles(argumentsRecord, "files");
|
|
246
|
+
if (text === undefined && files === undefined) {
|
|
247
|
+
throw new Error("ompclaw_send requires text or files");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const content: OutboundContent = {
|
|
251
|
+
...(text === undefined ? {} : { text }),
|
|
252
|
+
...(files === undefined ? {} : { attachments: files }),
|
|
253
|
+
};
|
|
254
|
+
return context.delivery.send(context.address, content, context.deliveryContext, signal);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function executeReact(
|
|
258
|
+
arguments_: unknown,
|
|
259
|
+
context: GatewayHostToolContext,
|
|
260
|
+
signal?: AbortSignal,
|
|
261
|
+
): Promise<{ reacted: true }> {
|
|
262
|
+
const argumentsRecord = parseArguments(arguments_, ["message_id", "emoji"]);
|
|
263
|
+
const messageId = requiredNonEmptyString(argumentsRecord, "message_id");
|
|
264
|
+
const emoji = requiredNonEmptyString(argumentsRecord, "emoji");
|
|
265
|
+
const receipt: OutboundReceipt = { transport: context.address.transport, messageId };
|
|
266
|
+
await context.delivery.react(context.address, receipt, { emoji }, context.deliveryContext, signal);
|
|
267
|
+
return { reacted: true };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function executeAsk(
|
|
271
|
+
arguments_: unknown,
|
|
272
|
+
context: GatewayHostToolContext,
|
|
273
|
+
signal?: AbortSignal,
|
|
274
|
+
): Promise<{ answer: string | readonly string[] | undefined }> {
|
|
275
|
+
const argumentsRecord = parseArguments(arguments_, ["title", "question", "options", "multi"]);
|
|
276
|
+
const title = optionalString(argumentsRecord, "title") ?? "OMP question";
|
|
277
|
+
const question = requiredNonEmptyString(argumentsRecord, "question");
|
|
278
|
+
const options = optionalOptions(argumentsRecord, "options");
|
|
279
|
+
const multi = optionalBoolean(argumentsRecord, "multi");
|
|
280
|
+
if (options === undefined && multi !== undefined) {
|
|
281
|
+
throw new Error("ompclaw_ask multi requires options");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (options === undefined) {
|
|
285
|
+
const response = await context.delivery.presentUi(
|
|
286
|
+
context.address,
|
|
287
|
+
{ type: "input", title, prompt: question },
|
|
288
|
+
context.deliveryContext,
|
|
289
|
+
signal,
|
|
290
|
+
);
|
|
291
|
+
return { answer: response.cancelled ? undefined : response.value };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const response = await context.delivery.presentUi(
|
|
295
|
+
context.address,
|
|
296
|
+
{
|
|
297
|
+
type: "select",
|
|
298
|
+
title,
|
|
299
|
+
options: options.map((option) => ({ value: option, label: option })),
|
|
300
|
+
...(multi === true ? { multiSelect: true } : {}),
|
|
301
|
+
},
|
|
302
|
+
context.deliveryContext,
|
|
303
|
+
signal,
|
|
304
|
+
);
|
|
305
|
+
return { answer: multi === true ? response.selected : response.selected[0] };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function executeScheduleJob(arguments_: unknown, context: GatewayHostToolContext): { job: string } {
|
|
309
|
+
const automation = requireAutomation(context);
|
|
310
|
+
const argumentsRecord = parseArguments(arguments_, ["name", "prompt", "at", "cron", "timezone"]);
|
|
311
|
+
const job = automation.create({
|
|
312
|
+
name: requiredNonEmptyString(argumentsRecord, "name"),
|
|
313
|
+
prompt: requiredNonEmptyString(argumentsRecord, "prompt"),
|
|
314
|
+
...(optionalNonEmptyString(argumentsRecord, "at") === undefined ? {} : { at: optionalNonEmptyString(argumentsRecord, "at") }),
|
|
315
|
+
...(optionalNonEmptyString(argumentsRecord, "cron") === undefined ? {} : { cron: optionalNonEmptyString(argumentsRecord, "cron") }),
|
|
316
|
+
...(optionalNonEmptyString(argumentsRecord, "timezone") === undefined ? {} : { timezone: optionalNonEmptyString(argumentsRecord, "timezone") }),
|
|
317
|
+
}, scheduledContext(context));
|
|
318
|
+
return { job: formatScheduledJob(job) };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function executeUpdateJob(arguments_: unknown, context: GatewayHostToolContext): { job: string } {
|
|
322
|
+
const automation = requireAutomation(context);
|
|
323
|
+
const argumentsRecord = parseArguments(arguments_, ["id", "name", "prompt", "at", "cron", "timezone"]);
|
|
324
|
+
const job = automation.update({
|
|
325
|
+
id: requiredNonEmptyString(argumentsRecord, "id"),
|
|
326
|
+
...(optionalNonEmptyString(argumentsRecord, "name") === undefined ? {} : { name: optionalNonEmptyString(argumentsRecord, "name") }),
|
|
327
|
+
...(optionalNonEmptyString(argumentsRecord, "prompt") === undefined ? {} : { prompt: optionalNonEmptyString(argumentsRecord, "prompt") }),
|
|
328
|
+
...(optionalNonEmptyString(argumentsRecord, "at") === undefined ? {} : { at: optionalNonEmptyString(argumentsRecord, "at") }),
|
|
329
|
+
...(optionalNonEmptyString(argumentsRecord, "cron") === undefined ? {} : { cron: optionalNonEmptyString(argumentsRecord, "cron") }),
|
|
330
|
+
...(optionalNonEmptyString(argumentsRecord, "timezone") === undefined ? {} : { timezone: optionalNonEmptyString(argumentsRecord, "timezone") }),
|
|
331
|
+
}, scheduledContext(context));
|
|
332
|
+
return { job: formatScheduledJob(job) };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function executeListJobs(arguments_: unknown, context: GatewayHostToolContext): { jobs: readonly string[] } {
|
|
336
|
+
parseArguments(arguments_, []);
|
|
337
|
+
return {
|
|
338
|
+
jobs: requireAutomation(context).list(context.deliveryContext.principal.id).map(formatScheduledJob),
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function executeSetJobEnabled(arguments_: unknown, context: GatewayHostToolContext): { job: string } {
|
|
343
|
+
const argumentsRecord = parseArguments(arguments_, ["id", "enabled"]);
|
|
344
|
+
const enabled = optionalBoolean(argumentsRecord, "enabled");
|
|
345
|
+
if (enabled === undefined) throw new Error("enabled is required");
|
|
346
|
+
const job = requireAutomation(context).setEnabled(
|
|
347
|
+
requiredNonEmptyString(argumentsRecord, "id"),
|
|
348
|
+
context.deliveryContext.principal.id,
|
|
349
|
+
enabled,
|
|
350
|
+
);
|
|
351
|
+
return { job: formatScheduledJob(job) };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function executeDeleteJob(arguments_: unknown, context: GatewayHostToolContext): { deleted: boolean } {
|
|
355
|
+
const argumentsRecord = parseArguments(arguments_, ["id"]);
|
|
356
|
+
return {
|
|
357
|
+
deleted: requireAutomation(context).remove(
|
|
358
|
+
requiredNonEmptyString(argumentsRecord, "id"),
|
|
359
|
+
context.deliveryContext.principal.id,
|
|
360
|
+
),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function executeRunJob(arguments_: unknown, context: GatewayHostToolContext): { job: string } {
|
|
365
|
+
const argumentsRecord = parseArguments(arguments_, ["id"]);
|
|
366
|
+
const job = requireAutomation(context).runNow(
|
|
367
|
+
requiredNonEmptyString(argumentsRecord, "id"),
|
|
368
|
+
context.deliveryContext.principal.id,
|
|
369
|
+
);
|
|
370
|
+
return { job: formatScheduledJob(job) };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function requireAutomation(context: GatewayHostToolContext): GatewayAutomationControl {
|
|
374
|
+
if (context.automation === undefined) throw new Error("OmpClaw automation is disabled");
|
|
375
|
+
return context.automation;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function scheduledContext(context: GatewayHostToolContext): ScheduledJobContext {
|
|
379
|
+
return {
|
|
380
|
+
principal: context.deliveryContext.principal,
|
|
381
|
+
identity: context.identity,
|
|
382
|
+
address: context.address,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function parseArguments(value: unknown, allowedKeys: readonly string[]): RpcRecord {
|
|
387
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
388
|
+
throw new Error("Host tool arguments must be an object");
|
|
389
|
+
}
|
|
390
|
+
const argumentsRecord = value as RpcRecord;
|
|
391
|
+
for (const key of Object.keys(argumentsRecord)) {
|
|
392
|
+
if (!allowedKeys.includes(key)) throw new Error(`Unknown host tool argument: ${key}`);
|
|
393
|
+
}
|
|
394
|
+
return argumentsRecord;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function optionalString(argumentsRecord: RpcRecord, key: string): string | undefined {
|
|
398
|
+
if (!Object.hasOwn(argumentsRecord, key)) return undefined;
|
|
399
|
+
const value = argumentsRecord[key];
|
|
400
|
+
if (typeof value !== "string") throw new Error(`${key} must be a string`);
|
|
401
|
+
return value;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function optionalNonEmptyString(argumentsRecord: RpcRecord, key: string): string | undefined {
|
|
405
|
+
const value = optionalString(argumentsRecord, key);
|
|
406
|
+
if (value !== undefined && value.length === 0) throw new Error(`${key} must not be empty`);
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function requiredNonEmptyString(argumentsRecord: RpcRecord, key: string): string {
|
|
411
|
+
const value = optionalNonEmptyString(argumentsRecord, key);
|
|
412
|
+
if (value === undefined) throw new Error(`${key} is required`);
|
|
413
|
+
return value;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function optionalAbsoluteFiles(argumentsRecord: RpcRecord, key: string): readonly MessageAttachment[] | undefined {
|
|
417
|
+
if (!Object.hasOwn(argumentsRecord, key)) return undefined;
|
|
418
|
+
const value = argumentsRecord[key];
|
|
419
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error(`${key} must be a non-empty array`);
|
|
420
|
+
return value.map((file) => {
|
|
421
|
+
if (typeof file !== "string" || file.length === 0 || !isAbsolute(file)) {
|
|
422
|
+
throw new Error(`${key} must contain absolute local paths`);
|
|
423
|
+
}
|
|
424
|
+
return { url: pathToFileURL(file).href };
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function optionalOptions(argumentsRecord: RpcRecord, key: string): readonly string[] | undefined {
|
|
429
|
+
if (!Object.hasOwn(argumentsRecord, key)) return undefined;
|
|
430
|
+
const value = argumentsRecord[key];
|
|
431
|
+
if (!Array.isArray(value) || value.length === 0) throw new Error(`${key} must be a non-empty array`);
|
|
432
|
+
return value.map((option) => {
|
|
433
|
+
if (typeof option !== "string" || option.length === 0) throw new Error(`${key} must contain non-empty strings`);
|
|
434
|
+
return option;
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function optionalBoolean(argumentsRecord: RpcRecord, key: string): boolean | undefined {
|
|
439
|
+
if (!Object.hasOwn(argumentsRecord, key)) return undefined;
|
|
440
|
+
const value = argumentsRecord[key];
|
|
441
|
+
if (typeof value !== "boolean") throw new Error(`${key} must be a boolean`);
|
|
442
|
+
return value;
|
|
443
|
+
}
|