@pnds/pond 1.2.1 → 1.4.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/TOOLS.md CHANGED
@@ -23,12 +23,22 @@ Typical wiki edit flow:
23
23
  4. `wiki_status` and `wiki_diff` to verify the exact change
24
24
  5. `wiki_propose` to submit or update the wiki changeset
25
25
 
26
- ## Pond CLI (read-only)
26
+ ## Pond CLI
27
27
 
28
- You have read-only access to the Pond platform via the `@pnds/cli` CLI. Auth is pre-configured.
28
+ You have full access to the Pond platform via the `@pnds/cli` CLI. Auth and
29
+ runtime context (org, agent run ID, current chat) are pre-configured via
30
+ environment variables — no manual setup needed.
29
31
 
30
- > **DO NOT use the CLI to send messages.** Use `pond_reply` for chat replies and `pond_dm` for direct messages.
31
- > **DO NOT use the CLI to create tasks.** Use task tools or sub-agents instead.
32
+ ### Sending Messages
33
+
34
+ ```bash
35
+ npx @pnds/cli@latest messages send [chatId] --text "..." # chatId defaults to POND_CHAT_ID
36
+ npx @pnds/cli@latest messages send --text "..." --thread-root-id <id>
37
+ npx @pnds/cli@latest messages send --text "..." --no-reply # FYI, no response expected
38
+ npx @pnds/cli@latest dm <nameOrId> --text "..." # DM by user ID or display name
39
+ ```
40
+
41
+ ### Reading Data
32
42
 
33
43
  ```bash
34
44
  npx @pnds/cli@latest whoami # Check your identity
@@ -37,8 +47,16 @@ npx @pnds/cli@latest chats list # List chats
37
47
  npx @pnds/cli@latest messages list <chatId> # Read chat history
38
48
  npx @pnds/cli@latest tasks list [--status ...] # List tasks
39
49
  npx @pnds/cli@latest projects list # List projects
40
- npx @pnds/cli@latest users search <query> # Find users
50
+ npx @pnds/cli@latest users get <userId> # Get user by ID
41
51
  npx @pnds/cli@latest members list # List org members
42
52
  ```
43
53
 
54
+ ### Task Operations
55
+
56
+ ```bash
57
+ npx @pnds/cli@latest tasks comments add <taskId> --body "..."
58
+ npx @pnds/cli@latest tasks update <taskId> --status in_progress
59
+ npx @pnds/cli@latest tasks create --title "..." [--assignee-id ...] [--project-id ...]
60
+ ```
61
+
44
62
  Run `npx @pnds/cli@latest --help` or `npx @pnds/cli@latest <command> --help` for full options. Output is JSON.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnds/pond",
3
- "version": "1.2.1",
3
+ "version": "1.4.0",
4
4
  "description": "OpenClaw channel plugin for Pond IM",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,8 +16,8 @@
16
16
  ],
17
17
  "dependencies": {
18
18
  "@sinclair/typebox": "^0.34.48",
19
- "@pnds/sdk": "1.2.1",
20
- "@pnds/cli": "1.2.1"
19
+ "@pnds/cli": "1.4.0",
20
+ "@pnds/sdk": "1.4.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^22.0.0",
@@ -19,9 +19,8 @@ interface ConfigManagerOpts {
19
19
 
20
20
  const CACHE_FILENAME = "pond-platform-config.json";
21
21
 
22
- /** All tool names registered by this plugin — must stay in sync with registerPondActionTools + registerPondWikiTools. */
22
+ /** All tool names registered by this plugin — must stay in sync with registerPondWikiTools. */
23
23
  const POND_TOOL_NAMES = [
24
- "pond_reply", "pond_dm", "pond_task_comment", "pond_task_update", "pond_chat_history", "pond_typing",
25
24
  "wiki_list", "wiki_status", "wiki_diff", "wiki_tree", "wiki_blob",
26
25
  "wiki_search", "wiki_query", "wiki_outline", "wiki_section", "wiki_changeset_diff", "wiki_propose",
27
26
  ];
package/src/hooks.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  import { extractAccountIdFromSessionKey } from "./session.js";
3
- import { clearDispatchMessageId, clearSessionMessageId, getActiveRunId, getPondAccountState, getSessionChatId } from "./runtime.js";
3
+ import { clearDispatchMessageId, clearSessionMessageId, getActiveRunId, getDispatchMessageId, getDispatchNoReply, getPondAccountState, getSessionChatId } from "./runtime.js";
4
4
  import { loadToolsMarkdown } from "./tools-md.js";
5
5
 
6
6
  /**
@@ -25,63 +25,77 @@ async function resolveClientForHook(sessionKey: string | undefined) {
25
25
  return { ...state, chatId, activeRunId };
26
26
  }
27
27
 
28
- const ORCHESTRATOR_PREFIX = `## CRITICAL — Pond Orchestrator Mode
29
-
30
- > **Your text output is completely invisible.** No user or agent will ever see
31
- > anything you write as plain text. The ONLY way to communicate is through
32
- > Pond action tools. If you respond with text alone, your response is silently
33
- > discarded.
28
+ const ORCHESTRATOR_PREFIX = `## Pond Orchestrator Mode
34
29
 
35
30
  You are the Pond orchestrator. Events from multiple chats and tasks arrive as
36
- structured \`[Event: ...]\` blocks. You decide how to handle each event and act
37
- through tools.
31
+ structured \`[Event: ...]\` blocks. You decide how to handle each event and
32
+ respond using Bash commands via the Pond CLI.
33
+
34
+ Your text output is internal (not delivered to users). Use the CLI to interact.
35
+
36
+ ### Pond CLI — Pre-Authenticated
37
+
38
+ The Pond CLI (\`npx @pnds/cli@latest\`) is pre-authenticated via environment
39
+ variables injected into every Bash command. You do NOT need to set
40
+ POND_API_URL, POND_API_KEY, etc. — they are already configured.
41
+
42
+ \`POND_CHAT_ID\` is set to the chat that triggered the current event. When
43
+ replying to the trigger chat, you can omit the chatId argument.
38
44
 
39
45
  ### How to Reply to a Message
40
46
 
41
47
  When you receive \`[Event: message.new]\` with \`[Chat: ... (cht_abc123)]\`:
42
48
 
43
- pond_reply(chatId: "cht_abc123", text: "Your response here")
49
+ npx @pnds/cli@latest messages send cht_abc123 --text "Your response here"
50
+
51
+ Or, since POND_CHAT_ID is set to the trigger chat:
52
+
53
+ npx @pnds/cli@latest messages send --text "Your response here"
44
54
 
45
- If the event includes \`[Thread: 01JWC...]\`, reply inside the thread:
55
+ For thread replies (when the event includes \`[Thread: 01JWC...]\`):
46
56
 
47
- pond_reply(chatId: "cht_abc123", threadRootId: "01JWC...", text: "Thread reply here")
57
+ npx @pnds/cli@latest messages send --text "Thread reply" --thread-root-id 01JWC...
48
58
 
49
- DO NOT write text. ONLY \`pond_reply\` delivers messages to users.
59
+ ### How to Send a Direct Message
60
+
61
+ npx @pnds/cli@latest dm usr_xyz --text "Hello"
62
+
63
+ Or by display name:
64
+
65
+ npx @pnds/cli@latest dm "Alice" --text "Hello"
50
66
 
51
67
  ### How to Act on a Task
52
68
 
53
69
  When you receive \`[Event: task.assigned]\` with \`[Task: ... (tsk_xyz789)]\`:
54
70
 
55
- - \`pond_task_comment(taskId: "tsk_xyz789", body: "Starting work...")\` — post progress
56
- - \`pond_task_update(taskId: "tsk_xyz789", status: "in_progress")\` — change status
71
+ npx @pnds/cli@latest tasks comments add tsk_xyz789 --body "Starting work..."
72
+ npx @pnds/cli@latest tasks update tsk_xyz789 --status in_progress
57
73
 
58
- ### Pond Action Tools
74
+ ### Reading Data
59
75
 
60
- - \`pond_reply\` Send a text message to a chat. Params: \`chatId\`, \`text\`, optional \`threadRootId\`, \`noReply\`.
61
- - \`pond_dm\` Send a direct message to a user by ID (auto-creates DM chat). Params: \`userId\`, \`text\`, optional \`noReply\`. Use this for proactive outreach (e.g., notifying a task assigner of completion).
62
- - \`pond_task_comment\` Post a comment on a task. Params: \`taskId\`, \`body\`.
63
- - \`pond_task_update\` Update task fields (status, title, description, priority). Params: \`taskId\`, plus optional fields.
64
- - \`pond_chat_history\` — Read recent messages from a chat (default 20, max 50). Params: \`chatId\`, optional \`limit\`, \`before\`, \`threadRootId\`.
65
- - \`pond_typing\` — Start or stop the typing indicator. Params: \`chatId\`, \`action\` ("start" | "stop").
76
+ npx @pnds/cli@latest messages list <chatId>
77
+ npx @pnds/cli@latest tasks list --status open
78
+ npx @pnds/cli@latest chats list
79
+ npx @pnds/cli@latest members list
66
80
 
67
81
  ### Sub-Agents
68
82
 
69
- For long-running tasks, use \`sessions_spawn\` to delegate to a sub-agent. Return quickly so the orchestrator can process other events.
83
+ For long-running tasks, use \`sessions_spawn\` to delegate to a sub-agent.
84
+ Return quickly so the orchestrator can process other events.
70
85
 
71
- When a sub-agent completes and you receive a completion event, deliver the result to the relevant chat using \`pond_reply\` (or \`pond_dm\` if the target is a user, not a chat). Do NOT rely on text output — it is invisible.
86
+ When a sub-agent completes, deliver the result using the CLI:
87
+
88
+ npx @pnds/cli@latest messages send <chatId> --text "Result: ..."
72
89
 
73
90
  ### Agent-to-Agent Interaction
74
91
 
75
- When you receive a message from another agent (not a human):
76
- - If the message has a \`no_reply\` hint, do not call \`pond_reply\`. You may still reason and use other tools.
77
- - If you have nothing meaningful to add, produce an empty response to avoid unnecessary back-and-forth.
78
- - Use \`pond_reply\` with \`noReply: true\` when sending messages that don't need a response (FYI, results delivery).
92
+ When a message has a \`[Hint: no_reply]\`, do not send a reply. The CLI
93
+ automatically suppresses sends when POND_NO_REPLY is set, but you should
94
+ also skip unnecessary computation.
79
95
 
80
- ### Prohibitions
96
+ Use \`--no-reply\` for FYI messages that don't need a response:
81
97
 
82
- - **DO NOT** write text responses they are invisible and silently discarded.
83
- - **DO NOT** use the Pond CLI (\`@pnds/cli\`) to send messages — use \`pond_reply\` for chat replies and \`pond_dm\` for direct messages.
84
- - **DO NOT** use the \`message\` tool to send to Pond chats — use \`pond_reply\`.`;
98
+ npx @pnds/cli@latest messages send <chatId> --text "FYI: done" --no-reply`;
85
99
 
86
100
  // Load wiki tools, CLI docs, and other tool docs from TOOLS.md (maintained separately)
87
101
  const POND_ORCHESTRATOR_CONTEXT = ORCHESTRATOR_PREFIX + "\n\n" + loadToolsMarkdown();
@@ -94,12 +108,13 @@ export function registerPondHooks(api: OpenClawPluginApi) {
94
108
  return { prependSystemContext: POND_ORCHESTRATOR_CONTEXT };
95
109
  });
96
110
 
97
- // before_tool_call -> send tool_call step to AgentRun
98
- // Fire-and-forget: before_tool_call is an awaited hook in OpenClaw (can block/modify
99
- // tool params), so we must not block tool execution waiting for run creation.
100
- api.on("before_tool_call", (event, ctx) => {
111
+ // before_tool_call -> (1) fire-and-forget step creation, (2) ENV injection for exec tool
112
+ api.on("before_tool_call", async (event, ctx) => {
113
+ const sessionKey = ctx.sessionKey;
114
+
115
+ // (1) Fire-and-forget step creation (all tools)
101
116
  void (async () => {
102
- const resolved = await resolveClientForHook(ctx.sessionKey);
117
+ const resolved = await resolveClientForHook(sessionKey);
103
118
  if (!resolved || !resolved.activeRunId || !event.toolCallId) return;
104
119
  try {
105
120
  await resolved.client.createAgentStep(resolved.orgId, resolved.agentUserId, resolved.activeRunId, {
@@ -116,6 +131,40 @@ export function registerPondHooks(api: OpenClawPluginApi) {
116
131
  log?.warn(`pond hook before_tool_call failed: ${String(err)}`);
117
132
  }
118
133
  })();
134
+
135
+ // (2) ENV injection — only for the exec (Bash) tool
136
+ if (event.toolName !== "exec") return;
137
+ if (!sessionKey) return;
138
+ const accountId = extractAccountIdFromSessionKey(sessionKey);
139
+ if (!accountId) return;
140
+ const state = getPondAccountState(accountId);
141
+ if (!state) return;
142
+
143
+ // Dynamic per-dispatch context (changes each dispatch)
144
+ const runId = await getActiveRunId(state, sessionKey);
145
+ const chatId = getSessionChatId(sessionKey);
146
+ const triggerMsgId = getDispatchMessageId(sessionKey);
147
+ const noReply = getDispatchNoReply(sessionKey);
148
+
149
+ const injectedEnv: Record<string, string> = {};
150
+ if (runId) injectedEnv.POND_RUN_ID = runId;
151
+ if (chatId) injectedEnv.POND_CHAT_ID = chatId;
152
+ if (triggerMsgId) injectedEnv.POND_TRIGGER_MESSAGE_ID = triggerMsgId;
153
+ if (noReply) injectedEnv.POND_NO_REPLY = "1";
154
+ if (state.wikiMountRoot) injectedEnv.POND_WIKI_MOUNT_ROOT = state.wikiMountRoot;
155
+
156
+ // OpenClaw context (upstream doesn't inject these into exec env yet)
157
+ injectedEnv.OPENCLAW_SESSION_KEY = sessionKey;
158
+ if (event.toolCallId) injectedEnv.OPENCLAW_TOOL_CALL_ID = event.toolCallId;
159
+
160
+ // Shallow merge — preserve agent's original env params
161
+ const existingEnv = (event.params as Record<string, unknown>)?.env as Record<string, string> | undefined;
162
+
163
+ return {
164
+ params: {
165
+ env: { ...existingEnv, ...injectedEnv },
166
+ },
167
+ };
119
168
  });
120
169
 
121
170
  // after_tool_call -> send tool_result step to AgentRun
package/src/index.ts CHANGED
@@ -4,7 +4,6 @@ import { pondPlugin } from "./channel.js";
4
4
  import { setPondRuntime } from "./runtime.js";
5
5
  import { registerPondHooks } from "./hooks.js";
6
6
  import { registerPondWikiTools } from "./wiki-tools.js";
7
- import { registerPondActionTools } from "./action-tools.js";
8
7
 
9
8
  const plugin = {
10
9
  id: "pond",
@@ -15,7 +14,6 @@ const plugin = {
15
14
  setPondRuntime(api.runtime);
16
15
  api.registerChannel({ plugin: pondPlugin });
17
16
  registerPondWikiTools(api);
18
- registerPondActionTools(api);
19
17
  registerPondHooks(api);
20
18
  },
21
19
  };
package/src/runtime.ts CHANGED
@@ -95,7 +95,7 @@ export function clearDispatchMessageId(sessionKey: string) {
95
95
  }
96
96
 
97
97
  // Per-dispatch noReply flag — deterministic suppression for agent-to-agent loop prevention.
98
- // When true, pond_reply hard-blocks sends and records a suppressed AgentStep instead.
98
+ // Injected as POND_NO_REPLY=1 into Bash env; the CLI checks and suppresses sends.
99
99
  const dispatchNoReplyMap = new Map<string, boolean>();
100
100
 
101
101
  export function setDispatchNoReply(sessionKey: string, noReply: boolean) {
package/src/wiki-tools.ts CHANGED
@@ -142,15 +142,8 @@ function formatWikiBlobResult(result: Awaited<ReturnType<typeof getWikiBlob>>) {
142
142
  slug: result.wiki.slug,
143
143
  name: result.wiki.name,
144
144
  },
145
- blob: {
146
- path: result.blob.path,
147
- ref: result.blob.ref,
148
- size: result.blob.size,
149
- mime_type: result.blob.mime_type,
150
- is_binary: result.blob.is_binary,
151
- encoding: result.blob.encoding,
152
- },
153
- text: result.text,
145
+ content: result.content,
146
+ size: result.size,
154
147
  };
155
148
  }
156
149
 
@@ -223,7 +216,6 @@ function createWikiTools(toolCtx: ToolSessionContext): AnyAgentTool[] {
223
216
  return jsonResult(
224
217
  await getWikiTree(pondContext(), readStringParam(params, "wiki", { required: true }), {
225
218
  path: readStringParam(params, "path"),
226
- ref: readStringParam(params, "ref"),
227
219
  }),
228
220
  );
229
221
  },
@@ -236,7 +228,6 @@ function createWikiTools(toolCtx: ToolSessionContext): AnyAgentTool[] {
236
228
  execute: async (_toolCallId, params) => {
237
229
  const result = await getWikiBlob(pondContext(), readStringParam(params, "wiki", { required: true }), {
238
230
  path: readStringParam(params, "path", { required: true }),
239
- ref: readStringParam(params, "ref"),
240
231
  });
241
232
  return jsonResult(formatWikiBlobResult(result));
242
233
  },
@@ -1,256 +0,0 @@
1
- import { Type } from "@sinclair/typebox";
2
- import type { OpenClawPluginApi, AnyAgentTool } from "openclaw/plugin-sdk";
3
- import { readNumberParam, readStringParam } from "openclaw/plugin-sdk/param-readers";
4
- import { jsonResult } from "./tool-helpers.js";
5
- import { PondClient } from "@pnds/sdk";
6
- import type { Message } from "@pnds/sdk";
7
- import {
8
- getActiveRunId,
9
- getAllPondAccountStates,
10
- getPondAccountState,
11
- getDispatchNoReply,
12
- } from "./runtime.js";
13
- import { extractAccountIdFromSessionKey } from "./session.js";
14
-
15
- type ToolSessionContext = {
16
- sessionKey?: string;
17
- agentAccountId?: string;
18
- agentId?: string;
19
- };
20
-
21
- function resolvePondState(ctx: ToolSessionContext) {
22
- const accountId = ctx.agentAccountId ?? extractAccountIdFromSessionKey(ctx.sessionKey);
23
- if (accountId) {
24
- const state = getPondAccountState(accountId);
25
- if (state) return state;
26
- }
27
-
28
- const states = Array.from(getAllPondAccountStates().values());
29
- if (states.length === 1) return states[0];
30
-
31
- const baseUrl = process.env.POND_API_URL?.trim();
32
- const token = process.env.POND_API_KEY?.trim();
33
- const orgId = process.env.POND_ORG_ID?.trim();
34
- if (baseUrl && token && orgId) {
35
- return {
36
- client: new PondClient({ baseUrl, token }),
37
- orgId,
38
- agentUserId: "",
39
- activeRuns: new Map<string, Promise<string | undefined>>(),
40
- };
41
- }
42
-
43
- throw new Error(
44
- "Pond action tools are unavailable: missing live Pond runtime state and POND_API_URL/POND_API_KEY/POND_ORG_ID.",
45
- );
46
- }
47
-
48
- // ---- Schemas ----
49
-
50
- const ReplySchema = Type.Object({
51
- chatId: Type.String({ minLength: 1, description: "Chat ID to send the message to." }),
52
- text: Type.String({ minLength: 1, description: "Message text." }),
53
- threadRootId: Type.Optional(Type.String({ description: "If provided, send the message as a thread reply under this root message ID." })),
54
- noReply: Type.Optional(Type.Boolean({ description: "If true, hints that no reply is expected." })),
55
- });
56
-
57
- const TaskCommentSchema = Type.Object({
58
- taskId: Type.String({ minLength: 1, description: "Task ID to comment on." }),
59
- body: Type.String({ minLength: 1, description: "Comment body text." }),
60
- });
61
-
62
- const TaskUpdateSchema = Type.Object({
63
- taskId: Type.String({ minLength: 1, description: "Task ID to update." }),
64
- status: Type.Optional(Type.String({ description: "New task status." })),
65
- title: Type.Optional(Type.String({ description: "New task title." })),
66
- description: Type.Optional(Type.String({ description: "New task description." })),
67
- priority: Type.Optional(Type.String({ description: "New task priority." })),
68
- });
69
-
70
- const ChatHistorySchema = Type.Object({
71
- chatId: Type.String({ minLength: 1, description: "Chat ID to read messages from." }),
72
- limit: Type.Optional(Type.Number({ minimum: 1, maximum: 50, description: "Number of messages to fetch (default 20, max 50)." })),
73
- before: Type.Optional(Type.String({ description: "Message ID cursor — fetch messages before this ID." })),
74
- threadRootId: Type.Optional(Type.String({ description: "If provided, fetch messages within this thread instead of top-level messages." })),
75
- });
76
-
77
- const TypingSchema = Type.Object({
78
- chatId: Type.String({ minLength: 1, description: "Chat ID for the typing indicator." }),
79
- action: Type.Union([Type.Literal("start"), Type.Literal("stop")], { description: "Start or stop the typing indicator." }),
80
- });
81
-
82
- const DmSchema = Type.Object({
83
- userId: Type.String({ minLength: 1, description: "Target user ID to DM." }),
84
- text: Type.String({ minLength: 1, description: "Message text." }),
85
- noReply: Type.Optional(Type.Boolean({ description: "If true, hints that no reply is expected." })),
86
- });
87
-
88
- // ---- Helpers ----
89
-
90
- function formatMessage(msg: Message): string {
91
- const senderName = msg.sender?.display_name ?? msg.sender_id;
92
- const content = msg.content as Record<string, unknown>;
93
- if (msg.message_type === "text") {
94
- return `${senderName} (${msg.sender_id}): ${(content as { text?: string }).text ?? ""}`;
95
- }
96
- if (msg.message_type === "file") {
97
- return `${senderName} (${msg.sender_id}): [file: ${(content as { file_name?: string }).file_name ?? "unknown"}]`;
98
- }
99
- if (msg.message_type === "system") {
100
- return `[system: ${(content as { text?: string }).text ?? JSON.stringify(content)}]`;
101
- }
102
- return `${senderName} (${msg.sender_id}): [${msg.message_type}]`;
103
- }
104
-
105
- // ---- Tool factory ----
106
-
107
- function createActionTools(toolCtx: ToolSessionContext): AnyAgentTool[] {
108
- const state = () => resolvePondState(toolCtx);
109
-
110
- return [
111
- {
112
- label: "Pond Reply",
113
- name: "pond_reply",
114
- description: "Send a text message to a Pond chat.",
115
- parameters: ReplySchema,
116
- execute: async (_toolCallId, params) => {
117
- const s = state();
118
- const chatId = readStringParam(params, "chatId", { required: true });
119
- const text = readStringParam(params, "text", { required: true });
120
- const threadRootId = readStringParam(params, "threadRootId");
121
- const noReply = params.noReply === true;
122
- const runId = s.activeRuns && toolCtx.sessionKey
123
- ? await getActiveRunId(s, toolCtx.sessionKey)
124
- : undefined;
125
-
126
- // Deterministic no_reply suppression: if the inbound event had no_reply hint,
127
- // hard-block the send and record a suppressed step instead of posting to chat.
128
- // This prevents agent-to-agent reply loops at the plugin layer (not just prompt).
129
- if (toolCtx.sessionKey && getDispatchNoReply(toolCtx.sessionKey)) {
130
- if (runId) {
131
- try {
132
- await s.client.createAgentStep(s.orgId, s.agentUserId, runId, {
133
- step_type: "text",
134
- content: { text, suppressed: true, reason: "no_reply" },
135
- });
136
- } catch { /* best-effort step recording */ }
137
- }
138
- return jsonResult({ suppressed: true, reason: "no_reply", chat_id: chatId });
139
- }
140
-
141
- const result = await s.client.sendMessage(s.orgId, chatId, {
142
- message_type: "text",
143
- content: { text },
144
- agent_run_id: runId,
145
- ...(threadRootId ? { thread_root_id: threadRootId } : {}),
146
- ...(noReply ? { hints: { no_reply: true } } : {}),
147
- });
148
- return jsonResult({ message_id: result.id, chat_id: chatId });
149
- },
150
- },
151
- {
152
- label: "Pond Task Comment",
153
- name: "pond_task_comment",
154
- description: "Post a comment on a Pond task.",
155
- parameters: TaskCommentSchema,
156
- execute: async (_toolCallId, params) => {
157
- const s = state();
158
- const taskId = readStringParam(params, "taskId", { required: true });
159
- const body = readStringParam(params, "body", { required: true });
160
- const result = await s.client.createTaskComment(s.orgId, taskId, { body });
161
- return jsonResult(result);
162
- },
163
- },
164
- {
165
- label: "Pond Task Update",
166
- name: "pond_task_update",
167
- description: "Update fields on a Pond task (status, title, description, priority).",
168
- parameters: TaskUpdateSchema,
169
- execute: async (_toolCallId, params) => {
170
- const s = state();
171
- const taskId = readStringParam(params, "taskId", { required: true });
172
- const update: Record<string, string> = {};
173
- for (const field of ["status", "title", "description", "priority"] as const) {
174
- const val = readStringParam(params, field);
175
- if (val !== undefined) update[field] = val;
176
- }
177
- const result = await s.client.updateTask(s.orgId, taskId, update);
178
- return jsonResult(result);
179
- },
180
- },
181
- {
182
- label: "Pond Chat History",
183
- name: "pond_chat_history",
184
- description: "Read recent messages from a Pond chat. Use this to get context on unfamiliar conversations.",
185
- parameters: ChatHistorySchema,
186
- execute: async (_toolCallId, params) => {
187
- const s = state();
188
- const chatId = readStringParam(params, "chatId", { required: true });
189
- const limit = Math.min(readNumberParam(params, "limit", { integer: true }) ?? 20, 50);
190
- const before = readStringParam(params, "before");
191
- const threadRootId = readStringParam(params, "threadRootId");
192
- const threadParams = threadRootId
193
- ? { thread_root_id: threadRootId }
194
- : { top_level: true as const };
195
- const result = await s.client.getMessages(s.orgId, chatId, {
196
- limit,
197
- before,
198
- ...threadParams,
199
- });
200
- const messages = result.data.map(formatMessage);
201
- return jsonResult({
202
- messages,
203
- has_more: result.has_more,
204
- ...(result.next_cursor ? { next_cursor: result.next_cursor } : {}),
205
- });
206
- },
207
- },
208
- {
209
- label: "Pond Typing",
210
- name: "pond_typing",
211
- description: "Start or stop the typing indicator in a Pond chat.",
212
- parameters: TypingSchema,
213
- execute: async (_toolCallId, params) => {
214
- const s = state();
215
- if (!s.ws) {
216
- return jsonResult({ error: "WebSocket not available — typing indicator requires a live connection." });
217
- }
218
- const chatId = readStringParam(params, "chatId", { required: true });
219
- const action = readStringParam(params, "action", { required: true }) as "start" | "stop";
220
- s.ws.sendTyping(chatId, action);
221
- return jsonResult({ ok: true });
222
- },
223
- },
224
- {
225
- label: "Pond DM",
226
- name: "pond_dm",
227
- description: "Send a direct message to a user by ID. Automatically finds or creates the DM chat.",
228
- parameters: DmSchema,
229
- execute: async (_toolCallId, params) => {
230
- const s = state();
231
- const userId = readStringParam(params, "userId", { required: true });
232
- const text = readStringParam(params, "text", { required: true });
233
- const noReply = params.noReply === true;
234
- const runId = s.activeRuns && toolCtx.sessionKey
235
- ? await getActiveRunId(s, toolCtx.sessionKey)
236
- : undefined;
237
-
238
- const result = await s.client.sendDirectMessage(s.orgId, {
239
- user_id: userId,
240
- message_type: "text",
241
- content: { text },
242
- agent_run_id: runId,
243
- ...(noReply ? { hints: { no_reply: true } } : {}),
244
- });
245
- return jsonResult({ chat_id: result.chat.id, message_id: result.message.id });
246
- },
247
- },
248
- ];
249
- }
250
-
251
- export function registerPondActionTools(api: OpenClawPluginApi) {
252
- api.registerTool(
253
- (ctx) => createActionTools(ctx),
254
- { names: ["pond_reply", "pond_task_comment", "pond_task_update", "pond_chat_history", "pond_typing", "pond_dm"] },
255
- );
256
- }