@andocorp/openclaw-plugin 0.0.1 → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ando Corp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,35 @@
1
+ # @andocorp/openclaw-plugin
2
+
3
+ OpenClaw adapter for Ando.
4
+
5
+ Configure `channels.ando.apiKey` in OpenClaw. REST requests use
6
+ `https://api.ando.so` unless `channels.ando.baseUrl` is set. Realtime monitoring
7
+ uses `sokachu.ando.so` unless `channels.ando.realtimeHost` is set. MCP tool
8
+ proxying uses `https://mcp.ando.so/mcp` unless `channels.ando.mcpUrl` is set.
9
+
10
+ ## Publish
11
+
12
+ This package is published from the open-source Ando package monorepo. Publish
13
+ with `pnpm` so the `@andocorp/sdk` workspace dependency is rewritten in the
14
+ published tarball.
15
+
16
+ Before publishing, publish `@andocorp/sdk@<current version>` first.
17
+
18
+ From the package directory:
19
+
20
+ ```sh
21
+ pnpm publish --access public --no-git-checks
22
+ ```
23
+
24
+ From the repo root:
25
+
26
+ ```sh
27
+ pnpm --filter @andocorp/openclaw-plugin publish --access public --no-git-checks
28
+ ```
29
+
30
+ Do not publish this package with `npm`:
31
+
32
+ ```sh
33
+ npm pack
34
+ npm publish
35
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andocorp/openclaw-plugin",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "OpenClaw adapter for Ando",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,14 +13,9 @@
13
13
  "engines": {
14
14
  "node": ">=22"
15
15
  },
16
- "scripts": {
17
- "typecheck": "pnpm --filter @andocorp/sdk build && tsc --noEmit",
18
- "test": "pnpm --filter @andocorp/sdk build && vitest run",
19
- "test:boundaries": "pnpm --filter @andocorp/sdk build && vitest run src/monitor.test.ts src/channel.test.ts"
20
- },
21
16
  "dependencies": {
22
- "@andocorp/sdk": "workspace:*",
23
- "zod": "^4.3.6"
17
+ "zod": "^4.3.6",
18
+ "@andocorp/sdk": "0.1.0"
24
19
  },
25
20
  "peerDependencies": {
26
21
  "openclaw": ">=2026.3.23"
@@ -31,13 +26,13 @@
31
26
  }
32
27
  },
33
28
  "publishConfig": {
34
- "access": "restricted"
29
+ "access": "public"
35
30
  },
36
31
  "devDependencies": {
37
32
  "@types/node": "^24.10.9",
38
33
  "openclaw": "2026.3.23",
39
34
  "typescript": "^5.9.3",
40
- "vitest": "^3.2.4"
35
+ "vitest": "^4.1.5"
41
36
  },
42
37
  "openclaw": {
43
38
  "extensions": [
@@ -50,5 +45,12 @@
50
45
  "docsPath": "/channels/ando",
51
46
  "blurb": "Live agent membership inside Ando conversations."
52
47
  }
48
+ },
49
+ "scripts": {
50
+ "build": "tsc --noEmit",
51
+ "lint": "tsc --noEmit",
52
+ "typecheck": "tsc --noEmit",
53
+ "test": "vitest run",
54
+ "test:boundaries": "vitest run src/monitor.test.ts src/channel.test.ts"
53
55
  }
54
- }
56
+ }
package/src/channel.ts CHANGED
@@ -6,7 +6,10 @@ import {
6
6
  import packageJson from "../package.json" with { type: "json" };
7
7
  import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-resolution";
8
8
  import { createTopLevelChannelConfigBase } from "openclaw/plugin-sdk/channel-config-helpers";
9
- import { formatAndoTarget, parseAndoTarget } from "@andocorp/sdk";
9
+ import {
10
+ DEFAULT_ANDO_BASE_URL, DEFAULT_ANDO_REALTIME_HOST, formatAndoTarget,
11
+ parseAndoTarget
12
+ } from "@andocorp/sdk";
10
13
  import { z } from "zod";
11
14
  import { postAndoMessage } from "./client.js";
12
15
  import { monitorAndoProvider } from "./monitor.js";
@@ -20,6 +23,8 @@ const meta = packageJson.openclaw.channel as {
20
23
  blurb: string;
21
24
  };
22
25
 
26
+ export const DEFAULT_ANDO_MCP_URL = "https://mcp.ando.so/mcp";
27
+
23
28
  type AndoConfig = {
24
29
  baseUrl?: string;
25
30
  apiKey?: string;
@@ -35,20 +40,8 @@ function normalizeUrl(value?: string | null): string {
35
40
  return trimValue(value).replace(/\/+$/, "");
36
41
  }
37
42
 
38
- function waitForAbort(signal?: AbortSignal): Promise<void> {
39
- return new Promise((resolve) => {
40
- if (signal?.aborted) {
41
- resolve();
42
- return;
43
- }
44
-
45
- signal?.addEventListener("abort", () => resolve(), {
46
- once: true,
47
- });
48
- });
49
- }
50
43
  export function resolveAndoMcpUrl(params: {
51
- baseUrl: string;
44
+ baseUrl?: string | null;
52
45
  mcpUrl?: string | null;
53
46
  }): string {
54
47
  const explicitMcpUrl = normalizeUrl(params.mcpUrl);
@@ -56,8 +49,7 @@ export function resolveAndoMcpUrl(params: {
56
49
  return explicitMcpUrl;
57
50
  }
58
51
 
59
- const baseUrl = normalizeUrl(params.baseUrl);
60
- return baseUrl ? `${baseUrl}/mcp` : "";
52
+ return DEFAULT_ANDO_MCP_URL;
61
53
  }
62
54
  export function resolveAndoAccount(cfg: OpenClawConfig): ResolvedAndoAccount {
63
55
  const config = (cfg.channels?.ando ?? {}) as AndoConfig;
@@ -65,9 +57,9 @@ export function resolveAndoAccount(cfg: OpenClawConfig): ResolvedAndoAccount {
65
57
 
66
58
  return {
67
59
  accountId: DEFAULT_ACCOUNT_ID,
68
- baseUrl: normalizeUrl(config.baseUrl),
60
+ baseUrl: normalizeUrl(config.baseUrl) || DEFAULT_ANDO_BASE_URL,
69
61
  apiKey: trimValue(config.apiKey),
70
- realtimeHost: realtimeHost || null,
62
+ realtimeHost: realtimeHost || DEFAULT_ANDO_REALTIME_HOST,
71
63
  mcpUrl: resolveAndoMcpUrl({
72
64
  baseUrl: config.baseUrl ?? "",
73
65
  mcpUrl: config.mcpUrl,
@@ -105,25 +97,19 @@ type AndoOutboundParams = {
105
97
  };
106
98
 
107
99
  function isAndoRestConfigured(account: ResolvedAndoAccount): boolean {
108
- return Boolean(account.baseUrl && account.apiKey);
100
+ return Boolean(account.apiKey);
109
101
  }
110
102
 
111
103
  function assertAndoRestConfigured(account: ResolvedAndoAccount): void {
112
104
  if (!isAndoRestConfigured(account)) {
113
105
  throw new Error(
114
- `[ando] account "${account.accountId}" is missing baseUrl or apiKey`,
106
+ `[ando] account "${account.accountId}" is missing apiKey`,
115
107
  );
116
108
  }
117
109
  }
118
110
 
119
111
  function assertAndoRealtimeConfigured(account: ResolvedAndoAccount): void {
120
112
  assertAndoRestConfigured(account);
121
-
122
- if (!account.realtimeHost) {
123
- throw new Error(
124
- `[ando] account "${account.accountId}" is missing realtimeHost for realtime monitoring`,
125
- );
126
- }
127
113
  }
128
114
 
129
115
  export function resolveThreadRootId(params: {
@@ -213,8 +199,8 @@ export const andoPlugin: ChannelPlugin<ResolvedAndoAccount> = {
213
199
  ...andoConfigBase,
214
200
  isConfigured: isAndoRestConfigured,
215
201
  unconfiguredReason: (account) => {
216
- if (!account.baseUrl || !account.apiKey) {
217
- return "baseUrl and apiKey are required";
202
+ if (!account.apiKey) {
203
+ return "apiKey is required";
218
204
  }
219
205
  return "Ando channel is configured";
220
206
  },
@@ -261,14 +247,6 @@ export const andoPlugin: ChannelPlugin<ResolvedAndoAccount> = {
261
247
  gateway: {
262
248
  startAccount: async (ctx) => {
263
249
  const account = ctx.account;
264
- if (!account.realtimeHost) {
265
- ctx.runtime.log?.(
266
- `[ando] skipping realtime monitoring for account "${account.accountId}" because realtimeHost is not configured`
267
- );
268
- await waitForAbort(ctx.abortSignal);
269
- return;
270
- }
271
-
272
250
  assertAndoRealtimeConfigured(account);
273
251
  const channelRuntime = ctx.channelRuntime;
274
252
  if (!channelRuntime) {
package/src/client.ts CHANGED
@@ -4,7 +4,7 @@ import { ResolvedAndoAccount } from "./types.js";
4
4
  export function createAndoSdkClient(account: ResolvedAndoAccount) {
5
5
  return new AndoClient({
6
6
  baseUrl: account.baseUrl,
7
- realtimeHost: account.realtimeHost ?? undefined,
7
+ realtimeHost: account.realtimeHost,
8
8
  auth: {
9
9
  apiKey: account.apiKey,
10
10
  },
@@ -38,7 +38,7 @@ const CURATED_ANDO_MCP_TOOLS: ToolDefinition[] = [
38
38
  {
39
39
  name: "search_messages",
40
40
  description:
41
- "Search messages across conversations by keyword or semantic similarity. Requires a real search query — do not use wildcards like '*'. To browse recent messages or filter by author without a keyword, use get_conversation_messages instead.",
41
+ "Search messages across conversations by keyword. Requires a real search query — do not use wildcards like '*'. To browse recent messages or filter by author without a keyword, use get_conversation_messages instead.",
42
42
  inputSchema: objectSchema(
43
43
  {
44
44
  q: stringProperty(
@@ -61,7 +61,7 @@ const CURATED_ANDO_MCP_TOOLS: ToolDefinition[] = [
61
61
  type: "string",
62
62
  enum: ["full-text", "semantic"],
63
63
  description:
64
- "Search mode: 'full-text' for keyword search (default), 'semantic' for AI-powered similarity",
64
+ "Accepted for backwards compatibility; message search uses Convex keyword search",
65
65
  },
66
66
  },
67
67
  ["q"]
@@ -89,6 +89,17 @@ const CURATED_ANDO_MCP_TOOLS: ToolDefinition[] = [
89
89
  ["q"]
90
90
  ),
91
91
  },
92
+ {
93
+ name: "list_conversations",
94
+ description:
95
+ "List conversations (channels and DMs) that the MCP caller is a member of. Optionally filter by conversation name or description.",
96
+ inputSchema: objectSchema({
97
+ q: stringProperty("Optional query text to filter conversations"),
98
+ limit: numberProperty(
99
+ "Maximum number of conversations to return (default 50, max 100)"
100
+ ),
101
+ }),
102
+ },
92
103
  {
93
104
  name: "search_clipboard",
94
105
  description:
@@ -216,6 +227,44 @@ const CURATED_ANDO_MCP_TOOLS: ToolDefinition[] = [
216
227
  ["message_id"]
217
228
  ),
218
229
  },
230
+ {
231
+ name: "list_conversation_members",
232
+ description:
233
+ "List active members in a conversation the MCP caller can access. Returns member IDs, display names, member types, and workspace roles.",
234
+ inputSchema: objectSchema(
235
+ {
236
+ conversation_id: stringProperty("The conversation ID"),
237
+ },
238
+ ["conversation_id"]
239
+ ),
240
+ },
241
+ {
242
+ name: "react_to_message",
243
+ description:
244
+ "React to a message with a supported standard emoji or active workspace custom emoji.",
245
+ inputSchema: objectSchema(
246
+ {
247
+ message_id: stringProperty("The message ID to react to"),
248
+ emoji: stringProperty(
249
+ "Emoji to react with, such as :thumbs_up: or :ship-it:"
250
+ ),
251
+ },
252
+ ["message_id", "emoji"]
253
+ ),
254
+ },
255
+ {
256
+ name: "delete_message",
257
+ description:
258
+ "Delete one of your own sent messages. This tool can only delete messages authored by the MCP caller; it cannot delete messages from other members or agents.",
259
+ inputSchema: objectSchema(
260
+ {
261
+ message_id: stringProperty(
262
+ "The ID of one of your own sent messages to delete"
263
+ ),
264
+ },
265
+ ["message_id"]
266
+ ),
267
+ },
219
268
  ];
220
269
 
221
270
  export function listCuratedAndoMcpTools(): ToolDefinition[] {
package/src/mcp-tools.ts CHANGED
@@ -227,7 +227,10 @@ async function closeMcpSession(params: {
227
227
  );
228
228
  }
229
229
 
230
- async function initializeMcpSession(mcpUrl: string, apiKey: string): Promise<string> {
230
+ async function initializeMcpSession(
231
+ mcpUrl: string,
232
+ apiKey: string,
233
+ ): Promise<string | null> {
231
234
  const response = await runMcpJsonRpc({
232
235
  mcpUrl,
233
236
  apiKey,
@@ -250,11 +253,7 @@ async function initializeMcpSession(mcpUrl: string, apiKey: string): Promise<str
250
253
  ensureMcpOk(payload);
251
254
 
252
255
  const sessionId = response.sessionId?.trim();
253
- if (!sessionId) {
254
- throw new Error("Missing MCP session ID");
255
- }
256
-
257
- return sessionId;
256
+ return sessionId || null;
258
257
  }
259
258
 
260
259
  async function notifyMcpSessionInitialized(params: {
@@ -281,16 +280,18 @@ async function callAndoMcpTool(params: {
281
280
  }): Promise<string> {
282
281
  const sessionId = await initializeMcpSession(params.mcpUrl, params.apiKey);
283
282
  try {
284
- await notifyMcpSessionInitialized({
285
- mcpUrl: params.mcpUrl,
286
- apiKey: params.apiKey,
287
- sessionId,
288
- });
283
+ if (sessionId) {
284
+ await notifyMcpSessionInitialized({
285
+ mcpUrl: params.mcpUrl,
286
+ apiKey: params.apiKey,
287
+ sessionId,
288
+ });
289
+ }
289
290
 
290
291
  const response = await runMcpJsonRpc({
291
292
  mcpUrl: params.mcpUrl,
292
293
  apiKey: params.apiKey,
293
- sessionId,
294
+ ...(sessionId ? { sessionId } : {}),
294
295
  action: `tools/call(${params.name})`,
295
296
  request: buildMcpJsonRpcRequest({
296
297
  id: "2",
@@ -306,11 +307,13 @@ async function callAndoMcpTool(params: {
306
307
  ensureMcpOk(payload);
307
308
  return formatMcpToolResultText(asMcpToolResult(payload.result));
308
309
  } finally {
309
- await closeMcpSession({
310
- mcpUrl: params.mcpUrl,
311
- apiKey: params.apiKey,
312
- sessionId,
313
- }).catch(() => undefined);
310
+ if (sessionId) {
311
+ await closeMcpSession({
312
+ mcpUrl: params.mcpUrl,
313
+ apiKey: params.apiKey,
314
+ sessionId,
315
+ }).catch(() => undefined);
316
+ }
314
317
  }
315
318
  }
316
319
 
@@ -356,7 +359,10 @@ export function registerAndoMcpTools(
356
359
  } catch (error) {
357
360
  const message =
358
361
  error instanceof Error ? error.message : String(error);
359
- throw new Error(`Error calling Ando MCP tool ${tool.name}: ${message}`);
362
+ throw new Error(
363
+ `Error calling Ando MCP tool ${tool.name}: ${message}`,
364
+ { cause: error }
365
+ );
360
366
  }
361
367
  },
362
368
  };
package/src/types.ts CHANGED
@@ -2,6 +2,6 @@ export type ResolvedAndoAccount = {
2
2
  accountId: string;
3
3
  baseUrl: string;
4
4
  apiKey: string;
5
- realtimeHost: string | null;
5
+ realtimeHost: string;
6
6
  mcpUrl: string;
7
7
  };