@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 +23 -15
- package/package.json +3 -3
- package/src/config-manager.ts +17 -0
- package/src/hooks.ts +104 -24
- package/src/index.ts +0 -2
- package/src/runtime.ts +1 -1
- package/src/action-tools.ts +0 -223
package/TOOLS.md
CHANGED
|
@@ -25,30 +25,38 @@ Typical wiki edit flow:
|
|
|
25
25
|
|
|
26
26
|
## Pond CLI
|
|
27
27
|
|
|
28
|
-
You
|
|
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
|
|
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
|
|
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
|
-
|
|
54
|
+
### Task Operations
|
|
48
55
|
|
|
49
|
-
|
|
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
|
-
|
|
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.
|
|
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/
|
|
20
|
-
"@pnds/
|
|
19
|
+
"@pnds/sdk": "1.3.0",
|
|
20
|
+
"@pnds/cli": "1.3.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/node": "^22.0.0",
|
package/src/config-manager.ts
CHANGED
|
@@ -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
|
|
28
|
+
const ORCHESTRATOR_PREFIX = `## Pond Orchestrator Mode
|
|
29
29
|
|
|
30
|
-
You are
|
|
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
|
-
|
|
34
|
+
Your text output is internal (not delivered to users). Use the CLI to interact.
|
|
33
35
|
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
+
### How to Reply to a Message
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
When you receive \`[Event: message.new]\` with \`[Chat: ... (cht_abc123)]\`:
|
|
47
48
|
|
|
48
|
-
|
|
49
|
+
npx @pnds/cli@latest messages send cht_abc123 --text "Your response here"
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 {
|
|
108
|
+
return { prependSystemContext: POND_ORCHESTRATOR_CONTEXT };
|
|
64
109
|
});
|
|
65
110
|
|
|
66
|
-
// before_tool_call ->
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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(
|
|
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
|
-
//
|
|
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/action-tools.ts
DELETED
|
@@ -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
|
-
}
|