@pnds/pond 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/gateway.ts +99 -16
- package/src/hooks.ts +12 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pnds/pond",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "OpenClaw channel plugin for Pond IM",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"openclaw.plugin.json"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@pnds/sdk": "1.0
|
|
17
|
+
"@pnds/sdk": "1.1.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"typescript": "^5.7.0"
|
package/src/gateway.ts
CHANGED
|
@@ -38,6 +38,60 @@ async function fetchAllChats(
|
|
|
38
38
|
return total;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Fetch recent messages from a chat (or thread) and format them as a history context block.
|
|
43
|
+
* Pass threadRootId to scope to a thread; omit (or null) to fetch top-level messages only.
|
|
44
|
+
* Returns empty string on failure or timeout (non-fatal).
|
|
45
|
+
*/
|
|
46
|
+
async function buildChatHistoryContext(
|
|
47
|
+
client: PondClient,
|
|
48
|
+
orgId: string,
|
|
49
|
+
chatId: string,
|
|
50
|
+
beforeMessageId: string,
|
|
51
|
+
agentUserId: string,
|
|
52
|
+
limit: number = 30,
|
|
53
|
+
log?: { warn: (msg: string) => void },
|
|
54
|
+
threadRootId?: string | null,
|
|
55
|
+
): Promise<string> {
|
|
56
|
+
try {
|
|
57
|
+
const threadParams = threadRootId
|
|
58
|
+
? { thread_root_id: threadRootId }
|
|
59
|
+
: { top_level: true as const };
|
|
60
|
+
const res = await client.getMessages(orgId, chatId, { before: beforeMessageId, limit, ...threadParams });
|
|
61
|
+
if (!res.data.length) return "";
|
|
62
|
+
|
|
63
|
+
const escapeHistoryValue = (value: string): string =>
|
|
64
|
+
value
|
|
65
|
+
.replace(/\r?\n/g, "\\n")
|
|
66
|
+
.replace(/\[Recent chat history\]|\[End of chat history\]/g, (m) => `\\${m}`);
|
|
67
|
+
|
|
68
|
+
const lines: string[] = [];
|
|
69
|
+
for (const msg of res.data) {
|
|
70
|
+
if (msg.message_type !== "text" && msg.message_type !== "file") continue;
|
|
71
|
+
const senderLabel = escapeHistoryValue(msg.sender?.display_name ?? msg.sender_id);
|
|
72
|
+
const role = msg.sender_id === agentUserId ? "You" : senderLabel;
|
|
73
|
+
|
|
74
|
+
if (msg.message_type === "text") {
|
|
75
|
+
const content = msg.content as TextContent;
|
|
76
|
+
const text = escapeHistoryValue(content.text?.trim() ?? "");
|
|
77
|
+
if (text) lines.push(`${role}: ${text}`);
|
|
78
|
+
} else {
|
|
79
|
+
const content = msg.content as MediaContent;
|
|
80
|
+
const caption = escapeHistoryValue(
|
|
81
|
+
content.caption?.trim() || `[file: ${content.file_name || "attachment"}]`,
|
|
82
|
+
);
|
|
83
|
+
lines.push(`${role}: ${caption}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!lines.length) return "";
|
|
88
|
+
return `[Recent chat history]\n${lines.join("\n")}\n[End of chat history]\n\n`;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
log?.warn(`pond: failed to fetch chat history for context: ${String(err)}`);
|
|
91
|
+
return "";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
41
95
|
/**
|
|
42
96
|
* Dispatch an inbound message to the OpenClaw agent and handle the reply.
|
|
43
97
|
* Shared by both text and file message handlers to avoid duplication.
|
|
@@ -53,9 +107,10 @@ async function dispatchToAgent(opts: {
|
|
|
53
107
|
chatId: string;
|
|
54
108
|
messageId: string;
|
|
55
109
|
inboundCtx: Record<string, unknown>;
|
|
110
|
+
noReply?: boolean;
|
|
56
111
|
log?: { info: (msg: string) => void; warn: (msg: string) => void; error: (msg: string) => void };
|
|
57
112
|
}) {
|
|
58
|
-
const { core, cfg, client, config, agentUserId, accountId, sessionKey, chatId, messageId, inboundCtx, log } = opts;
|
|
113
|
+
const { core, cfg, client, config, agentUserId, accountId, sessionKey, chatId, messageId, inboundCtx, noReply, log } = opts;
|
|
59
114
|
const sessionKeyLower = sessionKey.toLowerCase();
|
|
60
115
|
let thinkingSent = false;
|
|
61
116
|
let runIdPromise: Promise<string | undefined> | undefined;
|
|
@@ -72,7 +127,23 @@ async function dispatchToAgent(opts: {
|
|
|
72
127
|
const runId = runIdPromise ? await runIdPromise : undefined;
|
|
73
128
|
|
|
74
129
|
if (info.kind === "final") {
|
|
75
|
-
|
|
130
|
+
if (noReply) {
|
|
131
|
+
// no_reply: unconditionally suppress chat output; runId only affects step recording
|
|
132
|
+
if (runId) {
|
|
133
|
+
try {
|
|
134
|
+
await client.createAgentStep(config.org_id, agentUserId, runId, {
|
|
135
|
+
step_type: "text",
|
|
136
|
+
content: { text: replyText, suppressed: true },
|
|
137
|
+
});
|
|
138
|
+
} catch (err) {
|
|
139
|
+
log?.warn(`pond[${accountId}]: failed to create suppressed text step: ${String(err)}`);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
log?.warn(`pond[${accountId}]: suppressing final reply but no runId available`);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// Normal: final reply → chat message
|
|
76
147
|
await client.sendMessage(config.org_id, chatId, {
|
|
77
148
|
message_type: "text",
|
|
78
149
|
content: { text: replyText },
|
|
@@ -320,10 +391,32 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
320
391
|
const sessionKey = buildSessionKey(ctx.accountId, chatType, chatId);
|
|
321
392
|
setSessionChatId(sessionKey, chatId);
|
|
322
393
|
|
|
394
|
+
// Start typing indicator immediately before any async work so the user
|
|
395
|
+
// sees feedback right away. Reference-counted for concurrent dispatches.
|
|
396
|
+
const existingDispatchEarly = activeDispatches.get(chatId);
|
|
397
|
+
if (existingDispatchEarly) {
|
|
398
|
+
existingDispatchEarly.count++;
|
|
399
|
+
} else {
|
|
400
|
+
if (ws.state === "connected") ws.sendTyping(chatId, "start");
|
|
401
|
+
// Frontend auto-clears typing after 3s, so refresh at 2s to avoid flicker
|
|
402
|
+
const typingRefresh = setInterval(() => {
|
|
403
|
+
if (ws.state === "connected") ws.sendTyping(chatId, "start");
|
|
404
|
+
}, 2000);
|
|
405
|
+
activeDispatches.set(chatId, { count: 1, typingTimer: typingRefresh });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Fetch history with a 2s timeout — degrade gracefully to empty on slow API
|
|
409
|
+
const historyTimeout = new Promise<string>((resolve) => setTimeout(() => resolve(""), 2000));
|
|
410
|
+
const historyFetch = buildChatHistoryContext(
|
|
411
|
+
client, config.org_id, chatId, data.id, agentUserId, 30, log,
|
|
412
|
+
data.thread_root_id,
|
|
413
|
+
);
|
|
414
|
+
const historyPrefix = await Promise.race([historyFetch, historyTimeout]);
|
|
415
|
+
|
|
323
416
|
// Build inbound context for OpenClaw agent
|
|
324
417
|
const inboundCtx = core.channel.reply.finalizeInboundContext({
|
|
325
418
|
Body: body,
|
|
326
|
-
BodyForAgent: body,
|
|
419
|
+
BodyForAgent: historyPrefix ? `${historyPrefix}${body}` : body,
|
|
327
420
|
RawBody: body,
|
|
328
421
|
CommandBody: body,
|
|
329
422
|
...mediaFields,
|
|
@@ -343,19 +436,8 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
343
436
|
OriginatingTo: `pond:${chatId}`,
|
|
344
437
|
});
|
|
345
438
|
|
|
346
|
-
//
|
|
347
|
-
|
|
348
|
-
const existingDispatch = activeDispatches.get(chatId);
|
|
349
|
-
if (existingDispatch) {
|
|
350
|
-
existingDispatch.count++;
|
|
351
|
-
} else {
|
|
352
|
-
if (ws.state === "connected") ws.sendTyping(chatId, "start");
|
|
353
|
-
// Frontend auto-clears typing after 3s, so refresh at 2s to avoid flicker
|
|
354
|
-
const typingRefresh = setInterval(() => {
|
|
355
|
-
if (ws.state === "connected") ws.sendTyping(chatId, "start");
|
|
356
|
-
}, 2000);
|
|
357
|
-
activeDispatches.set(chatId, { count: 1, typingTimer: typingRefresh });
|
|
358
|
-
}
|
|
439
|
+
// Extract no_reply hint from message
|
|
440
|
+
const noReply = data.hints?.no_reply ?? false;
|
|
359
441
|
|
|
360
442
|
try {
|
|
361
443
|
await dispatchToAgent({
|
|
@@ -369,6 +451,7 @@ export const pondGateway: ChannelGatewayAdapter<ResolvedPondAccount> = {
|
|
|
369
451
|
chatId,
|
|
370
452
|
messageId: data.id,
|
|
371
453
|
inboundCtx,
|
|
454
|
+
noReply,
|
|
372
455
|
log,
|
|
373
456
|
});
|
|
374
457
|
} finally {
|
package/src/hooks.ts
CHANGED
|
@@ -30,9 +30,13 @@ You have access to the Pond platform via the \`@pnds/cli\` CLI. Auth is pre-conf
|
|
|
30
30
|
|
|
31
31
|
\`\`\`
|
|
32
32
|
npx @pnds/cli@latest whoami # Check your identity
|
|
33
|
+
npx @pnds/cli@latest agents list # List all agents (name, model, status)
|
|
34
|
+
npx @pnds/cli@latest dm <name-or-id> --text "..." # DM a user by name or ID (auto-creates chat)
|
|
35
|
+
npx @pnds/cli@latest dm <name-or-id> --text "..." --no-reply # DM, signal no reply expected
|
|
33
36
|
npx @pnds/cli@latest chats list # List chats
|
|
34
37
|
npx @pnds/cli@latest messages list <chatId> # Read chat history
|
|
35
38
|
npx @pnds/cli@latest messages send <chatId> --text "..." # Send a message
|
|
39
|
+
npx @pnds/cli@latest messages send <chatId> --text "..." --no-reply # Send, no reply expected
|
|
36
40
|
npx @pnds/cli@latest tasks list [--status ...] # List tasks
|
|
37
41
|
npx @pnds/cli@latest tasks create --title "..." # Create a task
|
|
38
42
|
npx @pnds/cli@latest tasks update <taskId> --status in_progress
|
|
@@ -41,7 +45,14 @@ npx @pnds/cli@latest users search <query> # Find users
|
|
|
41
45
|
npx @pnds/cli@latest members list # List org members
|
|
42
46
|
\`\`\`
|
|
43
47
|
|
|
44
|
-
Run \`npx @pnds/cli@latest --help\` or \`npx @pnds/cli@latest <command> --help\` for full options. Output is JSON
|
|
48
|
+
Run \`npx @pnds/cli@latest --help\` or \`npx @pnds/cli@latest <command> --help\` for full options. Output is JSON.
|
|
49
|
+
|
|
50
|
+
## Agent-to-Agent Interaction
|
|
51
|
+
|
|
52
|
+
When you receive a message from another agent (not a human):
|
|
53
|
+
- If the message has a no_reply hint, you may still reason and use tools, but your response will not be sent to chat.
|
|
54
|
+
- If you have nothing meaningful to add to the conversation, produce an empty response to avoid unnecessary back-and-forth.
|
|
55
|
+
- Use --no-reply when sending messages that don't require a response (e.g., delivering results, FYI notifications).`;
|
|
45
56
|
|
|
46
57
|
export function registerPondHooks(api: OpenClawPluginApi) {
|
|
47
58
|
const log = api.logger;
|