@otterware/openclaw-otter 0.2.2 → 0.2.3

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.
Files changed (3) hide show
  1. package/index.js +20 -11
  2. package/otter.js +32 -1
  3. package/package.json +1 -1
package/index.js CHANGED
@@ -15,14 +15,16 @@ import { runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guar
15
15
  import { readWorkspaceFile } from "./files.js";
16
16
  import {
17
17
  accountOf,
18
- chatOf,
19
18
  chatTarget,
19
+ isNewChat,
20
20
  isOwnerConfigured,
21
21
  messageOf,
22
- OTTER_TARGET,
22
+ newChatKey,
23
+ otterTargetsNote,
23
24
  OWNER,
24
25
  pointerOf,
25
26
  sessionOf,
27
+ targetOf,
26
28
  tell,
27
29
  } from "./otter.js";
28
30
 
@@ -61,13 +63,15 @@ export const otterChannel = createChatChannelPlugin({
61
63
  messaging: {
62
64
  targetResolver: {
63
65
  looksLikeId: () => true,
64
- hint: `"${OTTER_TARGET}", or "chat:<session key>" for one of Otter's chats`,
66
+ hint: '"chat:<session key>" for one of Otter\'s chats, "new" for a new chat, or "inbox"',
65
67
  },
66
- // A delivery to a chat is kept in its session; any other, in the agent's Otter session.
68
+ // A delivery to a chat is kept in its session; to "new", in a chat it starts (the gateway
69
+ // routes each delivery once, and sends it to that session); any other, in the inbox.
67
70
  resolveOutboundSessionRoute: ({ agentId, target }) => {
68
- const chat = chatOf(target);
69
- const sessionKey = sessionOf(target, agentId);
70
- const to = chat ? chatTarget(chat) : OTTER_TARGET;
71
+ const to = targetOf(target);
72
+ const sessionKey = isNewChat(to)
73
+ ? newChatKey(agentId, randomUUID())
74
+ : sessionOf(target, agentId);
71
75
  return {
72
76
  sessionKey,
73
77
  baseSessionKey: sessionKey,
@@ -82,10 +86,7 @@ export const otterChannel = createChatChannelPlugin({
82
86
  },
83
87
  outbound: {
84
88
  deliveryMode: "direct",
85
- resolveTarget: (params) => {
86
- const chat = chatOf(params?.to);
87
- return { ok: true, to: chat ? chatTarget(chat) : OTTER_TARGET };
88
- },
89
+ resolveTarget: (params) => ({ ok: true, to: targetOf(params?.to) }),
89
90
  sendText: (ctx) => deliver(ctx),
90
91
  sendPayload: (ctx) => deliver(ctx, ctx.payload),
91
92
  },
@@ -155,6 +156,14 @@ export default defineChannelPluginEntry({
155
156
  description: "Send what your agents want you to see to Otter.",
156
157
  plugin: otterChannel,
157
158
  registerFull(api) {
159
+ // What Otter's targets are, for a turn in an Otter chat: the Codex harness leaves channels'
160
+ // message-tool hints out of its instructions, but every harness runs this hook.
161
+ api.on("before_prompt_build", (_event, ctx) =>
162
+ ctx?.channel === "otter" || ctx?.messageProvider === "otter"
163
+ ? { appendSystemContext: otterTargetsNote(ctx.sessionKey) }
164
+ : undefined,
165
+ );
166
+
158
167
  // What you send in an Otter chat, as the gateway's owner: `chat.send`'s run, with the authority
159
168
  // a configured channel owner has (an automation another chat made, for one).
160
169
  api.registerGatewayMethod(
package/otter.js CHANGED
@@ -12,6 +12,36 @@ export const OWNER_ENTRY = `otter:${OWNER}`;
12
12
  /** A chat in Otter is a target of its own: what's sent to it is kept in its session. */
13
13
  export const chatTarget = (sessionKey) => `chat:${sessionKey}`;
14
14
 
15
+ /** Like posting to a Slack channel rather than in a thread: each delivery starts a chat of its own. */
16
+ export const NEW_CHAT_TARGET = "new";
17
+
18
+ export const isNewChat = (to) =>
19
+ typeof to === "string" && to.trim().replace(/^otter:/, "") === NEW_CHAT_TARGET;
20
+
21
+ /** A target as the channel keeps it: one of Otter's chats, a new one each time, or the inbox. */
22
+ export function targetOf(to) {
23
+ const chat = chatOf(to);
24
+ if (chat) return chatTarget(chat);
25
+ return isNewChat(to) ? NEW_CHAT_TARGET : OTTER_TARGET;
26
+ }
27
+
28
+ /** A session for a new chat, an Otter chat like one you start. */
29
+ export const newChatKey = (agentId, id) => `agent:${agentId}:otter-mail:${id}`;
30
+
31
+ /**
32
+ * What an agent in an Otter chat is told about sending to Otter (the message tool, an automation's
33
+ * delivery), with this chat's own target.
34
+ */
35
+ export function otterTargetsNote(sessionKey) {
36
+ const here = agentOf(sessionKey) ? chatTarget(sessionKey) : "chat:<session key>";
37
+ return [
38
+ "Otter delivery targets (channel `otter`), like a thread or a new post in Slack:",
39
+ `- "${here}": this chat. Any Otter chat is "chat:<its session key>".`,
40
+ '- "new": a new Otter chat for each delivery (an automation that should post a fresh chat every run, such as a morning brief).',
41
+ '- "inbox": the agent\'s single Otter inbox chat.',
42
+ ].join("\n");
43
+ }
44
+
15
45
  export const agentOf = (sessionKey) =>
16
46
  typeof sessionKey === "string" && sessionKey.startsWith("agent:")
17
47
  ? sessionKey.split(":")[1]
@@ -41,7 +71,8 @@ export function pointerOf({ sessionKey, to, payload, at = Date.now() }) {
41
71
  const approval = data.execApproval;
42
72
  const question = data.askUser;
43
73
  const session = approval?.sessionKey ?? sessionKey;
44
- const chat = chatOf(to);
74
+ // A new chat's session is the one its delivery was routed to.
75
+ const chat = chatOf(to) ?? (isNewChat(to) && agentOf(sessionKey) ? sessionKey : undefined);
45
76
  const agentId = approval?.agentId ?? agentOf(chat ?? session) ?? "main";
46
77
  return {
47
78
  kind: approval ? "approval" : question ? "question" : "message",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@otterware/openclaw-otter",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Otter as an OpenClaw channel: what your agents send you waits in Otter, and Otter tells your devices.",
5
5
  "license": "MIT",
6
6
  "repository": {