@hywkp/test-openclaw-sider 1.0.2

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.
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # Chrome Channel for OpenClaw in Sider
2
+
3
+ The official Chrome channel plugin for connecting OpenClaw to the Sider Chrome extension.
4
+
5
+ Once connected, you can control OpenClaw directly from the Sider Chrome sidebar.
6
+
7
+ ## 【【【 Quick install from the terminal 】】】
8
+
9
+ Run in Terminal:
10
+
11
+ ================================
12
+ ```bash
13
+ npx -y @sider-ai/chrome-openclaw-sider-cli install
14
+ ```
15
+ ================================
16
+
17
+ You can also paste this command into the OpenClaw chat box and ask OpenClaw to install it for you. This will install or update the plugin and automatically start terminal pairing.
18
+
19
+ ## 【【【 Manual installation 】】】
20
+
21
+ If the quick install does not work, follow these steps:
22
+
23
+ ### 【 1. Install or update the plugin 】
24
+
25
+ Install in Terminal:
26
+
27
+ ================================
28
+ ```bash
29
+ openclaw plugins install clawhub:chrome-openclaw-sider
30
+ ```
31
+ ================================
32
+
33
+ If the plugin is already installed, update it with:
34
+
35
+ ================================
36
+ ```bash
37
+ openclaw plugins update clawhub:chrome-openclaw-sider
38
+ ```
39
+ ================================
40
+
41
+ You can run these commands directly in your terminal, or paste them into the OpenClaw chat box and ask OpenClaw to execute them.
42
+
43
+ ### 【 2. Pair the plugin with Sider’s Chrome extension using a pairing code 】
44
+
45
+ Run in Terminal:
46
+
47
+ ================================
48
+ ```bash
49
+ openclaw channels login --channel chrome-openclaw-sider
50
+ ```
51
+ ================================
52
+
53
+ Your terminal will display a short pairing code. Enter this code in the Sider browser extension.
54
+
55
+ In the Sider Chrome extension, open the **Claw** widget from the right column of the extension and enter the pairing code there. This will connect the Sider Chrome extension to your OpenClaw instance.
56
+
57
+ ### 【 3. Restart the gateway 】
58
+
59
+ After entering the pairing code, restart the gateway in Terminal:
60
+
61
+ ================================
62
+ ```bash
63
+ openclaw gateway restart
64
+ ```
65
+ ================================
@@ -0,0 +1,106 @@
1
+ # chrome-openclaw-sider
2
+
3
+ OpenClaw 官方 Sider 渠道插件,用于在 Sider 中连接 OpenClaw。
4
+
5
+ 它支持 OpenClaw 标准登录配对流程、一次性 setup token 换取长期 token,以及直接写入 relay token 这三种接入方式。
6
+
7
+ ## 兼容性
8
+
9
+ - `openclaw >= 2026.3.22`
10
+
11
+ ## 推荐安装方式
12
+
13
+ 大多数用户建议直接使用配套 CLI:
14
+
15
+ ```bash
16
+ npx -y @sider-ai/chrome-openclaw-sider-cli install
17
+ ```
18
+
19
+ 它会自动安装或更新插件,并立即进入终端配对流程。
20
+
21
+ ## 手动安装
22
+
23
+ 安装插件:
24
+
25
+ ```bash
26
+ openclaw plugins install chrome-openclaw-sider
27
+ ```
28
+
29
+ 如果本地已经安装过,可改为更新:
30
+
31
+ ```bash
32
+ openclaw plugins update chrome-openclaw-sider
33
+ ```
34
+
35
+ 发起配对:
36
+
37
+ ```bash
38
+ openclaw channels login --channel chrome-openclaw-sider
39
+ ```
40
+
41
+ 终端会显示一条短期 pairing code。用户在 Sider 浏览器扩展中输入这条 code 后,插件会把长期 `token` 自动写回 `channels.chrome-openclaw-sider`。
42
+
43
+ ## 配置方式
44
+
45
+ 插件支持三种常见初始化方式。
46
+
47
+ ### 终端配对
48
+
49
+ 直接使用 OpenClaw 自带登录流程:
50
+
51
+ ```bash
52
+ openclaw channels login --channel chrome-openclaw-sider
53
+ ```
54
+
55
+ ### Setup Token
56
+
57
+ 把一次性 token 写入 `channels.chrome-openclaw-sider.setupToken`:
58
+
59
+ ```json
60
+ {
61
+ "channels": {
62
+ "chrome-openclaw-sider": {
63
+ "enabled": true,
64
+ "setupToken": "<one-time-token>"
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ 插件启动后会自动把 `setupToken` 换成长期 `token`,写回配置,并删除 `setupToken`。
71
+
72
+ ### 直接写入 Relay Token
73
+
74
+ 直接写入长期 relay token:
75
+
76
+ ```json
77
+ {
78
+ "channels": {
79
+ "chrome-openclaw-sider": {
80
+ "enabled": true,
81
+ "token": "<relay-token>"
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ 命名账号可写在 `channels.chrome-openclaw-sider.accounts.<id>` 下。
88
+
89
+ 如果是手动改配置,完成后需要重启 gateway:
90
+
91
+ ```bash
92
+ openclaw gateway restart
93
+ ```
94
+
95
+ ## 运行时选项
96
+
97
+ - `SIDER_BASE_URL`:覆盖默认 API base(`https://selfclaw.apps.wisebox.ai`)
98
+ - `SIDER_SETUP_TOKEN`:默认账号的 setup token 环境变量 fallback
99
+ - `SIDER_SETUP_TOKEN_<ACCOUNT_ID>`:命名账号的 setup token 环境变量 fallback
100
+ - `channels.chrome-openclaw-sider.relayId`:可选自定义 relay ID,默认是 `openclaw-<accountId>`
101
+ - `channels.chrome-openclaw-sider.subscribeSessionIds`:可选的入站 session 过滤列表
102
+
103
+ ## 说明
104
+
105
+ - 同一账号下不要混用 `setupToken` 和 `token`。
106
+ - 对大多数用户来说,终端配对是最简单的初始化方式。
package/index.ts ADDED
@@ -0,0 +1,80 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/plugin-entry";
3
+ import { listSiderAccountIds, readSiderAccountConfig } from "./src/account.js";
4
+ import { createSiderAuthSetupService, setSiderAuthRuntime } from "./src/auth.js";
5
+ import {
6
+ emitSiderToolHookEvent,
7
+ recordSiderLlmOutputUsage,
8
+ recordSiderPersistedAgentMessage,
9
+ siderPlugin,
10
+ setSiderRuntime,
11
+ } from "./src/channel.js";
12
+ import { SIDER_PLUGIN_DESCRIPTION, SIDER_PLUGIN_ID, SIDER_PLUGIN_NAME } from "./src/config.js";
13
+ import { registerSiderRemoteBrowserSupport } from "./src/remote-browser-support.js";
14
+
15
+ const plugin = {
16
+ id: SIDER_PLUGIN_ID,
17
+ name: SIDER_PLUGIN_NAME,
18
+ description: SIDER_PLUGIN_DESCRIPTION,
19
+ configSchema: emptyPluginConfigSchema(),
20
+ register(api: OpenClawPluginApi) {
21
+ setSiderRuntime(api.runtime);
22
+ setSiderAuthRuntime(api.runtime);
23
+ api.registerChannel({ plugin: siderPlugin });
24
+ api.registerService(
25
+ createSiderAuthSetupService({
26
+ listAccountIds: listSiderAccountIds,
27
+ getAccountSetupConfig: (cfg, accountId) => {
28
+ const account = readSiderAccountConfig(cfg, accountId);
29
+ return {
30
+ enabled: account.enabled,
31
+ setupToken: account.setupToken?.trim() || undefined,
32
+ token: account.token?.trim() || undefined,
33
+ };
34
+ },
35
+ }),
36
+ );
37
+ registerSiderRemoteBrowserSupport(api);
38
+ api.on("before_tool_call", async (event, ctx) => {
39
+ await emitSiderToolHookEvent({
40
+ sessionKey: ctx.sessionKey,
41
+ phase: "start",
42
+ toolName: event.toolName ?? ctx.toolName,
43
+ toolCallId: event.toolCallId ?? ctx.toolCallId,
44
+ runId: event.runId ?? ctx.runId,
45
+ params: event.params,
46
+ });
47
+ });
48
+ api.on("after_tool_call", async (event, ctx) => {
49
+ await emitSiderToolHookEvent({
50
+ sessionKey: ctx.sessionKey,
51
+ phase: event.error ? "error" : "end",
52
+ toolName: event.toolName ?? ctx.toolName,
53
+ toolCallId: event.toolCallId ?? ctx.toolCallId,
54
+ runId: event.runId ?? ctx.runId,
55
+ params: event.params,
56
+ result: event.result,
57
+ error: event.error,
58
+ durationMs: event.durationMs,
59
+ });
60
+ });
61
+ api.on("before_message_write", (event, ctx) => {
62
+ recordSiderPersistedAgentMessage({
63
+ sessionKey: ctx.sessionKey,
64
+ message: event.message,
65
+ });
66
+ });
67
+ api.on("llm_output", (event, ctx) => {
68
+ recordSiderLlmOutputUsage({
69
+ sessionKey: ctx.sessionKey,
70
+ runId: event.runId,
71
+ provider: event.provider,
72
+ model: event.model,
73
+ usage: event.usage,
74
+ lastAssistant: event.lastAssistant,
75
+ });
76
+ });
77
+ },
78
+ };
79
+
80
+ export default plugin;
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "test-openclaw-sider",
3
+ "channels": ["test-openclaw-sider"],
4
+ "skills": [],
5
+ "configSchema": {
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {}
9
+ }
10
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@hywkp/test-openclaw-sider",
3
+ "private": false,
4
+ "version": "1.0.2",
5
+ "description": "Official Chrome channel plugin for connecting OpenClaw in Sider Chrome extension",
6
+ "type": "module",
7
+ "files": [
8
+ "src/",
9
+ "!src/**/*.test.ts",
10
+ "!src/**/node_modules/",
11
+ "index.ts",
12
+ "setup-entry.ts",
13
+ "README.md",
14
+ "README.zh_CN.md",
15
+ "openclaw.plugin.json"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsup --config tsup.config.ts",
19
+ "typecheck": "tsc -p tsconfig.build.json --noEmit"
20
+ },
21
+ "dependencies": {
22
+ },
23
+ "peerDependencies": {
24
+ "openclaw": ">=2026.3.22"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.19.13",
28
+ "tsup": "^8.5.1",
29
+ "typescript": "^5.9.3"
30
+ },
31
+ "openclaw": {
32
+ "extensions": [
33
+ "./index.ts"
34
+ ],
35
+ "compat": {
36
+ "pluginApi": ">=2026.3.22"
37
+ },
38
+ "build": {
39
+ "openclawVersion": "2026.3.22"
40
+ },
41
+ "setupEntry": "./setup-entry.ts",
42
+ "channel": {
43
+ "id": "test-openclaw-sider",
44
+ "label": "Test OpenClaw Sider",
45
+ "selectionLabel": "Test OpenClaw Sider"
46
+ },
47
+ "install": {
48
+ "minHostVersion": ">=2026.3.22"
49
+ }
50
+ }
51
+ }
package/setup-entry.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
2
+ import { siderPlugin } from "./src/channel.js";
3
+
4
+ export { siderPlugin } from "./src/channel.js";
5
+
6
+ export default defineSetupPluginEntry(siderPlugin);
package/src/account.ts ADDED
@@ -0,0 +1,350 @@
1
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
+ import {
3
+ createStandardChannelSetupStatus,
4
+ setSetupChannelEnabled,
5
+ type ChannelSetupWizard,
6
+ type OpenClawConfig,
7
+ } from "openclaw/plugin-sdk/setup";
8
+ import {
9
+ ensureSiderAccountSetup,
10
+ isSiderAccountSetupPending,
11
+ resolveSiderBaseUrl,
12
+ resolveSiderSetupToken,
13
+ type SiderSetupConfigSnapshot,
14
+ } from "./auth.js";
15
+ import { SIDER_CHANNEL_ID, SIDER_CHANNEL_LABEL } from "./config.js";
16
+ import {
17
+ applySiderSetupAccountConfig,
18
+ createSiderPairingPendingUpdateReporter,
19
+ formatSiderPairingInstructions,
20
+ getSiderSetupChannelId,
21
+ requestSiderPairing,
22
+ SiderPairingExpiredError,
23
+ waitForSiderPairing,
24
+ } from "./setup-core.js";
25
+
26
+ const DEFAULT_CONNECT_TIMEOUT_MS = 8_000;
27
+ const DEFAULT_SEND_TIMEOUT_MS = 12_000;
28
+ const DEFAULT_RECONNECT_DELAY_MS = 2_000;
29
+
30
+ export type SiderAccountConfig = {
31
+ enabled?: boolean;
32
+ name?: string;
33
+ setupToken?: string;
34
+ sessionId?: string;
35
+ subscribeSessionIds?: string[];
36
+ relayId?: string;
37
+ token?: string;
38
+ defaultTo?: string;
39
+ connectTimeoutMs?: number;
40
+ sendTimeoutMs?: number;
41
+ reconnectDelayMs?: number;
42
+ };
43
+
44
+ type SiderChannelConfig = SiderAccountConfig & {
45
+ accounts?: Record<string, SiderAccountConfig>;
46
+ };
47
+
48
+ export type ResolvedSiderAccount = {
49
+ accountId: string;
50
+ name: string;
51
+ enabled: boolean;
52
+ gatewayUrl: string;
53
+ sessionId?: string;
54
+ subscribeSessionIds?: string[];
55
+ relayId: string;
56
+ token?: string;
57
+ defaultTo?: string;
58
+ connectTimeoutMs: number;
59
+ sendTimeoutMs: number;
60
+ reconnectDelayMs: number;
61
+ configured: boolean;
62
+ config: SiderAccountConfig;
63
+ };
64
+
65
+ const siderEphemeralRelayIds = new Map<string, string>();
66
+ const setupChannel = getSiderSetupChannelId();
67
+
68
+ function resolveEphemeralRelayId(accountId: string): string {
69
+ const existing = siderEphemeralRelayIds.get(accountId);
70
+ if (existing) {
71
+ return existing;
72
+ }
73
+ const created = `openclaw-${accountId}-${crypto.randomUUID()}`;
74
+ siderEphemeralRelayIds.set(accountId, created);
75
+ return created;
76
+ }
77
+
78
+ function normalizeTimeout(raw: unknown, fallback: number): number {
79
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw <= 0) {
80
+ return fallback;
81
+ }
82
+ return Math.floor(raw);
83
+ }
84
+
85
+ function normalizeSessionIdList(raw: unknown): string[] | undefined {
86
+ if (Array.isArray(raw)) {
87
+ const values = Array.from(
88
+ new Set(raw.filter((item): item is string => typeof item === "string").map((item) => item.trim()).filter(Boolean)),
89
+ );
90
+ return values.length > 0 ? values : undefined;
91
+ }
92
+ if (typeof raw === "string" && raw.trim()) {
93
+ return [raw.trim()];
94
+ }
95
+ return undefined;
96
+ }
97
+
98
+ function getSiderConfig(cfg: OpenClawConfig): SiderChannelConfig {
99
+ return (cfg.channels?.[SIDER_CHANNEL_ID] ?? {}) as SiderChannelConfig;
100
+ }
101
+
102
+ export function readSiderAccountConfig(
103
+ cfg: OpenClawConfig,
104
+ accountId?: string | null,
105
+ ): SiderSetupConfigSnapshot & SiderAccountConfig {
106
+ const id = normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID;
107
+ const channelCfg = getSiderConfig(cfg);
108
+ return id === DEFAULT_ACCOUNT_ID ? channelCfg : (channelCfg.accounts?.[id] ?? {});
109
+ }
110
+
111
+ export function resolveSiderAccount(
112
+ cfg: OpenClawConfig,
113
+ accountId?: string | null,
114
+ ): ResolvedSiderAccount {
115
+ const id = normalizeAccountId(accountId) ?? DEFAULT_ACCOUNT_ID;
116
+ const accountCfg = readSiderAccountConfig(cfg, id);
117
+ const gatewayUrl = resolveSiderBaseUrl();
118
+ const sessionId = accountCfg.sessionId?.trim() || undefined;
119
+ const subscribeSessionIds = normalizeSessionIdList(accountCfg.subscribeSessionIds);
120
+ const relayId = accountCfg.relayId?.trim() || resolveEphemeralRelayId(id);
121
+ const token = accountCfg.token?.trim() || undefined;
122
+ const defaultTo = accountCfg.defaultTo?.trim() || (sessionId ? `session:${sessionId}` : undefined);
123
+
124
+ return {
125
+ accountId: id,
126
+ name: accountCfg.name?.trim() || id,
127
+ enabled: accountCfg.enabled !== false,
128
+ gatewayUrl,
129
+ sessionId,
130
+ subscribeSessionIds,
131
+ relayId,
132
+ token,
133
+ defaultTo,
134
+ connectTimeoutMs: normalizeTimeout(accountCfg.connectTimeoutMs, DEFAULT_CONNECT_TIMEOUT_MS),
135
+ sendTimeoutMs: normalizeTimeout(accountCfg.sendTimeoutMs, DEFAULT_SEND_TIMEOUT_MS),
136
+ reconnectDelayMs: normalizeTimeout(accountCfg.reconnectDelayMs, DEFAULT_RECONNECT_DELAY_MS),
137
+ configured: Boolean(token && relayId),
138
+ config: accountCfg,
139
+ };
140
+ }
141
+
142
+ export function listSiderAccountIds(cfg: OpenClawConfig): string[] {
143
+ const channelCfg = getSiderConfig(cfg);
144
+ const ids = new Set<string>();
145
+ if (
146
+ channelCfg.enabled !== undefined ||
147
+ channelCfg.setupToken ||
148
+ channelCfg.token ||
149
+ channelCfg.sessionId ||
150
+ channelCfg.relayId
151
+ ) {
152
+ ids.add(DEFAULT_ACCOUNT_ID);
153
+ }
154
+ for (const id of Object.keys(channelCfg.accounts ?? {})) {
155
+ ids.add(id);
156
+ }
157
+ return ids.size > 0 ? Array.from(ids) : [DEFAULT_ACCOUNT_ID];
158
+ }
159
+
160
+ function isSiderConfiguredForSetup(cfg: OpenClawConfig): boolean {
161
+ return listSiderAccountIds(cfg).some((accountId) => {
162
+ const account = resolveSiderAccount(cfg, accountId);
163
+ if (account.configured) {
164
+ return true;
165
+ }
166
+ return Boolean(resolveSiderSetupToken(accountId, readSiderAccountConfig(cfg, accountId)));
167
+ });
168
+ }
169
+
170
+ export const siderSetupWizard: ChannelSetupWizard = {
171
+ channel: setupChannel,
172
+ status: createStandardChannelSetupStatus({
173
+ channelLabel: SIDER_CHANNEL_LABEL,
174
+ configuredLabel: "configured",
175
+ unconfiguredLabel: "needs pairing",
176
+ configuredHint: "configured",
177
+ unconfiguredHint: "needs pairing",
178
+ configuredScore: 1,
179
+ unconfiguredScore: 0,
180
+ includeStatusLine: true,
181
+ resolveConfigured: ({ cfg }) => isSiderConfiguredForSetup(cfg),
182
+ }),
183
+ credentials: [],
184
+ finalize: async ({ cfg, accountId, prompter }) => {
185
+ for (;;) {
186
+ const pairing = await requestSiderPairing();
187
+ await prompter.note(
188
+ formatSiderPairingInstructions({ pairingCode: pairing.pairingCode }),
189
+ "Sider pairing",
190
+ );
191
+
192
+ const progress = prompter.progress("Waiting for connection...");
193
+ const reportPendingUpdate = createSiderPairingPendingUpdateReporter({
194
+ pairingCode: pairing.pairingCode,
195
+ report: (message) => {
196
+ progress.update(message);
197
+ },
198
+ });
199
+ try {
200
+ const paired = await waitForSiderPairing({
201
+ pairing,
202
+ onPending: reportPendingUpdate,
203
+ onRetryableError: (message) => {
204
+ progress.update(message);
205
+ },
206
+ });
207
+ progress.stop("Connected.");
208
+
209
+ await prompter.note(
210
+ "Connected! You can now chat with me in the browser Side Panel.",
211
+ "Sider connected",
212
+ );
213
+ return {
214
+ cfg: applySiderSetupAccountConfig({
215
+ cfg,
216
+ accountId,
217
+ input: {
218
+ token: paired.token,
219
+ },
220
+ }),
221
+ };
222
+ } catch (error) {
223
+ if (error instanceof SiderPairingExpiredError) {
224
+ progress.stop("Pairing code expired.");
225
+ await prompter.note(
226
+ "Pairing code expired.\nGenerating a new code...",
227
+ "Sider pairing",
228
+ );
229
+ continue;
230
+ }
231
+ progress.stop("Pairing failed.");
232
+ throw error;
233
+ }
234
+ }
235
+ },
236
+ disable: (cfg) => setSetupChannelEnabled(cfg, setupChannel, false),
237
+ };
238
+
239
+ export function describeSiderAccountConfigurationError(account: ResolvedSiderAccount): string {
240
+ if (isSiderAccountSetupPending(account.accountId)) {
241
+ return (
242
+ `sider account "${account.accountId}" is waiting for setup token exchange; ` +
243
+ `selfclaw registration is still in progress`
244
+ );
245
+ }
246
+ const missing: string[] = [];
247
+ if (!account.token?.trim()) {
248
+ missing.push("token");
249
+ }
250
+ if (!account.relayId?.trim()) {
251
+ missing.push("relayId");
252
+ }
253
+ return (
254
+ `sider account "${account.accountId}" is not configured: missing ${missing.join("/") || "token/relayId"}; ` +
255
+ `run \`openclaw channels login --channel ${SIDER_CHANNEL_ID}\` or configure a token`
256
+ );
257
+ }
258
+
259
+ export function isSiderAccountBootstrappable(account: ResolvedSiderAccount): boolean {
260
+ return Boolean(resolveSiderSetupToken(account.accountId, account.config));
261
+ }
262
+
263
+ export async function resolveManagedSiderAccount(params: {
264
+ cfg: OpenClawConfig;
265
+ accountId?: string | null;
266
+ }): Promise<ResolvedSiderAccount> {
267
+ const initial = resolveSiderAccount(params.cfg, params.accountId);
268
+ if (!initial.enabled) {
269
+ return initial;
270
+ }
271
+ const setupToken = resolveSiderSetupToken(initial.accountId, initial.config);
272
+ if (initial.configured && !setupToken) {
273
+ return initial;
274
+ }
275
+ const nextCfg = await ensureSiderAccountSetup({
276
+ cfg: params.cfg,
277
+ accountId: initial.accountId,
278
+ getAccountSetupConfig: readSiderAccountConfig,
279
+ });
280
+ return resolveSiderAccount(nextCfg, initial.accountId);
281
+ }
282
+
283
+ function parseSessionTarget(raw: string): string {
284
+ const trimmed = raw.trim();
285
+ if (!trimmed) {
286
+ throw new Error("Missing sider session target");
287
+ }
288
+ if (trimmed.startsWith("session:")) {
289
+ const sessionId = trimmed.slice("session:".length).trim();
290
+ if (!sessionId) {
291
+ throw new Error("Invalid sider target, empty session id");
292
+ }
293
+ return sessionId;
294
+ }
295
+ return trimmed;
296
+ }
297
+
298
+ export function normalizeSiderMessagingTarget(raw: string): string | undefined {
299
+ const trimmed = raw.trim();
300
+ if (!trimmed) {
301
+ return undefined;
302
+ }
303
+ const withoutProviderPrefix = trimmed.replace(/^sider:/i, "").trim();
304
+ return withoutProviderPrefix || undefined;
305
+ }
306
+
307
+ export function looksLikeSiderTargetId(raw: string, normalized?: string): boolean {
308
+ const candidate = (normalized ?? raw ?? "").trim();
309
+ if (!candidate) {
310
+ return false;
311
+ }
312
+ const match = candidate.match(/^session:(.+)$/i);
313
+ if (match) {
314
+ return match[1].trim().length > 0;
315
+ }
316
+ return !/\s/.test(candidate);
317
+ }
318
+
319
+ export function resolveOutboundSessionId(params: {
320
+ account: ResolvedSiderAccount;
321
+ to?: string;
322
+ }): string {
323
+ const target = params.to?.trim() || params.account.defaultTo?.trim() || "";
324
+ if (!target) {
325
+ throw new Error(
326
+ `sider account "${params.account.accountId}" missing target; set 'to' or channels.${SIDER_CHANNEL_ID}.defaultTo`,
327
+ );
328
+ }
329
+ return parseSessionTarget(target);
330
+ }
331
+
332
+ export function extractSiderToolSend(args: Record<string, unknown>): {
333
+ to: string;
334
+ accountId?: string;
335
+ threadId?: string;
336
+ } | null {
337
+ const action = typeof args.action === "string" ? args.action.trim() : "";
338
+ if (action !== "sendAttachment" && action !== "send") {
339
+ return null;
340
+ }
341
+ const to = typeof args.to === "string" ? args.to.trim() : "";
342
+ if (!to) {
343
+ return null;
344
+ }
345
+ const accountId =
346
+ typeof args.accountId === "string" && args.accountId.trim() ? args.accountId.trim() : undefined;
347
+ const threadId =
348
+ typeof args.threadId === "string" && args.threadId.trim() ? args.threadId.trim() : undefined;
349
+ return { to, accountId, threadId };
350
+ }