@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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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-
|
|
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 = [
|
|
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,
|
|
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,
|
|
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
|
|
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:
|
|
706
|
-
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:
|
|
799
|
+
actionParams: attachmentParams
|
|
713
800
|
});
|
|
714
801
|
return textResult({
|
|
715
802
|
ok: true,
|
|
716
803
|
action: requestedAction,
|
|
717
|
-
canonicalAction:
|
|
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-
|
|
1050
|
+
const { monitorShadowProvider: monitorShadowProvider2 } = await import("./monitor-5FSCOBE4.js");
|
|
964
1051
|
await monitorShadowProvider2({
|
|
965
1052
|
account,
|
|
966
1053
|
accountId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shadowob/openclaw-shadowob",
|
|
3
|
-
"version": "1.1.
|
|
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.
|
|
61
|
+
"@shadowob/sdk": "1.1.8"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@types/node": "^22.15.21",
|
package/skills/shadowob/SKILL.md
CHANGED
|
@@ -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/
|
|
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/
|
|
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
|
|
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
|
|
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
|
-
##
|
|
144
|
+
## Buddies
|
|
145
145
|
|
|
146
146
|
```bash
|
|
147
|
-
# List
|
|
148
|
-
shadowob
|
|
147
|
+
# List buddies
|
|
148
|
+
shadowob buddies list --json
|
|
149
149
|
|
|
150
|
-
# Get
|
|
151
|
-
shadowob
|
|
150
|
+
# Get buddy
|
|
151
|
+
shadowob buddies get <buddy-id> --json
|
|
152
152
|
|
|
153
153
|
# Create/Update/Delete
|
|
154
|
-
shadowob
|
|
155
|
-
shadowob
|
|
156
|
-
shadowob
|
|
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
|
|
160
|
-
shadowob
|
|
159
|
+
shadowob buddies start <buddy-id>
|
|
160
|
+
shadowob buddies stop <buddy-id>
|
|
161
161
|
|
|
162
162
|
# Token
|
|
163
|
-
shadowob
|
|
163
|
+
shadowob buddies token <buddy-id> --json
|
|
164
164
|
|
|
165
165
|
# Config
|
|
166
|
-
shadowob
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|