@shadowob/openclaw-shadowob 1.1.7 → 1.1.8

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.
@@ -216,10 +216,13 @@ async function getDataDir() {
216
216
  // src/monitor/media.ts
217
217
  function isShadowResolvableMediaUrl(url) {
218
218
  if (url.startsWith("/")) {
219
- return url.includes("/uploads/") || url.startsWith("/api/media/signed/");
219
+ return url.includes("/uploads/") || url.includes("/voice/") || url.startsWith("/api/media/signed/");
220
220
  }
221
221
  return url.startsWith("http");
222
222
  }
223
+ function visibleMessageText(text) {
224
+ return text.replace(/\u200B/gu, "").trim();
225
+ }
223
226
  function inferMimeType(filename, headerType) {
224
227
  const ext = filename.split(".").pop()?.toLowerCase() ?? "";
225
228
  const map = {
@@ -240,7 +243,12 @@ function inferMimeType(filename, headerType) {
240
243
  }
241
244
  async function resolveShadowInboundMediaContext(params) {
242
245
  const { account, message, rawBody, runtime } = params;
243
- const attachmentUrls = (message.attachments ?? []).map((a) => a.url).filter(Boolean);
246
+ const attachments = message.attachments ?? [];
247
+ const attachmentUrls = attachments.map((a) => a.url).filter(Boolean);
248
+ const voiceAttachment = attachments.find(
249
+ (attachment) => attachment.kind === "voice" || String(attachment.contentType ?? "").startsWith("audio/")
250
+ );
251
+ const voiceTranscript = voiceAttachment?.transcript?.status === "ready" && voiceAttachment.transcript.text?.trim() ? voiceAttachment.transcript.text.trim() : "";
244
252
  const markdownMediaRegex = /!?\[[^\]]*\]\(([^)]+)\)/g;
245
253
  const markdownUrls = [];
246
254
  for (const mdMatch of rawBody.matchAll(markdownMediaRegex)) {
@@ -280,10 +288,22 @@ async function resolveShadowInboundMediaContext(params) {
280
288
  runtime.error?.(`[media] Failed to download ${rawUrl}: ${String(err)}`);
281
289
  }
282
290
  }
291
+ const bodyWithoutMedia = rawBody.replace(
292
+ /!?\[[^\]]*\]\((?:[^)]*\/uploads\/[^)]+|[^)]*\/voice\/[^)]+|[^)]*\/api\/media\/signed\/[^)]+)\)/g,
293
+ ""
294
+ ).replace(/\u200B/gu, "").replace(/\n{2,}/g, "\n").trim();
295
+ const cleanBody = bodyWithoutMedia || (voiceTranscript && !visibleMessageText(rawBody) ? voiceTranscript : "") || voiceTranscript || "[Media attached]";
296
+ const voiceFields = voiceAttachment ? {
297
+ VoiceMessage: true,
298
+ VoiceAttachmentId: voiceAttachment.id,
299
+ VoiceDurationMs: voiceAttachment.durationMs ?? null,
300
+ VoiceWaveformPeaks: voiceAttachment.waveformPeaks ?? null,
301
+ VoiceTranscript: voiceAttachment.transcript?.text ?? null,
302
+ VoiceTranscriptStatus: voiceAttachment.transcript?.status ?? null
303
+ } : {};
283
304
  if (localMediaPaths.length === 0) {
284
- return { cleanBody: rawBody, fields: {} };
305
+ return { cleanBody, fields: voiceFields };
285
306
  }
286
- const cleanBody = rawBody.replace(/!?\[[^\]]*\]\((?:[^)]*\/uploads\/[^)]+|[^)]*\/api\/media\/signed\/[^)]+)\)/g, "").replace(/\n{2,}/g, "\n").trim() || "[Media attached]";
287
307
  return {
288
308
  cleanBody,
289
309
  fields: {
@@ -292,7 +312,8 @@ async function resolveShadowInboundMediaContext(params) {
292
312
  MediaUrl: resolvedMediaUrls[0],
293
313
  MediaUrls: resolvedMediaUrls,
294
314
  MediaType: localMediaTypes[0],
295
- MediaTypes: localMediaTypes
315
+ MediaTypes: localMediaTypes,
316
+ ...voiceFields
296
317
  }
297
318
  };
298
319
  }
@@ -303,6 +324,20 @@ function normalizeTriggerUserIds(policyConfig) {
303
324
  if (!Array.isArray(value)) return null;
304
325
  return value.filter((item) => typeof item === "string" && item.length > 0);
305
326
  }
327
+ function messageHasActiveTaskForBot(message, botUserId) {
328
+ const cards = message.metadata?.cards;
329
+ if (!Array.isArray(cards)) return false;
330
+ return cards.some((card) => {
331
+ if (!card || typeof card !== "object" || Array.isArray(card)) return false;
332
+ const record = card;
333
+ if (record.kind !== "task") return false;
334
+ if (record.status === "completed" || record.status === "failed" || record.status === "canceled" || record.status === "transferred") {
335
+ return false;
336
+ }
337
+ const assignee = record.assignee && typeof record.assignee === "object" && !Array.isArray(record.assignee) ? record.assignee : null;
338
+ return assignee?.userId === botUserId;
339
+ });
340
+ }
306
341
  function evaluateShadowMessagePreflight(params) {
307
342
  const { message, botUserId, botUsername, channelPolicies, runtime } = params;
308
343
  const senderLabel = message.author?.username ?? message.authorId;
@@ -375,7 +410,7 @@ function evaluateShadowMessagePreflight(params) {
375
410
  const structuredMentions = getShadowMessageMentions(message);
376
411
  const escapedBotUsername = botUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
377
412
  const mentionRegex = new RegExp(`@${escapedBotUsername}(?:\\s|$)`, "i");
378
- const wasMentionedExplicitly = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionsTargetServerApp(structuredMentions) || mentionRegex.test(message.content);
413
+ const wasMentionedExplicitly = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionsTargetServerApp(structuredMentions) || messageHasActiveTaskForBot(message, botUserId) || mentionRegex.test(message.content);
379
414
  if (policy?.mentionOnly && !wasMentionedExplicitly) {
380
415
  return {
381
416
  ok: false,
@@ -506,7 +541,7 @@ async function deliverShadowReply(params) {
506
541
  await withDeliveryRetry({
507
542
  label: "reply-media",
508
543
  runtime,
509
- operation: () => client.uploadMediaFromUrl(mediaUrl, messageId)
544
+ operation: () => client.uploadMediaFromUrl(mediaUrl, { messageId })
510
545
  });
511
546
  runtime.log?.("[reply] Media uploaded successfully");
512
547
  } catch (err) {
@@ -1187,6 +1222,50 @@ function normalizeStringList(value) {
1187
1222
  if (!Array.isArray(value)) return [];
1188
1223
  return value.filter((item) => typeof item === "string" && item.trim().length > 0);
1189
1224
  }
1225
+ function isRuntimeTaskCard(card) {
1226
+ return card.kind === "task" && typeof card.id === "string" && typeof card.title === "string" && typeof card.status === "string";
1227
+ }
1228
+ function isTerminalTaskStatus(status) {
1229
+ return status === "completed" || status === "failed" || status === "canceled" || status === "transferred";
1230
+ }
1231
+ function taskClaimExpired(card) {
1232
+ if (!card.claim?.expiresAt) return true;
1233
+ return new Date(card.claim.expiresAt).getTime() <= Date.now();
1234
+ }
1235
+ function findRuntimeTaskCard(message, botUserId) {
1236
+ const cards = message.metadata?.cards;
1237
+ if (!Array.isArray(cards)) return null;
1238
+ return cards.find(
1239
+ (card) => isRuntimeTaskCard(card) && card.assignee?.userId === botUserId && !isTerminalTaskStatus(card.status)
1240
+ ) ?? null;
1241
+ }
1242
+ function findTaskCardById(message, cardId) {
1243
+ const cards = message?.metadata?.cards;
1244
+ if (!Array.isArray(cards)) return null;
1245
+ return cards.find((card) => isRuntimeTaskCard(card) && card.id === cardId) ?? null;
1246
+ }
1247
+ function taskCardPrompt(message, card) {
1248
+ const workspaceId = card.data?.task && typeof card.data.task.workspaceId === "string" ? card.data.task.workspaceId : void 0;
1249
+ const claimId = typeof card.claim?.id === "string" ? card.claim.id : void 0;
1250
+ return [
1251
+ "Shadow Inbox task:",
1252
+ `Task message id: ${message.id}`,
1253
+ `Task card id: ${card.id}`,
1254
+ claimId ? `Task claim id: ${claimId}` : "",
1255
+ workspaceId ? `Task workspace id: ${workspaceId}` : "",
1256
+ `Task status: ${card.status}`,
1257
+ `Task title: ${card.title}`,
1258
+ card.priority ? `Task priority: ${card.priority}` : "",
1259
+ card.body ? `Task body:
1260
+ ${card.body}` : "",
1261
+ card.source ? `Task source: ${JSON.stringify(card.source)}` : "",
1262
+ claimId ? [
1263
+ "When calling Shadow Server App commands for this task, bind the call with:",
1264
+ `--task-message-id ${message.id} --task-card-id ${card.id} --task-claim-id ${claimId}`
1265
+ ].join("\n") : "",
1266
+ "When you complete useful work for this task, reply with the concrete result and any next action."
1267
+ ].filter(Boolean).join("\n");
1268
+ }
1190
1269
  function isSenderCommandAuthorized(policyConfig, senderId) {
1191
1270
  const triggerUserIds = normalizeStringList(
1192
1271
  policyConfig?.allowedTriggerUserIds ?? policyConfig?.triggerUserIds
@@ -1361,6 +1440,31 @@ async function processShadowMessage(params) {
1361
1440
  });
1362
1441
  runtime.log?.(`[routing] Resolved agent: ${route.agentId} (account ${accountId})`);
1363
1442
  const mediaClient = new ShadowClient2(account.serverUrl, account.token);
1443
+ let runtimeTaskCard = findRuntimeTaskCard(message, botUserId);
1444
+ if (runtimeTaskCard && (runtimeTaskCard.status === "queued" || (runtimeTaskCard.status === "claimed" || runtimeTaskCard.status === "running") && taskClaimExpired(runtimeTaskCard))) {
1445
+ try {
1446
+ const claimed = await mediaClient.claimTaskCard(message.id, runtimeTaskCard.id, {
1447
+ ttlSeconds: 3600,
1448
+ note: "OpenClaw runtime claimed task"
1449
+ });
1450
+ runtimeTaskCard = findTaskCardById(claimed, runtimeTaskCard.id) ?? runtimeTaskCard;
1451
+ } catch (err) {
1452
+ runtime.error?.(`[task] Failed claiming task card ${runtimeTaskCard.id}: ${String(err)}`);
1453
+ return;
1454
+ }
1455
+ }
1456
+ if (runtimeTaskCard && runtimeTaskCard.status === "claimed") {
1457
+ await mediaClient.updateTaskCard(message.id, runtimeTaskCard.id, {
1458
+ status: "running",
1459
+ note: "OpenClaw runtime started work"
1460
+ }).then((updated) => {
1461
+ runtimeTaskCard = findTaskCardById(updated, runtimeTaskCard.id) ?? runtimeTaskCard;
1462
+ }).catch((err) => {
1463
+ runtime.error?.(
1464
+ `[task] Failed marking task card ${runtimeTaskCard?.id} running: ${String(err)}`
1465
+ );
1466
+ });
1467
+ }
1364
1468
  const mediaContext = await resolveShadowInboundMediaContext({
1365
1469
  account,
1366
1470
  message,
@@ -1425,6 +1529,7 @@ async function processShadowMessage(params) {
1425
1529
  viewerCommerceContext,
1426
1530
  mentionContext,
1427
1531
  serverAppContext.prompt,
1532
+ runtimeTaskCard ? taskCardPrompt(message, runtimeTaskCard) : "",
1428
1533
  messageBodyForAgent
1429
1534
  ].filter(Boolean).join("\n\n");
1430
1535
  const body = core.channel.reply.formatAgentEnvelope({
@@ -1437,6 +1542,7 @@ async function processShadowMessage(params) {
1437
1542
  const escapedBotUsername = botUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1438
1543
  const mentionRegex = new RegExp(`@${escapedBotUsername}(?:\\s|$)`, "i");
1439
1544
  const wasMentioned = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionsTargetServerApp(structuredMentions) || Boolean(slashCommandMatch) || mentionRegex.test(message.content);
1545
+ const taskSessionKey = runtimeTaskCard ? `${route.sessionKey}:task:${runtimeTaskCard.id}` : route.sessionKey;
1440
1546
  const ctxPayload = core.channel.reply.finalizeInboundContext({
1441
1547
  Body: body,
1442
1548
  BodyForAgent: bodyForAgent,
@@ -1447,7 +1553,7 @@ async function processShadowMessage(params) {
1447
1553
  CommandSource: "text",
1448
1554
  From: `shadowob:user:${senderId}`,
1449
1555
  To: `shadowob:channel:${channelId}`,
1450
- SessionKey: route.sessionKey,
1556
+ SessionKey: taskSessionKey,
1451
1557
  AccountId: route.accountId,
1452
1558
  ChatType: chatType,
1453
1559
  ConversationLabel: conversationLabel,
@@ -1473,6 +1579,12 @@ async function processShadowMessage(params) {
1473
1579
  } : {},
1474
1580
  BotUserId: botUserId,
1475
1581
  BotUsername: botUsername,
1582
+ ...runtimeTaskCard ? {
1583
+ TaskCardId: runtimeTaskCard.id,
1584
+ TaskCardTitle: runtimeTaskCard.title,
1585
+ TaskCardStatus: runtimeTaskCard.status,
1586
+ TaskCardPriority: runtimeTaskCard.priority
1587
+ } : {},
1476
1588
  AgentId: route.agentId,
1477
1589
  ChannelId: channelId,
1478
1590
  ...slashCommandMatch ? {
@@ -1593,9 +1705,29 @@ async function processShadowMessage(params) {
1593
1705
  }).catch((err) => {
1594
1706
  runtime.error?.(`[usage] Failed to report usage snapshot for ${message.id}: ${String(err)}`);
1595
1707
  });
1708
+ if (runtimeTaskCard) {
1709
+ await client.updateTaskCard(message.id, runtimeTaskCard.id, {
1710
+ status: "completed",
1711
+ note: "OpenClaw runtime completed reply dispatch"
1712
+ }).catch((err) => {
1713
+ runtime.error?.(
1714
+ `[task] Failed marking task card ${runtimeTaskCard?.id} completed: ${String(err)}`
1715
+ );
1716
+ });
1717
+ }
1596
1718
  socket.updateActivity(channelId, "ready");
1597
1719
  } catch (err) {
1598
1720
  runtime.error?.(`[msg] AI dispatch failed for message ${message.id}: ${String(err)}`);
1721
+ if (runtimeTaskCard) {
1722
+ await client.updateTaskCard(message.id, runtimeTaskCard.id, {
1723
+ status: "failed",
1724
+ note: `OpenClaw runtime failed: ${String(err)}`.slice(0, 4e3)
1725
+ }).catch((updateErr) => {
1726
+ runtime.error?.(
1727
+ `[task] Failed marking task card ${runtimeTaskCard?.id} failed: ${String(updateErr)}`
1728
+ );
1729
+ });
1730
+ }
1599
1731
  socket.updateActivity(channelId, null);
1600
1732
  throw err;
1601
1733
  } finally {
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  resolveOutboundMentions,
16
16
  setShadowRuntime,
17
17
  tryGetShadowRuntime
18
- } from "./chunk-XOO7S6K5.js";
18
+ } from "./chunk-6B7WPA2D.js";
19
19
 
20
20
  // index.ts
21
21
  import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
@@ -151,7 +151,7 @@ async function sendMediaToShadow(params) {
151
151
  const uploadErrors = [];
152
152
  for (const mediaUrl of mediaUrls) {
153
153
  try {
154
- await params.client.uploadMediaFromUrl(mediaUrl, sent.message.id);
154
+ await params.client.uploadMediaFromUrl(mediaUrl, { messageId: sent.message.id });
155
155
  } catch (err) {
156
156
  const fallback = await sendTextChunks({
157
157
  client: params.client,
@@ -507,6 +507,20 @@ var shadowMessageToolSchemaProperties = {
507
507
  filename: optionalSchema(stringSchema("Attachment filename when buffer is used.")),
508
508
  contentType: optionalSchema(stringSchema("Attachment MIME type when buffer is used.")),
509
509
  mimeType: optionalSchema(stringSchema("Alias for contentType.")),
510
+ attachmentKind: optionalSchema(
511
+ enumSchema(["file", "image", "voice"], "Use voice when sending a Shadow voice message.")
512
+ ),
513
+ durationMs: optionalSchema(numberSchema("Voice message duration in milliseconds.")),
514
+ waveformPeaks: optionalSchema(
515
+ arraySchema(numberSchema("Voice waveform peak from 0 to 100."), {
516
+ minItems: 32,
517
+ maxItems: 96
518
+ })
519
+ ),
520
+ transcript: optionalSchema(stringSchema("Optional transcript text for a voice message.")),
521
+ transcriptLanguage: optionalSchema(
522
+ stringSchema("Optional BCP-47 transcript language, e.g. zh-CN.")
523
+ ),
510
524
  caption: optionalSchema(stringSchema("Optional text sent with an attachment.")),
511
525
  commerceOfferId: optionalSchema(
512
526
  stringSchema("Shadow CommerceOfferId to attach as a purchasable product card.")
@@ -533,7 +547,14 @@ function buildShadowMessageToolSchemaProperties(input) {
533
547
  }
534
548
 
535
549
  // src/channel/actions.ts
536
- var SHADOW_DISCOVERED_ACTIONS = ["send", "upload-file", "react", "edit", "delete"];
550
+ var SHADOW_DISCOVERED_ACTIONS = [
551
+ "send",
552
+ "upload-file",
553
+ "send-voice",
554
+ "react",
555
+ "edit",
556
+ "delete"
557
+ ];
537
558
  var SHADOW_HANDLED_ACTIONS = [...SHADOW_DISCOVERED_ACTIONS, "get-connection-status"];
538
559
  function textResult(value) {
539
560
  return {
@@ -571,6 +592,39 @@ function readAttachmentFilename(params) {
571
592
  function readCommerceOfferId(params) {
572
593
  return firstString(params.commerceOfferId, params.offerId);
573
594
  }
595
+ function readNumber(params, ...keys) {
596
+ for (const key of keys) {
597
+ const value = params[key];
598
+ if (typeof value === "number" && Number.isFinite(value)) return value;
599
+ if (typeof value === "string" && value.trim()) {
600
+ const parsed = Number(value);
601
+ if (Number.isFinite(parsed)) return parsed;
602
+ }
603
+ }
604
+ return void 0;
605
+ }
606
+ function readWaveformPeaks(params) {
607
+ const value = params.waveformPeaks ?? params.waveform_peaks;
608
+ if (Array.isArray(value)) {
609
+ const peaks = value.map((item) => Number(item));
610
+ return peaks.every((item) => Number.isInteger(item) && item >= 0 && item <= 100) ? peaks : void 0;
611
+ }
612
+ if (typeof value === "string" && value.trim()) {
613
+ try {
614
+ const parsed = JSON.parse(value);
615
+ if (!Array.isArray(parsed)) return void 0;
616
+ const peaks = parsed.map((item) => Number(item));
617
+ return peaks.every((item) => Number.isInteger(item) && item >= 0 && item <= 100) ? peaks : void 0;
618
+ } catch {
619
+ return void 0;
620
+ }
621
+ }
622
+ return void 0;
623
+ }
624
+ function readAttachmentKind(params) {
625
+ const kind = firstString(params.attachmentKind, params.kind);
626
+ return kind === "voice" || kind === "image" || kind === "file" ? kind : void 0;
627
+ }
574
628
  function buildSendMetadata(params) {
575
629
  const metadata = {};
576
630
  if (params.interactiveBlock) metadata.interactive = params.interactiveBlock;
@@ -585,16 +639,36 @@ async function uploadShadowAttachment(params) {
585
639
  const uploadTarget = params.messageId;
586
640
  const base64Buffer = firstString(params.actionParams.buffer);
587
641
  const mediaUrl = readAttachmentSource(params.actionParams);
642
+ const kind = readAttachmentKind(params.actionParams);
643
+ const durationMs = readNumber(params.actionParams, "durationMs", "duration_ms");
644
+ const waveformPeaks = readWaveformPeaks(params.actionParams);
645
+ const transcriptText = firstString(
646
+ params.actionParams.transcript,
647
+ params.actionParams.transcriptText
648
+ );
649
+ const transcriptLanguage = firstString(
650
+ params.actionParams.transcriptLanguage,
651
+ params.actionParams.transcript_language
652
+ );
653
+ const uploadOptions = { messageId: uploadTarget };
654
+ if (kind) uploadOptions.kind = kind;
655
+ if (typeof durationMs === "number") uploadOptions.durationMs = durationMs;
656
+ if (waveformPeaks) uploadOptions.waveformPeaks = waveformPeaks;
657
+ if (transcriptText) {
658
+ uploadOptions.transcriptText = transcriptText;
659
+ uploadOptions.transcriptSource = "runtime";
660
+ }
661
+ if (transcriptLanguage) uploadOptions.transcriptLanguage = transcriptLanguage;
588
662
  if (base64Buffer) {
589
663
  const raw = base64Buffer.includes(",") ? base64Buffer.split(",")[1] ?? "" : base64Buffer;
590
664
  if (!raw) throw new Error("Invalid base64 attachment payload");
591
665
  const bytes = Buffer.from(raw, "base64");
592
666
  const blob = new Blob([Uint8Array.from(bytes)], { type: contentType });
593
- await params.client.uploadMedia(blob, filename, contentType, uploadTarget);
667
+ await params.client.uploadMedia(blob, filename, contentType, uploadOptions);
594
668
  return { filename, contentType, source: "buffer" };
595
669
  }
596
670
  if (mediaUrl) {
597
- await params.client.uploadMediaFromUrl(mediaUrl, uploadTarget);
671
+ await params.client.uploadMediaFromUrl(mediaUrl, uploadOptions);
598
672
  return { filename, contentType, source: "media", mediaUrl };
599
673
  }
600
674
  throw new Error("No buffer or media URL provided for attachment");
@@ -624,12 +698,14 @@ var shadowMessageActions = {
624
698
  "file",
625
699
  "fileUrl",
626
700
  "buffer"
627
- ]
701
+ ],
702
+ "send-voice": ["media", "mediaUrl", "url", "path", "filePath", "file", "fileUrl", "buffer"]
628
703
  }
629
704
  };
630
705
  },
631
706
  messageActionTargetAliases: {
632
- "upload-file": { aliases: ["recipient", "to", "channelId"] }
707
+ "upload-file": { aliases: ["recipient", "to", "channelId"] },
708
+ "send-voice": { aliases: ["recipient", "to", "channelId"] }
633
709
  },
634
710
  supportsAction: ({ action }) => SHADOW_HANDLED_ACTIONS.includes(action),
635
711
  handleAction: async (ctx) => {
@@ -686,7 +762,7 @@ var shadowMessageActions = {
686
762
  return textResult({ ok: false, error: err instanceof Error ? err.message : String(err) });
687
763
  }
688
764
  }
689
- if (action === "upload-file") {
765
+ if (action === "upload-file" || action === "send-voice") {
690
766
  try {
691
767
  const client = new ShadowClient2(account.serverUrl, account.token);
692
768
  const to = readMessageTarget(params);
@@ -697,24 +773,35 @@ var shadowMessageActions = {
697
773
  error: "upload-file requires buffer, media, path, or filePath"
698
774
  });
699
775
  }
700
- const text = firstString(params.message, params.content, params.text, params.caption) ?? "";
776
+ const attachmentParams = action === "send-voice" ? { ...params, kind: "voice", attachmentKind: "voice" } : params;
777
+ if (action === "send-voice") {
778
+ if (readNumber(attachmentParams, "durationMs", "duration_ms") === void 0) {
779
+ return textResult({ ok: false, error: "send-voice requires durationMs" });
780
+ }
781
+ }
782
+ const text = firstString(
783
+ attachmentParams.message,
784
+ attachmentParams.content,
785
+ attachmentParams.text,
786
+ attachmentParams.caption
787
+ ) ?? "";
701
788
  const message = await sendShadowMessage({
702
789
  client,
703
790
  to,
704
791
  content: text || "\u200B",
705
- threadId: params.threadId,
706
- replyToId: params.replyTo ?? params.replyToId
792
+ threadId: attachmentParams.threadId,
793
+ replyToId: attachmentParams.replyTo ?? attachmentParams.replyToId
707
794
  });
708
795
  const attachment = await uploadShadowAttachment({
709
796
  client,
710
797
  to,
711
798
  messageId: message.id,
712
- actionParams: params
799
+ actionParams: attachmentParams
713
800
  });
714
801
  return textResult({
715
802
  ok: true,
716
803
  action: requestedAction,
717
- canonicalAction: "upload-file",
804
+ canonicalAction: action,
718
805
  messageId: message.id,
719
806
  filename: attachment.filename
720
807
  });
@@ -960,7 +1047,7 @@ shadowPlugin.gateway = {
960
1047
  lastError: null
961
1048
  });
962
1049
  ctx.log?.info(`Starting Shadow connection for account ${accountId}`);
963
- const { monitorShadowProvider: monitorShadowProvider2 } = await import("./monitor-K7U33EFS.js");
1050
+ const { monitorShadowProvider: monitorShadowProvider2 } = await import("./monitor-5FSCOBE4.js");
964
1051
  await monitorShadowProvider2({
965
1052
  account,
966
1053
  accountId,
@@ -5,7 +5,7 @@ import {
5
5
  normalizeShadowSlashCommands,
6
6
  resolveShadowAgentIdFromConfig,
7
7
  shouldCatchUpShadowMessage
8
- } from "./chunk-XOO7S6K5.js";
8
+ } from "./chunk-6B7WPA2D.js";
9
9
  export {
10
10
  formatSlashCommandPrompt,
11
11
  matchShadowSlashCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shadowob/openclaw-shadowob",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "OpenClaw Shadow channel plugin — enables AI agents to interact in Shadow server channels",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -58,7 +58,7 @@
58
58
  "dependencies": {
59
59
  "zod": "^3.25.67",
60
60
  "openclaw": "^2026.5.7",
61
- "@shadowob/sdk": "1.1.7"
61
+ "@shadowob/sdk": "1.1.8"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/node": "^22.15.21",
@@ -282,12 +282,19 @@ shadowob app discover --server <server-id-or-slug> --json
282
282
  shadowob app inspect <app-key> --server <server-id-or-slug> --json
283
283
  shadowob app skills <app-key> --server <server-id-or-slug>
284
284
  shadowob app call <app-key> <command> --server <server-id-or-slug> --channel-id <channel-id> --json-input '<raw-command-input-json>' --json
285
+ shadowob app call <app-key> <command> --server <server-id-or-slug> --help
286
+ shadowob app call <app-key> <command> --server <server-id-or-slug> --file <path> --json-input '<raw-command-input-json>' --json
287
+ shadowob app events <app-key> --server <server-id-or-slug> --json
285
288
  ```
286
289
 
287
290
  For server App commands, use the `shadowob app` CLI path only. Do not use curl, fetch, raw HTTP
288
291
  routes, or the JavaScript SDK to call server App commands. Pass the command input object directly
289
292
  to `--json-input`, for example `{"title":"Example","priority":"high"}`; the CLI wraps the HTTP
290
293
  request for you and binds Shadow OAuth identity, server membership, App grants, and command policy.
294
+ Use progressive disclosure: start with `shadowob app skills` or `shadowob app discover`, then call
295
+ `shadowob app call <app-key> <command> --server <server> --help` only when you need that command's
296
+ full schema, file-upload support, or examples. For realtime app updates, subscribe with
297
+ `shadowob app events <app-key> --server <server> --json` instead of polling.
291
298
  When a channel message mentions a server App, use the mentioned app key/server id directly and pass
292
299
  the current channel id with `--channel-id` when available. If a server App command requires
293
300
  approval, do not send a chat form or call the approval endpoint yourself as a Buddy. Wait for a