@shadowob/openclaw-shadowob 1.1.0 → 1.1.3

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/dist/index.js CHANGED
@@ -1,16 +1,1023 @@
1
1
  import {
2
- shadowPlugin
3
- } from "./chunk-XPNVTXKL.js";
2
+ DEFAULT_ACCOUNT_ID,
3
+ getAccountConfig,
4
+ listAccountIds,
5
+ resolveAccount,
6
+ shadowPluginCapabilities,
7
+ shadowPluginConfig,
8
+ shadowPluginConfigSchema,
9
+ shadowPluginMeta,
10
+ shadowPluginSetup
11
+ } from "./chunk-NBNZ7NVR.js";
4
12
  import {
5
13
  getShadowRuntime,
6
14
  monitorShadowProvider,
15
+ resolveOutboundMentions,
7
16
  setShadowRuntime,
8
17
  tryGetShadowRuntime
9
- } from "./chunk-QFUUQPJZ.js";
18
+ } from "./chunk-PEV3R2R7.js";
10
19
 
11
20
  // index.ts
12
21
  import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
22
+
23
+ // src/channel/plugin.ts
24
+ import { ShadowClient as ShadowClient3 } from "@shadowob/sdk";
25
+ import {
26
+ buildChannelOutboundSessionRoute,
27
+ buildThreadAwareOutboundSessionRoute,
28
+ createChatChannelPlugin
29
+ } from "openclaw/plugin-sdk/core";
30
+
31
+ // src/outbound.ts
13
32
  import { ShadowClient } from "@shadowob/sdk";
33
+ var CHUNK_SIZE = 16e3;
34
+ function chunkText(text, maxLen = CHUNK_SIZE) {
35
+ if (text.length <= maxLen) return [text];
36
+ const chunks = [];
37
+ let remaining = text;
38
+ while (remaining.length > maxLen) {
39
+ let splitAt = maxLen;
40
+ const paraIdx = remaining.lastIndexOf("\n\n", maxLen);
41
+ if (paraIdx > maxLen * 0.5) {
42
+ splitAt = paraIdx + 2;
43
+ } else {
44
+ const lineIdx = remaining.lastIndexOf("\n", maxLen);
45
+ if (lineIdx > maxLen * 0.6) {
46
+ splitAt = lineIdx + 1;
47
+ } else {
48
+ const head = remaining.slice(0, maxLen);
49
+ const sentenceRe = /[。!?.!?][\s\u200B]*$/;
50
+ const m = head.match(sentenceRe);
51
+ if (m && m.index !== void 0 && m.index > maxLen * 0.4) {
52
+ splitAt = m.index + m[0].length;
53
+ }
54
+ }
55
+ }
56
+ chunks.push(remaining.slice(0, splitAt).trimEnd());
57
+ remaining = remaining.slice(splitAt).trimStart();
58
+ }
59
+ if (remaining.length > 0) {
60
+ chunks.push(remaining);
61
+ }
62
+ return chunks;
63
+ }
64
+ function parseTarget(to) {
65
+ const parts = to.split(":");
66
+ const prefix = parts[0];
67
+ if ((prefix === "shadowob" || prefix === "openclaw-shadowob") && parts[1] === "channel" && parts[2]) {
68
+ return {
69
+ channelId: parts[2],
70
+ ...parts[3] === "thread" && parts[4] ? { threadId: parts[4] } : {}
71
+ };
72
+ }
73
+ if ((prefix === "shadowob" || prefix === "openclaw-shadowob") && parts[1] === "thread" && parts[2]) {
74
+ return { threadId: parts[2] };
75
+ }
76
+ return { channelId: to };
77
+ }
78
+ function resolveClient(cfg, accountId) {
79
+ const account = getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
80
+ if (!account) return null;
81
+ return { client: new ShadowClient(account.serverUrl, account.token), account };
82
+ }
83
+ async function sendTextChunks(params) {
84
+ const { channelId, threadId: parsedThreadId } = parseTarget(params.to);
85
+ const threadId = params.threadId !== void 0 && params.threadId !== null ? String(params.threadId) : parsedThreadId;
86
+ const chunks = chunkText(params.text);
87
+ let lastMessage;
88
+ for (let i = 0; i < chunks.length; i++) {
89
+ const chunk = chunks[i];
90
+ const replyTo = i === 0 ? params.replyToMessageId ?? void 0 : lastMessage?.id;
91
+ if (threadId && channelId) {
92
+ const mentions = await resolveOutboundMentions({
93
+ client: params.client,
94
+ channelId,
95
+ content: chunk
96
+ });
97
+ lastMessage = await params.client.sendMessage(channelId, chunk, {
98
+ threadId,
99
+ replyToId: replyTo,
100
+ ...mentions ? { mentions } : {}
101
+ });
102
+ } else if (threadId) {
103
+ lastMessage = replyTo ? await params.client.sendToThread(threadId, chunk, { replyToId: replyTo }) : await params.client.sendToThread(threadId, chunk);
104
+ } else if (channelId) {
105
+ const mentions = await resolveOutboundMentions({
106
+ client: params.client,
107
+ channelId,
108
+ content: chunk
109
+ });
110
+ lastMessage = await params.client.sendMessage(channelId, chunk, {
111
+ replyToId: replyTo,
112
+ ...mentions ? { mentions } : {}
113
+ });
114
+ } else {
115
+ throw new Error("Could not resolve target channel or thread");
116
+ }
117
+ }
118
+ if (!lastMessage) {
119
+ throw new Error("No message was sent");
120
+ }
121
+ return { message: lastMessage, channelId, threadId };
122
+ }
123
+ function toDeliveryResult(params) {
124
+ return {
125
+ channel: "shadowob",
126
+ messageId: params.message.id,
127
+ channelId: params.channelId ?? params.message.channelId,
128
+ conversationId: params.threadId ?? params.channelId ?? params.message.channelId,
129
+ ...params.threadId ? {
130
+ meta: {
131
+ threadId: params.threadId
132
+ }
133
+ } : {}
134
+ };
135
+ }
136
+ async function sendMediaToShadow(params) {
137
+ const mediaUrls = [params.mediaUrl ?? params.filePath, ...params.mediaUrls ?? []].filter(
138
+ Boolean
139
+ );
140
+ if (mediaUrls.length === 0) {
141
+ throw new Error("No media URL or file path provided");
142
+ }
143
+ const sent = await sendTextChunks({
144
+ client: params.client,
145
+ to: params.to,
146
+ text: params.text || "\u200B",
147
+ threadId: params.threadId,
148
+ replyToMessageId: params.replyToMessageId
149
+ });
150
+ let result = toDeliveryResult(sent);
151
+ const uploadErrors = [];
152
+ for (const mediaUrl of mediaUrls) {
153
+ try {
154
+ await params.client.uploadMediaFromUrl(mediaUrl, sent.message.id);
155
+ } catch (err) {
156
+ const fallback = await sendTextChunks({
157
+ client: params.client,
158
+ to: params.to,
159
+ text: mediaUrl,
160
+ threadId: params.threadId,
161
+ replyToMessageId: result.messageId
162
+ });
163
+ uploadErrors.push(err instanceof Error ? err.message : String(err));
164
+ const fallbackResult = toDeliveryResult(fallback);
165
+ result = {
166
+ ...fallbackResult,
167
+ meta: {
168
+ ...fallbackResult.meta ?? {},
169
+ mediaUploadFallback: true,
170
+ mediaUploadErrors: uploadErrors
171
+ }
172
+ };
173
+ }
174
+ }
175
+ return result;
176
+ }
177
+ var shadowOutbound = {
178
+ deliveryMode: "direct",
179
+ chunker: chunkText,
180
+ textChunkLimit: CHUNK_SIZE,
181
+ sendText: async (params) => {
182
+ const resolved = resolveClient(params.cfg, params.accountId ?? void 0);
183
+ if (!resolved) throw new Error("Shadow account not configured");
184
+ const sent = await sendTextChunks({
185
+ client: resolved.client,
186
+ to: params.to,
187
+ text: params.text,
188
+ threadId: params.threadId,
189
+ replyToMessageId: params.replyToId
190
+ });
191
+ return toDeliveryResult(sent);
192
+ },
193
+ sendMedia: async (params) => {
194
+ const resolved = resolveClient(params.cfg, params.accountId ?? void 0);
195
+ if (!resolved) throw new Error("Shadow account not configured");
196
+ return sendMediaToShadow({
197
+ client: resolved.client,
198
+ to: params.to,
199
+ filePath: params.filePath,
200
+ mediaUrl: params.mediaUrl,
201
+ mediaUrls: params.mediaUrls,
202
+ text: params.text,
203
+ threadId: params.threadId,
204
+ replyToMessageId: params.replyToId
205
+ });
206
+ }
207
+ };
208
+
209
+ // src/channel/actions.ts
210
+ import { ShadowClient as ShadowClient2 } from "@shadowob/sdk";
211
+
212
+ // src/channel/interactive.ts
213
+ var SHADOW_INTERACTIVE_KINDS = ["buttons", "select", "form", "approval"];
214
+ function isRecord(value) {
215
+ return !!value && typeof value === "object" && !Array.isArray(value);
216
+ }
217
+ function readStringLike(value, trim = true) {
218
+ if (value === void 0 || value === null) return void 0;
219
+ if (typeof value === "string") return trim ? value.trim() : value;
220
+ if (typeof value === "number" || typeof value === "boolean") {
221
+ const stringValue = String(value);
222
+ return trim ? stringValue.trim() : stringValue;
223
+ }
224
+ return void 0;
225
+ }
226
+ function firstString(...values) {
227
+ for (const value of values) {
228
+ const stringValue = readStringLike(value);
229
+ if (stringValue) return stringValue;
230
+ }
231
+ return void 0;
232
+ }
233
+ function readMessageTarget(params) {
234
+ return firstString(params.to, params.target, params.recipient, params.channelId) ?? "";
235
+ }
236
+ function normalizeInteractiveKind(value) {
237
+ const raw = readStringLike(value)?.toLowerCase();
238
+ if (!raw) return void 0;
239
+ return SHADOW_INTERACTIVE_KINDS.includes(raw) ? raw : void 0;
240
+ }
241
+ function normalizeButtonStyle(value) {
242
+ const raw = readStringLike(value)?.toLowerCase();
243
+ if (raw === "primary" || raw === "secondary" || raw === "destructive") return raw;
244
+ if (raw === "danger") return "destructive";
245
+ return void 0;
246
+ }
247
+ function normalizeButtonItems(value) {
248
+ if (!Array.isArray(value)) return void 0;
249
+ const items = value.filter(isRecord).map((button, index) => {
250
+ const label = firstString(
251
+ button.label,
252
+ button.text,
253
+ button.title,
254
+ button.value,
255
+ `Option ${index + 1}`
256
+ );
257
+ const id = firstString(button.id, button.actionId, button.value, label, `button_${index + 1}`);
258
+ const normalized = { id, label };
259
+ const value2 = readStringLike(button.value, false);
260
+ const style = normalizeButtonStyle(button.style);
261
+ if (value2 !== void 0) normalized.value = value2;
262
+ if (style) normalized.style = style;
263
+ return normalized;
264
+ }).filter((button) => button.id && button.label);
265
+ return items.length > 0 ? items : void 0;
266
+ }
267
+ function normalizeSelectItems(value) {
268
+ if (!Array.isArray(value)) return void 0;
269
+ const items = value.filter(isRecord).map((option, index) => {
270
+ const label = firstString(
271
+ option.label,
272
+ option.text,
273
+ option.title,
274
+ option.value,
275
+ `Option ${index + 1}`
276
+ );
277
+ const value2 = firstString(option.value, option.id, label, `option_${index + 1}`);
278
+ const id = firstString(option.id, value2, `option_${index + 1}`);
279
+ return { id, label, value: value2 };
280
+ }).filter((option) => option.id && option.label && option.value);
281
+ return items.length > 0 ? items : void 0;
282
+ }
283
+ function normalizeFormFieldKind(value) {
284
+ const raw = readStringLike(value)?.toLowerCase();
285
+ if (["text", "textarea", "number", "checkbox", "select"].includes(raw ?? "")) return raw;
286
+ return void 0;
287
+ }
288
+ function normalizeFormFields(value) {
289
+ if (!Array.isArray(value)) return void 0;
290
+ const fields = value.filter(isRecord).map((field, index) => {
291
+ const label = firstString(field.label, field.name, field.id, `Field ${index + 1}`);
292
+ const id = firstString(field.id, field.name, label, `field_${index + 1}`);
293
+ const kind = normalizeFormFieldKind(field.kind) ?? normalizeFormFieldKind(field.type) ?? "text";
294
+ const normalized = { id, kind, label };
295
+ for (const key of ["placeholder", "defaultValue"]) {
296
+ const value2 = readStringLike(field[key], false);
297
+ if (value2 !== void 0) normalized[key] = value2;
298
+ }
299
+ if (typeof field.required === "boolean") normalized.required = field.required;
300
+ if (typeof field.maxLength === "number") normalized.maxLength = field.maxLength;
301
+ if (typeof field.min === "number") normalized.min = field.min;
302
+ if (typeof field.max === "number") normalized.max = field.max;
303
+ const options = normalizeSelectItems(field.options);
304
+ if (options) normalized.options = options;
305
+ return normalized;
306
+ }).filter((field) => field.id && field.kind && field.label);
307
+ return fields.length > 0 ? fields : void 0;
308
+ }
309
+ function resolveShadowInteractiveBlock(params) {
310
+ const rawInteractive = isRecord(params.interactive) ? params.interactive : void 0;
311
+ const source = rawInteractive && normalizeInteractiveKind(rawInteractive.kind) ? rawInteractive : params;
312
+ let kind = normalizeInteractiveKind(source.kind);
313
+ const buttons = normalizeButtonItems(source.buttons);
314
+ const options = normalizeSelectItems(source.options);
315
+ const fields = normalizeFormFields(source.fields);
316
+ if (!kind) {
317
+ if (fields) kind = "form";
318
+ else if (options) kind = "select";
319
+ else if (buttons) kind = "buttons";
320
+ }
321
+ if (!kind) return void 0;
322
+ const prompt = firstString(source.prompt, source.message, source.content, source.text);
323
+ const block = {
324
+ id: firstString(source.blockId, source.id) ?? `ia_${Date.now().toString(36)}`,
325
+ kind,
326
+ ...prompt ? { prompt } : {}
327
+ };
328
+ if (buttons) block.buttons = buttons;
329
+ if (options) block.options = options;
330
+ if (fields) block.fields = fields;
331
+ const submitLabel = readStringLike(source.submitLabel);
332
+ const responsePrompt = readStringLike(source.responsePrompt);
333
+ const approvalCommentLabel = readStringLike(source.approvalCommentLabel);
334
+ if (submitLabel) block.submitLabel = submitLabel;
335
+ if (responsePrompt) block.responsePrompt = responsePrompt;
336
+ if (approvalCommentLabel) block.approvalCommentLabel = approvalCommentLabel;
337
+ if (typeof source.oneShot === "boolean") block.oneShot = source.oneShot;
338
+ return block;
339
+ }
340
+ function validateApprovalMessageContent(content, interactiveBlock) {
341
+ if (interactiveBlock?.kind !== "approval") return null;
342
+ const trimmed = content.trim();
343
+ const prompt = readStringLike(interactiveBlock.prompt);
344
+ const normalized = trimmed.replace(/\s+/g, " ");
345
+ const normalizedPrompt = prompt?.replace(/\s+/g, " ");
346
+ const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
347
+ if (lines.length >= 4 || trimmed.length >= 240) return null;
348
+ if (trimmed.length >= 180 && normalized !== normalizedPrompt) return null;
349
+ return [
350
+ "approval dialogs must be attached to a visible proposal in the same message",
351
+ "include the concrete roadmap, MVP scope, plan, or decision before asking for approval"
352
+ ].join("; ");
353
+ }
354
+
355
+ // src/channel/send.ts
356
+ async function sendShadowMessage(params) {
357
+ const { channelId, threadId: parsedThreadId } = parseTarget(params.to);
358
+ const threadId = params.threadId ?? parsedThreadId;
359
+ if (threadId && channelId) {
360
+ const mentions = params.mentions ?? await resolveOutboundMentions({
361
+ client: params.client,
362
+ channelId,
363
+ content: params.content
364
+ });
365
+ return params.client.sendMessage(channelId, params.content, {
366
+ threadId,
367
+ replyToId: params.replyToId,
368
+ ...mentions ? { mentions } : {},
369
+ metadata: params.metadata
370
+ });
371
+ }
372
+ if (threadId) {
373
+ return params.client.sendToThread(threadId, params.content, {
374
+ ...params.replyToId ? { replyToId: params.replyToId } : {},
375
+ metadata: params.metadata
376
+ });
377
+ }
378
+ if (channelId) {
379
+ const mentions = params.mentions ?? await resolveOutboundMentions({
380
+ client: params.client,
381
+ channelId,
382
+ content: params.content
383
+ });
384
+ return params.client.sendMessage(channelId, params.content, {
385
+ replyToId: params.replyToId,
386
+ ...mentions ? { mentions } : {},
387
+ metadata: params.metadata
388
+ });
389
+ }
390
+ throw new Error("Could not resolve target channel or thread");
391
+ }
392
+
393
+ // src/channel/typebox-schema.ts
394
+ var TYPEBOX_KIND = /* @__PURE__ */ Symbol.for("TypeBox.Kind");
395
+ var TYPEBOX_OPTIONAL = /* @__PURE__ */ Symbol.for("TypeBox.Optional");
396
+ var OPENCLAW_TYPEBOX_KIND = "~kind";
397
+ var OPENCLAW_TYPEBOX_OPTIONAL = "~optional";
398
+ function typeboxSchema(kind, schema) {
399
+ Object.defineProperties(schema, {
400
+ [TYPEBOX_KIND]: { value: kind },
401
+ [OPENCLAW_TYPEBOX_KIND]: { value: kind }
402
+ });
403
+ return schema;
404
+ }
405
+ function optionalSchema(schema) {
406
+ Object.defineProperties(schema, {
407
+ [TYPEBOX_OPTIONAL]: { value: "Optional" },
408
+ [OPENCLAW_TYPEBOX_OPTIONAL]: { value: "Optional" }
409
+ });
410
+ return schema;
411
+ }
412
+ function stringSchema(description) {
413
+ return typeboxSchema("String", {
414
+ type: "string",
415
+ ...description ? { description } : {}
416
+ });
417
+ }
418
+ function numberSchema(description) {
419
+ return typeboxSchema("Number", {
420
+ type: "number",
421
+ ...description ? { description } : {}
422
+ });
423
+ }
424
+ function booleanSchema(description) {
425
+ return typeboxSchema("Boolean", {
426
+ type: "boolean",
427
+ ...description ? { description } : {}
428
+ });
429
+ }
430
+ function literalSchema(value) {
431
+ return typeboxSchema("Literal", { const: value, type: "string" });
432
+ }
433
+ function enumSchema(values, description) {
434
+ return typeboxSchema("Union", {
435
+ anyOf: values.map((value) => literalSchema(value)),
436
+ ...description ? { description } : {}
437
+ });
438
+ }
439
+ function arraySchema(items, options = {}) {
440
+ return typeboxSchema("Array", { type: "array", items, ...options });
441
+ }
442
+ function objectSchema(properties, options = {}) {
443
+ const required = Object.entries(properties).filter(([, schema]) => schema[TYPEBOX_OPTIONAL] !== "Optional").filter(([, schema]) => schema[OPENCLAW_TYPEBOX_OPTIONAL] !== "Optional").map(([key]) => key);
444
+ return typeboxSchema("Object", {
445
+ type: "object",
446
+ properties,
447
+ required,
448
+ ...options
449
+ });
450
+ }
451
+ var shadowInteractiveButtonSchema = objectSchema({
452
+ id: stringSchema("Stable button id returned in the interaction response."),
453
+ label: stringSchema("Button text shown to the user."),
454
+ value: optionalSchema(stringSchema("Optional value returned when selected.")),
455
+ style: optionalSchema(enumSchema(["primary", "secondary", "destructive"]))
456
+ });
457
+ var shadowInteractiveSelectOptionSchema = objectSchema({
458
+ id: stringSchema("Stable option id returned in the interaction response."),
459
+ label: stringSchema("Option text shown to the user."),
460
+ value: stringSchema("Value returned when selected.")
461
+ });
462
+ var shadowInteractiveFormFieldSchema = objectSchema({
463
+ id: stringSchema("Stable field id returned in submitted values."),
464
+ kind: optionalSchema(enumSchema(["text", "textarea", "number", "checkbox", "select"])),
465
+ type: optionalSchema(
466
+ enumSchema(["text", "textarea", "number", "checkbox", "select"], "Alias for kind.")
467
+ ),
468
+ label: stringSchema("Field label shown to the user."),
469
+ placeholder: optionalSchema(stringSchema()),
470
+ defaultValue: optionalSchema(stringSchema()),
471
+ required: optionalSchema(booleanSchema()),
472
+ options: optionalSchema(arraySchema(shadowInteractiveSelectOptionSchema, { maxItems: 20 })),
473
+ maxLength: optionalSchema(numberSchema()),
474
+ min: optionalSchema(numberSchema()),
475
+ max: optionalSchema(numberSchema())
476
+ });
477
+ var shadowMessageToolSchemaProperties = {
478
+ kind: optionalSchema(
479
+ enumSchema(
480
+ ["buttons", "select", "form", "approval"],
481
+ 'Shadow interactive dialog kind. Use with action "send" when buttons, select, form, or approval UI is needed.'
482
+ )
483
+ ),
484
+ prompt: optionalSchema(
485
+ stringSchema("Prompt rendered inside a Shadow interactive block; usually match message.")
486
+ ),
487
+ blockId: optionalSchema(stringSchema("Optional stable interactive block id.")),
488
+ buttons: optionalSchema(arraySchema(shadowInteractiveButtonSchema, { maxItems: 8 })),
489
+ options: optionalSchema(arraySchema(shadowInteractiveSelectOptionSchema, { maxItems: 20 })),
490
+ fields: optionalSchema(arraySchema(shadowInteractiveFormFieldSchema, { maxItems: 12 })),
491
+ submitLabel: optionalSchema(stringSchema("Submit button label for form dialogs.")),
492
+ responsePrompt: optionalSchema(
493
+ stringSchema("Instruction sent back to the Buddy when this form is submitted.")
494
+ ),
495
+ approvalCommentLabel: optionalSchema(
496
+ stringSchema("Optional comment label for approval dialogs.")
497
+ ),
498
+ oneShot: optionalSchema(booleanSchema("Disable the dialog after one response.")),
499
+ media: optionalSchema(stringSchema("Attachment source URL or local path for file upload.")),
500
+ mediaUrl: optionalSchema(stringSchema("Alias for media.")),
501
+ url: optionalSchema(stringSchema("Alias for media.")),
502
+ path: optionalSchema(stringSchema("Local attachment path.")),
503
+ filePath: optionalSchema(stringSchema("Local attachment path alias.")),
504
+ file: optionalSchema(stringSchema("Local attachment path alias.")),
505
+ fileUrl: optionalSchema(stringSchema("Attachment URL alias.")),
506
+ buffer: optionalSchema(stringSchema("Base64 attachment payload for file upload.")),
507
+ filename: optionalSchema(stringSchema("Attachment filename when buffer is used.")),
508
+ contentType: optionalSchema(stringSchema("Attachment MIME type when buffer is used.")),
509
+ mimeType: optionalSchema(stringSchema("Alias for contentType.")),
510
+ caption: optionalSchema(stringSchema("Optional text sent with an attachment.")),
511
+ commerceOfferId: optionalSchema(
512
+ stringSchema("Shadow CommerceOfferId to attach as a purchasable product card.")
513
+ ),
514
+ offerId: optionalSchema(stringSchema("Alias for commerceOfferId."))
515
+ };
516
+ function buildShadowMessageToolSchemaProperties(input) {
517
+ const offers = (input?.commerceOffers ?? []).filter((offer) => offer.offerId?.trim() && !offer.offerId.includes("${env:")).slice(0, 12);
518
+ if (offers.length === 0) return shadowMessageToolSchemaProperties;
519
+ const offerHints = offers.map((offer) => {
520
+ const label = offer.name ?? offer.seedId ?? offer.offerId;
521
+ const summary = offer.summary ? ` - ${offer.summary}` : "";
522
+ return `${label}: ${offer.offerId}${summary}`;
523
+ }).join("; ");
524
+ return {
525
+ ...shadowMessageToolSchemaProperties,
526
+ commerceOfferId: optionalSchema(
527
+ stringSchema(
528
+ `Attach one of the available Shadow commerce offers as a purchasable product card. Available CommerceOfferIds: ${offerHints}. Use only when the user wants to buy, view pricing, or receive a product card.`
529
+ )
530
+ ),
531
+ offerId: optionalSchema(stringSchema("Alias for commerceOfferId."))
532
+ };
533
+ }
534
+
535
+ // src/channel/actions.ts
536
+ var SHADOW_DISCOVERED_ACTIONS = ["send", "upload-file", "react", "edit", "delete"];
537
+ var SHADOW_HANDLED_ACTIONS = [...SHADOW_DISCOVERED_ACTIONS, "get-connection-status"];
538
+ function textResult(value) {
539
+ return {
540
+ content: [
541
+ {
542
+ type: "text",
543
+ text: JSON.stringify(value)
544
+ }
545
+ ],
546
+ details: value
547
+ };
548
+ }
549
+ function readAttachmentSource(params) {
550
+ return firstString(
551
+ params.media,
552
+ params.mediaUrl,
553
+ params.mediaURL,
554
+ params.url,
555
+ params.path,
556
+ params.filePath,
557
+ params.file,
558
+ params.fileUrl,
559
+ params.fileURL
560
+ ) ?? "";
561
+ }
562
+ function hasAttachmentPayload(params) {
563
+ return Boolean(firstString(params.buffer) || readAttachmentSource(params));
564
+ }
565
+ function readAttachmentContentType(params) {
566
+ return firstString(params.contentType, params.mimeType) ?? "application/octet-stream";
567
+ }
568
+ function readAttachmentFilename(params) {
569
+ return firstString(params.filename, params.title) ?? "file";
570
+ }
571
+ function readCommerceOfferId(params) {
572
+ return firstString(params.commerceOfferId, params.offerId);
573
+ }
574
+ function buildSendMetadata(params) {
575
+ const metadata = {};
576
+ if (params.interactiveBlock) metadata.interactive = params.interactiveBlock;
577
+ if (params.commerceOfferId) {
578
+ metadata.commerceCards = [{ kind: "offer", offerId: params.commerceOfferId }];
579
+ }
580
+ return Object.keys(metadata).length > 0 ? metadata : void 0;
581
+ }
582
+ async function uploadShadowAttachment(params) {
583
+ const contentType = readAttachmentContentType(params.actionParams);
584
+ const filename = readAttachmentFilename(params.actionParams);
585
+ const uploadTarget = params.messageId;
586
+ const base64Buffer = firstString(params.actionParams.buffer);
587
+ const mediaUrl = readAttachmentSource(params.actionParams);
588
+ if (base64Buffer) {
589
+ const raw = base64Buffer.includes(",") ? base64Buffer.split(",")[1] ?? "" : base64Buffer;
590
+ if (!raw) throw new Error("Invalid base64 attachment payload");
591
+ const bytes = Buffer.from(raw, "base64");
592
+ const blob = new Blob([Uint8Array.from(bytes)], { type: contentType });
593
+ await params.client.uploadMedia(blob, filename, contentType, uploadTarget);
594
+ return { filename, contentType, source: "buffer" };
595
+ }
596
+ if (mediaUrl) {
597
+ await params.client.uploadMediaFromUrl(mediaUrl, uploadTarget);
598
+ return { filename, contentType, source: "media", mediaUrl };
599
+ }
600
+ throw new Error("No buffer or media URL provided for attachment");
601
+ }
602
+ var shadowMessageActions = {
603
+ describeMessageTool: ({
604
+ cfg,
605
+ accountId
606
+ }) => {
607
+ const account = getAccountConfig(cfg, accountId ?? DEFAULT_ACCOUNT_ID);
608
+ return {
609
+ actions: [...SHADOW_DISCOVERED_ACTIONS],
610
+ capabilities: ["interactive"],
611
+ schema: {
612
+ visibility: "current-channel",
613
+ properties: buildShadowMessageToolSchemaProperties({
614
+ commerceOffers: account?.commerceOffers
615
+ })
616
+ },
617
+ mediaSourceParams: {
618
+ "upload-file": [
619
+ "media",
620
+ "mediaUrl",
621
+ "url",
622
+ "path",
623
+ "filePath",
624
+ "file",
625
+ "fileUrl",
626
+ "buffer"
627
+ ]
628
+ }
629
+ };
630
+ },
631
+ messageActionTargetAliases: {
632
+ "upload-file": { aliases: ["recipient", "to", "channelId"] }
633
+ },
634
+ supportsAction: ({ action }) => SHADOW_HANDLED_ACTIONS.includes(action),
635
+ handleAction: async (ctx) => {
636
+ const account = getAccountConfig(ctx.cfg, ctx.accountId ?? DEFAULT_ACCOUNT_ID);
637
+ if (!account) {
638
+ return textResult({ ok: false, error: "Shadow account not configured" });
639
+ }
640
+ const requestedAction = String(ctx.action);
641
+ const action = requestedAction;
642
+ const { params } = ctx;
643
+ if (action === "send") {
644
+ try {
645
+ const client = new ShadowClient2(account.serverUrl, account.token);
646
+ const to = readMessageTarget(params);
647
+ if (!to) return textResult({ ok: false, error: "target is required" });
648
+ const interactiveBlock = resolveShadowInteractiveBlock(params);
649
+ const commerceOfferId = readCommerceOfferId(params);
650
+ const hasAttachment = hasAttachmentPayload(params);
651
+ const content = firstString(params.message, params.content, params.text, params.caption, params.prompt) ?? (interactiveBlock ? "[interactive]" : "");
652
+ if (!content.trim() && !interactiveBlock && !hasAttachment && !commerceOfferId) {
653
+ return textResult({
654
+ ok: false,
655
+ error: "message, attachment, or commerceOfferId is required"
656
+ });
657
+ }
658
+ const approvalError = validateApprovalMessageContent(content, interactiveBlock);
659
+ if (approvalError) return textResult({ ok: false, error: approvalError });
660
+ const message = await sendShadowMessage({
661
+ client,
662
+ to,
663
+ content: content.trim() ? content : interactiveBlock ? "[interactive]" : "\u200B",
664
+ threadId: params.threadId,
665
+ replyToId: params.replyTo ?? params.replyToId,
666
+ metadata: buildSendMetadata({ interactiveBlock, commerceOfferId })
667
+ });
668
+ const attachment = hasAttachment ? await uploadShadowAttachment({
669
+ client,
670
+ to,
671
+ messageId: message.id,
672
+ actionParams: params
673
+ }) : void 0;
674
+ return textResult({
675
+ ok: true,
676
+ action: "send",
677
+ messageId: message.id,
678
+ interactive: !!interactiveBlock,
679
+ kind: interactiveBlock?.kind,
680
+ commerceCard: !!commerceOfferId,
681
+ offerId: commerceOfferId,
682
+ attachment: !!attachment,
683
+ filename: attachment?.filename
684
+ });
685
+ } catch (err) {
686
+ return textResult({ ok: false, error: err instanceof Error ? err.message : String(err) });
687
+ }
688
+ }
689
+ if (action === "upload-file") {
690
+ try {
691
+ const client = new ShadowClient2(account.serverUrl, account.token);
692
+ const to = readMessageTarget(params);
693
+ if (!to) return textResult({ ok: false, error: "target is required" });
694
+ if (!hasAttachmentPayload(params)) {
695
+ return textResult({
696
+ ok: false,
697
+ error: "upload-file requires buffer, media, path, or filePath"
698
+ });
699
+ }
700
+ const text = firstString(params.message, params.content, params.text, params.caption) ?? "";
701
+ const message = await sendShadowMessage({
702
+ client,
703
+ to,
704
+ content: text || "\u200B",
705
+ threadId: params.threadId,
706
+ replyToId: params.replyTo ?? params.replyToId
707
+ });
708
+ const attachment = await uploadShadowAttachment({
709
+ client,
710
+ to,
711
+ messageId: message.id,
712
+ actionParams: params
713
+ });
714
+ return textResult({
715
+ ok: true,
716
+ action: requestedAction,
717
+ canonicalAction: "upload-file",
718
+ messageId: message.id,
719
+ filename: attachment.filename
720
+ });
721
+ } catch (err) {
722
+ return textResult({ ok: false, error: err instanceof Error ? err.message : String(err) });
723
+ }
724
+ }
725
+ if (action === "react") {
726
+ const client = new ShadowClient2(account.serverUrl, account.token);
727
+ const messageId = params.messageId ?? params.message_id ?? "";
728
+ const emoji = params.emoji ?? params.reaction ?? "";
729
+ if (!messageId || !emoji) {
730
+ return textResult({ ok: false, error: "messageId and emoji are required" });
731
+ }
732
+ try {
733
+ await client.addReaction(messageId, emoji);
734
+ return textResult({ ok: true, action: "react", messageId, emoji });
735
+ } catch (err) {
736
+ return textResult({ ok: false, error: String(err) });
737
+ }
738
+ }
739
+ if (action === "edit") {
740
+ const client = new ShadowClient2(account.serverUrl, account.token);
741
+ const messageId = params.messageId ?? params.message_id ?? "";
742
+ const content = params.message ?? params.content ?? "";
743
+ if (!messageId || !content) {
744
+ return textResult({ ok: false, error: "messageId and content are required" });
745
+ }
746
+ try {
747
+ await client.editMessage(messageId, content);
748
+ return textResult({ ok: true, action: "edit", messageId });
749
+ } catch (err) {
750
+ return textResult({ ok: false, error: String(err) });
751
+ }
752
+ }
753
+ if (action === "delete") {
754
+ const client = new ShadowClient2(account.serverUrl, account.token);
755
+ const messageId = params.messageId ?? params.message_id ?? "";
756
+ if (!messageId) {
757
+ return textResult({ ok: false, error: "messageId is required" });
758
+ }
759
+ try {
760
+ await client.deleteMessage(messageId);
761
+ return textResult({ ok: true, action: "delete", messageId });
762
+ } catch (err) {
763
+ return textResult({ ok: false, error: String(err) });
764
+ }
765
+ }
766
+ if (action === "pin" || action === "unpin") {
767
+ return textResult({ ok: false, error: `${action} is not yet supported for Shadow channels` });
768
+ }
769
+ if (action === "get-connection-status") {
770
+ const accountIds = listAccountIds(ctx.cfg);
771
+ const results = await Promise.all(
772
+ accountIds.map(async (id) => {
773
+ const acc = getAccountConfig(ctx.cfg, id);
774
+ if (!acc) return { accountId: id, configured: false, ok: false, error: "not configured" };
775
+ if (!acc.token?.trim())
776
+ return { accountId: id, configured: false, ok: false, error: "no token" };
777
+ try {
778
+ const client = new ShadowClient2(acc.serverUrl, acc.token);
779
+ const me = await client.getMe();
780
+ return {
781
+ accountId: id,
782
+ configured: true,
783
+ enabled: acc.enabled !== false,
784
+ ok: true,
785
+ serverUrl: acc.serverUrl,
786
+ user: me
787
+ };
788
+ } catch (err) {
789
+ return {
790
+ accountId: id,
791
+ configured: true,
792
+ enabled: acc.enabled !== false,
793
+ ok: false,
794
+ serverUrl: acc.serverUrl,
795
+ error: err instanceof Error ? err.message : String(err)
796
+ };
797
+ }
798
+ })
799
+ );
800
+ return textResult({ ok: true, action: "get-connection-status", accounts: results });
801
+ }
802
+ return textResult({ ok: false, error: `Action ${action} not yet implemented` });
803
+ }
804
+ };
805
+
806
+ // src/channel/prompt.ts
807
+ var shadowAgentPromptHints = [
808
+ "- When a Shadow user asks for buttons, choices, a select menu, a form, or approval, prefer sending a Shadow interactive dialog instead of plain text options.",
809
+ "- ShadowOwnBuddy enables inline buttons, forms, and file uploads for Shadow channels by default. Do not tell the user that `shadowob.capabilities.inlineButtons` or file sending is unavailable; use the message tool instead.",
810
+ '- Shadow interactive dialogs use the shared message tool with `action: "send"` plus `target`, `message`, `kind`, `prompt`, and shape fields. `message` is required by the shared tool; set `message` and `prompt` to the same user-visible text unless there is a specific reason not to. Supported `kind` values are `buttons`, `select`, `form`, and `approval`; Shadow stores these as `metadata.interactive` so the user can answer in-channel.',
811
+ '- Example buttons dialog: `action: "send"`, `target: "shadowob:channel:<ChannelId>"`, `message: "Choose the next step"`, `kind: "buttons"`, `prompt: "Choose the next step"`, `buttons: [{"id":"icp","label":"ICP / JTBD","value":"icp"}]`.',
812
+ '- Example form dialog: `action: "send"`, `target: "shadowob:channel:<ChannelId>"`, `message: "Fill the decision inputs"`, `kind: "form"`, `fields: [{"id":"decision","label":"Decision","kind":"textarea","required":true}]`.',
813
+ '- When Shadow context includes CommerceOfferIds and the user is interested in buying, viewing pricing, or receiving a product card, call the shared message tool yourself: `action: "send"`, `target: "shadowob:channel:<ChannelId>"`, `message: "A natural sales message"`, `commerceOfferId: "<CommerceOfferId>"`. Plain final text will not attach a product card.',
814
+ '- When a Shadow user asks for a file, prefer the shared message tool with `action: "send"` plus `target`, `message` or `caption`, and `path`/`filePath`/`media`. For a file-only upload, use OpenClaw\'s canonical `action: "upload-file"` plus `target`, `path`/`filePath`/`media` or `buffer`, optional `filename`, `contentType`, and `caption`. Shadow accepts arbitrary attachment types, including HTML; send the file directly instead of pasting source code as a workaround.',
815
+ "- Never use an `approval` dialog as a substitute for the proposal. Put the concrete roadmap, MVP scope, plan, or decision in `message` first; the approval block only locks that visible proposal."
816
+ ];
817
+
818
+ // src/channel/plugin.ts
819
+ var shadowPlugin = createChatChannelPlugin({
820
+ base: {
821
+ id: "shadowob",
822
+ meta: shadowPluginMeta,
823
+ capabilities: shadowPluginCapabilities,
824
+ config: shadowPluginConfig,
825
+ setup: shadowPluginSetup
826
+ },
827
+ threading: {
828
+ topLevelReplyToMode: "reply",
829
+ resolveReplyToMode: ({ cfg }) => {
830
+ const shadow = cfg.channels?.shadowob ?? cfg.channels?.["openclaw-shadowob"];
831
+ const mode = shadow?.replyToMode;
832
+ if (mode === "first" || mode === "all" || mode === "off") return mode;
833
+ return "first";
834
+ }
835
+ },
836
+ outbound: shadowOutbound
837
+ });
838
+ shadowPlugin.meta = shadowPluginMeta;
839
+ shadowPlugin.capabilities = shadowPluginCapabilities;
840
+ shadowPlugin.reload = {
841
+ configPrefixes: ["channels.shadowob"]
842
+ };
843
+ shadowPlugin.defaults = {
844
+ queue: { debounceMs: 500 }
845
+ };
846
+ shadowPlugin.configSchema = shadowPluginConfigSchema;
847
+ shadowPlugin.agentPrompt = {
848
+ messageToolHints: () => shadowAgentPromptHints
849
+ };
850
+ shadowPlugin.mentions = {
851
+ stripPatterns: () => ["@[\\w-]+"]
852
+ };
853
+ shadowPlugin.streaming = {
854
+ blockStreamingCoalesceDefaults: { minChars: 1500, idleMs: 1e3 }
855
+ };
856
+ shadowPlugin.messaging = {
857
+ normalizeTarget: (raw) => {
858
+ if (/^(shadowob|openclaw-shadowob):(channel|thread):.+$/i.test(raw)) return raw;
859
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw)) {
860
+ return `shadowob:channel:${raw}`;
861
+ }
862
+ return void 0;
863
+ },
864
+ parseExplicitTarget: ({ raw }) => {
865
+ const normalized = shadowPlugin.messaging?.normalizeTarget?.(raw);
866
+ if (!normalized) return null;
867
+ const match = normalized.match(/^(?:shadowob|openclaw-shadowob):(channel|thread):(.+)$/i);
868
+ if (!match) return { to: normalized, chatType: "channel" };
869
+ return match[1] === "thread" ? { to: normalized, threadId: match[2], chatType: "channel" } : { to: normalized, chatType: "channel" };
870
+ },
871
+ inferTargetChatType: () => "channel",
872
+ resolveSessionTarget: ({ id, threadId }) => threadId ? `shadowob:channel:${id}:thread:${threadId}` : `shadowob:channel:${id}`,
873
+ resolveOutboundSessionRoute: ({ cfg, agentId, accountId, target, threadId }) => {
874
+ const normalized = shadowPlugin.messaging?.normalizeTarget?.(target) ?? target;
875
+ const match = normalized.match(/^(?:shadowob|openclaw-shadowob):(channel|thread):(.+)$/i);
876
+ if (!match) return null;
877
+ const kind = match[1];
878
+ const id = match[2];
879
+ const route = buildChannelOutboundSessionRoute({
880
+ cfg,
881
+ agentId,
882
+ channel: "shadowob",
883
+ accountId,
884
+ peer: { kind: "channel", id },
885
+ chatType: "channel",
886
+ from: `shadowob:${kind}:${id}`,
887
+ to: `shadowob:${kind}:${id}`,
888
+ threadId: kind === "thread" ? id : void 0
889
+ });
890
+ return buildThreadAwareOutboundSessionRoute({
891
+ route,
892
+ threadId: kind === "thread" ? id : threadId,
893
+ precedence: ["threadId"],
894
+ useSuffix: false
895
+ });
896
+ },
897
+ targetResolver: {
898
+ looksLikeId: (raw) => /^(shadowob|openclaw-shadowob):(channel|thread):.+$/i.test(raw) || /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(raw),
899
+ hint: "Provide a Shadow channel UUID or shadowob:channel:<uuid>"
900
+ }
901
+ };
902
+ shadowPlugin.status = {
903
+ defaultRuntime: {
904
+ accountId: DEFAULT_ACCOUNT_ID,
905
+ running: false,
906
+ lastStartAt: null,
907
+ lastStopAt: null,
908
+ lastError: null
909
+ },
910
+ probeAccount: async ({
911
+ account,
912
+ timeoutMs
913
+ }) => {
914
+ const controller = new AbortController();
915
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
916
+ try {
917
+ const client = new ShadowClient3(account.serverUrl, account.token);
918
+ const me = await client.getMe();
919
+ return { ok: true, user: me };
920
+ } catch (err) {
921
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
922
+ } finally {
923
+ clearTimeout(timeout);
924
+ }
925
+ },
926
+ buildAccountSnapshot: ({
927
+ account,
928
+ runtime,
929
+ probe
930
+ }) => ({
931
+ accountId: DEFAULT_ACCOUNT_ID,
932
+ enabled: account?.enabled !== false,
933
+ configured: !!account?.token?.trim(),
934
+ running: runtime?.running ?? false,
935
+ lastStartAt: runtime?.lastStartAt ?? null,
936
+ lastStopAt: runtime?.lastStopAt ?? null,
937
+ lastError: runtime?.lastError ?? null,
938
+ probe
939
+ }),
940
+ buildChannelSummary: ({
941
+ snapshot
942
+ }) => ({
943
+ configured: snapshot.configured ?? false,
944
+ running: snapshot.running ?? false,
945
+ lastStartAt: snapshot.lastStartAt ?? null,
946
+ lastStopAt: snapshot.lastStopAt ?? null,
947
+ lastError: snapshot.lastError ?? null,
948
+ probe: snapshot.probe
949
+ })
950
+ };
951
+ shadowPlugin.gateway = {
952
+ startAccount: async (ctx) => {
953
+ const account = ctx.account;
954
+ const accountId = ctx.accountId;
955
+ ctx.setStatus({
956
+ accountId,
957
+ running: true,
958
+ lastStartAt: Date.now(),
959
+ lastError: null
960
+ });
961
+ ctx.log?.info(`Starting Shadow connection for account ${accountId}`);
962
+ const { monitorShadowProvider: monitorShadowProvider2 } = await import("./monitor-AE3LRQYD.js");
963
+ await monitorShadowProvider2({
964
+ account,
965
+ accountId,
966
+ config: ctx.cfg,
967
+ runtime: {
968
+ log: (msg) => ctx.log?.info(msg),
969
+ error: (msg) => ctx.log?.error(msg)
970
+ },
971
+ abortSignal: ctx.abortSignal,
972
+ channelRuntime: ctx.channelRuntime
973
+ });
974
+ },
975
+ stopAccount: async (ctx) => {
976
+ ctx.setStatus({
977
+ accountId: ctx.accountId,
978
+ running: false,
979
+ lastStopAt: Date.now()
980
+ });
981
+ ctx.log?.info(`Stopped Shadow connection for account ${ctx.accountId}`);
982
+ }
983
+ };
984
+ shadowPlugin.heartbeat = {
985
+ sendTyping: async ({ cfg, to, accountId, threadId }) => {
986
+ const account = resolveAccount(cfg, accountId);
987
+ if (!account.token?.trim()) return;
988
+ const normalized = shadowPlugin.messaging?.normalizeTarget?.(to) ?? to;
989
+ const match = normalized.match(/^(?:shadowob|openclaw-shadowob):(channel|thread):(.+)$/i);
990
+ const channelId = match?.[1] === "channel" ? match[2] : void 0;
991
+ const targetChannelId = channelId ?? (threadId ? void 0 : normalized);
992
+ if (!targetChannelId) return;
993
+ const { ShadowSocket } = await import("@shadowob/sdk");
994
+ const socket = new ShadowSocket({
995
+ serverUrl: account.serverUrl,
996
+ token: account.token,
997
+ transports: ["websocket", "polling"]
998
+ });
999
+ socket.connect();
1000
+ await new Promise((resolve) => {
1001
+ socket.onConnect(() => {
1002
+ socket.sendTyping(targetChannelId);
1003
+ socket.disconnect();
1004
+ resolve();
1005
+ });
1006
+ socket.onConnectError(() => {
1007
+ socket.disconnect();
1008
+ resolve();
1009
+ });
1010
+ setTimeout(() => {
1011
+ socket.disconnect();
1012
+ resolve();
1013
+ }, 3e3);
1014
+ });
1015
+ }
1016
+ };
1017
+ shadowPlugin.actions = shadowMessageActions;
1018
+
1019
+ // index.ts
1020
+ import { ShadowClient as ShadowClient4 } from "@shadowob/sdk";
14
1021
  var index_default = defineChannelPluginEntry({
15
1022
  id: "openclaw-shadowob",
16
1023
  name: "ShadowOwnBuddy",
@@ -21,7 +1028,7 @@ var index_default = defineChannelPluginEntry({
21
1028
  }
22
1029
  });
23
1030
  export {
24
- ShadowClient,
1031
+ ShadowClient4 as ShadowClient,
25
1032
  index_default as default,
26
1033
  getShadowRuntime,
27
1034
  monitorShadowProvider,