@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 +23 -5
- package/package.json +3 -3
- package/src/config-manager.ts +1 -2
- package/src/hooks.ts +86 -37
- package/src/index.ts +0 -2
- package/src/runtime.ts +1 -1
- package/src/wiki-tools.ts +2 -11
- package/src/action-tools.ts +0 -256
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
|
|
26
|
+
## Pond CLI
|
|
27
27
|
|
|
28
|
-
You have
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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/
|
|
20
|
-
"@pnds/
|
|
19
|
+
"@pnds/cli": "1.4.0",
|
|
20
|
+
"@pnds/sdk": "1.4.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^22.0.0",
|
package/src/config-manager.ts
CHANGED
|
@@ -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
|
|
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 = `##
|
|
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
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
For thread replies (when the event includes \`[Thread: 01JWC...]\`):
|
|
46
56
|
|
|
47
|
-
|
|
57
|
+
npx @pnds/cli@latest messages send --text "Thread reply" --thread-root-id 01JWC...
|
|
48
58
|
|
|
49
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
###
|
|
74
|
+
### Reading Data
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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.
|
|
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
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
96
|
+
Use \`--no-reply\` for FYI messages that don't need a response:
|
|
81
97
|
|
|
82
|
-
|
|
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 ->
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
|
|
146
|
-
|
|
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
|
},
|
package/src/action-tools.ts
DELETED
|
@@ -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
|
-
}
|