openclaw-channel-openswitchy 0.1.0 → 0.1.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/dist/channel.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- import type { ChannelPlugin, AccountConfig } from "./types.js";
1
+ import type { ChannelPlugin } from "openclaw/plugin-sdk";
2
+ import type { AccountConfig } from "./types.js";
2
3
  export declare const openswitchyChannel: ChannelPlugin<AccountConfig>;
package/dist/channel.js CHANGED
@@ -47,7 +47,7 @@ async function listenSse(conn) {
47
47
  signal,
48
48
  });
49
49
  if (!res.ok || !res.body) {
50
- console.error(`[openswitchy] SSE connect failed (${res.status}), retrying in 5s`);
50
+ conn.gatewayCtx.log?.error(`SSE connect failed (${res.status}), retrying in 5s`);
51
51
  await sleep(5000);
52
52
  continue;
53
53
  }
@@ -72,7 +72,7 @@ async function listenSse(conn) {
72
72
  await handleNewMessage(conn, data);
73
73
  }
74
74
  catch (err) {
75
- console.error("[openswitchy] Failed to parse SSE data:", err);
75
+ conn.gatewayCtx.log?.error(`Failed to parse SSE data: ${err}`);
76
76
  }
77
77
  currentEvent = "";
78
78
  }
@@ -85,7 +85,7 @@ async function listenSse(conn) {
85
85
  catch (err) {
86
86
  if (signal.aborted)
87
87
  return;
88
- console.error("[openswitchy] SSE error, reconnecting in 5s:", err);
88
+ conn.gatewayCtx.log?.error(`SSE error, reconnecting in 5s: ${err}`);
89
89
  await sleep(5000);
90
90
  }
91
91
  }
@@ -99,23 +99,69 @@ async function handleNewMessage(conn, data) {
99
99
  const latest = history.messages[history.messages.length - 1];
100
100
  if (!latest)
101
101
  return;
102
- const mentioned = data.mentioned === true ||
103
- (latest.metadata?.mentionedAgentIds || []).includes(conn.agentId);
104
- const envelope = {
102
+ const ctx = conn.gatewayCtx;
103
+ const { channelRuntime } = ctx;
104
+ if (!channelRuntime) {
105
+ ctx.log?.warn("channelRuntime not available, skipping AI dispatch");
106
+ return;
107
+ }
108
+ // Resolve agent route for this sender
109
+ const route = channelRuntime.routing.resolveAgentRoute({
110
+ cfg: ctx.cfg,
105
111
  channel: "openswitchy",
106
- accountId: conn.accountId,
107
- from: data.from.agentId,
108
- to: data.chatRoomId,
109
- body: latest.content,
112
+ accountId: ctx.accountId,
113
+ peer: { kind: "direct", id: data.from.agentId },
114
+ });
115
+ // Build the inbound envelope body via the SDK runtime
116
+ const storePath = channelRuntime.session.resolveStorePath(undefined, {
117
+ agentId: route.agentId,
118
+ });
119
+ const envelopeOpts = channelRuntime.reply.resolveEnvelopeFormatOptions(ctx.cfg);
120
+ const body = channelRuntime.reply.formatAgentEnvelope({
121
+ channel: "openswitchy",
122
+ from: data.from.name,
110
123
  timestamp: new Date(latest.createdAt).getTime() || Date.now(),
111
- metadata: {
112
- messageId: latest._id || data.messageId,
113
- fromName: data.from.name,
114
- chatRoomId: data.chatRoomId,
115
- mentioned,
116
- },
124
+ envelope: envelopeOpts,
125
+ body: latest.content,
126
+ });
127
+ const mentioned = data.mentioned === true ||
128
+ (latest.metadata?.mentionedAgentIds || []).includes(conn.agentId);
129
+ // Build MsgContext for the SDK pipeline
130
+ const msgCtx = {
131
+ Body: body,
132
+ From: data.from.agentId,
133
+ To: data.chatRoomId,
134
+ AccountId: ctx.accountId,
135
+ SessionKey: route.sessionKey,
136
+ ChatType: "direct",
137
+ SenderName: data.from.name,
117
138
  };
118
- conn.dispatchInbound(envelope);
139
+ // Record inbound session
140
+ const chatRoomId = data.chatRoomId;
141
+ await channelRuntime.session.recordInboundSession({
142
+ storePath,
143
+ sessionKey: route.sessionKey,
144
+ ctx: msgCtx,
145
+ onRecordError: (err) => {
146
+ ctx.log?.error(`Session record error: ${err}`);
147
+ },
148
+ });
149
+ // Dispatch AI reply
150
+ await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
151
+ ctx: msgCtx,
152
+ cfg: ctx.cfg,
153
+ dispatcherOptions: {
154
+ deliver: async (payload) => {
155
+ const text = payload.text;
156
+ if (!text)
157
+ return;
158
+ await apiCall(conn.baseUrl, conn.apiKey, "POST", "/chat", {
159
+ chatRoomId,
160
+ message: text,
161
+ });
162
+ },
163
+ },
164
+ });
119
165
  }
120
166
  function sleep(ms) {
121
167
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -137,20 +183,22 @@ export const openswitchyChannel = {
137
183
  /* ── Config Adapter ── */
138
184
  config: {
139
185
  listAccountIds(cfg) {
140
- const accounts = cfg.channels?.openswitchy?.accounts;
141
- if (!accounts)
186
+ const channels = cfg.channels;
187
+ const osSection = channels?.openswitchy;
188
+ if (!osSection?.accounts)
142
189
  return [];
143
- return Object.keys(accounts);
190
+ return Object.keys(osSection.accounts);
144
191
  },
145
192
  resolveAccount(cfg, accountId) {
146
- const accounts = cfg.channels?.openswitchy?.accounts;
147
- if (!accounts)
193
+ const channels = cfg.channels;
194
+ const osSection = channels?.openswitchy;
195
+ if (!osSection?.accounts)
148
196
  return {};
149
- if (accountId && accounts[accountId])
150
- return accounts[accountId];
197
+ if (accountId && osSection.accounts[accountId])
198
+ return osSection.accounts[accountId];
151
199
  // Fall back to first account
152
- const firstKey = Object.keys(accounts)[0];
153
- return firstKey ? accounts[firstKey] : {};
200
+ const firstKey = Object.keys(osSection.accounts)[0];
201
+ return firstKey ? osSection.accounts[firstKey] : {};
154
202
  },
155
203
  isConfigured(account) {
156
204
  return Boolean(account.joinCode && account.agentName);
@@ -167,25 +215,23 @@ export const openswitchyChannel = {
167
215
  throw new Error("[openswitchy] Missing joinCode or agentName in config");
168
216
  }
169
217
  const baseUrl = account.url || DEFAULT_URL;
170
- console.log(`[openswitchy] Registering "${account.agentName}" at ${baseUrl}`);
218
+ ctx.log?.info(`Registering "${account.agentName}" at ${baseUrl}`);
171
219
  const reg = await registerAgent(account);
172
- console.log(`[openswitchy] Registered as ${reg.name} (${reg.agentId}) in "${reg.orgName}" — status: ${reg.status}`);
220
+ ctx.log?.info(`Registered as ${reg.name} (${reg.agentId}) in "${reg.orgName}" — status: ${reg.status}`);
173
221
  const conn = {
174
222
  apiKey: reg.apiKey,
175
223
  agentId: reg.agentId,
176
224
  baseUrl,
177
225
  accountId,
178
226
  abortController: new AbortController(),
179
- dispatchInbound: (envelope) => {
180
- ctx.channelRuntime?.reply.dispatchInbound(envelope);
181
- },
227
+ gatewayCtx: ctx,
182
228
  };
183
229
  // Link abort to OpenClaw's signal
184
230
  abortSignal.addEventListener("abort", () => conn.abortController.abort());
185
231
  connections.set(accountId, conn);
186
232
  // Start SSE listener (fire-and-forget, reconnects internally)
187
233
  listenSse(conn);
188
- console.log("[openswitchy] SSE connected, listening for messages");
234
+ ctx.log?.info("SSE connected, listening for messages");
189
235
  },
190
236
  async stopAccount(ctx) {
191
237
  const conn = connections.get(ctx.accountId);
@@ -193,7 +239,7 @@ export const openswitchyChannel = {
193
239
  conn.abortController.abort();
194
240
  connections.delete(ctx.accountId);
195
241
  }
196
- console.log(`[openswitchy] Disconnected account ${ctx.accountId}`);
242
+ ctx.log?.info(`Disconnected account ${ctx.accountId}`);
197
243
  },
198
244
  },
199
245
  /* ── Outbound Adapter ── */
@@ -206,7 +252,7 @@ export const openswitchyChannel = {
206
252
  throw new Error("[openswitchy] No active connection for this account");
207
253
  }
208
254
  const result = await apiCall(conn.baseUrl, conn.apiKey, "POST", "/chat", { chatRoomId: ctx.to, message: ctx.text });
209
- return { messageId: result.messageId };
255
+ return { channel: "openswitchy", messageId: result.messageId };
210
256
  },
211
257
  },
212
258
  /* ── Security Adapter ── */
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,211 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { openswitchyChannel } from "./channel.js";
3
+ /* ── Helper to build a fake OpenClawConfig ── */
4
+ function makeConfig(accounts) {
5
+ return { channels: { openswitchy: { accounts } } };
6
+ }
7
+ /* ── Config Adapter ── */
8
+ describe("config adapter", () => {
9
+ const { config } = openswitchyChannel;
10
+ it("listAccountIds returns account IDs from config", () => {
11
+ const cfg = makeConfig({
12
+ default: { joinCode: "abc", agentName: "Bot" },
13
+ work: { joinCode: "xyz", agentName: "WorkBot" },
14
+ });
15
+ expect(config.listAccountIds(cfg)).toEqual(["default", "work"]);
16
+ });
17
+ it("listAccountIds returns empty array when no config", () => {
18
+ expect(config.listAccountIds({})).toEqual([]);
19
+ });
20
+ it("listAccountIds returns empty when no accounts section", () => {
21
+ const cfg = { channels: { openswitchy: {} } };
22
+ expect(config.listAccountIds(cfg)).toEqual([]);
23
+ });
24
+ it("resolveAccount returns the correct account config", () => {
25
+ const cfg = makeConfig({
26
+ default: { joinCode: "abc", agentName: "Bot1" },
27
+ work: { joinCode: "xyz", agentName: "Bot2" },
28
+ });
29
+ const account = config.resolveAccount(cfg, "work");
30
+ expect(account).toEqual({ joinCode: "xyz", agentName: "Bot2" });
31
+ });
32
+ it("resolveAccount falls back to first account when ID not found", () => {
33
+ const cfg = makeConfig({
34
+ default: { joinCode: "abc", agentName: "Bot1" },
35
+ });
36
+ const account = config.resolveAccount(cfg, "nonexistent");
37
+ expect(account).toEqual({ joinCode: "abc", agentName: "Bot1" });
38
+ });
39
+ it("resolveAccount returns empty object when no accounts", () => {
40
+ const account = config.resolveAccount({}, "anything");
41
+ expect(account).toEqual({});
42
+ });
43
+ it("isConfigured returns true when joinCode and agentName are set", () => {
44
+ expect(config.isConfigured({ joinCode: "abc", agentName: "Bot" }, {})).toBe(true);
45
+ });
46
+ it("isConfigured returns false when joinCode is missing", () => {
47
+ expect(config.isConfigured({ agentName: "Bot" }, {})).toBe(false);
48
+ });
49
+ it("isConfigured returns false when agentName is missing", () => {
50
+ expect(config.isConfigured({ joinCode: "abc" }, {})).toBe(false);
51
+ });
52
+ it("isEnabled returns true by default", () => {
53
+ expect(config.isEnabled({}, {})).toBe(true);
54
+ });
55
+ it("isEnabled returns false when explicitly disabled", () => {
56
+ expect(config.isEnabled({ enabled: false }, {})).toBe(false);
57
+ });
58
+ it("isEnabled returns true when explicitly enabled", () => {
59
+ expect(config.isEnabled({ enabled: true }, {})).toBe(true);
60
+ });
61
+ });
62
+ /* ── Meta & Capabilities ── */
63
+ describe("meta and capabilities", () => {
64
+ it("has correct id", () => {
65
+ expect(openswitchyChannel.id).toBe("openswitchy");
66
+ });
67
+ it("meta.label is OpenSwitchy", () => {
68
+ expect(openswitchyChannel.meta.label).toBe("OpenSwitchy");
69
+ });
70
+ it("meta has docsPath", () => {
71
+ expect(openswitchyChannel.meta.docsPath).toBe("channels/openswitchy");
72
+ });
73
+ it("supports direct and group chats", () => {
74
+ expect(openswitchyChannel.capabilities.chatTypes).toContain("direct");
75
+ expect(openswitchyChannel.capabilities.chatTypes).toContain("group");
76
+ });
77
+ });
78
+ /* ── Security Adapter ── */
79
+ describe("security adapter", () => {
80
+ const security = openswitchyChannel.security;
81
+ it("resolveDmPolicy returns open by default", () => {
82
+ const result = security.resolveDmPolicy({
83
+ cfg: {},
84
+ account: {},
85
+ });
86
+ expect(result?.policy).toBe("open");
87
+ });
88
+ it("resolveDmPolicy returns configured policy", () => {
89
+ const result = security.resolveDmPolicy({
90
+ cfg: {},
91
+ account: { dmPolicy: "pairing" },
92
+ });
93
+ expect(result?.policy).toBe("pairing");
94
+ });
95
+ it("resolveDmPolicy includes allowFromPath", () => {
96
+ const result = security.resolveDmPolicy({
97
+ cfg: {},
98
+ account: {},
99
+ });
100
+ expect(result?.allowFromPath).toBe("channels.openswitchy.allowFrom");
101
+ });
102
+ });
103
+ /* ── Outbound Adapter ── */
104
+ describe("outbound adapter", () => {
105
+ const outbound = openswitchyChannel.outbound;
106
+ it("textChunkLimit is 4096", () => {
107
+ expect(outbound.textChunkLimit).toBe(4096);
108
+ });
109
+ it("deliveryMode is direct", () => {
110
+ expect(outbound.deliveryMode).toBe("direct");
111
+ });
112
+ it("sendText throws when no active connection", async () => {
113
+ await expect(outbound.sendText({
114
+ cfg: {},
115
+ to: "room-123",
116
+ text: "hello",
117
+ accountId: "nonexistent",
118
+ })).rejects.toThrow("No active connection");
119
+ });
120
+ });
121
+ /* ── Gateway Adapter ── */
122
+ describe("gateway adapter", () => {
123
+ const gateway = openswitchyChannel.gateway;
124
+ beforeEach(() => {
125
+ vi.restoreAllMocks();
126
+ });
127
+ it("startAccount throws when joinCode is missing", async () => {
128
+ const ctx = {
129
+ cfg: {},
130
+ accountId: "test",
131
+ account: { agentName: "Bot" },
132
+ abortSignal: new AbortController().signal,
133
+ runtime: {},
134
+ getStatus: vi.fn(),
135
+ setStatus: vi.fn(),
136
+ };
137
+ await expect(gateway.startAccount(ctx)).rejects.toThrow("Missing joinCode or agentName");
138
+ });
139
+ it("startAccount throws when agentName is missing", async () => {
140
+ const ctx = {
141
+ cfg: {},
142
+ accountId: "test",
143
+ account: { joinCode: "abc" },
144
+ abortSignal: new AbortController().signal,
145
+ runtime: {},
146
+ getStatus: vi.fn(),
147
+ setStatus: vi.fn(),
148
+ };
149
+ await expect(gateway.startAccount(ctx)).rejects.toThrow("Missing joinCode or agentName");
150
+ });
151
+ it("startAccount registers and stores connection", async () => {
152
+ const mockFetch = vi.fn()
153
+ // registration call
154
+ .mockResolvedValueOnce({
155
+ ok: true,
156
+ json: () => Promise.resolve({
157
+ agentId: "agent-1",
158
+ name: "TestBot",
159
+ orgId: "org-1",
160
+ orgName: "TestOrg",
161
+ apiKey: "sb_test_key",
162
+ status: "approved",
163
+ message: "Registered",
164
+ }),
165
+ })
166
+ // SSE call (return a never-resolving body to keep SSE alive)
167
+ .mockResolvedValueOnce({
168
+ ok: true,
169
+ body: {
170
+ getReader: () => ({
171
+ read: () => new Promise(() => { }), // hangs forever (SSE stream)
172
+ }),
173
+ },
174
+ });
175
+ vi.stubGlobal("fetch", mockFetch);
176
+ const abortController = new AbortController();
177
+ const ctx = {
178
+ cfg: {},
179
+ accountId: "test-acc",
180
+ account: { joinCode: "abc", agentName: "TestBot", url: "http://localhost:3000" },
181
+ abortSignal: abortController.signal,
182
+ runtime: {},
183
+ getStatus: vi.fn(),
184
+ setStatus: vi.fn(),
185
+ log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
186
+ };
187
+ await gateway.startAccount(ctx);
188
+ // Verify registration was called
189
+ expect(mockFetch).toHaveBeenCalledWith("http://localhost:3000/register", expect.objectContaining({
190
+ method: "POST",
191
+ body: expect.stringContaining('"joinCode":"abc"'),
192
+ }));
193
+ // Cleanup
194
+ abortController.abort();
195
+ vi.unstubAllGlobals();
196
+ });
197
+ it("stopAccount cleans up", async () => {
198
+ const ctx = {
199
+ cfg: {},
200
+ accountId: "nonexistent-acc",
201
+ account: {},
202
+ abortSignal: new AbortController().signal,
203
+ runtime: {},
204
+ getStatus: vi.fn(),
205
+ setStatus: vi.fn(),
206
+ log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
207
+ };
208
+ // Should not throw even if no connection exists
209
+ await expect(gateway.stopAccount(ctx)).resolves.toBeUndefined();
210
+ });
211
+ });
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OpenClawPluginApi } from "./types.js";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  declare const _default: {
3
3
  id: string;
4
4
  name: string;
package/dist/types.d.ts CHANGED
@@ -1,117 +1,9 @@
1
1
  /**
2
- * OpenClaw plugin types matches the real adapter pattern from openclaw SDK.
3
- * Only includes what this plugin needs.
2
+ * OpenSwitchy-specific types for the channel plugin.
3
+ *
4
+ * OpenClaw SDK types (ChannelPlugin, OpenClawPluginApi, etc.) are imported
5
+ * directly from "openclaw/plugin-sdk" — only OpenSwitchy API shapes live here.
4
6
  */
5
- export interface OpenClawPluginApi {
6
- id: string;
7
- name: string;
8
- config: OpenClawConfig;
9
- logger: PluginLogger;
10
- registerChannel(registration: {
11
- plugin: ChannelPlugin;
12
- }): void;
13
- }
14
- export interface PluginLogger {
15
- info(msg: string, ...args: unknown[]): void;
16
- warn(msg: string, ...args: unknown[]): void;
17
- error(msg: string, ...args: unknown[]): void;
18
- }
19
- export type ChatType = "direct" | "group";
20
- export interface ChannelPlugin<ResolvedAccount = unknown> {
21
- id: string;
22
- meta: ChannelMeta;
23
- capabilities: ChannelCapabilities;
24
- config: ChannelConfigAdapter<ResolvedAccount>;
25
- gateway?: ChannelGatewayAdapter<ResolvedAccount>;
26
- outbound?: ChannelOutboundAdapter;
27
- security?: ChannelSecurityAdapter<ResolvedAccount>;
28
- }
29
- export interface ChannelMeta {
30
- id: string;
31
- label: string;
32
- selectionLabel: string;
33
- docsPath: string;
34
- blurb: string;
35
- aliases?: string[];
36
- }
37
- export interface ChannelCapabilities {
38
- chatTypes: ChatType[];
39
- media?: boolean;
40
- reactions?: boolean;
41
- threads?: boolean;
42
- }
43
- export interface ChannelConfigAdapter<ResolvedAccount> {
44
- listAccountIds(cfg: OpenClawConfig): string[];
45
- resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedAccount;
46
- isConfigured?(account: ResolvedAccount, cfg: OpenClawConfig): boolean;
47
- isEnabled?(account: ResolvedAccount, cfg: OpenClawConfig): boolean;
48
- }
49
- export interface ChannelGatewayAdapter<ResolvedAccount> {
50
- startAccount?(ctx: ChannelGatewayContext<ResolvedAccount>): Promise<unknown>;
51
- stopAccount?(ctx: ChannelGatewayContext<ResolvedAccount>): Promise<void>;
52
- }
53
- export interface ChannelGatewayContext<ResolvedAccount> {
54
- cfg: OpenClawConfig;
55
- accountId: string;
56
- account: ResolvedAccount;
57
- abortSignal: AbortSignal;
58
- log?: {
59
- info(msg: string): void;
60
- error(msg: string): void;
61
- };
62
- channelRuntime?: PluginChannelRuntime;
63
- }
64
- export interface PluginChannelRuntime {
65
- reply: {
66
- dispatchInbound(envelope: InboundEnvelope): void;
67
- };
68
- }
69
- export interface InboundEnvelope {
70
- channel: string;
71
- accountId: string;
72
- from: string;
73
- to: string;
74
- body: string;
75
- timestamp?: number;
76
- metadata?: Record<string, unknown>;
77
- }
78
- export interface ChannelOutboundAdapter {
79
- deliveryMode: "direct" | "gateway" | "hybrid";
80
- textChunkLimit?: number;
81
- sendText?(ctx: ChannelOutboundContext): Promise<OutboundDeliveryResult>;
82
- }
83
- export interface ChannelOutboundContext {
84
- cfg: OpenClawConfig;
85
- to: string;
86
- text: string;
87
- accountId?: string | null;
88
- }
89
- export interface OutboundDeliveryResult {
90
- messageId?: string;
91
- }
92
- export interface ChannelSecurityAdapter<ResolvedAccount> {
93
- resolveDmPolicy?(ctx: ChannelSecurityContext<ResolvedAccount>): ChannelSecurityDmPolicy | null;
94
- }
95
- export interface ChannelSecurityContext<ResolvedAccount> {
96
- cfg: OpenClawConfig;
97
- accountId?: string | null;
98
- account: ResolvedAccount;
99
- }
100
- export interface ChannelSecurityDmPolicy {
101
- policy: string;
102
- allowFrom?: Array<string | number> | null;
103
- allowFromPath: string;
104
- approveHint: string;
105
- }
106
- export interface OpenClawConfig {
107
- channels?: {
108
- openswitchy?: {
109
- accounts?: Record<string, AccountConfig>;
110
- };
111
- [key: string]: unknown;
112
- };
113
- [key: string]: unknown;
114
- }
115
7
  export interface AccountConfig {
116
8
  url?: string;
117
9
  joinCode?: string;
package/dist/types.js CHANGED
@@ -1,5 +1,7 @@
1
1
  /**
2
- * OpenClaw plugin types matches the real adapter pattern from openclaw SDK.
3
- * Only includes what this plugin needs.
2
+ * OpenSwitchy-specific types for the channel plugin.
3
+ *
4
+ * OpenClaw SDK types (ChannelPlugin, OpenClawPluginApi, etc.) are imported
5
+ * directly from "openclaw/plugin-sdk" — only OpenSwitchy API shapes live here.
4
6
  */
5
7
  export {};
package/package.json CHANGED
@@ -1,24 +1,36 @@
1
1
  {
2
2
  "name": "openclaw-channel-openswitchy",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "OpenSwitchy channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
- "files": ["dist", "openclaw.plugin.json"],
8
+ "files": [
9
+ "dist",
10
+ "openclaw.plugin.json"
11
+ ],
9
12
  "scripts": {
10
13
  "build": "tsc",
11
14
  "dev": "tsc --watch",
12
- "prepublishOnly": "npm run build"
15
+ "prepublishOnly": "npm run build",
16
+ "test": "vitest run"
13
17
  },
14
- "keywords": ["openclaw", "openswitchy", "channel", "plugin", "ai-agents"],
18
+ "keywords": [
19
+ "openclaw",
20
+ "openswitchy",
21
+ "channel",
22
+ "plugin",
23
+ "ai-agents"
24
+ ],
15
25
  "repository": {
16
26
  "type": "git",
17
27
  "url": "https://github.com/OpenSwitchy/openclaw-channel-openswitchy.git"
18
28
  },
19
29
  "license": "MIT",
20
30
  "devDependencies": {
31
+ "@types/node": "^22.0.0",
32
+ "openclaw": "^2026.3.7",
21
33
  "typescript": "^5.7.0",
22
- "@types/node": "^22.0.0"
34
+ "vitest": "^4.0.18"
23
35
  }
24
36
  }