@xmoxmo/bncr 0.6.4 → 0.6.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.
@@ -20,7 +20,7 @@ import {
20
20
  } from '../../openclaw/routing-runtime.ts';
21
21
  import type { BncrInboundApi, BncrInboundConfig, BncrRememberSessionRoute } from './contracts.ts';
22
22
  import { INBOUND_MEDIA_URL_MAX_BYTES, isHttpMediaUrl } from './media-url-download.ts';
23
- import { parseBncrNativeCommand } from './native-command.ts';
23
+ import { parseBncrNativeCommand, resolveBncrNativeCommandParseOptions } from './native-command.ts';
24
24
  import { loadInboundRemoteMedia } from './remote-media.ts';
25
25
 
26
26
  export type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
@@ -460,9 +460,13 @@ export async function prepareBncrInboundDispatch(args: {
460
460
  const senderIdForContext = parsed.userId || parsed.clientId || resolution.canonicalTo;
461
461
  const senderDisplayName = parsed.userName || resolution.canonicalTo;
462
462
  const isBncrNativeCommand =
463
- parseBncrNativeCommand(parsed.extracted.text, {
464
- allowBareWhoami: parsed.isAdmin !== true,
465
- }) !== null;
463
+ parseBncrNativeCommand(
464
+ parsed.extracted.text,
465
+ resolveBncrNativeCommandParseOptions({
466
+ isAdmin: parsed.isAdmin,
467
+ peerKind: parsed.peer.kind as 'direct' | 'group',
468
+ }),
469
+ ) !== null;
466
470
  const isOpenClawNativeCommand =
467
471
  !isBncrNativeCommand &&
468
472
  hasControlCommand(parsed.extracted.text, cfg as Parameters<typeof hasControlCommand>[1]);
@@ -176,6 +176,8 @@ export async function dispatchBncrInbound(params: {
176
176
  // A stop command must not wait behind the history serial chain or mutate
177
177
  // the accumulated conversation window. Deliver it immediately so the host
178
178
  // can abort the active run instead of queuing behind it.
179
+ const stopOwnerAllowFrom =
180
+ parsed.peer.kind === 'direct' && senderIdForContext ? [senderIdForContext] : ownerAllowFrom;
179
181
  const ctxPayload = await buildBncrInboundTurnContext({
180
182
  api,
181
183
  cfg,
@@ -185,7 +187,7 @@ export async function dispatchBncrInbound(params: {
185
187
  peer,
186
188
  senderIdForContext,
187
189
  senderDisplayName,
188
- ownerAllowFrom,
190
+ ownerAllowFrom: stopOwnerAllowFrom,
189
191
  bridgeSenderId,
190
192
  bridgeSenderName,
191
193
  historyLimit: resolvedHistoryLimit,
@@ -81,6 +81,7 @@ export function createNativeCommandTurnContext(args: {
81
81
  peer: ParsedInbound['peer'];
82
82
  resolvedRoute: { sessionKey: string; agentId: string; mainSessionKey?: string };
83
83
  sessionKey: string;
84
+ ownerAllowFrom?: string[];
84
85
  displayTo: string;
85
86
  originatingTo: string;
86
87
  senderIdForContext: string;
@@ -167,6 +168,9 @@ export function createNativeCommandTurnContext(args: {
167
168
  authorized: true,
168
169
  body: args.body,
169
170
  };
171
+ if (args.ownerAllowFrom?.length) {
172
+ ctx.OwnerAllowFrom = args.ownerAllowFrom;
173
+ }
170
174
  return ctx;
171
175
  });
172
176
  }
@@ -50,7 +50,74 @@ export type NativeSessionResetCommand = {
50
50
  text: string;
51
51
  };
52
52
 
53
- const BNCR_NATIVE_COMMANDS = new Set([
53
+ /**
54
+ * Commands that non-admin private-chat users may self-elevate for.
55
+ * When a non-admin direct-chat caller issues one of these commands,
56
+ * the plugin temporarily treats them as admin so the command follows
57
+ * the same code path as an admin caller. This whitelist is shared
58
+ * across permission checks so admin and non-admin logic stay in sync.
59
+ *
60
+ * `stop` stays in this whitelist for semantic consistency, but it is
61
+ * routed through the unified stop fast path in dispatch.ts rather than
62
+ * the native command handler, so it is never processed here.
63
+ */
64
+ export const BNCR_SELF_SERVICE_COMMANDS: ReadonlySet<string> = new Set([
65
+ 'whoami',
66
+ 'status',
67
+ 'verbose',
68
+ 'model',
69
+ 'new',
70
+ 'reset',
71
+ 'stop',
72
+ ]);
73
+
74
+ /**
75
+ * Synthetic command name used when an admin bare command is routed to the
76
+ * OpenClaw native parser. It must never be exposed to the user.
77
+ */
78
+ export const BNCR_OPENCLAW_NATIVE_COMMAND = '__openclaw_native__';
79
+
80
+ /**
81
+ * Detect if the raw text is a bare whitelist command that should be
82
+ * elevated to admin for OpenClaw native parsing (private non-admin)
83
+ * or mapped to /bncr (group non-admin).
84
+ */
85
+ export function isBncrWhitelistBareCommandText(rawBody: string): string | null {
86
+ const raw = String(rawBody || '')
87
+ .trim()
88
+ .toLowerCase();
89
+ if (!raw.startsWith('/')) return null;
90
+ const bare = raw.slice(1);
91
+ if (!bare) return null;
92
+ // Whitelist commands match the exact command name only. @bot suffixes are
93
+ // intentionally unsupported so bare commands stay consistent with /stop.
94
+ const commandName = bare.split(/\s+/, 1)[0];
95
+ if (!commandName) return null;
96
+ return BNCR_SELF_SERVICE_COMMANDS.has(commandName) ? commandName : null;
97
+ }
98
+
99
+ /**
100
+ * Resolve which bare slash commands bncr should treat as native commands.
101
+ *
102
+ * Admin callers never have whitelist restrictions: their bare commands go to
103
+ * the OpenClaw parser. Non-admin group callers map the whitelist onto /bncr
104
+ * native commands; non-admin private callers are elevated and routed to the
105
+ * OpenClaw parser instead (handled before this helper runs).
106
+ */
107
+ export function resolveBncrNativeCommandParseOptions(args: {
108
+ isAdmin: boolean;
109
+ peerKind: 'direct' | 'group';
110
+ }): ParseBncrNativeCommandOptions {
111
+ const isGroup = args.peerKind === 'group';
112
+ const isNonAdmin = args.isAdmin !== true;
113
+ return {
114
+ allowBareWhoami: isNonAdmin && isGroup,
115
+ allowBareStatus: isNonAdmin && isGroup,
116
+ allowBareSessionReset: isNonAdmin && isGroup,
117
+ };
118
+ }
119
+
120
+ export const BNCR_NATIVE_COMMANDS = new Set([
54
121
  'help',
55
122
  'new',
56
123
  'reset',
@@ -128,7 +195,9 @@ export function parseBncrUnsupportedDirectCommand(text: string): {
128
195
  const raw = String(text || '').trim();
129
196
  if (!raw.startsWith('/')) return null;
130
197
 
131
- const bareMatch = raw.match(/^\/([A-Za-z0-9_]+)(?:\s+.*)?$/);
198
+ // Hyphens are allowed in command names (e.g. /history-limit) so bare slash
199
+ // parsing matches the same command vocabulary as the /bncr subcommands.
200
+ const bareMatch = raw.match(/^\/([A-Za-z0-9_-]+)(?:\s+.*)?$/);
132
201
  if (!bareMatch) return null;
133
202
  const bareCommand = String(bareMatch[1] || '')
134
203
  .trim()
@@ -158,7 +227,7 @@ const BNCR_HELP_SECTIONS = [
158
227
  { label: '/bncr status', scopes: ['admin', 'direct', 'group'] },
159
228
  { label: '/bncr new', scopes: ['admin', 'direct'] },
160
229
  { label: '/bncr reset', scopes: ['admin', 'direct'] },
161
- { label: '/bncr verbose on|off|full', scopes: ['admin'] },
230
+ { label: '/bncr verbose on|off|full', scopes: ['admin', 'direct'] },
162
231
  ],
163
232
  },
164
233
  {
@@ -180,20 +249,20 @@ const BNCR_HELP_SECTIONS = [
180
249
  },
181
250
  {
182
251
  title: '📋 Conversation history',
183
- audience: 'admin',
252
+ audience: 'all',
184
253
  commands: [
185
- { label: '/bncr history-help', scopes: ['admin'] },
186
- { label: '/bncr history-limit [<number>|clear] [<SceneId>]', scopes: ['admin'] },
187
- { label: '/bncr history-force on|off|clear [<SceneId>]', scopes: ['admin'] },
254
+ { label: '/bncr history-help', scopes: ['admin', 'direct'] },
255
+ { label: '/bncr history-limit [<number>|clear] [<SceneId>]', scopes: ['admin', 'direct'] },
256
+ { label: '/bncr history-force on|off|clear [<SceneId>]', scopes: ['admin', 'direct'] },
188
257
  ],
189
258
  },
190
259
  {
191
260
  title: '🌐 Remote media',
192
- audience: 'admin',
261
+ audience: 'all',
193
262
  commands: [
194
263
  {
195
264
  label: '/bncr download-media on|off|clear|default on|off [<SceneId>]',
196
- scopes: ['admin'],
265
+ scopes: ['admin', 'direct'],
197
266
  },
198
267
  ],
199
268
  },
@@ -272,26 +341,31 @@ export function resolveBncrNativeSessionResetCommand(args: {
272
341
  }): NativeSessionResetCommand | null {
273
342
  if (args.command.command !== 'new' && args.command.command !== 'reset') return null;
274
343
  const reason = args.command.command === 'new' ? 'new' : 'reset';
275
- const scope = args.peerKind === 'group' ? 'this group chat' : 'this private chat';
276
344
  return {
277
345
  handled: true,
278
346
  reason,
279
- text:
280
- reason === 'new'
281
- ? `Started a new session for ${scope}.`
282
- : `Reset the current session for ${scope}.`,
347
+ text: reason === 'new' ? '✅ New session started.' : '✅ Session reset.',
283
348
  };
284
349
  }
285
350
 
286
351
  export function resolveBncrNativeVerboseCommand(
287
352
  command: NativeCommand,
353
+ currentLevel?: 'on' | 'off' | 'full',
288
354
  ): NativeVerboseCommand | null {
289
355
  if (command.command !== 'verbose') return null;
290
356
  const rawLevel = String(command.argsText || '')
291
357
  .trim()
292
358
  .toLowerCase();
293
359
  if (!rawLevel || rawLevel === 'status') {
294
- return { handled: true, text: 'Current verbose level is unchanged.' };
360
+ const label =
361
+ currentLevel === 'full'
362
+ ? 'full'
363
+ : currentLevel === 'off'
364
+ ? 'off'
365
+ : currentLevel === 'on'
366
+ ? 'on'
367
+ : 'default';
368
+ return { handled: true, text: `Current verbose level: ${label}` };
295
369
  }
296
370
  if (rawLevel === 'on')
297
371
  return { handled: true, verboseLevel: 'on', text: 'Verbose logging enabled.' };
@@ -6,6 +6,7 @@ import {
6
6
  resolveBncrPinnedMainDmOwnerFromAllowlist,
7
7
  } from '../../openclaw/inbound-session-runtime.ts';
8
8
  import { dispatchOpenClawReplyWithBufferedBlockDispatcher } from '../../openclaw/reply-runtime.ts';
9
+ import { mergeBncrOwnerAllowFromIntoConfig } from './command-owner.ts';
9
10
  import type {
10
11
  BncrEnqueueFromReply,
11
12
  BncrInboundApi,
@@ -18,7 +19,11 @@ import type {
18
19
  ParsedInbound,
19
20
  } from './dispatch-prep.ts';
20
21
  import { buildBncrInboundRecordUpdateLastRoute } from './last-route.ts';
21
- import { isBncrStopCommandText, parseBncrNativeCommand } from './native-command.ts';
22
+ import {
23
+ isBncrStopCommandText,
24
+ parseBncrNativeCommand,
25
+ resolveBncrNativeCommandParseOptions,
26
+ } from './native-command.ts';
22
27
  import { buildBncrReplyConfig } from './reply-config.ts';
23
28
  import { runBncrReplyDispatchSerial } from './reply-dispatch-serial.ts';
24
29
  import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
@@ -29,32 +34,6 @@ import {
29
34
  } from './session-label.ts';
30
35
  import { createBncrSessionMetaTaskBarrier } from './session-meta-task.ts';
31
36
 
32
- function mergeBncrCommandOwnerAllowFrom(args: {
33
- cfg: BncrInboundConfig;
34
- parsed: ParsedInbound;
35
- isBncrNativeCommand: boolean;
36
- senderIdForContext: string;
37
- }) {
38
- const { cfg, parsed, isBncrNativeCommand, senderIdForContext } = args;
39
- if (parsed.isAdmin !== true || isBncrNativeCommand) return cfg;
40
- const senderId = String(senderIdForContext || '').trim();
41
- if (!senderId) return cfg;
42
-
43
- const currentCommands = (cfg.commands || {}) as { ownerAllowFrom?: string[] };
44
- const currentOwnerAllowFrom = Array.isArray(currentCommands.ownerAllowFrom)
45
- ? currentCommands.ownerAllowFrom
46
- : [];
47
- if (currentOwnerAllowFrom.includes(senderId)) return cfg;
48
-
49
- return {
50
- ...cfg,
51
- commands: {
52
- ...currentCommands,
53
- ownerAllowFrom: [...currentOwnerAllowFrom, senderId],
54
- },
55
- } satisfies BncrInboundConfig;
56
- }
57
-
58
37
  function sleep(ms: number) {
59
38
  return new Promise((resolve) => setTimeout(resolve, ms));
60
39
  }
@@ -180,15 +159,17 @@ export async function runBncrInboundReplyDispatch(args: {
180
159
  pinnedMainDmOwner,
181
160
  });
182
161
  const isBncrNativeCommand =
183
- parseBncrNativeCommand(rawBody, {
184
- allowBareWhoami: parsed.isAdmin !== true,
185
- }) !== null;
186
- const commandDispatchCfg = mergeBncrCommandOwnerAllowFrom({
187
- cfg,
188
- parsed,
189
- isBncrNativeCommand,
190
- senderIdForContext,
191
- });
162
+ parseBncrNativeCommand(
163
+ rawBody,
164
+ resolveBncrNativeCommandParseOptions({
165
+ isAdmin: parsed.isAdmin,
166
+ peerKind: parsed.peer.kind as 'direct' | 'group',
167
+ }),
168
+ ) !== null;
169
+ const commandDispatchCfg =
170
+ parsed.isAdmin === true && !isBncrNativeCommand
171
+ ? mergeBncrOwnerAllowFromIntoConfig({ cfg, senderIdForContext })
172
+ : cfg;
192
173
 
193
174
  if (!shouldDispatch) {
194
175
  await wrapBncrInboundRecordSessionLabelCorrection({
@@ -435,13 +435,41 @@ export function executeSceneAdminCommand(args: {
435
435
  defaultAdminAgentId: string;
436
436
  defaultPublicAgentId: string;
437
437
  now: () => number;
438
+ /**
439
+ * When true, non-admin callers in direct chat are allowed to execute
440
+ * self-admin commands (history-*, download-media) scoped to
441
+ * their own session only. The flag is set by the caller after an
442
+ * explicit allow-list check – it must NOT be granted unconditionally
443
+ * for direct chat.
444
+ */
445
+ allowNonAdminSelfAdmin?: boolean;
438
446
  }): { ok: true; text: string } | { ok: false; text: string } {
439
- const { parsed, command, sceneRegistry, defaultAdminAgentId, defaultPublicAgentId, now } = args;
440
-
441
- if (!parsed.isAdmin) {
447
+ const {
448
+ parsed,
449
+ command,
450
+ sceneRegistry,
451
+ defaultAdminAgentId,
452
+ defaultPublicAgentId,
453
+ now,
454
+ allowNonAdminSelfAdmin,
455
+ } = args;
456
+
457
+ // Admin always passes; non-admin only passes when the caller explicitly
458
+ // opted in via allowNonAdminSelfAdmin (self-admin commands in direct chat).
459
+ if (!parsed.isAdmin && !allowNonAdminSelfAdmin) {
442
460
  return { ok: false, text: 'Admin permission required.' };
443
461
  }
444
462
 
463
+ // Non-admin self-admin: enforce that sceneId targets only the caller's own session.
464
+ if (allowNonAdminSelfAdmin && !parsed.isAdmin) {
465
+ const currentSceneKey = resolveCurrentSceneKey(parsed);
466
+ const cmdSceneKey =
467
+ 'sceneKey' in command ? (command as { sceneKey?: string }).sceneKey : undefined;
468
+ if (cmdSceneKey && currentSceneKey && cmdSceneKey !== currentSceneKey) {
469
+ return { ok: false, text: 'You can only manage your own private chat session.' };
470
+ }
471
+ }
472
+
445
473
  if (command.kind === 'history-help') {
446
474
  return { ok: true, text: HISTORY_HELP_TEXT };
447
475
  }
@@ -1,6 +1,7 @@
1
1
  import { recordInboundSession as sdkRecordInboundSession } from 'openclaw/plugin-sdk/conversation-runtime';
2
2
  import { resolvePinnedMainDmOwnerFromAllowlist as sdkResolvePinnedMainDmOwnerFromAllowlist } from 'openclaw/plugin-sdk/security-runtime';
3
3
  import {
4
+ getSessionEntry as sdkGetSessionEntry,
4
5
  recordSessionMetaFromInbound as sdkRecordSessionMetaFromInbound,
5
6
  resolveStorePath as sdkResolveStorePath,
6
7
  updateSessionStoreEntry as sdkUpdateSessionStoreEntry,
@@ -14,6 +15,7 @@ type ResolveStorePathFn = (
14
15
  type RecordInboundSessionFn = typeof sdkRecordInboundSession;
15
16
  type RecordSessionMetaFromInboundFn = typeof sdkRecordSessionMetaFromInbound;
16
17
  type UpdateSessionStoreEntryFn = typeof sdkUpdateSessionStoreEntry;
18
+ type GetSessionEntryFn = typeof sdkGetSessionEntry;
17
19
  type ReadSessionUpdatedAtFn = (params: { storePath: string; sessionKey: string }) => unknown;
18
20
  type ResolvePinnedMainDmOwnerFromAllowlistFn = typeof sdkResolvePinnedMainDmOwnerFromAllowlist;
19
21
 
@@ -22,6 +24,7 @@ type BncrInboundSessionRuntime = {
22
24
  recordInboundSession: RecordInboundSessionFn;
23
25
  recordSessionMetaFromInbound: RecordSessionMetaFromInboundFn;
24
26
  updateSessionStoreEntry: UpdateSessionStoreEntryFn;
27
+ getSessionEntry: GetSessionEntryFn;
25
28
  readSessionUpdatedAt?: ReadSessionUpdatedAtFn;
26
29
  resolvePinnedMainDmOwnerFromAllowlist: ResolvePinnedMainDmOwnerFromAllowlistFn;
27
30
  };
@@ -36,6 +39,7 @@ function resolveRuntime(): BncrInboundSessionRuntime {
36
39
  testRuntimeOverride?.recordSessionMetaFromInbound ?? sdkRecordSessionMetaFromInbound,
37
40
  updateSessionStoreEntry:
38
41
  testRuntimeOverride?.updateSessionStoreEntry ?? sdkUpdateSessionStoreEntry,
42
+ getSessionEntry: testRuntimeOverride?.getSessionEntry ?? sdkGetSessionEntry,
39
43
  readSessionUpdatedAt: testRuntimeOverride?.readSessionUpdatedAt,
40
44
  resolvePinnedMainDmOwnerFromAllowlist:
41
45
  testRuntimeOverride?.resolvePinnedMainDmOwnerFromAllowlist ??
@@ -81,6 +85,19 @@ export function readBncrSessionUpdatedAt(
81
85
  return readSessionUpdatedAt(params);
82
86
  }
83
87
 
88
+ export async function readBncrSessionEntry(params: {
89
+ storePath: string;
90
+ sessionKey: string;
91
+ }): Promise<Record<string, unknown> | undefined> {
92
+ const runtime = resolveRuntime();
93
+ try {
94
+ const entry = await runtime.getSessionEntry(params);
95
+ return entry && typeof entry === 'object' ? (entry as Record<string, unknown>) : undefined;
96
+ } catch {
97
+ return undefined;
98
+ }
99
+ }
100
+
84
101
  export function resolveBncrPinnedMainDmOwnerFromAllowlist(
85
102
  params: Parameters<ResolvePinnedMainDmOwnerFromAllowlistFn>[0],
86
103
  ): ReturnType<ResolvePinnedMainDmOwnerFromAllowlistFn> {
@@ -0,0 +1,77 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+
5
+ type SessionResetResult = { ok?: boolean } | undefined;
6
+ type SessionResetParams = {
7
+ key: string;
8
+ reason: 'new' | 'reset';
9
+ agentId?: string;
10
+ commandSource?: string;
11
+ };
12
+ type SessionResetFn = (params: SessionResetParams) => Promise<SessionResetResult>;
13
+
14
+ let testOverride: SessionResetFn | null = null;
15
+ let resolvedSessionRuntimeUrlCache: string | null = null;
16
+
17
+ export function setBncrSessionResetRuntimeForTest(fn: SessionResetFn | null): () => void {
18
+ const previous = testOverride;
19
+ testOverride = fn;
20
+ return () => {
21
+ testOverride = previous;
22
+ };
23
+ }
24
+
25
+ function resolveOpenClawSessionsRuntimeUrl(): string {
26
+ if (resolvedSessionRuntimeUrlCache) return resolvedSessionRuntimeUrlCache;
27
+ let current = dirname(fileURLToPath(import.meta.url));
28
+ let pluginRoot: string | null = null;
29
+ while (true) {
30
+ const pkgPath = join(current, 'package.json');
31
+ try {
32
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { name?: string };
33
+ if (pkg.name === '@xmoxmo/bncr') {
34
+ pluginRoot = current;
35
+ break;
36
+ }
37
+ } catch {
38
+ // Keep walking up toward the plugin root.
39
+ }
40
+ const parent = dirname(current);
41
+ if (parent === current) break;
42
+ current = parent;
43
+ }
44
+ if (!pluginRoot) {
45
+ throw new Error('bncr plugin root not found for session reset runtime');
46
+ }
47
+
48
+ // Try candidate paths for the sessions runtime re-export file.
49
+ // The hash-stamped bundle name may change across OpenClaw releases, so we
50
+ // first try the stable re-export entry and fall back to the known hash.
51
+ const candidates = [
52
+ join(pluginRoot, 'node_modules', 'openclaw', 'dist', 'sessions.runtime.js'),
53
+ join(pluginRoot, 'node_modules', 'openclaw', 'dist', 'sessions.runtime-BQwwkuFq.js'),
54
+ ];
55
+ for (const target of candidates) {
56
+ if (existsSync(target)) {
57
+ resolvedSessionRuntimeUrlCache = pathToFileURL(target).href;
58
+ return resolvedSessionRuntimeUrlCache;
59
+ }
60
+ }
61
+ throw new Error(
62
+ `OpenClaw sessions runtime not found under ${join(pluginRoot, 'node_modules', 'openclaw', 'dist')}`,
63
+ );
64
+ }
65
+
66
+ export async function performBncrGatewaySessionReset(
67
+ params: SessionResetParams,
68
+ ): Promise<SessionResetResult> {
69
+ if (testOverride) return testOverride(params);
70
+ const mod = (await import(resolveOpenClawSessionsRuntimeUrl())) as {
71
+ performGatewaySessionReset?: SessionResetFn;
72
+ };
73
+ if (typeof mod.performGatewaySessionReset !== 'function') {
74
+ throw new Error('OpenClaw performGatewaySessionReset is unavailable');
75
+ }
76
+ return mod.performGatewaySessionReset(params);
77
+ }
@@ -1,7 +1,10 @@
1
1
  import { hasControlCommand } from 'openclaw/plugin-sdk/command-auth';
2
2
  import { resolveAccount } from '../core/accounts.ts';
3
3
  import { checkBncrMessageGate } from '../messaging/inbound/gate.ts';
4
- import { parseBncrNativeCommand } from '../messaging/inbound/native-command.ts';
4
+ import {
5
+ parseBncrNativeCommand,
6
+ resolveBncrNativeCommandParseOptions,
7
+ } from '../messaging/inbound/native-command.ts';
5
8
  import type { parseBncrInboundParams } from '../messaging/inbound/parse.ts';
6
9
  import type { OpenClawResolvedAgentRoute } from '../openclaw/routing-runtime.ts';
7
10
  import type { buildInboundResponsePayload } from './channel-inbound-helpers.ts';
@@ -21,9 +24,13 @@ function resolveDispatchBy(args: {
21
24
  if (!admission.allowed) return 'denied';
22
25
 
23
26
  const isBncrNativeCommand =
24
- parseBncrNativeCommand(parsed.extracted.text, {
25
- allowBareWhoami: parsed.isAdmin !== true,
26
- }) !== null;
27
+ parseBncrNativeCommand(
28
+ parsed.extracted.text,
29
+ resolveBncrNativeCommandParseOptions({
30
+ isAdmin: parsed.isAdmin,
31
+ peerKind: parsed.peer.kind as 'direct' | 'group',
32
+ }),
33
+ ) !== null;
27
34
  const isAdminOpenClawNativeCommand =
28
35
  parsed.isAdmin === true &&
29
36
  !isBncrNativeCommand &&
@@ -49,9 +56,13 @@ function shouldDispatchForScene(args: {
49
56
  if (!admission.allowed) return false;
50
57
 
51
58
  const isBncrNativeCommand =
52
- parseBncrNativeCommand(parsed.extracted.text, {
53
- allowBareWhoami: parsed.isAdmin !== true,
54
- }) !== null;
59
+ parseBncrNativeCommand(
60
+ parsed.extracted.text,
61
+ resolveBncrNativeCommandParseOptions({
62
+ isAdmin: parsed.isAdmin,
63
+ peerKind: parsed.peer.kind as 'direct' | 'group',
64
+ }),
65
+ ) !== null;
55
66
  const isAdminOpenClawNativeCommand =
56
67
  parsed.isAdmin === true &&
57
68
  !isBncrNativeCommand &&