@shadowob/openclaw-shadowob 1.1.3-dev.281 → 1.1.4

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
- ].join("\n");
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()}` : "",
@@ -358,7 +375,7 @@ function evaluateShadowMessagePreflight(params) {
358
375
  const structuredMentions = getShadowMessageMentions(message);
359
376
  const escapedBotUsername = botUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
360
377
  const mentionRegex = new RegExp(`@${escapedBotUsername}(?:\\s|$)`, "i");
361
- const wasMentionedExplicitly = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionRegex.test(message.content);
378
+ const wasMentionedExplicitly = mentionTargetsBot({ mentions: structuredMentions, botUserId, botUsername }) || mentionsTargetServerApp(structuredMentions) || mentionRegex.test(message.content);
362
379
  if (policy?.mentionOnly && !wasMentionedExplicitly) {
363
380
  return {
364
381
  ok: false,
@@ -543,6 +560,7 @@ function resolveSessionStore(cfg) {
543
560
  // src/monitor/slash-commands.ts
544
561
  import fsPromises2 from "fs/promises";
545
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";
546
564
  function normalizeSlashCommandName(value) {
547
565
  if (typeof value !== "string") return null;
548
566
  const name = value.trim().replace(/^\/+/, "");
@@ -641,6 +659,7 @@ function normalizeShadowSlashCommands(input) {
641
659
  )
642
660
  ].filter((alias) => alias.toLowerCase() !== key) : void 0;
643
661
  const interaction = normalizeSlashInteraction(record.interaction);
662
+ const dispatch = readString(record.dispatch, 40);
644
663
  commands.push({
645
664
  name,
646
665
  ...typeof record.description === "string" && record.description.trim() ? { description: record.description.trim().slice(0, 240) } : {},
@@ -648,13 +667,14 @@ function normalizeShadowSlashCommands(input) {
648
667
  ...typeof record.packId === "string" && record.packId.trim() ? { packId: record.packId.trim().slice(0, 80) } : {},
649
668
  ...typeof record.sourcePath === "string" && record.sourcePath.trim() ? { sourcePath: record.sourcePath.trim().slice(0, 500) } : {},
650
669
  ...typeof record.body === "string" && record.body.trim() ? { body: record.body.trim().slice(0, 2e4) } : {},
670
+ ...dispatch === "agent" || dispatch === "passthrough" ? { dispatch } : {},
651
671
  ...interaction ? { interaction } : {}
652
672
  });
653
673
  }
654
674
  return commands.slice(0, 200);
655
675
  }
656
676
  function toPublicSlashCommands(commands) {
657
- return commands.map(({ body: _body, ...command }) => command);
677
+ return commands.map(({ body: _body, dispatch: _dispatch, ...command }) => command);
658
678
  }
659
679
  async function fileExists(path) {
660
680
  try {
@@ -676,10 +696,27 @@ async function loadSlashCommandFile(indexPath, runtime) {
676
696
  return [];
677
697
  }
678
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
+ }
679
715
  async function runtimeExtensionSlashCommandPaths(runtime) {
680
716
  const candidates = [
681
717
  process.env.SHADOW_RUNTIME_EXTENSIONS_PATH,
682
718
  process.env.OPENCLAW_RUNTIME_EXTENSIONS_PATH,
719
+ "/etc/shadowob/runtime-extensions.json",
683
720
  "/etc/openclaw/runtime-extensions.json"
684
721
  ].filter((path) => Boolean(path));
685
722
  const paths = [];
@@ -703,16 +740,25 @@ async function runtimeExtensionSlashCommandPaths(runtime) {
703
740
  return paths;
704
741
  }
705
742
  async function loadShadowSlashCommands(runtime) {
743
+ const defaultIndexPath = process.env.SHADOW_DEFAULT_SLASH_COMMANDS_PATH || DEFAULT_SLASH_COMMANDS_PATH;
706
744
  const paths = [
745
+ defaultIndexPath,
707
746
  process.env.SHADOW_SLASH_COMMANDS_PATH,
708
747
  ...await runtimeExtensionSlashCommandPaths(runtime)
709
748
  ].filter((path) => Boolean(path));
710
749
  const seenPaths = [...new Set(paths)];
711
- const loaded = await Promise.all(seenPaths.map((path) => loadSlashCommandFile(path, runtime)));
712
- const merged = normalizeShadowSlashCommands(loaded.flat());
713
- if (seenPaths.length > 1) {
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) {
714
760
  runtime.log?.(
715
- `[slash] Merged ${merged.length} slash command(s) from ${seenPaths.length} source(s)`
761
+ `[slash] Merged ${merged.length} slash command(s) from ${existingPaths.length} source(s)`
716
762
  );
717
763
  }
718
764
  return merged;
@@ -1137,6 +1183,66 @@ function buildChannelContextForAgent(info, channelId) {
1137
1183
  `Shadow channel id: ${channelId}`
1138
1184
  ].join("\n");
1139
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
+ }
1140
1246
  async function processShadowMessage(params) {
1141
1247
  const {
1142
1248
  message,
@@ -1198,6 +1304,7 @@ async function processShadowMessage(params) {
1198
1304
  slashCommands
1199
1305
  });
1200
1306
  const slashCommandMatch = matchShadowSlashCommand(cleanBody, slashCommands);
1307
+ const slashCommandPassThrough = slashCommandMatch?.command.dispatch === "passthrough";
1201
1308
  if (slashCommandMatch) {
1202
1309
  runtime.log?.(
1203
1310
  `[slash] Matched /${slashCommandMatch.invokedName} -> /${slashCommandMatch.command.name}`
@@ -1220,17 +1327,22 @@ async function processShadowMessage(params) {
1220
1327
  });
1221
1328
  return;
1222
1329
  }
1223
- 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);
1224
1333
  const structuredMentions = getShadowMessageMentions(message);
1225
1334
  const mentionContext = formatShadowMentionsForAgent(structuredMentions);
1226
1335
  const serverInfo = channelServerMap.get(channelId);
1227
1336
  const channelLabel = serverInfo ? `#${serverInfo.channelName}` : `channel:${channelId}`;
1228
1337
  const conversationLabel = serverInfo ? `${serverInfo.serverName} ${channelLabel}` : peerId;
1229
- const messageBodyForAgent = interactiveResponseContext.text ? `${interactiveResponseContext.text}
1230
-
1231
- User message:
1232
- ${baseBodyForAgent}` : baseBodyForAgent;
1338
+ const messageBodyForAgent = interactiveResponseContext.text || baseBodyForAgent;
1233
1339
  const client = new ShadowClient2(account.serverUrl, account.token);
1340
+ const serverAppSkillsContext = await buildMentionedServerAppSkillsContext({
1341
+ mentions: structuredMentions,
1342
+ client,
1343
+ serverInfo,
1344
+ runtime
1345
+ });
1234
1346
  const viewerCommerceContext = await buildCommerceViewerContextForAgent({
1235
1347
  account,
1236
1348
  client,
@@ -1241,6 +1353,7 @@ ${baseBodyForAgent}` : baseBodyForAgent;
1241
1353
  buildCommerceContextForAgent(account),
1242
1354
  viewerCommerceContext,
1243
1355
  mentionContext,
1356
+ serverAppSkillsContext,
1244
1357
  messageBodyForAgent
1245
1358
  ].filter(Boolean).join("\n\n");
1246
1359
  const body = core.channel.reply.formatAgentEnvelope({
@@ -1252,12 +1365,15 @@ ${baseBodyForAgent}` : baseBodyForAgent;
1252
1365
  });
1253
1366
  const escapedBotUsername = botUsername.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1254
1367
  const mentionRegex = new RegExp(`@${escapedBotUsername}(?:\\s|$)`, "i");
1255
- 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);
1256
1369
  const ctxPayload = core.channel.reply.finalizeInboundContext({
1257
1370
  Body: body,
1258
1371
  BodyForAgent: bodyForAgent,
1259
1372
  RawBody: rawBody,
1260
- CommandBody: slashCommandMatch?.args ?? cleanBody,
1373
+ CommandBody: commandBody,
1374
+ BodyForCommands: commandBody,
1375
+ CommandAuthorized: isSenderCommandAuthorized(preflight.policyConfig, senderId),
1376
+ CommandSource: "text",
1261
1377
  From: `shadowob:user:${senderId}`,
1262
1378
  To: `shadowob:channel:${channelId}`,
1263
1379
  SessionKey: route.sessionKey,
@@ -1274,6 +1390,8 @@ ${baseBodyForAgent}` : baseBodyForAgent;
1274
1390
  ...mentionContextFields(structuredMentions),
1275
1391
  OriginatingChannel: "shadowob",
1276
1392
  OriginatingTo: `shadowob:channel:${channelId}`,
1393
+ NativeChannelId: channelId,
1394
+ ...ownerAllowFrom ? { OwnerAllowFrom: ownerAllowFrom } : {},
1277
1395
  ...serverInfo ? {
1278
1396
  ServerId: serverInfo.serverId,
1279
1397
  ServerSlug: serverInfo.serverSlug,
@@ -1922,9 +2040,7 @@ async function monitorShadowProvider(options) {
1922
2040
  channelName: ch.name
1923
2041
  });
1924
2042
  channelPolicies.set(ch.id, {
1925
- listen: true,
1926
- reply: true,
1927
- mentionOnly: false,
2043
+ ...ch.policy,
1928
2044
  config: { ...ch.policy.config, ...accessConfig2 }
1929
2045
  });
1930
2046
  if (!allChannelIds.includes(ch.id)) {
@@ -1945,9 +2061,6 @@ async function monitorShadowProvider(options) {
1945
2061
  if (channelServerMap.has(channelId)) continue;
1946
2062
  channelPolicies.set(channelId, {
1947
2063
  ...existing2,
1948
- listen: true,
1949
- reply: true,
1950
- mentionOnly: false,
1951
2064
  config: { ...existing2.config, ...accessConfig2 }
1952
2065
  });
1953
2066
  }
@@ -1958,23 +2071,24 @@ async function monitorShadowProvider(options) {
1958
2071
  })();
1959
2072
  return;
1960
2073
  }
1961
- const mentionOnly = false;
2074
+ const existing = channelPolicies.get(data.channelId);
2075
+ const mentionOnly = data.mentionOnly ?? existing?.mentionOnly ?? false;
2076
+ const reply = data.reply ?? existing?.reply ?? true;
1962
2077
  const accessConfig = buildAccessPolicyConfig(remoteConfig);
1963
2078
  runtime.log?.(
1964
- `[ws] Received agent:policy-changed for channel ${data.channelId}: mentionOnly=${mentionOnly}, reply=${data.reply}, config=${JSON.stringify(data.config ?? {})}`
2079
+ `[ws] Received agent:policy-changed for channel ${data.channelId}: mentionOnly=${mentionOnly}, reply=${reply}, config=${JSON.stringify(data.config ?? {})}`
1965
2080
  );
1966
- const existing = channelPolicies.get(data.channelId);
1967
2081
  if (existing) {
1968
2082
  channelPolicies.set(data.channelId, {
1969
2083
  ...existing,
1970
2084
  mentionOnly,
1971
- reply: true,
2085
+ reply,
1972
2086
  config: { ...existing.config, ...accessConfig, ...data.config ?? {} }
1973
2087
  });
1974
2088
  } else {
1975
2089
  channelPolicies.set(data.channelId, {
1976
2090
  listen: true,
1977
- reply: true,
2091
+ reply,
1978
2092
  mentionOnly,
1979
2093
  config: { ...accessConfig, ...data.config ?? {} }
1980
2094
  });
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  resolveOutboundMentions,
16
16
  setShadowRuntime,
17
17
  tryGetShadowRuntime
18
- } from "./chunk-ATPTVU3K.js";
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-PUXW5FI2.js");
963
+ const { monitorShadowProvider: monitorShadowProvider2 } = await import("./monitor-XDUBTELB.js");
963
964
  await monitorShadowProvider2({
964
965
  account,
965
966
  accountId,
@@ -5,7 +5,7 @@ import {
5
5
  normalizeShadowSlashCommands,
6
6
  resolveShadowAgentIdFromConfig,
7
7
  shouldCatchUpShadowMessage
8
- } from "./chunk-ATPTVU3K.js";
8
+ } from "./chunk-4G572ZLC.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.3-dev.281",
3
+ "version": "1.1.4",
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.3-dev.281"
61
+ "@shadowob/sdk": "1.1.4"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/node": "^22.15.21",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: shadowob
3
- description: "Shadow server CLI complete command-line interface for Shadow servers including servers, channels, DMs, workspace, shop, apps, agents, marketplace, OAuth, and more"
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,19 +178,27 @@ 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 download <file-id> [--output <path>]
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
@@ -216,7 +223,23 @@ shadowob shop wallet balance --json
216
223
  ## Apps
217
224
 
218
225
  ```bash
219
- # List apps
226
+ # Server App integrations
227
+ shadowob app list --server <server-id-or-slug> --json
228
+ shadowob app preview --server <server-id-or-slug> --manifest-url <manifest-url> --json
229
+ shadowob app discover --server <server-id-or-slug> --json
230
+ shadowob app inspect <app-key> --server <server-id-or-slug> --json
231
+ shadowob app skills <app-key> --server <server-id-or-slug>
232
+ shadowob app call <app-key> <command> --server <server-id-or-slug> --json-input '<raw-command-input-json>' --json
233
+ ```
234
+
235
+ For server App commands, use the `shadowob app` CLI path only. Do not use curl, fetch, raw HTTP
236
+ routes, or the JavaScript SDK to call server App commands. Pass the command input object directly
237
+ to `--json-input`, for example `{"title":"Example","priority":"high"}`; the CLI wraps the HTTP
238
+ request for you and binds Shadow OAuth identity, server membership, App grants, and command policy.
239
+ When a channel message mentions a server App, use the mentioned app key/server id directly.
240
+
241
+ ```bash
242
+ # Legacy workspace apps
220
243
  shadowob apps list <server-id> --json
221
244
 
222
245
  # Get app