@shadowob/openclaw-shadowob 1.1.6 → 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
@@ -1201,22 +1280,89 @@ function resolveOwnerAllowFrom(policyConfig) {
1201
1280
  const ownerId = typeof policyConfig?.ownerId === "string" ? policyConfig.ownerId.trim() : "";
1202
1281
  return ownerId ? [ownerId] : void 0;
1203
1282
  }
1204
- async function buildMentionedServerAppSkillsContext(params) {
1283
+ var MAX_SERVER_APPS_IN_CONTEXT = 8;
1284
+ function serverAppCommandSummary(app) {
1285
+ return app.manifest.commands.slice(0, 6).map(
1286
+ (command) => `${command.name}(${command.action}, permission=${command.permission}, approval=${command.approvalMode ?? app.defaultApprovalMode})`
1287
+ ).join("; ");
1288
+ }
1289
+ function formatInstalledServerAppSummary(ref) {
1290
+ const app = ref.app;
1291
+ if (!app) {
1292
+ return `- ${ref.label}: appKey=${ref.appKey}, server=${ref.server}${ref.mentioned ? ", mentioned=true" : ""}`;
1293
+ }
1294
+ return [
1295
+ `- ${app.name}: appKey=${app.appKey}, server=${ref.server}, defaultPermissions=${app.defaultPermissions.join(",") || "none"}, defaultApproval=${app.defaultApprovalMode}`,
1296
+ app.description ? ` description=${app.description}` : "",
1297
+ ` commands=${serverAppCommandSummary(app)}`
1298
+ ].filter(Boolean).join("\n");
1299
+ }
1300
+ function serverAppContextFields(apps) {
1301
+ if (apps.length === 0) return {};
1302
+ return {
1303
+ ServerApps: apps.map((app) => ({
1304
+ id: app.id,
1305
+ serverId: app.serverId,
1306
+ appKey: app.appKey,
1307
+ name: app.name,
1308
+ description: app.description,
1309
+ defaultPermissions: app.defaultPermissions,
1310
+ defaultApprovalMode: app.defaultApprovalMode,
1311
+ commands: app.manifest.commands.map((command) => ({
1312
+ name: command.name,
1313
+ title: command.title,
1314
+ description: command.description,
1315
+ permission: command.permission,
1316
+ action: command.action,
1317
+ dataClass: command.dataClass,
1318
+ approvalMode: command.approvalMode ?? app.defaultApprovalMode
1319
+ }))
1320
+ })),
1321
+ ServerAppSummary: apps.map((app) => `${app.name} (${app.appKey})`).join(", ")
1322
+ };
1323
+ }
1324
+ async function buildServerAppSkillsContext(params) {
1205
1325
  const appRefs = /* @__PURE__ */ new Map();
1326
+ const installedApps = [];
1327
+ if (params.serverInfo) {
1328
+ const server = params.serverInfo.serverSlug || params.serverInfo.serverId;
1329
+ try {
1330
+ const apps = await params.client.listServerApps(params.serverInfo.serverId);
1331
+ for (const app of apps.filter((item) => item.status !== "disabled")) {
1332
+ installedApps.push(app);
1333
+ appRefs.set(`${params.serverInfo.serverId}:${app.appKey}`, {
1334
+ appKey: app.appKey,
1335
+ server,
1336
+ label: app.name,
1337
+ app,
1338
+ mentioned: false
1339
+ });
1340
+ }
1341
+ } catch (err) {
1342
+ params.runtime.error?.(
1343
+ `[server-app] Failed listing apps for ${params.serverInfo.serverId}: ${String(err)}`
1344
+ );
1345
+ }
1346
+ }
1206
1347
  for (const mention of params.mentions) {
1207
1348
  if (mention.kind !== "app") continue;
1208
1349
  const appKey = mention.appKey ?? mention.targetId;
1209
1350
  const server = mention.serverId ?? mention.serverSlug ?? params.serverInfo?.serverId;
1210
1351
  if (!appKey || !server) continue;
1211
- appRefs.set(`${server}:${appKey}`, {
1352
+ const key = `${server}:${appKey}`;
1353
+ const existing = appRefs.get(key);
1354
+ appRefs.set(key, {
1355
+ ...existing,
1212
1356
  appKey,
1213
1357
  server,
1214
- label: mention.label || mention.sourceToken || mention.token || appKey
1358
+ label: mention.label || mention.sourceToken || mention.token || appKey,
1359
+ mentioned: true
1215
1360
  });
1216
1361
  }
1217
- if (appRefs.size === 0) return "";
1362
+ if (appRefs.size === 0) return { prompt: "", fields: {} };
1363
+ const refs = Array.from(appRefs.values()).slice(0, MAX_SERVER_APPS_IN_CONTEXT);
1218
1364
  const documents = await Promise.all(
1219
- Array.from(appRefs.values()).map(async (ref) => {
1365
+ refs.map(async (ref) => {
1220
1366
  try {
1221
1367
  const skill = await params.client.getServerAppSkills(ref.server, ref.appKey);
1222
1368
  return [
@@ -1235,13 +1381,17 @@ async function buildMentionedServerAppSkillsContext(params) {
1235
1381
  })
1236
1382
  );
1237
1383
  const loaded = documents.filter(Boolean);
1238
- if (loaded.length === 0) return "";
1239
- return [
1240
- "Injected Shadow Server App Skills:",
1241
- "These instructions are authoritative for mentioned server apps in this message. Use the Shadow CLI path described below so Shadow can bind identity, app grants, and policy.",
1384
+ const prompt = [
1385
+ "Shadow Server Apps available in this server:",
1386
+ ...refs.map(formatInstalledServerAppSummary),
1242
1387
  "",
1388
+ "Use these apps when the user asks natural-language questions or tasks that match an installed app name, description, or command capability. Do not wait for the user to say a CLI command or explicitly mention the app.",
1389
+ 'Operate server apps through the mounted Shadow CLI only so Shadow can bind the Buddy identity, app grants, approval prompts, and policy: run `shadowob app discover --server "<current-server-id-or-slug>" --json` when needed, then `shadowob app call "<appKey>" <command> --server "<current-server-id-or-slug>" --channel-id "<current-channel-id>" --json-input \'<raw-command-input-json>\' --json`. Do not use curl, fetch, raw HTTP routes, or SDK calls for server-app commands.',
1390
+ "Shadow App command approvals are system permission prompts, not chat interactive dialogs. Never send a Shadow interactive form/buttons/approval message as a substitute for App command approval, and never call the App approval endpoint yourself as a Buddy. If the CLI returns SERVER_APP_COMMAND_APPROVAL_REQUIRED, tell the user that Shadow opened the approval popup, then stop until a person confirms and asks you to retry.",
1391
+ loaded.length > 0 ? "Injected Shadow Server App Skills:" : "",
1243
1392
  ...loaded
1244
- ].join("\n");
1393
+ ].filter(Boolean).join("\n");
1394
+ return { prompt, fields: serverAppContextFields(installedApps) };
1245
1395
  }
1246
1396
  async function processShadowMessage(params) {
1247
1397
  const {
@@ -1290,6 +1440,31 @@ async function processShadowMessage(params) {
1290
1440
  });
1291
1441
  runtime.log?.(`[routing] Resolved agent: ${route.agentId} (account ${accountId})`);
1292
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
+ }
1293
1468
  const mediaContext = await resolveShadowInboundMediaContext({
1294
1469
  account,
1295
1470
  message,
@@ -1337,7 +1512,7 @@ async function processShadowMessage(params) {
1337
1512
  const conversationLabel = serverInfo ? `${serverInfo.serverName} ${channelLabel}` : peerId;
1338
1513
  const messageBodyForAgent = interactiveResponseContext.text || baseBodyForAgent;
1339
1514
  const client = new ShadowClient2(account.serverUrl, account.token);
1340
- const serverAppSkillsContext = await buildMentionedServerAppSkillsContext({
1515
+ const serverAppContext = await buildServerAppSkillsContext({
1341
1516
  mentions: structuredMentions,
1342
1517
  client,
1343
1518
  serverInfo,
@@ -1353,7 +1528,8 @@ async function processShadowMessage(params) {
1353
1528
  buildCommerceContextForAgent(account),
1354
1529
  viewerCommerceContext,
1355
1530
  mentionContext,
1356
- serverAppSkillsContext,
1531
+ serverAppContext.prompt,
1532
+ runtimeTaskCard ? taskCardPrompt(message, runtimeTaskCard) : "",
1357
1533
  messageBodyForAgent
1358
1534
  ].filter(Boolean).join("\n\n");
1359
1535
  const body = core.channel.reply.formatAgentEnvelope({
@@ -1366,6 +1542,7 @@ async function processShadowMessage(params) {
1366
1542
  const escapedBotUsername = botUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1367
1543
  const mentionRegex = new RegExp(`@${escapedBotUsername}(?:\\s|$)`, "i");
1368
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;
1369
1546
  const ctxPayload = core.channel.reply.finalizeInboundContext({
1370
1547
  Body: body,
1371
1548
  BodyForAgent: bodyForAgent,
@@ -1376,7 +1553,7 @@ async function processShadowMessage(params) {
1376
1553
  CommandSource: "text",
1377
1554
  From: `shadowob:user:${senderId}`,
1378
1555
  To: `shadowob:channel:${channelId}`,
1379
- SessionKey: route.sessionKey,
1556
+ SessionKey: taskSessionKey,
1380
1557
  AccountId: route.accountId,
1381
1558
  ChatType: chatType,
1382
1559
  ConversationLabel: conversationLabel,
@@ -1388,6 +1565,7 @@ async function processShadowMessage(params) {
1388
1565
  MessageSid: message.id,
1389
1566
  WasMentioned: wasMentioned,
1390
1567
  ...mentionContextFields(structuredMentions),
1568
+ ...serverAppContext.fields,
1391
1569
  OriginatingChannel: "shadowob",
1392
1570
  OriginatingTo: `shadowob:channel:${channelId}`,
1393
1571
  NativeChannelId: channelId,
@@ -1401,6 +1579,12 @@ async function processShadowMessage(params) {
1401
1579
  } : {},
1402
1580
  BotUserId: botUserId,
1403
1581
  BotUsername: botUsername,
1582
+ ...runtimeTaskCard ? {
1583
+ TaskCardId: runtimeTaskCard.id,
1584
+ TaskCardTitle: runtimeTaskCard.title,
1585
+ TaskCardStatus: runtimeTaskCard.status,
1586
+ TaskCardPriority: runtimeTaskCard.priority
1587
+ } : {},
1404
1588
  AgentId: route.agentId,
1405
1589
  ChannelId: channelId,
1406
1590
  ...slashCommandMatch ? {
@@ -1521,9 +1705,29 @@ async function processShadowMessage(params) {
1521
1705
  }).catch((err) => {
1522
1706
  runtime.error?.(`[usage] Failed to report usage snapshot for ${message.id}: ${String(err)}`);
1523
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
+ }
1524
1718
  socket.updateActivity(channelId, "ready");
1525
1719
  } catch (err) {
1526
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
+ }
1527
1731
  socket.updateActivity(channelId, null);
1528
1732
  throw err;
1529
1733
  } finally {
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  resolveOutboundMentions,
16
16
  setShadowRuntime,
17
17
  tryGetShadowRuntime
18
- } from "./chunk-4G572ZLC.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-XDUBTELB.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-4G572ZLC.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.6",
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.6"
61
+ "@shadowob/sdk": "1.1.8"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/node": "^22.15.21",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: shadowob
3
- description: "Use when live Shadow context or actions are needed: channel/DM history, pins, members, server/channel/workspace/shop/app/agent data, or sending/managing Shadow content via the shadowob CLI."
3
+ description: "Use when live Shadow context or actions are needed: channel/DM history, pins, members, server/channel/workspace/shop/app/buddy data, or sending/managing Shadow content via the shadowob CLI."
4
4
  metadata:
5
5
  {
6
6
  "openclaw":
@@ -18,7 +18,7 @@ allowed-tools: ["exec"]
18
18
  Use `shadowob` CLI to interact with Shadow servers.
19
19
 
20
20
  Activate this skill when you need current Shadow context, such as recent channel or DM history,
21
- pinned messages, member/server/channel state, workspace/shop/app/agent data, or when you need to
21
+ pinned messages, member/server/channel state, workspace/shop/app/buddy data, or when you need to
22
22
  send or manage Shadow content. Prefer narrow `--json` reads before acting.
23
23
 
24
24
  ## Quickstart
@@ -78,13 +78,13 @@ shadowob servers discover --json
78
78
 
79
79
  ```bash
80
80
  # List channels
81
- shadowob channels list --server-id <server-id> --json
81
+ shadowob channels list --server <server> --json
82
82
 
83
83
  # Get channel
84
84
  shadowob channels get <channel-id> --json
85
85
 
86
86
  # Create/Delete
87
- shadowob channels create --server-id <id> --name <name> [--type text] --json
87
+ shadowob channels create --server <server> --name <name> [--type text] --json
88
88
  shadowob channels delete <channel-id>
89
89
 
90
90
  # Messages
@@ -141,29 +141,29 @@ shadowob dms send <dm-channel-id> --content "text" --json
141
141
  shadowob dms delete <dm-channel-id>
142
142
  ```
143
143
 
144
- ## Agents
144
+ ## Buddies
145
145
 
146
146
  ```bash
147
- # List agents
148
- shadowob agents list --json
147
+ # List buddies
148
+ shadowob buddies list --json
149
149
 
150
- # Get agent
151
- shadowob agents get <agent-id> --json
150
+ # Get buddy
151
+ shadowob buddies get <buddy-id> --json
152
152
 
153
153
  # Create/Update/Delete
154
- shadowob agents create --name <name> [--display-name <name>] [--avatar-url <url>] --json
155
- shadowob agents update <agent-id> [--name <name>] [--display-name <name>] --json
156
- shadowob agents delete <agent-id>
154
+ shadowob buddies create --name <name> --username <username> [--display-name <name>] [--avatar-url <url>] --json
155
+ shadowob buddies update <buddy-id> [--name <name>] [--display-name <name>] --json
156
+ shadowob buddies delete <buddy-id>
157
157
 
158
158
  # Control
159
- shadowob agents start <agent-id>
160
- shadowob agents stop <agent-id>
159
+ shadowob buddies start <buddy-id>
160
+ shadowob buddies stop <buddy-id>
161
161
 
162
162
  # Token
163
- shadowob agents token <agent-id> --json
163
+ shadowob buddies token <buddy-id> --json
164
164
 
165
165
  # Config
166
- shadowob agents config <agent-id> --json
166
+ shadowob buddies config <buddy-id> --json
167
167
  ```
168
168
 
169
169
  ## Workspace
@@ -211,7 +211,6 @@ shadowob shop me get --json
211
211
  shadowob shop products list <server-id> [--status active] [--keyword <text>] [--limit <n>] --json
212
212
  shadowob shop products list-by-shop <shop-id> [--status active] [--limit <n>] --json
213
213
  shadowob shop products get <server-id> <product-id> --json
214
- shadowob shop products context <product-id> --json
215
214
  shadowob shop products purchase <shop-id> <product-id> --idempotency-key <unique-operation-id> --json
216
215
 
217
216
  # Offers, deliverables, and shop assets
@@ -246,7 +245,7 @@ shadowob commerce cards list --channel-id <channel-id> [--keyword <text>] --json
246
245
  shadowob commerce cards purchase <message-id> <card-id> --idempotency-key <unique-operation-id> --json
247
246
 
248
247
  # Purchases, delivery, protected files, and community assets
249
- shadowob commerce entitlements list [--server-id <server-id>] --json
248
+ shadowob commerce entitlements list [--server <server>] --json
250
249
  shadowob commerce entitlements get <entitlement-id> --json
251
250
  shadowob commerce entitlements verify <entitlement-id> --json
252
251
  shadowob commerce paid-files open <file-id> --json
@@ -277,17 +276,29 @@ shadowob commerce gifts send --recipient-user-id <user-id> --assets '<json-array
277
276
  # Server App integrations
278
277
  shadowob app list --server <server-id-or-slug> --json
279
278
  shadowob app preview --server <server-id-or-slug> --manifest-url <manifest-url> --json
279
+ shadowob app install --server <server-id-or-slug> --manifest-url <manifest-url> --json
280
+ shadowob app uninstall <app-key> --server <server-id-or-slug>
280
281
  shadowob app discover --server <server-id-or-slug> --json
281
282
  shadowob app inspect <app-key> --server <server-id-or-slug> --json
282
283
  shadowob app skills <app-key> --server <server-id-or-slug>
283
- shadowob app call <app-key> <command> --server <server-id-or-slug> --json-input '<raw-command-input-json>' --json
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
284
288
  ```
285
289
 
286
290
  For server App commands, use the `shadowob app` CLI path only. Do not use curl, fetch, raw HTTP
287
291
  routes, or the JavaScript SDK to call server App commands. Pass the command input object directly
288
292
  to `--json-input`, for example `{"title":"Example","priority":"high"}`; the CLI wraps the HTTP
289
293
  request for you and binds Shadow OAuth identity, server membership, App grants, and command policy.
290
- When a channel message mentions a server App, use the mentioned app key/server id directly.
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.
298
+ When a channel message mentions a server App, use the mentioned app key/server id directly and pass
299
+ the current channel id with `--channel-id` when available. If a server App command requires
300
+ approval, do not send a chat form or call the approval endpoint yourself as a Buddy. Wait for a
301
+ person to confirm the Shadow approval popup, then retry the original command.
291
302
 
292
303
  ```bash
293
304
  # Legacy workspace apps
@@ -404,7 +415,7 @@ shadowob marketplace contracts extend <contract-id> --hours <n> --json
404
415
 
405
416
  ```bash
406
417
  # Upload a file
407
- shadowob media upload --file <path> [--server-id <id>] [--channel-id <id>] --json
418
+ shadowob media upload --file <path> [--server <server>] [--channel-id <id>] --json
408
419
 
409
420
  # Download a file
410
421
  shadowob media download <file-url> [--output <path>]
@@ -414,7 +425,7 @@ shadowob media download <file-url> [--output <path>]
414
425
 
415
426
  ```bash
416
427
  # Search messages
417
- shadowob search messages --query <text> [--server-id <id>] [--channel-id <id>] [--author-id <id>] [--after <date>] [--before <date>] [--has-attachments] [--limit <n>] --json
428
+ shadowob search messages --query <text> [--server <server>] [--channel-id <id>] [--author-id <id>] [--after <date>] [--before <date>] [--has-attachments] [--limit <n>] --json
418
429
  ```
419
430
 
420
431
  ## Listen (Real-time Events)