@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pnds/pond",
3
- "version": "1.0.1",
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.1"
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
- // Final reply → chat message
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
- // Manage typing indicator so the user sees the agent is working.
347
- // Reference-counted: concurrent dispatches for the same chat share one timer.
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;