@shadowob/openclaw-shadowob 1.1.3 → 1.1.5
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.
|
@@ -22,6 +22,9 @@ function mentionTargetsBot(params) {
|
|
|
22
22
|
return mention.username?.toLowerCase() === botUsername;
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
+
function mentionsTargetServerApp(mentions) {
|
|
26
|
+
return mentions.some((mention) => mention.kind === "app" && (mention.appKey || mention.targetId));
|
|
27
|
+
}
|
|
25
28
|
function formatShadowMentionsForAgent(mentions) {
|
|
26
29
|
if (mentions.length === 0) return "";
|
|
27
30
|
const lines = mentions.map((mention) => {
|
|
@@ -32,6 +35,9 @@ function formatShadowMentionsForAgent(mentions) {
|
|
|
32
35
|
if (mention.kind === "server") {
|
|
33
36
|
return `- ${label} [server] serverId=${mention.serverId ?? mention.targetId} slug=${mention.serverSlug ?? ""}`;
|
|
34
37
|
}
|
|
38
|
+
if (mention.kind === "app") {
|
|
39
|
+
return `- ${label} [server-app] appKey=${mention.appKey ?? mention.targetId} appId=${mention.appId ?? mention.targetId} serverId=${mention.serverId ?? ""} server=${mention.serverName ?? ""}`;
|
|
40
|
+
}
|
|
35
41
|
if (mention.kind === "user" || mention.kind === "buddy") {
|
|
36
42
|
return `- ${label} [${mention.kind}] userId=${mention.userId ?? mention.targetId} username=${mention.username ?? ""}`;
|
|
37
43
|
}
|
|
@@ -40,8 +46,9 @@ function formatShadowMentionsForAgent(mentions) {
|
|
|
40
46
|
return [
|
|
41
47
|
"Shadow mentions:",
|
|
42
48
|
...lines,
|
|
43
|
-
"To mention a Shadow entity in a reply, write its visible handle (for example @username or #channel); Shadow will resolve it before delivery."
|
|
44
|
-
|
|
49
|
+
"To mention a Shadow entity in a reply, write its visible handle (for example @username or #channel); Shadow will resolve it before delivery.",
|
|
50
|
+
mentionsTargetServerApp(mentions) ? 'If a server app is mentioned, operate it through the Shadow CLI only: first run `shadowob app discover --server "<serverId-or-slug>" --json`, then run `shadowob app call "<appKey>" <command> --server "<serverId-or-slug>" --json-input \'<raw-command-input-json>\' --json`. Do not use curl, fetch, raw HTTP routes, or the JavaScript SDK for server-app commands. Use the mentioned appKey/serverId; do not ask the user to describe the CLI path.' : ""
|
|
51
|
+
].filter(Boolean).join("\n");
|
|
45
52
|
}
|
|
46
53
|
function mentionContextFields(mentions) {
|
|
47
54
|
if (mentions.length === 0) return {};
|
|
@@ -66,6 +73,14 @@ function mentionContextFields(mentions) {
|
|
|
66
73
|
serverId: mention.serverId ?? mention.targetId,
|
|
67
74
|
serverSlug: mention.serverSlug,
|
|
68
75
|
serverName: mention.serverName
|
|
76
|
+
})),
|
|
77
|
+
MentionedApps: mentions.filter((mention) => mention.kind === "app").map((mention) => ({
|
|
78
|
+
appId: mention.appId ?? mention.targetId,
|
|
79
|
+
appKey: mention.appKey,
|
|
80
|
+
appName: mention.appName,
|
|
81
|
+
serverId: mention.serverId,
|
|
82
|
+
serverSlug: mention.serverSlug,
|
|
83
|
+
serverName: mention.serverName
|
|
69
84
|
}))
|
|
70
85
|
};
|
|
71
86
|
}
|
|
@@ -162,6 +177,8 @@ async function buildInteractiveResponseContext(params) {
|
|
|
162
177
|
const responsePrompt = sourceInteractive && typeof sourceInteractive === "object" && !Array.isArray(sourceInteractive) ? sourceInteractive.responsePrompt : void 0;
|
|
163
178
|
const lines = [
|
|
164
179
|
"Shadow interactive response received.",
|
|
180
|
+
"Use the submitted values once. Do not separately restate or grade the submitted form unless the source command explicitly asks for an evaluation.",
|
|
181
|
+
"If the next step is another Shadow interactive dialog, send that dialog only and do not add a separate normal text reply for the same step.",
|
|
165
182
|
`Source message: ${source?.content ?? "(unavailable)"}`,
|
|
166
183
|
typeof sourcePrompt === "string" && sourcePrompt.trim() ? `Source prompt: ${sourcePrompt.trim()}` : "",
|
|
167
184
|
typeof responsePrompt === "string" && responsePrompt.trim() ? `Follow-up instruction: ${responsePrompt.trim()}` : "",
|
|
@@ -281,8 +298,10 @@ async function resolveShadowInboundMediaContext(params) {
|
|
|
281
298
|
}
|
|
282
299
|
|
|
283
300
|
// src/monitor/preflight.ts
|
|
284
|
-
function
|
|
285
|
-
|
|
301
|
+
function normalizeTriggerUserIds(policyConfig) {
|
|
302
|
+
const value = policyConfig?.allowedTriggerUserIds ?? policyConfig?.triggerUserIds;
|
|
303
|
+
if (!Array.isArray(value)) return null;
|
|
304
|
+
return value.filter((item) => typeof item === "string" && item.length > 0);
|
|
286
305
|
}
|
|
287
306
|
function evaluateShadowMessagePreflight(params) {
|
|
288
307
|
const { message, botUserId, botUsername, channelPolicies, runtime } = params;
|
|
@@ -292,7 +311,6 @@ function evaluateShadowMessagePreflight(params) {
|
|
|
292
311
|
}
|
|
293
312
|
const policy = channelPolicies.get(message.channelId);
|
|
294
313
|
const policyConfig = policy?.config;
|
|
295
|
-
const structuredMentions = getShadowMessageMentions(message);
|
|
296
314
|
let isProcessingBuddyMessage = false;
|
|
297
315
|
if (message.author?.isBot) {
|
|
298
316
|
if (!policyConfig?.replyToBuddy) {
|
|
@@ -347,82 +365,22 @@ function evaluateShadowMessagePreflight(params) {
|
|
|
347
365
|
reason: `[msg] Policy blocks reply for channel ${message.channelId}, skipping (${message.id})`
|
|
348
366
|
};
|
|
349
367
|
}
|
|
350
|
-
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
ok: false,
|
|
357
|
-
reason: `[msg] mentionOnly policy \u2014 no @${botUsername} mention found, skipping (${message.id})`
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
runtime.log?.(
|
|
361
|
-
`[msg] mentionOnly policy \u2014 @${botUsername} mentioned, processing (${message.id})`
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
if (policyConfig?.replyToUsers?.length) {
|
|
365
|
-
const allowedUsers = policyConfig.replyToUsers.map((u) => u.toLowerCase());
|
|
366
|
-
const senderUser = (message.author?.username ?? "").toLowerCase();
|
|
367
|
-
if (!allowedUsers.includes(senderUser)) {
|
|
368
|
-
return {
|
|
369
|
-
ok: false,
|
|
370
|
-
reason: `[msg] replyToUsers policy \u2014 sender "${senderUser}" not in allowed list, skipping (${message.id})`
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
if (policyConfig?.keywords?.length) {
|
|
375
|
-
const lowerContent = message.content.toLowerCase();
|
|
376
|
-
const matched = policyConfig.keywords.some((kw) => lowerContent.includes(kw.toLowerCase()));
|
|
377
|
-
if (!matched) {
|
|
378
|
-
return {
|
|
379
|
-
ok: false,
|
|
380
|
-
reason: `[msg] keywords policy \u2014 no matching keyword found, skipping (${message.id})`
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
runtime.log?.(`[msg] keywords policy \u2014 keyword matched, processing (${message.id})`);
|
|
368
|
+
const triggerUserIds = normalizeTriggerUserIds(policyConfig);
|
|
369
|
+
if (triggerUserIds && !triggerUserIds.includes(message.authorId)) {
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
reason: `[msg] Sender ${senderLabel} is not the Buddy owner or active tenant, skipping (${message.id})`
|
|
373
|
+
};
|
|
384
374
|
}
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
botUserId,
|
|
395
|
-
botUsername
|
|
396
|
-
});
|
|
397
|
-
if (userMentions.length > 0 && !structuredSelfMentioned) {
|
|
398
|
-
return {
|
|
399
|
-
ok: false,
|
|
400
|
-
reason: `[msg] Smart reply: message mentions other users but not @${botUsername}, skipping (${message.id})`
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
const mentionsWithoutSelf = userMentions.length > 0 ? [] : allMentions.filter((m) => {
|
|
404
|
-
const mentionedUser = m.slice(1).toLowerCase();
|
|
405
|
-
return mentionedUser !== botUsername.toLowerCase();
|
|
406
|
-
});
|
|
407
|
-
if (userMentions.length === 0 && allMentions.length > 0 && mentionsWithoutSelf.length === allMentions.length) {
|
|
408
|
-
return {
|
|
409
|
-
ok: false,
|
|
410
|
-
reason: `[msg] Smart reply: message @mentions others (${allMentions.join(", ")}) but not @${botUsername}, skipping (${message.id})`
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
const replyToData = message.replyTo;
|
|
414
|
-
if (replyToData?.authorId && replyToData.authorId !== botUserId) {
|
|
415
|
-
const selfMentioned = allMentions.some((m) => {
|
|
416
|
-
const mentionedUser = m.slice(1).toLowerCase();
|
|
417
|
-
return mentionedUser === botUsername.toLowerCase();
|
|
418
|
-
}) || structuredSelfMentioned;
|
|
419
|
-
if (!selfMentioned) {
|
|
420
|
-
return {
|
|
421
|
-
ok: false,
|
|
422
|
-
reason: `[msg] Smart reply: message is a reply to another user (${replyToData.authorId}), not this Buddy, skipping (${message.id})`
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
}
|
|
375
|
+
const structuredMentions = getShadowMessageMentions(message);
|
|
376
|
+
const escapedBotUsername = botUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
377
|
+
const mentionRegex = new RegExp(`@${escapedBotUsername}(?:\\s|$)`, "i");
|
|
378
|
+
const wasMentionedExplicitly = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionsTargetServerApp(structuredMentions) || mentionRegex.test(message.content);
|
|
379
|
+
if (policy?.mentionOnly && !wasMentionedExplicitly) {
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
reason: `[msg] Policy requires mention for channel ${message.channelId}, skipping (${message.id})`
|
|
383
|
+
};
|
|
426
384
|
}
|
|
427
385
|
return {
|
|
428
386
|
ok: true,
|
|
@@ -602,6 +560,7 @@ function resolveSessionStore(cfg) {
|
|
|
602
560
|
// src/monitor/slash-commands.ts
|
|
603
561
|
import fsPromises2 from "fs/promises";
|
|
604
562
|
var SLASH_COMMAND_RE = /^\/([a-zA-Z][a-zA-Z0-9._-]{0,63})(?:\s+([\s\S]*))?$/;
|
|
563
|
+
var DEFAULT_SLASH_COMMANDS_PATH = "/etc/shadowob/slash-commands.json";
|
|
605
564
|
function normalizeSlashCommandName(value) {
|
|
606
565
|
if (typeof value !== "string") return null;
|
|
607
566
|
const name = value.trim().replace(/^\/+/, "");
|
|
@@ -700,6 +659,7 @@ function normalizeShadowSlashCommands(input) {
|
|
|
700
659
|
)
|
|
701
660
|
].filter((alias) => alias.toLowerCase() !== key) : void 0;
|
|
702
661
|
const interaction = normalizeSlashInteraction(record.interaction);
|
|
662
|
+
const dispatch = readString(record.dispatch, 40);
|
|
703
663
|
commands.push({
|
|
704
664
|
name,
|
|
705
665
|
...typeof record.description === "string" && record.description.trim() ? { description: record.description.trim().slice(0, 240) } : {},
|
|
@@ -707,16 +667,24 @@ function normalizeShadowSlashCommands(input) {
|
|
|
707
667
|
...typeof record.packId === "string" && record.packId.trim() ? { packId: record.packId.trim().slice(0, 80) } : {},
|
|
708
668
|
...typeof record.sourcePath === "string" && record.sourcePath.trim() ? { sourcePath: record.sourcePath.trim().slice(0, 500) } : {},
|
|
709
669
|
...typeof record.body === "string" && record.body.trim() ? { body: record.body.trim().slice(0, 2e4) } : {},
|
|
670
|
+
...dispatch === "agent" || dispatch === "passthrough" ? { dispatch } : {},
|
|
710
671
|
...interaction ? { interaction } : {}
|
|
711
672
|
});
|
|
712
673
|
}
|
|
713
674
|
return commands.slice(0, 200);
|
|
714
675
|
}
|
|
715
676
|
function toPublicSlashCommands(commands) {
|
|
716
|
-
return commands.map(({ body: _body, ...command }) => command);
|
|
677
|
+
return commands.map(({ body: _body, dispatch: _dispatch, ...command }) => command);
|
|
717
678
|
}
|
|
718
|
-
async function
|
|
719
|
-
|
|
679
|
+
async function fileExists(path) {
|
|
680
|
+
try {
|
|
681
|
+
await fsPromises2.access(path);
|
|
682
|
+
return true;
|
|
683
|
+
} catch {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
async function loadSlashCommandFile(indexPath, runtime) {
|
|
720
688
|
if (!indexPath) return [];
|
|
721
689
|
try {
|
|
722
690
|
const raw = await fsPromises2.readFile(indexPath, "utf-8");
|
|
@@ -728,6 +696,73 @@ async function loadLocalSlashCommands(runtime) {
|
|
|
728
696
|
return [];
|
|
729
697
|
}
|
|
730
698
|
}
|
|
699
|
+
function logDuplicateSlashCommands(sources, runtime) {
|
|
700
|
+
const owners = /* @__PURE__ */ new Map();
|
|
701
|
+
for (const source of sources) {
|
|
702
|
+
for (const command of source.commands) {
|
|
703
|
+
const key = command.name.toLowerCase();
|
|
704
|
+
const existingPath = owners.get(key);
|
|
705
|
+
if (existingPath) {
|
|
706
|
+
runtime.log?.(
|
|
707
|
+
`[slash] Ignoring duplicate command /${command.name} from ${source.path}; already defined by ${existingPath}`
|
|
708
|
+
);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
owners.set(key, source.path);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
async function runtimeExtensionSlashCommandPaths(runtime) {
|
|
716
|
+
const candidates = [
|
|
717
|
+
process.env.SHADOW_RUNTIME_EXTENSIONS_PATH,
|
|
718
|
+
process.env.OPENCLAW_RUNTIME_EXTENSIONS_PATH,
|
|
719
|
+
"/etc/shadowob/runtime-extensions.json",
|
|
720
|
+
"/etc/openclaw/runtime-extensions.json"
|
|
721
|
+
].filter((path) => Boolean(path));
|
|
722
|
+
const paths = [];
|
|
723
|
+
for (const manifestPath of [...new Set(candidates)]) {
|
|
724
|
+
if (!await fileExists(manifestPath)) continue;
|
|
725
|
+
try {
|
|
726
|
+
const raw = await fsPromises2.readFile(manifestPath, "utf-8");
|
|
727
|
+
const manifest = JSON.parse(raw);
|
|
728
|
+
const artifacts = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
|
|
729
|
+
for (const artifact of artifacts) {
|
|
730
|
+
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) continue;
|
|
731
|
+
const record = artifact;
|
|
732
|
+
if (record.kind === "shadow.slashCommands" && typeof record.path === "string") {
|
|
733
|
+
paths.push(record.path);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
} catch (err) {
|
|
737
|
+
runtime.error?.(`[slash] Failed to read runtime extensions ${manifestPath}: ${String(err)}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return paths;
|
|
741
|
+
}
|
|
742
|
+
async function loadShadowSlashCommands(runtime) {
|
|
743
|
+
const defaultIndexPath = process.env.SHADOW_DEFAULT_SLASH_COMMANDS_PATH || DEFAULT_SLASH_COMMANDS_PATH;
|
|
744
|
+
const paths = [
|
|
745
|
+
defaultIndexPath,
|
|
746
|
+
process.env.SHADOW_SLASH_COMMANDS_PATH,
|
|
747
|
+
...await runtimeExtensionSlashCommandPaths(runtime)
|
|
748
|
+
].filter((path) => Boolean(path));
|
|
749
|
+
const seenPaths = [...new Set(paths)];
|
|
750
|
+
const existingPaths = (await Promise.all(seenPaths.map(async (path) => await fileExists(path) ? path : null))).filter((path) => Boolean(path));
|
|
751
|
+
const sources = await Promise.all(
|
|
752
|
+
existingPaths.map(async (path) => ({
|
|
753
|
+
path,
|
|
754
|
+
commands: await loadSlashCommandFile(path, runtime)
|
|
755
|
+
}))
|
|
756
|
+
);
|
|
757
|
+
logDuplicateSlashCommands(sources, runtime);
|
|
758
|
+
const merged = normalizeShadowSlashCommands(sources.flatMap((source) => source.commands));
|
|
759
|
+
if (existingPaths.length > 1) {
|
|
760
|
+
runtime.log?.(
|
|
761
|
+
`[slash] Merged ${merged.length} slash command(s) from ${existingPaths.length} source(s)`
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
return merged;
|
|
765
|
+
}
|
|
731
766
|
async function registerAgentSlashCommands(params) {
|
|
732
767
|
const baseUrl = params.account.serverUrl.replace(/\/api\/?$/, "").replace(/\/$/, "");
|
|
733
768
|
const response = await fetch(`${baseUrl}/api/agents/${params.agentId}/slash-commands`, {
|
|
@@ -1148,6 +1183,66 @@ function buildChannelContextForAgent(info, channelId) {
|
|
|
1148
1183
|
`Shadow channel id: ${channelId}`
|
|
1149
1184
|
].join("\n");
|
|
1150
1185
|
}
|
|
1186
|
+
function normalizeStringList(value) {
|
|
1187
|
+
if (!Array.isArray(value)) return [];
|
|
1188
|
+
return value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
1189
|
+
}
|
|
1190
|
+
function isSenderCommandAuthorized(policyConfig, senderId) {
|
|
1191
|
+
const triggerUserIds = normalizeStringList(
|
|
1192
|
+
policyConfig?.allowedTriggerUserIds ?? policyConfig?.triggerUserIds
|
|
1193
|
+
);
|
|
1194
|
+
if (triggerUserIds.length > 0) return triggerUserIds.includes(senderId);
|
|
1195
|
+
const ownerId = typeof policyConfig?.ownerId === "string" ? policyConfig.ownerId.trim() : "";
|
|
1196
|
+
if (ownerId && ownerId === senderId) return true;
|
|
1197
|
+
const activeTenantIds = normalizeStringList(policyConfig?.activeTenantIds);
|
|
1198
|
+
return activeTenantIds.includes(senderId);
|
|
1199
|
+
}
|
|
1200
|
+
function resolveOwnerAllowFrom(policyConfig) {
|
|
1201
|
+
const ownerId = typeof policyConfig?.ownerId === "string" ? policyConfig.ownerId.trim() : "";
|
|
1202
|
+
return ownerId ? [ownerId] : void 0;
|
|
1203
|
+
}
|
|
1204
|
+
async function buildMentionedServerAppSkillsContext(params) {
|
|
1205
|
+
const appRefs = /* @__PURE__ */ new Map();
|
|
1206
|
+
for (const mention of params.mentions) {
|
|
1207
|
+
if (mention.kind !== "app") continue;
|
|
1208
|
+
const appKey = mention.appKey ?? mention.targetId;
|
|
1209
|
+
const server = mention.serverId ?? mention.serverSlug ?? params.serverInfo?.serverId;
|
|
1210
|
+
if (!appKey || !server) continue;
|
|
1211
|
+
appRefs.set(`${server}:${appKey}`, {
|
|
1212
|
+
appKey,
|
|
1213
|
+
server,
|
|
1214
|
+
label: mention.label || mention.sourceToken || mention.token || appKey
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
if (appRefs.size === 0) return "";
|
|
1218
|
+
const documents = await Promise.all(
|
|
1219
|
+
Array.from(appRefs.values()).map(async (ref) => {
|
|
1220
|
+
try {
|
|
1221
|
+
const skill = await params.client.getServerAppSkills(ref.server, ref.appKey);
|
|
1222
|
+
return [
|
|
1223
|
+
`## ${ref.label}`,
|
|
1224
|
+
`Server reference: ${ref.server}`,
|
|
1225
|
+
`App key: ${ref.appKey}`,
|
|
1226
|
+
"",
|
|
1227
|
+
skill.markdown
|
|
1228
|
+
].join("\n");
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
params.runtime.error?.(
|
|
1231
|
+
`[server-app] Failed loading skills for ${ref.appKey} on ${ref.server}: ${String(err)}`
|
|
1232
|
+
);
|
|
1233
|
+
return "";
|
|
1234
|
+
}
|
|
1235
|
+
})
|
|
1236
|
+
);
|
|
1237
|
+
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.",
|
|
1242
|
+
"",
|
|
1243
|
+
...loaded
|
|
1244
|
+
].join("\n");
|
|
1245
|
+
}
|
|
1151
1246
|
async function processShadowMessage(params) {
|
|
1152
1247
|
const {
|
|
1153
1248
|
message,
|
|
@@ -1209,6 +1304,7 @@ async function processShadowMessage(params) {
|
|
|
1209
1304
|
slashCommands
|
|
1210
1305
|
});
|
|
1211
1306
|
const slashCommandMatch = matchShadowSlashCommand(cleanBody, slashCommands);
|
|
1307
|
+
const slashCommandPassThrough = slashCommandMatch?.command.dispatch === "passthrough";
|
|
1212
1308
|
if (slashCommandMatch) {
|
|
1213
1309
|
runtime.log?.(
|
|
1214
1310
|
`[slash] Matched /${slashCommandMatch.invokedName} -> /${slashCommandMatch.command.name}`
|
|
@@ -1231,17 +1327,22 @@ async function processShadowMessage(params) {
|
|
|
1231
1327
|
});
|
|
1232
1328
|
return;
|
|
1233
1329
|
}
|
|
1234
|
-
const baseBodyForAgent = slashCommandMatch ? formatSlashCommandPrompt(cleanBody, slashCommandMatch) : cleanBody;
|
|
1330
|
+
const baseBodyForAgent = slashCommandMatch && !slashCommandPassThrough ? formatSlashCommandPrompt(cleanBody, slashCommandMatch) : cleanBody;
|
|
1331
|
+
const commandBody = slashCommandPassThrough ? cleanBody : slashCommandMatch?.args ?? cleanBody;
|
|
1332
|
+
const ownerAllowFrom = resolveOwnerAllowFrom(preflight.policyConfig);
|
|
1235
1333
|
const structuredMentions = getShadowMessageMentions(message);
|
|
1236
1334
|
const mentionContext = formatShadowMentionsForAgent(structuredMentions);
|
|
1237
1335
|
const serverInfo = channelServerMap.get(channelId);
|
|
1238
1336
|
const channelLabel = serverInfo ? `#${serverInfo.channelName}` : `channel:${channelId}`;
|
|
1239
1337
|
const conversationLabel = serverInfo ? `${serverInfo.serverName} ${channelLabel}` : peerId;
|
|
1240
|
-
const messageBodyForAgent = interactiveResponseContext.text
|
|
1241
|
-
|
|
1242
|
-
User message:
|
|
1243
|
-
${baseBodyForAgent}` : baseBodyForAgent;
|
|
1338
|
+
const messageBodyForAgent = interactiveResponseContext.text || baseBodyForAgent;
|
|
1244
1339
|
const client = new ShadowClient2(account.serverUrl, account.token);
|
|
1340
|
+
const serverAppSkillsContext = await buildMentionedServerAppSkillsContext({
|
|
1341
|
+
mentions: structuredMentions,
|
|
1342
|
+
client,
|
|
1343
|
+
serverInfo,
|
|
1344
|
+
runtime
|
|
1345
|
+
});
|
|
1245
1346
|
const viewerCommerceContext = await buildCommerceViewerContextForAgent({
|
|
1246
1347
|
account,
|
|
1247
1348
|
client,
|
|
@@ -1252,6 +1353,7 @@ ${baseBodyForAgent}` : baseBodyForAgent;
|
|
|
1252
1353
|
buildCommerceContextForAgent(account),
|
|
1253
1354
|
viewerCommerceContext,
|
|
1254
1355
|
mentionContext,
|
|
1356
|
+
serverAppSkillsContext,
|
|
1255
1357
|
messageBodyForAgent
|
|
1256
1358
|
].filter(Boolean).join("\n\n");
|
|
1257
1359
|
const body = core.channel.reply.formatAgentEnvelope({
|
|
@@ -1263,12 +1365,15 @@ ${baseBodyForAgent}` : baseBodyForAgent;
|
|
|
1263
1365
|
});
|
|
1264
1366
|
const escapedBotUsername = botUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1265
1367
|
const mentionRegex = new RegExp(`@${escapedBotUsername}(?:\\s|$)`, "i");
|
|
1266
|
-
const wasMentioned = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionRegex.test(message.content);
|
|
1368
|
+
const wasMentioned = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionsTargetServerApp(structuredMentions) || Boolean(slashCommandMatch) || mentionRegex.test(message.content);
|
|
1267
1369
|
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
1268
1370
|
Body: body,
|
|
1269
1371
|
BodyForAgent: bodyForAgent,
|
|
1270
1372
|
RawBody: rawBody,
|
|
1271
|
-
CommandBody:
|
|
1373
|
+
CommandBody: commandBody,
|
|
1374
|
+
BodyForCommands: commandBody,
|
|
1375
|
+
CommandAuthorized: isSenderCommandAuthorized(preflight.policyConfig, senderId),
|
|
1376
|
+
CommandSource: "text",
|
|
1272
1377
|
From: `shadowob:user:${senderId}`,
|
|
1273
1378
|
To: `shadowob:channel:${channelId}`,
|
|
1274
1379
|
SessionKey: route.sessionKey,
|
|
@@ -1285,6 +1390,8 @@ ${baseBodyForAgent}` : baseBodyForAgent;
|
|
|
1285
1390
|
...mentionContextFields(structuredMentions),
|
|
1286
1391
|
OriginatingChannel: "shadowob",
|
|
1287
1392
|
OriginatingTo: `shadowob:channel:${channelId}`,
|
|
1393
|
+
NativeChannelId: channelId,
|
|
1394
|
+
...ownerAllowFrom ? { OwnerAllowFrom: ownerAllowFrom } : {},
|
|
1288
1395
|
...serverInfo ? {
|
|
1289
1396
|
ServerId: serverInfo.serverId,
|
|
1290
1397
|
ServerSlug: serverInfo.serverSlug,
|
|
@@ -1428,6 +1535,34 @@ ${baseBodyForAgent}` : baseBodyForAgent;
|
|
|
1428
1535
|
}
|
|
1429
1536
|
}
|
|
1430
1537
|
|
|
1538
|
+
// src/monitor/message-queue.ts
|
|
1539
|
+
function queueKeyForMessage(message) {
|
|
1540
|
+
return `${message.channelId}:${message.threadId ?? ""}`;
|
|
1541
|
+
}
|
|
1542
|
+
function createShadowMessageProcessingQueue(options) {
|
|
1543
|
+
const queues = /* @__PURE__ */ new Map();
|
|
1544
|
+
return {
|
|
1545
|
+
enqueue(message, source) {
|
|
1546
|
+
const key = queueKeyForMessage(message);
|
|
1547
|
+
const previous = queues.get(key) ?? Promise.resolve();
|
|
1548
|
+
const task = previous.catch(() => void 0).then(() => {
|
|
1549
|
+
if (options.isStopped?.()) {
|
|
1550
|
+
options.onSkipped?.(message, source);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
return options.process(message, source);
|
|
1554
|
+
}).finally(() => {
|
|
1555
|
+
if (queues.get(key) === task) queues.delete(key);
|
|
1556
|
+
});
|
|
1557
|
+
queues.set(key, task);
|
|
1558
|
+
return task;
|
|
1559
|
+
},
|
|
1560
|
+
pendingKeys() {
|
|
1561
|
+
return [...queues.keys()];
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1431
1566
|
// src/runtime.ts
|
|
1432
1567
|
import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
1433
1568
|
var store = createPluginRuntimeStore(
|
|
@@ -1534,7 +1669,7 @@ async function monitorShadowProvider(options) {
|
|
|
1534
1669
|
} else {
|
|
1535
1670
|
runtime.log?.(`[config] Resolved agentId: ${agentId}`);
|
|
1536
1671
|
}
|
|
1537
|
-
const slashCommands = await
|
|
1672
|
+
const slashCommands = await loadShadowSlashCommands(runtime);
|
|
1538
1673
|
if (agentId) {
|
|
1539
1674
|
try {
|
|
1540
1675
|
await runShadowApiOperation(
|
|
@@ -1554,6 +1689,23 @@ async function monitorShadowProvider(options) {
|
|
|
1554
1689
|
const messageWatermarks = await loadMessageWatermarks(accountId);
|
|
1555
1690
|
const processedMessageIds = /* @__PURE__ */ new Set();
|
|
1556
1691
|
const catchupInFlight = /* @__PURE__ */ new Map();
|
|
1692
|
+
const buildAccessPolicyConfig = (config2) => {
|
|
1693
|
+
const activeTenantIds = config2?.activeTenantIds ?? [];
|
|
1694
|
+
const allowedTriggerUserIds = config2?.allowedTriggerUserIds ?? [config2?.ownerId, ...activeTenantIds].filter((id) => Boolean(id));
|
|
1695
|
+
return {
|
|
1696
|
+
allowedTriggerUserIds,
|
|
1697
|
+
triggerUserIds: allowedTriggerUserIds,
|
|
1698
|
+
ownerId: config2?.ownerId,
|
|
1699
|
+
activeTenantIds,
|
|
1700
|
+
replyRequiresMention: false
|
|
1701
|
+
};
|
|
1702
|
+
};
|
|
1703
|
+
const buildDefaultAccessPolicy = (config2) => ({
|
|
1704
|
+
listen: true,
|
|
1705
|
+
reply: true,
|
|
1706
|
+
mentionOnly: false,
|
|
1707
|
+
config: buildAccessPolicyConfig(config2)
|
|
1708
|
+
});
|
|
1557
1709
|
const rememberProcessedMessage = (messageId) => {
|
|
1558
1710
|
processedMessageIds.add(messageId);
|
|
1559
1711
|
if (processedMessageIds.size > MAX_TRACKED_MESSAGE_IDS) {
|
|
@@ -1669,12 +1821,7 @@ async function monitorShadowProvider(options) {
|
|
|
1669
1821
|
for (const ch of directChannels) {
|
|
1670
1822
|
if (!allChannelIds.includes(ch.id)) allChannelIds.push(ch.id);
|
|
1671
1823
|
if (!channelPolicies.has(ch.id)) {
|
|
1672
|
-
channelPolicies.set(ch.id,
|
|
1673
|
-
listen: true,
|
|
1674
|
-
reply: true,
|
|
1675
|
-
mentionOnly: false,
|
|
1676
|
-
config: {}
|
|
1677
|
-
});
|
|
1824
|
+
channelPolicies.set(ch.id, buildDefaultAccessPolicy(remoteConfig));
|
|
1678
1825
|
}
|
|
1679
1826
|
}
|
|
1680
1827
|
runtime.log?.(`[config] Monitoring ${directChannels.length} direct channel(s)`);
|
|
@@ -1735,6 +1882,13 @@ async function monitorShadowProvider(options) {
|
|
|
1735
1882
|
);
|
|
1736
1883
|
}
|
|
1737
1884
|
};
|
|
1885
|
+
const messageQueue = createShadowMessageProcessingQueue({
|
|
1886
|
+
process: processChannelMessageWithRetry,
|
|
1887
|
+
isStopped: () => stopped,
|
|
1888
|
+
onSkipped: (message, source) => {
|
|
1889
|
+
runtime.log?.(`[${source}] Monitor stopped, skipping queued message ${message.id}`);
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1738
1892
|
const catchUpChannel = async (channelId, reason) => {
|
|
1739
1893
|
try {
|
|
1740
1894
|
const result = await client.getMessages(channelId, 50);
|
|
@@ -1759,7 +1913,7 @@ async function monitorShadowProvider(options) {
|
|
|
1759
1913
|
}
|
|
1760
1914
|
for (const message of candidates) {
|
|
1761
1915
|
rememberProcessedMessage(message.id);
|
|
1762
|
-
await
|
|
1916
|
+
await messageQueue.enqueue(message, "catchup");
|
|
1763
1917
|
}
|
|
1764
1918
|
} catch (err) {
|
|
1765
1919
|
runtime.error?.(`[catchup] Failed for channel ${channelId}: ${String(err)}`);
|
|
@@ -1865,25 +2019,78 @@ async function monitorShadowProvider(options) {
|
|
|
1865
2019
|
"agent:policy-changed",
|
|
1866
2020
|
(data) => {
|
|
1867
2021
|
if (data.agentId !== agentId) return;
|
|
1868
|
-
if (!data.channelId)
|
|
1869
|
-
|
|
2022
|
+
if (!data.channelId) {
|
|
2023
|
+
void (async () => {
|
|
2024
|
+
try {
|
|
2025
|
+
const updatedConfig = await runShadowApiOperation(
|
|
2026
|
+
"refresh remote config after access policy change",
|
|
2027
|
+
() => client.getAgentConfig(agentId),
|
|
2028
|
+
{ runtime, abortSignal }
|
|
2029
|
+
);
|
|
2030
|
+
remoteConfig = updatedConfig;
|
|
2031
|
+
const remoteChannelIds = /* @__PURE__ */ new Set();
|
|
2032
|
+
const accessConfig2 = buildAccessPolicyConfig(updatedConfig);
|
|
2033
|
+
for (const server of updatedConfig.servers) {
|
|
2034
|
+
for (const ch of server.channels) {
|
|
2035
|
+
remoteChannelIds.add(ch.id);
|
|
2036
|
+
channelServerMap.set(ch.id, {
|
|
2037
|
+
serverId: server.id,
|
|
2038
|
+
serverSlug: server.slug ?? server.id,
|
|
2039
|
+
serverName: server.name,
|
|
2040
|
+
channelName: ch.name
|
|
2041
|
+
});
|
|
2042
|
+
channelPolicies.set(ch.id, {
|
|
2043
|
+
...ch.policy,
|
|
2044
|
+
config: { ...ch.policy.config, ...accessConfig2 }
|
|
2045
|
+
});
|
|
2046
|
+
if (!allChannelIds.includes(ch.id)) {
|
|
2047
|
+
allChannelIds.push(ch.id);
|
|
2048
|
+
void socket.joinChannel(ch.id);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
for (const [channelId] of channelServerMap) {
|
|
2053
|
+
if (remoteChannelIds.has(channelId)) continue;
|
|
2054
|
+
channelServerMap.delete(channelId);
|
|
2055
|
+
channelPolicies.delete(channelId);
|
|
2056
|
+
const idx = allChannelIds.indexOf(channelId);
|
|
2057
|
+
if (idx !== -1) allChannelIds.splice(idx, 1);
|
|
2058
|
+
socket.leaveChannel(channelId);
|
|
2059
|
+
}
|
|
2060
|
+
for (const [channelId, existing2] of channelPolicies) {
|
|
2061
|
+
if (channelServerMap.has(channelId)) continue;
|
|
2062
|
+
channelPolicies.set(channelId, {
|
|
2063
|
+
...existing2,
|
|
2064
|
+
config: { ...existing2.config, ...accessConfig2 }
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
runtime.log?.("[config] Refreshed Buddy owner/tenant access policy");
|
|
2068
|
+
} catch (err) {
|
|
2069
|
+
runtime.error?.(`[config] Failed to refresh access policy: ${String(err)}`);
|
|
2070
|
+
}
|
|
2071
|
+
})();
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
const existing = channelPolicies.get(data.channelId);
|
|
2075
|
+
const mentionOnly = data.mentionOnly ?? existing?.mentionOnly ?? false;
|
|
2076
|
+
const reply = data.reply ?? existing?.reply ?? true;
|
|
2077
|
+
const accessConfig = buildAccessPolicyConfig(remoteConfig);
|
|
1870
2078
|
runtime.log?.(
|
|
1871
|
-
`[ws] Received agent:policy-changed for channel ${data.channelId}: mentionOnly=${mentionOnly}, reply=${
|
|
2079
|
+
`[ws] Received agent:policy-changed for channel ${data.channelId}: mentionOnly=${mentionOnly}, reply=${reply}, config=${JSON.stringify(data.config ?? {})}`
|
|
1872
2080
|
);
|
|
1873
|
-
const existing = channelPolicies.get(data.channelId);
|
|
1874
2081
|
if (existing) {
|
|
1875
2082
|
channelPolicies.set(data.channelId, {
|
|
1876
2083
|
...existing,
|
|
1877
2084
|
mentionOnly,
|
|
1878
|
-
reply
|
|
1879
|
-
config: data.config ??
|
|
2085
|
+
reply,
|
|
2086
|
+
config: { ...existing.config, ...accessConfig, ...data.config ?? {} }
|
|
1880
2087
|
});
|
|
1881
2088
|
} else {
|
|
1882
2089
|
channelPolicies.set(data.channelId, {
|
|
1883
2090
|
listen: true,
|
|
1884
|
-
reply
|
|
2091
|
+
reply,
|
|
1885
2092
|
mentionOnly,
|
|
1886
|
-
config: data.config ?? {}
|
|
2093
|
+
config: { ...accessConfig, ...data.config ?? {} }
|
|
1887
2094
|
});
|
|
1888
2095
|
}
|
|
1889
2096
|
}
|
|
@@ -1934,12 +2141,12 @@ async function monitorShadowProvider(options) {
|
|
|
1934
2141
|
if (!refreshed) {
|
|
1935
2142
|
await resolveChannelContext(data.channelId, "member-added");
|
|
1936
2143
|
}
|
|
1937
|
-
if (!
|
|
2144
|
+
if (!channelPolicies.has(data.channelId)) {
|
|
1938
2145
|
const defaultPolicy = {
|
|
1939
2146
|
listen: true,
|
|
1940
2147
|
reply: true,
|
|
1941
2148
|
mentionOnly: false,
|
|
1942
|
-
config:
|
|
2149
|
+
config: buildAccessPolicyConfig(remoteConfig)
|
|
1943
2150
|
};
|
|
1944
2151
|
channelPolicies.set(data.channelId, defaultPolicy);
|
|
1945
2152
|
}
|
|
@@ -1983,7 +2190,7 @@ async function monitorShadowProvider(options) {
|
|
|
1983
2190
|
runtime.log?.(`[ws] Message from unmonitored channel ${message.channelId}, ignoring`);
|
|
1984
2191
|
return;
|
|
1985
2192
|
}
|
|
1986
|
-
void
|
|
2193
|
+
void messageQueue.enqueue(message, "ws");
|
|
1987
2194
|
});
|
|
1988
2195
|
socket.connect();
|
|
1989
2196
|
const stop = () => {
|
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-4G572ZLC.js";
|
|
19
19
|
|
|
20
20
|
// index.ts
|
|
21
21
|
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
@@ -808,6 +808,7 @@ var shadowAgentPromptHints = [
|
|
|
808
808
|
"- When a Shadow user asks for buttons, choices, a select menu, a form, or approval, prefer sending a Shadow interactive dialog instead of plain text options.",
|
|
809
809
|
"- ShadowOwnBuddy enables inline buttons, forms, and file uploads for Shadow channels by default. Do not tell the user that `shadowob.capabilities.inlineButtons` or file sending is unavailable; use the message tool instead.",
|
|
810
810
|
'- Shadow interactive dialogs use the shared message tool with `action: "send"` plus `target`, `message`, `kind`, `prompt`, and shape fields. `message` is required by the shared tool; set `message` and `prompt` to the same user-visible text unless there is a specific reason not to. Supported `kind` values are `buttons`, `select`, `form`, and `approval`; Shadow stores these as `metadata.interactive` so the user can answer in-channel.',
|
|
811
|
+
"- When your next action is a Shadow interactive dialog, put the full user-visible prompt in that dialog and do not also send a separate ordinary reply in the same turn. This prevents duplicate form-plus-commentary messages.",
|
|
811
812
|
'- Example buttons dialog: `action: "send"`, `target: "shadowob:channel:<ChannelId>"`, `message: "Choose the next step"`, `kind: "buttons"`, `prompt: "Choose the next step"`, `buttons: [{"id":"icp","label":"ICP / JTBD","value":"icp"}]`.',
|
|
812
813
|
'- Example form dialog: `action: "send"`, `target: "shadowob:channel:<ChannelId>"`, `message: "Fill the decision inputs"`, `kind: "form"`, `fields: [{"id":"decision","label":"Decision","kind":"textarea","required":true}]`.',
|
|
813
814
|
'- When Shadow context includes CommerceOfferIds and the user is interested in buying, viewing pricing, or receiving a product card, call the shared message tool yourself: `action: "send"`, `target: "shadowob:channel:<ChannelId>"`, `message: "A natural sales message"`, `commerceOfferId: "<CommerceOfferId>"`. Plain final text will not attach a product card.',
|
|
@@ -959,7 +960,7 @@ shadowPlugin.gateway = {
|
|
|
959
960
|
lastError: null
|
|
960
961
|
});
|
|
961
962
|
ctx.log?.info(`Starting Shadow connection for account ${accountId}`);
|
|
962
|
-
const { monitorShadowProvider: monitorShadowProvider2 } = await import("./monitor-
|
|
963
|
+
const { monitorShadowProvider: monitorShadowProvider2 } = await import("./monitor-XDUBTELB.js");
|
|
963
964
|
await monitorShadowProvider2({
|
|
964
965
|
account,
|
|
965
966
|
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.5",
|
|
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.5"
|
|
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: "
|
|
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."
|
|
4
4
|
metadata:
|
|
5
5
|
{
|
|
6
6
|
"openclaw":
|
|
@@ -17,6 +17,10 @@ allowed-tools: ["exec"]
|
|
|
17
17
|
|
|
18
18
|
Use `shadowob` CLI to interact with Shadow servers.
|
|
19
19
|
|
|
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
|
|
22
|
+
send or manage Shadow content. Prefer narrow `--json` reads before acting.
|
|
23
|
+
|
|
20
24
|
## Quickstart
|
|
21
25
|
|
|
22
26
|
```bash
|
|
@@ -66,11 +70,6 @@ shadowob servers leave <server-id>
|
|
|
66
70
|
# Members
|
|
67
71
|
shadowob servers members <server-id> --json
|
|
68
72
|
|
|
69
|
-
# Homepage
|
|
70
|
-
shadowob servers homepage <server-id>
|
|
71
|
-
shadowob servers homepage <server-id> --set <file.html>
|
|
72
|
-
shadowob servers homepage <server-id> --clear
|
|
73
|
-
|
|
74
73
|
# Discover public servers
|
|
75
74
|
shadowob servers discover --json
|
|
76
75
|
```
|
|
@@ -179,28 +178,49 @@ shadowob workspace stats <server-id> --json
|
|
|
179
178
|
shadowob workspace children <server-id> [--parent-id <id>] --json
|
|
180
179
|
|
|
181
180
|
# Files
|
|
182
|
-
shadowob workspace files get <file-id> --json
|
|
183
|
-
shadowob workspace files create <server-id> --name <name> [--content <text>] [--parent-id <id>] --json
|
|
184
|
-
shadowob workspace files update <file-id> [--name <name>] [--content <text>] --json
|
|
185
|
-
shadowob workspace files delete <file-id>
|
|
181
|
+
shadowob workspace files get <server-id> <file-id> --json
|
|
186
182
|
shadowob workspace files upload <server-id> --file <path> [--name <name>] [--parent-id <id>] --json
|
|
187
|
-
shadowob workspace files
|
|
183
|
+
shadowob workspace files update <server-id> <file-id> [--name <name>] [--parent-id <id>] --json
|
|
184
|
+
shadowob workspace files delete <server-id> <file-id>
|
|
185
|
+
shadowob workspace files search <server-id> [--search-text <text>] [--ext <ext>] [--parent-id <id>] --json
|
|
186
|
+
# Note: files download is not yet implemented in CLI; download via contentRef URL instead.
|
|
188
187
|
|
|
189
188
|
# Folders
|
|
190
189
|
shadowob workspace folders create <server-id> --name <name> [--parent-id <id>] --json
|
|
191
|
-
shadowob workspace folders update <folder-id> --name <name> --json
|
|
192
|
-
shadowob workspace folders delete <folder-id>
|
|
190
|
+
shadowob workspace folders update <server-id> <folder-id> [--name <name>] [--parent-id <id>] --json
|
|
191
|
+
shadowob workspace folders delete <server-id> <folder-id>
|
|
193
192
|
```
|
|
194
193
|
|
|
194
|
+
### Workspace Node Metadata
|
|
195
|
+
|
|
196
|
+
Each workspace node has a `flags` JSONB field with optional metadata:
|
|
197
|
+
|
|
198
|
+
- **Access control**: `flags.access = { scope: "server" | "channel", serverId, channelId? }`. All nodes have at least `scope: "server"` + `serverId`. Channel-scoped nodes require channel membership for access.
|
|
199
|
+
- **Traceability**: `flags.source = "channel_message_attachment"` with `channelId` and `messageId` for files uploaded via channel messages, enabling reverse lookup to the originating message.
|
|
200
|
+
- **Path is server-computed**: `path` is derived from parent path + name, maintained server-side. Do not set path manually — it is auto-updated on rename/move.
|
|
201
|
+
|
|
195
202
|
## Shop
|
|
196
203
|
|
|
197
204
|
```bash
|
|
198
205
|
# Shop info
|
|
199
206
|
shadowob shop get <server-id> --json
|
|
207
|
+
shadowob shop get-by-id <shop-id> --json
|
|
208
|
+
shadowob shop me get --json
|
|
200
209
|
|
|
201
210
|
# Products
|
|
202
|
-
shadowob shop products list <server-id> --json
|
|
211
|
+
shadowob shop products list <server-id> [--status active] [--keyword <text>] [--limit <n>] --json
|
|
212
|
+
shadowob shop products list-by-shop <shop-id> [--status active] [--limit <n>] --json
|
|
203
213
|
shadowob shop products get <server-id> <product-id> --json
|
|
214
|
+
shadowob shop products context <product-id> --json
|
|
215
|
+
shadowob shop products purchase <shop-id> <product-id> --idempotency-key <unique-operation-id> --json
|
|
216
|
+
|
|
217
|
+
# Offers, deliverables, and shop assets
|
|
218
|
+
shadowob shop offers list <shop-id> --json
|
|
219
|
+
shadowob shop offers create <shop-id> --data '<offer-json>' --json
|
|
220
|
+
shadowob shop offers deliverables create <shop-id> <offer-id> --data '<deliverable-json>' --json
|
|
221
|
+
shadowob shop assets list <shop-id> --json
|
|
222
|
+
shadowob shop assets create <shop-id> --data '<asset-definition-json>' --json
|
|
223
|
+
shadowob shop entitlements list <shop-id> --json
|
|
204
224
|
|
|
205
225
|
# Cart
|
|
206
226
|
shadowob shop cart list <server-id> --json
|
|
@@ -213,10 +233,64 @@ shadowob shop orders get <server-id> <order-id> --json
|
|
|
213
233
|
shadowob shop wallet balance --json
|
|
214
234
|
```
|
|
215
235
|
|
|
236
|
+
## Commerce
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# Product and offer buyer context
|
|
240
|
+
shadowob commerce products context <product-id> --json
|
|
241
|
+
shadowob commerce offers preview <offer-id> --json
|
|
242
|
+
shadowob commerce offers purchase <offer-id> --idempotency-key <unique-operation-id> --json
|
|
243
|
+
|
|
244
|
+
# Chat commerce cards
|
|
245
|
+
shadowob commerce cards list --channel-id <channel-id> [--keyword <text>] --json
|
|
246
|
+
shadowob commerce cards purchase <message-id> <card-id> --idempotency-key <unique-operation-id> --json
|
|
247
|
+
|
|
248
|
+
# Purchases, delivery, protected files, and community assets
|
|
249
|
+
shadowob commerce entitlements list [--server-id <server-id>] --json
|
|
250
|
+
shadowob commerce entitlements get <entitlement-id> --json
|
|
251
|
+
shadowob commerce entitlements verify <entitlement-id> --json
|
|
252
|
+
shadowob commerce paid-files open <file-id> --json
|
|
253
|
+
shadowob commerce assets list --json
|
|
254
|
+
shadowob commerce assets consume <grant-id> --idempotency-key <unique-operation-id> --json
|
|
255
|
+
|
|
256
|
+
# Seller income and support actions
|
|
257
|
+
shadowob commerce settlements list --json
|
|
258
|
+
shadowob commerce settlements settle --json
|
|
259
|
+
shadowob commerce tips send --recipient-user-id <user-id> --amount <shrimp> [--message <text>] --json
|
|
260
|
+
shadowob commerce gifts send --recipient-user-id <user-id> --assets '<json-array>' --json
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Commerce Validation Notes
|
|
264
|
+
|
|
265
|
+
- Use the CLI for setup, inspection, and automation, but validate commerce user stories in the
|
|
266
|
+
browser before calling them complete.
|
|
267
|
+
- Do not add seed code to populate commerce surfaces. Create ordinary local/test records through
|
|
268
|
+
browser flows or explicit setup calls.
|
|
269
|
+
- When inspecting a commerce flow, preserve ids for the handoff: product, offer, order,
|
|
270
|
+
entitlement, shop, server, Buddy, and workspace file where applicable.
|
|
271
|
+
- External app entitlement automation must use Shadow OAuth commerce APIs and remain scoped to the
|
|
272
|
+
app's own `external_app` resource namespace.
|
|
273
|
+
|
|
216
274
|
## Apps
|
|
217
275
|
|
|
218
276
|
```bash
|
|
219
|
-
#
|
|
277
|
+
# Server App integrations
|
|
278
|
+
shadowob app list --server <server-id-or-slug> --json
|
|
279
|
+
shadowob app preview --server <server-id-or-slug> --manifest-url <manifest-url> --json
|
|
280
|
+
shadowob app discover --server <server-id-or-slug> --json
|
|
281
|
+
shadowob app inspect <app-key> --server <server-id-or-slug> --json
|
|
282
|
+
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
|
+
```
|
|
285
|
+
|
|
286
|
+
For server App commands, use the `shadowob app` CLI path only. Do not use curl, fetch, raw HTTP
|
|
287
|
+
routes, or the JavaScript SDK to call server App commands. Pass the command input object directly
|
|
288
|
+
to `--json-input`, for example `{"title":"Example","priority":"high"}`; the CLI wraps the HTTP
|
|
289
|
+
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.
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
# Legacy workspace apps
|
|
220
294
|
shadowob apps list <server-id> --json
|
|
221
295
|
|
|
222
296
|
# Get app
|
|
@@ -302,6 +376,10 @@ shadowob oauth consents --json
|
|
|
302
376
|
|
|
303
377
|
# Revoke consent for an app
|
|
304
378
|
shadowob oauth revoke <app-id>
|
|
379
|
+
|
|
380
|
+
# External app commerce entitlement checks use OAuth access tokens, not user JWTs
|
|
381
|
+
shadowob oauth commerce check --access-token <oauth-access-token> --resource-id <app-id>:premium --json
|
|
382
|
+
shadowob oauth commerce redeem --access-token <oauth-access-token> --resource-id <app-id>:premium --idempotency-key <provider-operation-id> --json
|
|
305
383
|
```
|
|
306
384
|
|
|
307
385
|
## Marketplace
|