@pnds/pond 1.2.0 → 1.3.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
@@ -25,30 +25,38 @@ Typical wiki edit flow:
25
25
 
26
26
  ## Pond CLI
27
27
 
28
- You also have 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.
31
+
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
29
42
 
30
43
  ```bash
31
44
  npx @pnds/cli@latest whoami # Check your identity
32
- npx @pnds/cli@latest agents list # List all agents (name, model, status)
33
- npx @pnds/cli@latest dm <name-or-id> --text "..." # DM a user by name or ID (auto-creates chat)
34
- npx @pnds/cli@latest dm <name-or-id> --text "..." --no-reply # DM, signal no reply expected
45
+ npx @pnds/cli@latest agents list # List all agents
35
46
  npx @pnds/cli@latest chats list # List chats
36
47
  npx @pnds/cli@latest messages list <chatId> # Read chat history
37
- npx @pnds/cli@latest messages send <chatId> --text "..." # Send a message
38
- npx @pnds/cli@latest messages send <chatId> --text "..." --no-reply # Send, no reply expected
39
48
  npx @pnds/cli@latest tasks list [--status ...] # List tasks
40
- npx @pnds/cli@latest tasks create --title "..." # Create a task
41
- npx @pnds/cli@latest tasks update <taskId> --status in_progress
42
49
  npx @pnds/cli@latest projects list # List projects
43
- npx @pnds/cli@latest users search <query> # Find users
50
+ npx @pnds/cli@latest users get <userId> # Get user by ID
44
51
  npx @pnds/cli@latest members list # List org members
45
52
  ```
46
53
 
47
- Run `npx @pnds/cli@latest --help` or `npx @pnds/cli@latest <command> --help` for full options. Output is JSON.
54
+ ### Task Operations
48
55
 
49
- ## Agent-to-Agent Interaction
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
+ ```
50
61
 
51
- When you receive a message from another agent (not a human):
52
- - If the message has a no_reply hint, you may still reason and use tools, but your response will not be sent to chat.
53
- - If you have nothing meaningful to add to the conversation, produce an empty response to avoid unnecessary back-and-forth.
54
- - Use --no-reply when sending messages that don't require a response (e.g., delivering results, FYI notifications).
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.0",
3
+ "version": "1.3.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/cli": "1.2.0",
20
- "@pnds/sdk": "1.2.0"
19
+ "@pnds/sdk": "1.3.0",
20
+ "@pnds/cli": "1.3.0"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/node": "^22.0.0",
@@ -19,6 +19,12 @@ 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 registerPondWikiTools. */
23
+ const POND_TOOL_NAMES = [
24
+ "wiki_list", "wiki_status", "wiki_diff", "wiki_tree", "wiki_blob",
25
+ "wiki_search", "wiki_query", "wiki_outline", "wiki_section", "wiki_changeset_diff", "wiki_propose",
26
+ ];
27
+
22
28
  function cachePath(stateDir: string): string {
23
29
  return path.join(stateDir, CACHE_FILENAME);
24
30
  }
@@ -91,6 +97,17 @@ function applyToOpenClawConfig(
91
97
  existing.agents = agents;
92
98
  }
93
99
 
100
+ // Ensure Pond plugin tools are allowed alongside whatever tools.profile is set.
101
+ // Uses `alsoAllow` (additive) so user's profile choice is preserved.
102
+ const tools =
103
+ typeof existing.tools === "object"
104
+ && existing.tools !== null
105
+ && !Array.isArray(existing.tools)
106
+ ? (existing.tools as Record<string, unknown>)
107
+ : {};
108
+ tools.alsoAllow = POND_TOOL_NAMES;
109
+ existing.tools = tools;
110
+
94
111
  // Atomic write
95
112
  const tmpPath = `${configPath}.tmp`;
96
113
  fs.mkdirSync(path.dirname(configPath), { recursive: true });
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,32 +25,77 @@ async function resolveClientForHook(sessionKey: string | undefined) {
25
25
  return { ...state, chatId, activeRunId };
26
26
  }
27
27
 
28
- const ORCHESTRATOR_PREFIX = `## Orchestrator Model
28
+ const ORCHESTRATOR_PREFIX = `## Pond Orchestrator Mode
29
29
 
30
- You are an orchestrator agent. Events arrive as structured \`[Event: ...]\` blocks. Your text output is internal reasoning only — users cannot see it. All visible actions must go through tools.
30
+ You are the Pond orchestrator. Events from multiple chats and tasks arrive as
31
+ structured \`[Event: ...]\` blocks. You decide how to handle each event and
32
+ respond using Bash commands via the Pond CLI.
31
33
 
32
- ## Pond Action Tools
34
+ Your text output is internal (not delivered to users). Use the CLI to interact.
33
35
 
34
- - \`pond_reply\`Send a text message to a chat. Params: \`chatId\`, \`text\`, optional \`noReply\`.
35
- - \`pond_task_comment\` — Post a comment on a task. Params: \`taskId\`, \`body\`.
36
- - \`pond_task_update\` — Update task fields (status, title, description, priority). Params: \`taskId\`, plus optional fields.
37
- - \`pond_chat_history\` — Read recent messages from a chat (default 20, max 50). Use this to get context on unfamiliar chats. Params: \`chatId\`, optional \`limit\`, \`before\`, \`threadRootId\`.
38
- - \`pond_typing\` — Start or stop the typing indicator. Params: \`chatId\`, \`action\` ("start" | "stop").
36
+ ### Pond CLI Pre-Authenticated
39
37
 
40
- ## Task Handling
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
41
 
42
- Use \`pond_task_comment\` to communicate progress on tasks. Use \`pond_task_update\` to change task status (e.g., in_progress, done), title, description, or priority.
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.
43
44
 
44
- ## Sub-Agents
45
+ ### How to Reply to a Message
45
46
 
46
- For long-running tasks, use \`sessions_spawn\` to delegate to a sub-agent. Return quickly so the orchestrator can process other events.
47
+ When you receive \`[Event: message.new]\` with \`[Chat: ... (cht_abc123)]\`:
47
48
 
48
- ## Agent-to-Agent Interaction
49
+ npx @pnds/cli@latest messages send cht_abc123 --text "Your response here"
49
50
 
50
- When you receive a message from another agent (not a human):
51
- - If the message has a no_reply hint, you may still reason and use tools, but your response will not be sent to chat.
52
- - If you have nothing meaningful to add to the conversation, produce an empty response to avoid unnecessary back-and-forth.
53
- - Replies go through \`pond_reply\` — use \`noReply: true\` when sending messages that don't require a response (e.g., delivering results, FYI notifications).`;
51
+ Or, since POND_CHAT_ID is set to the trigger chat:
52
+
53
+ npx @pnds/cli@latest messages send --text "Your response here"
54
+
55
+ For thread replies (when the event includes \`[Thread: 01JWC...]\`):
56
+
57
+ npx @pnds/cli@latest messages send --text "Thread reply" --thread-root-id 01JWC...
58
+
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"
66
+
67
+ ### How to Act on a Task
68
+
69
+ When you receive \`[Event: task.assigned]\` with \`[Task: ... (tsk_xyz789)]\`:
70
+
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
73
+
74
+ ### Reading Data
75
+
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
80
+
81
+ ### Sub-Agents
82
+
83
+ For long-running tasks, use \`sessions_spawn\` to delegate to a sub-agent.
84
+ Return quickly so the orchestrator can process other events.
85
+
86
+ When a sub-agent completes, deliver the result using the CLI:
87
+
88
+ npx @pnds/cli@latest messages send <chatId> --text "Result: ..."
89
+
90
+ ### Agent-to-Agent Interaction
91
+
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.
95
+
96
+ Use \`--no-reply\` for FYI messages that don't need a response:
97
+
98
+ npx @pnds/cli@latest messages send <chatId> --text "FYI: done" --no-reply`;
54
99
 
55
100
  // Load wiki tools, CLI docs, and other tool docs from TOOLS.md (maintained separately)
56
101
  const POND_ORCHESTRATOR_CONTEXT = ORCHESTRATOR_PREFIX + "\n\n" + loadToolsMarkdown();
@@ -60,15 +105,16 @@ export function registerPondHooks(api: OpenClawPluginApi) {
60
105
 
61
106
  // Inject CLI awareness into agent system prompt
62
107
  api.on("before_prompt_build", () => {
63
- return { prependContext: POND_ORCHESTRATOR_CONTEXT };
108
+ return { prependSystemContext: POND_ORCHESTRATOR_CONTEXT };
64
109
  });
65
110
 
66
- // before_tool_call -> send tool_call step to AgentRun
67
- // Fire-and-forget: before_tool_call is an awaited hook in OpenClaw (can block/modify
68
- // tool params), so we must not block tool execution waiting for run creation.
69
- 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)
70
116
  void (async () => {
71
- const resolved = await resolveClientForHook(ctx.sessionKey);
117
+ const resolved = await resolveClientForHook(sessionKey);
72
118
  if (!resolved || !resolved.activeRunId || !event.toolCallId) return;
73
119
  try {
74
120
  await resolved.client.createAgentStep(resolved.orgId, resolved.agentUserId, resolved.activeRunId, {
@@ -85,6 +131,40 @@ export function registerPondHooks(api: OpenClawPluginApi) {
85
131
  log?.warn(`pond hook before_tool_call failed: ${String(err)}`);
86
132
  }
87
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
+ };
88
168
  });
89
169
 
90
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) {
@@ -1,223 +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
- noReply: Type.Optional(Type.Boolean({ description: "If true, hints that no reply is expected." })),
54
- });
55
-
56
- const TaskCommentSchema = Type.Object({
57
- taskId: Type.String({ minLength: 1, description: "Task ID to comment on." }),
58
- body: Type.String({ minLength: 1, description: "Comment body text." }),
59
- });
60
-
61
- const TaskUpdateSchema = Type.Object({
62
- taskId: Type.String({ minLength: 1, description: "Task ID to update." }),
63
- status: Type.Optional(Type.String({ description: "New task status." })),
64
- title: Type.Optional(Type.String({ description: "New task title." })),
65
- description: Type.Optional(Type.String({ description: "New task description." })),
66
- priority: Type.Optional(Type.String({ description: "New task priority." })),
67
- });
68
-
69
- const ChatHistorySchema = Type.Object({
70
- chatId: Type.String({ minLength: 1, description: "Chat ID to read messages from." }),
71
- limit: Type.Optional(Type.Number({ minimum: 1, maximum: 50, description: "Number of messages to fetch (default 20, max 50)." })),
72
- before: Type.Optional(Type.String({ description: "Message ID cursor — fetch messages before this ID." })),
73
- threadRootId: Type.Optional(Type.String({ description: "If provided, fetch messages within this thread instead of top-level messages." })),
74
- });
75
-
76
- const TypingSchema = Type.Object({
77
- chatId: Type.String({ minLength: 1, description: "Chat ID for the typing indicator." }),
78
- action: Type.Union([Type.Literal("start"), Type.Literal("stop")], { description: "Start or stop the typing indicator." }),
79
- });
80
-
81
- // ---- Helpers ----
82
-
83
- function formatMessage(msg: Message): string {
84
- const senderName = msg.sender?.display_name ?? msg.sender_id;
85
- const content = msg.content as Record<string, unknown>;
86
- if (msg.message_type === "text") {
87
- return `${senderName} (${msg.sender_id}): ${(content as { text?: string }).text ?? ""}`;
88
- }
89
- if (msg.message_type === "file") {
90
- return `${senderName} (${msg.sender_id}): [file: ${(content as { file_name?: string }).file_name ?? "unknown"}]`;
91
- }
92
- if (msg.message_type === "system") {
93
- return `[system: ${(content as { text?: string }).text ?? JSON.stringify(content)}]`;
94
- }
95
- return `${senderName} (${msg.sender_id}): [${msg.message_type}]`;
96
- }
97
-
98
- // ---- Tool factory ----
99
-
100
- function createActionTools(toolCtx: ToolSessionContext): AnyAgentTool[] {
101
- const state = () => resolvePondState(toolCtx);
102
-
103
- return [
104
- {
105
- label: "Pond Reply",
106
- name: "pond_reply",
107
- description: "Send a text message to a Pond chat.",
108
- parameters: ReplySchema,
109
- execute: async (_toolCallId, params) => {
110
- const s = state();
111
- const chatId = readStringParam(params, "chatId", { required: true });
112
- const text = readStringParam(params, "text", { required: true });
113
- const noReply = params.noReply === true;
114
- const runId = s.activeRuns && toolCtx.sessionKey
115
- ? await getActiveRunId(s, toolCtx.sessionKey)
116
- : undefined;
117
-
118
- // Deterministic no_reply suppression: if the inbound event had no_reply hint,
119
- // hard-block the send and record a suppressed step instead of posting to chat.
120
- // This prevents agent-to-agent reply loops at the plugin layer (not just prompt).
121
- if (toolCtx.sessionKey && getDispatchNoReply(toolCtx.sessionKey)) {
122
- if (runId) {
123
- try {
124
- await s.client.createAgentStep(s.orgId, s.agentUserId, runId, {
125
- step_type: "text",
126
- content: { text, suppressed: true, reason: "no_reply" },
127
- });
128
- } catch { /* best-effort step recording */ }
129
- }
130
- return jsonResult({ suppressed: true, reason: "no_reply", chat_id: chatId });
131
- }
132
-
133
- const result = await s.client.sendMessage(s.orgId, chatId, {
134
- message_type: "text",
135
- content: { text },
136
- agent_run_id: runId,
137
- ...(noReply ? { hints: { no_reply: true } } : {}),
138
- });
139
- return jsonResult({ message_id: result.id, chat_id: chatId });
140
- },
141
- },
142
- {
143
- label: "Pond Task Comment",
144
- name: "pond_task_comment",
145
- description: "Post a comment on a Pond task.",
146
- parameters: TaskCommentSchema,
147
- execute: async (_toolCallId, params) => {
148
- const s = state();
149
- const taskId = readStringParam(params, "taskId", { required: true });
150
- const body = readStringParam(params, "body", { required: true });
151
- const result = await s.client.createTaskComment(s.orgId, taskId, { body });
152
- return jsonResult(result);
153
- },
154
- },
155
- {
156
- label: "Pond Task Update",
157
- name: "pond_task_update",
158
- description: "Update fields on a Pond task (status, title, description, priority).",
159
- parameters: TaskUpdateSchema,
160
- execute: async (_toolCallId, params) => {
161
- const s = state();
162
- const taskId = readStringParam(params, "taskId", { required: true });
163
- const update: Record<string, string> = {};
164
- for (const field of ["status", "title", "description", "priority"] as const) {
165
- const val = readStringParam(params, field);
166
- if (val !== undefined) update[field] = val;
167
- }
168
- const result = await s.client.updateTask(s.orgId, taskId, update);
169
- return jsonResult(result);
170
- },
171
- },
172
- {
173
- label: "Pond Chat History",
174
- name: "pond_chat_history",
175
- description: "Read recent messages from a Pond chat. Use this to get context on unfamiliar conversations.",
176
- parameters: ChatHistorySchema,
177
- execute: async (_toolCallId, params) => {
178
- const s = state();
179
- const chatId = readStringParam(params, "chatId", { required: true });
180
- const limit = Math.min(readNumberParam(params, "limit", { integer: true }) ?? 20, 50);
181
- const before = readStringParam(params, "before");
182
- const threadRootId = readStringParam(params, "threadRootId");
183
- const threadParams = threadRootId
184
- ? { thread_root_id: threadRootId }
185
- : { top_level: true as const };
186
- const result = await s.client.getMessages(s.orgId, chatId, {
187
- limit,
188
- before,
189
- ...threadParams,
190
- });
191
- const messages = result.data.map(formatMessage);
192
- return jsonResult({
193
- messages,
194
- has_more: result.has_more,
195
- ...(result.next_cursor ? { next_cursor: result.next_cursor } : {}),
196
- });
197
- },
198
- },
199
- {
200
- label: "Pond Typing",
201
- name: "pond_typing",
202
- description: "Start or stop the typing indicator in a Pond chat.",
203
- parameters: TypingSchema,
204
- execute: async (_toolCallId, params) => {
205
- const s = state();
206
- if (!s.ws) {
207
- return jsonResult({ error: "WebSocket not available — typing indicator requires a live connection." });
208
- }
209
- const chatId = readStringParam(params, "chatId", { required: true });
210
- const action = readStringParam(params, "action", { required: true }) as "start" | "stop";
211
- s.ws.sendTyping(chatId, action);
212
- return jsonResult({ ok: true });
213
- },
214
- },
215
- ];
216
- }
217
-
218
- export function registerPondActionTools(api: OpenClawPluginApi) {
219
- api.registerTool(
220
- (ctx) => createActionTools(ctx),
221
- { names: ["pond_reply", "pond_task_comment", "pond_task_update", "pond_chat_history", "pond_typing"] },
222
- );
223
- }