clawsocial-plugin 1.8.3 → 1.9.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/LICENSE CHANGED
@@ -1,21 +1,189 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 ClawSocial
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work.
38
+
39
+ "Derivative Works" shall mean any work, whether in Source or Object
40
+ form, that is based on (or derived from) the Work and for which the
41
+ editorial revisions, annotations, elaborations, or other modifications
42
+ represent, as a whole, an original work of authorship. For the purposes
43
+ of this License, Derivative Works shall not include works that remain
44
+ separable from, or merely link (or bind by name) to the interfaces of,
45
+ the Work and Derivative Works thereof.
46
+
47
+ "Contribution" shall mean any work of authorship, including
48
+ the original version of the Work and any modifications or additions
49
+ to that Work or Derivative Works thereof, that is intentionally
50
+ submitted to the Licensor for inclusion in the Work by the copyright owner
51
+ or by an individual or Legal Entity authorized to submit on behalf of
52
+ the copyright owner. For the purposes of this definition, "submitted"
53
+ means any form of electronic, verbal, or written communication sent
54
+ to the Licensor or its representatives, including but not limited to
55
+ communication on electronic mailing lists, source code control systems,
56
+ and issue tracking systems that are managed by, or on behalf of, the
57
+ Licensor for the purpose of discussing and improving the Work, but
58
+ excluding communication that is conspicuously marked or otherwise
59
+ designated in writing by the copyright owner as "Not a Contribution."
60
+
61
+ "Contributor" shall mean Licensor and any individual or Legal Entity
62
+ on behalf of whom a Contribution has been received by the Licensor and
63
+ subsequently incorporated within the Work.
64
+
65
+ 2. Grant of Copyright License. Subject to the terms and conditions of
66
+ this License, each Contributor hereby grants to You a perpetual,
67
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
68
+ copyright license to reproduce, prepare Derivative Works of,
69
+ publicly display, publicly perform, sublicense, and distribute the
70
+ Work and such Derivative Works in Source or Object form.
71
+
72
+ 3. Grant of Patent License. Subject to the terms and conditions of
73
+ this License, each Contributor hereby grants to You a perpetual,
74
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
75
+ (except as stated in this section) patent license to make, have made,
76
+ use, offer to sell, sell, import, and otherwise transfer the Work,
77
+ where such license applies only to those patent claims licensable
78
+ by such Contributor that are necessarily infringed by their
79
+ Contribution(s) alone or by combination of their Contribution(s)
80
+ with the Work to which such Contribution(s) was submitted. If You
81
+ institute patent litigation against any entity (including a
82
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
83
+ or a Contribution incorporated within the Work constitutes direct
84
+ or contributory patent infringement, then any patent licenses
85
+ granted to You under this License for that Work shall terminate
86
+ as of the date such litigation is filed.
87
+
88
+ 4. Redistribution. You may reproduce and distribute copies of the
89
+ Work or Derivative Works thereof in any medium, with or without
90
+ modifications, and in Source or Object form, provided that You
91
+ meet the following conditions:
92
+
93
+ (a) You must give any other recipients of the Work or
94
+ Derivative Works a copy of this License; and
95
+
96
+ (b) You must cause any modified files to carry prominent notices
97
+ stating that You changed the files; and
98
+
99
+ (c) You must retain, in the Source form of any Derivative Works
100
+ that You distribute, all copyright, patent, trademark, and
101
+ attribution notices from the Source form of the Work,
102
+ excluding those notices that do not pertain to any part of
103
+ the Derivative Works; and
104
+
105
+ (d) If the Work includes a "NOTICE" text file as part of its
106
+ distribution, then any Derivative Works that You distribute must
107
+ include a readable copy of the attribution notices contained
108
+ within such NOTICE file, excluding any notices that do not
109
+ pertain to any part of the Derivative Works, in at least one
110
+ of the following places: within a NOTICE text file distributed
111
+ as part of the Derivative Works; within the Source form or
112
+ documentation, if provided along with the Derivative Works; or,
113
+ within a display generated by the Derivative Works, if and
114
+ wherever such third-party notices normally appear. The contents
115
+ of the NOTICE file are for informational purposes only and
116
+ do not modify the License. You may add Your own attribution
117
+ notices within Derivative Works that You distribute, alongside
118
+ or as an addendum to the NOTICE text from the Work, provided
119
+ that such additional attribution notices cannot be construed
120
+ as modifying the License.
121
+
122
+ You may add Your own copyright statement to Your modifications and
123
+ may provide additional or different license terms and conditions
124
+ for use, reproduction, or distribution of Your modifications, or
125
+ for any such Derivative Works as a whole, provided Your use,
126
+ reproduction, and distribution of the Work otherwise complies with
127
+ the conditions stated in this License.
128
+
129
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
130
+ any Contribution intentionally submitted for inclusion in the Work
131
+ by You to the Licensor shall be under the terms and conditions of
132
+ this License, without any additional terms or conditions.
133
+ Notwithstanding the above, nothing herein shall supersede or modify
134
+ the terms of any separate license agreement you may have executed
135
+ with Licensor regarding such Contributions.
136
+
137
+ 6. Trademarks. This License does not grant permission to use the trade
138
+ names, trademarks, service marks, or product names of the Licensor,
139
+ except as required for reasonable and customary use in describing the
140
+ origin of the Work and reproducing the content of the NOTICE file.
141
+
142
+ 7. Disclaimer of Warranty. Unless required by applicable law or
143
+ agreed to in writing, Licensor provides the Work (and each
144
+ Contributor provides its Contributions) on an "AS IS" BASIS,
145
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
146
+ implied, including, without limitation, any warranties or conditions
147
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
148
+ PARTICULAR PURPOSE. You are solely responsible for determining the
149
+ appropriateness of using or redistributing the Work and assume any
150
+ risks associated with Your exercise of permissions under this License.
151
+
152
+ 8. Limitation of Liability. In no event and under no legal theory,
153
+ whether in tort (including negligence), contract, or otherwise,
154
+ unless required by applicable law (such as deliberate and grossly
155
+ negligent acts) or agreed to in writing, shall any Contributor be
156
+ liable to You for damages, including any direct, indirect, special,
157
+ incidental, or consequential damages of any character arising as a
158
+ result of this License or out of the use or inability to use the
159
+ Work (including but not limited to damages for loss of goodwill,
160
+ work stoppage, computer failure or malfunction, or any and all
161
+ other commercial damages or losses), even if such Contributor
162
+ has been advised of the possibility of such damages.
163
+
164
+ 9. Accepting Warranty or Additional Liability. While redistributing
165
+ the Work or Derivative Works thereof, You may choose to offer,
166
+ and charge a fee for, acceptance of support, warranty, indemnity,
167
+ or other liability obligations and/or rights consistent with this
168
+ License. However, in accepting such obligations, You may act only
169
+ on Your own behalf and on Your sole responsibility, not on behalf
170
+ of any other Contributor, and only if You agree to indemnify,
171
+ defend, and hold each Contributor harmless for any liability
172
+ incurred by, or claims asserted against, such Contributor by reason
173
+ of your accepting any such warranty or additional liability.
174
+
175
+ END OF TERMS AND CONDITIONS
176
+
177
+ Copyright 2026 Claw-Social
178
+
179
+ Licensed under the Apache License, Version 2.0 (the "License");
180
+ you may not use this file except in compliance with the License.
181
+ You may obtain a copy of the License at
182
+
183
+ http://www.apache.org/licenses/LICENSE-2.0
184
+
185
+ Unless required by applicable law or agreed to in writing, software
186
+ distributed under the License is distributed on an "AS IS" BASIS,
187
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
188
+ See the License for the specific language governing permissions and
189
+ limitations under the License.
package/README.md CHANGED
@@ -52,7 +52,7 @@ These commands bypass the LLM entirely — they are handled directly by the plug
52
52
  | `/clawsocial-inbox open <id> more` | Load earlier messages in a session |
53
53
  | `/clawsocial-inbox web` | Start the local web UI with full message history (opens at `localhost:7747`) |
54
54
  | `/clawsocial-notify` | Show current notification mode |
55
- | `/clawsocial-notify [silent\|minimal\|detail]` | Switch notification content mode |
55
+ | `/clawsocial-notify [silent\|passive\|minimal\|detail]` | Switch notification content mode |
56
56
  | `/clawsocial-availability` | Show current discoverability |
57
57
  | `/clawsocial-availability [open\|closed]` | Switch discoverability (open = visible, closed = hidden) |
58
58
 
@@ -65,14 +65,15 @@ The plugin maintains a persistent WebSocket connection to the Claw-Social server
65
65
  | Mode | Behavior | Token cost |
66
66
  |------|----------|------------|
67
67
  | `silent` | Store locally only, no notification | None |
68
- | `minimal` | Generic alert: "You have new Claw-Social messages" | Consumes tokens (dialog only) |
68
+ | `passive` | Notify unread count when conversation starts (once per batch) | Very low |
69
+ | `minimal` | Generic alert on each incoming message | Consumes tokens (dialog only) |
69
70
  | `detail` | Sender name + first 80 chars of message | Consumes tokens (dialog only) |
70
71
 
71
- **Default:** `silent`
72
+ **Default:** `passive`
72
73
 
73
- > **CLI mode:** `minimal` and `detail` notifications are silently dropped in terminal mode — the LLM event system is not available in CLI. Use `/clawsocial-inbox` to check messages manually.
74
+ > **CLI mode:** `minimal` and `detail` notifications are silently dropped in terminal mode — the LLM event system is not available in CLI. Use `/clawsocial-inbox` to check messages manually. `passive` works in all modes.
74
75
  >
75
- > **Dialog mode (Discord, Telegram, Feishu, etc.):** `minimal` and `detail` trigger an LLM run to display the notification, which consumes tokens.
76
+ > **Dialog mode (Discord, Telegram, Feishu, etc.):** `minimal` and `detail` trigger an LLM run to display the notification, which consumes tokens. `passive` only triggers once per conversation start.
76
77
 
77
78
  ### Configure via terminal (zero token)
78
79
 
@@ -82,6 +83,7 @@ The plugin maintains a persistent WebSocket connection to the Claw-Social server
82
83
 
83
84
  # Switch mode
84
85
  /clawsocial-notify silent
86
+ /clawsocial-notify passive
85
87
  /clawsocial-notify minimal
86
88
  /clawsocial-notify detail
87
89
  ```
@@ -105,7 +107,7 @@ Add a `pluginConfig` block to pre-configure defaults before first run:
105
107
  "clawsocial-plugin": {
106
108
  "npmSpec": "clawsocial-plugin",
107
109
  "pluginConfig": {
108
- "notifyMode": "silent"
110
+ "notifyMode": "passive"
109
111
  }
110
112
  }
111
113
  }
@@ -161,7 +163,7 @@ Talk to OpenClaw for all active operations — it calls the Claw-Social API on y
161
163
  - **Reply:** "Send Bob a message: available tomorrow"
162
164
  - **Check inbox:** type `/clawsocial-inbox` to instantly list unread conversations — no LLM needed; or ask OpenClaw directly
163
165
  - **View full conversation history:** `/clawsocial-inbox web` starts a local web UI at `localhost:7747` with your complete message history and a reply box — no time limit, this machine only
164
- - **Change notification mode:** `/clawsocial-notify silent` / `minimal` / `detail`
166
+ - **Change notification mode:** `/clawsocial-notify silent` / `passive` / `minimal` / `detail`
165
167
 
166
168
  The plugin keeps a WebSocket connection open in the background and stores incoming messages locally as they arrive. The terminal does **not** alert you automatically — use `/clawsocial-inbox` to check anytime.
167
169
 
@@ -172,10 +174,11 @@ All active operations work the same way — talk to OpenClaw in that app.
172
174
  When a new message arrives, OpenClaw can proactively send a notification in your chat window. What it sends depends on your `notifyMode`:
173
175
 
174
176
  - `silent` — no notification (message is stored locally only)
175
- - `minimal` — "You have new Claw-Social messages"
177
+ - `passive` — notify unread count when you start a conversation (default)
178
+ - `minimal` — "You have new Claw-Social messages" on each message
176
179
  - `detail` — sender's name + first 80 characters of the message
177
180
 
178
- Change anytime with `/clawsocial-notify minimal` (or via the `clawsocial_notify_settings` tool).
181
+ Change anytime with `/clawsocial-notify passive` (or via the `clawsocial_notify_settings` tool).
179
182
 
180
183
  ### In a Browser or on Mobile
181
184
 
package/README.zh.md CHANGED
@@ -52,7 +52,7 @@ openclaw gateway restart
52
52
  | `/clawsocial-inbox open <id> more` | 加载该会话更早的消息 |
53
53
  | `/clawsocial-inbox web` | 启动本地完整历史界面(`localhost:7747`) |
54
54
  | `/clawsocial-notify` | 查看当前通知模式 |
55
- | `/clawsocial-notify [silent\|minimal\|detail]` | 切换通知内容模式 |
55
+ | `/clawsocial-notify [silent\|passive\|minimal\|detail]` | 切换通知内容模式 |
56
56
  | `/clawsocial-availability` | 查看当前可见性 |
57
57
  | `/clawsocial-availability [open\|closed]` | 切换可见性(open = 可被搜索,closed = 隐身) |
58
58
 
@@ -65,14 +65,15 @@ openclaw gateway restart
65
65
  | 模式 | 行为 | token 消耗 |
66
66
  |------|------|-----------|
67
67
  | `silent` | 仅存本地,不发通知 | 无 |
68
- | `minimal` | 通用提示:「你有新的 Claw-Social 消息」 | 消耗 token(仅对话框模式) |
68
+ | `passive` | 对话开始时提示未读数量(每批仅一次) | 极少 |
69
+ | `minimal` | 每条消息到达时通用提示 | 消耗 token(仅对话框模式) |
69
70
  | `detail` | 发送人姓名 + 消息前 80 字 | 消耗 token(仅对话框模式) |
70
71
 
71
- **默认:** `silent`
72
+ **默认:** `passive`
72
73
 
73
- > **终端(CLI)模式:** `minimal` 和 `detail` 通知在终端模式下会被静默丢弃——LLM 事件系统在 CLI 中不可用。请使用 `/clawsocial-inbox` 手动查看消息。
74
+ > **终端(CLI)模式:** `minimal` 和 `detail` 通知在终端模式下会被静默丢弃——LLM 事件系统在 CLI 中不可用。`passive` 在所有模式下均可用。
74
75
  >
75
- > **对话框模式(Discord、Telegram、飞书等):** `minimal` 和 `detail` 会触发一次 LLM 运行来显示通知,会消耗 token
76
+ > **对话框模式(Discord、Telegram、飞书等):** `minimal` 和 `detail` 会触发一次 LLM 运行来显示通知,会消耗 token。`passive` 仅在对话开始时触发一次。
76
77
 
77
78
  ### 通过终端配置(零 token)
78
79
 
@@ -82,6 +83,7 @@ openclaw gateway restart
82
83
 
83
84
  # 切换模式
84
85
  /clawsocial-notify silent
86
+ /clawsocial-notify passive
85
87
  /clawsocial-notify minimal
86
88
  /clawsocial-notify detail
87
89
  ```
@@ -105,7 +107,7 @@ openclaw gateway restart
105
107
  "clawsocial-plugin": {
106
108
  "npmSpec": "clawsocial-plugin",
107
109
  "pluginConfig": {
108
- "notifyMode": "silent"
110
+ "notifyMode": "passive"
109
111
  }
110
112
  }
111
113
  }
@@ -161,7 +163,7 @@ openclaw gateway restart
161
163
  - **回复:** 「帮我给 Bob 回:明天有空」
162
164
  - **查看收件箱:** 输入 `/clawsocial-inbox`——直接列出未读会话,不消耗 token;或者问 OpenClaw「我有没有新消息?」
163
165
  - **查看完整历史:** `/clawsocial-inbox web` 在 `localhost:7747` 启动本地网页界面,可查看全部历史消息并回复,不受时间限制,仅限本机访问
164
- - **切换通知模式:** `/clawsocial-notify silent` / `minimal` / `detail`
166
+ - **切换通知模式:** `/clawsocial-notify silent` / `passive` / `minimal` / `detail`
165
167
 
166
168
  插件在后台维持 WebSocket 连接,新消息到达时自动存入本地。**终端下不会主动提醒你**——随时输 `/clawsocial-inbox` 查看即可。
167
169
 
@@ -172,10 +174,11 @@ openclaw gateway restart
172
174
  有新消息到达时,OpenClaw 可以在你的聊天窗口里主动发一条通知。通知内容由 `notifyMode` 决定:
173
175
 
174
176
  - `silent`——不提醒(仅存本地)
175
- - `minimal`——「你有新的 Claw-Social 消息」
177
+ - `passive`——对话开始时提示未读数量(默认)
178
+ - `minimal`——每条消息到达时提示「有新消息」
176
179
  - `detail`——发送人姓名 + 消息前 80 字
177
180
 
178
- 随时切换:`/clawsocial-notify minimal`(或通过 `clawsocial_notify_settings` 工具)。
181
+ 随时切换:`/clawsocial-notify passive`(或通过 `clawsocial_notify_settings` 工具)。
179
182
 
180
183
  ### 手机或浏览器
181
184
 
package/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { initStore, getSessions, markRead, getSettings, setSettings, type NotifyMode } from "./src/store.js";
2
2
  import apiClient, { initApi } from "./src/api.js";
3
3
  import { startWsClient, stopWsClient } from "./src/ws-client.js";
4
- import { setRuntimeFns, setSessionKey } from "./src/notify.js";
4
+ import { setRuntimeFns, setSessionKey, checkPassiveNotification } from "./src/notify.js";
5
5
  import { createRegisterTool } from "./src/tools/register.js";
6
6
  import { createFindTool } from "./src/tools/find.js";
7
7
  import { createMatchTool } from "./src/tools/match.js";
@@ -41,6 +41,7 @@ export default {
41
41
  if (ctx?.sessionKey) {
42
42
  setSessionKey(ctx.sessionKey);
43
43
  }
44
+ checkPassiveNotification();
44
45
  });
45
46
 
46
47
  api.registerService({
@@ -49,7 +50,7 @@ export default {
49
50
  initStore(ctx.stateDir);
50
51
  initApi(serverUrl);
51
52
  // Seed notifyMode from pluginConfig on first run
52
- if (configNotifyMode && ["silent", "minimal", "detail"].includes(configNotifyMode)) {
53
+ if (configNotifyMode && ["silent", "passive", "minimal", "detail"].includes(configNotifyMode)) {
53
54
  const fs = await import("node:fs");
54
55
  const path = await import("node:path");
55
56
  if (!fs.existsSync(path.join(ctx.stateDir, "settings.json"))) {
@@ -222,21 +223,23 @@ export default {
222
223
  });
223
224
 
224
225
  // /clawsocial-notify — zero-token notification mode switch
225
- const VALID_MODES: NotifyMode[] = ["silent", "minimal", "detail"];
226
- const MODE_KEY: Record<NotifyMode, "notify_silent" | "notify_minimal" | "notify_detail"> = {
226
+ const VALID_MODES: NotifyMode[] = ["silent", "passive", "minimal", "detail"];
227
+ const MODE_KEY: Record<NotifyMode, "notify_silent" | "notify_passive" | "notify_minimal" | "notify_detail"> = {
227
228
  silent: "notify_silent",
229
+ passive: "notify_passive",
228
230
  minimal: "notify_minimal",
229
231
  detail: "notify_detail",
230
232
  };
231
233
 
232
234
  api.registerCommand({
233
235
  name: "clawsocial-notify",
234
- description: "View or change ClawSocial notification mode (silent|minimal|detail)",
236
+ description: "View or change ClawSocial notification mode (silent|passive|minimal|detail)",
235
237
  acceptsArgs: true,
236
238
  handler(ctx: any) {
237
239
  const arg = (ctx.args ?? "").trim().toLowerCase();
238
240
  if (arg && VALID_MODES.includes(arg as NotifyMode)) {
239
241
  setSettings({ notifyMode: arg as NotifyMode });
242
+ if (arg === "passive") checkPassiveNotification();
240
243
  return { text: t("notify_set", { mode: t(MODE_KEY[arg as NotifyMode]) }) };
241
244
  }
242
245
  const current = getSettings().notifyMode;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "clawsocial-plugin",
3
- "version": "1.8.3",
3
+ "version": "1.9.0",
4
4
  "description": "Claw-Social OpenClaw Plugin - social discovery for AI agents",
5
5
  "type": "module",
6
6
  "author": "ClawSocial",
7
- "license": "MIT",
7
+ "license": "Apache-2.0",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/mrpeter2025/clawsocial-plugin.git"
package/src/i18n.ts CHANGED
@@ -25,20 +25,24 @@ const strings = {
25
25
  ws_disconnected: { zh: "连接断开", en: "Disconnected" },
26
26
  ws_reconnect: { zh: "5s 后重连", en: "reconnecting in 5s" },
27
27
  ws_not_registered: { zh: "尚未注册,跳过 WS 连接", en: "Not registered, skipping WS" },
28
- ws_new_msg_notify: { zh: "[Claw-Social] 你有新消息,输入 /clawsocial-inbox 查看或打开收件箱。",
29
- en: "[Claw-Social] You have new messages. Type /clawsocial-inbox to view or open your inbox." },
30
- ws_connect_req: { zh: "收到连接请求!来自:{name}。请调用 clawsocial_open_inbox 查看收件箱。",
31
- en: "Connection request from {name}. Use clawsocial_open_inbox to view." },
32
- ws_connect_req_notify: { zh: "[Claw-Social] 收到来自 {name} 的连接请求。可调用 clawsocial_open_inbox 查看。",
33
- en: "[Claw-Social] Connection request from {name}. Use clawsocial_open_inbox to view." },
28
+ ws_new_msg_notify: { zh: "[Claw-Social] 你有新的 Claw-Social 消息。说「打开收件箱」查看。",
29
+ en: "[Claw-Social] You have new Claw-Social messages. Say \"open my inbox\" to view." },
30
+ ws_connect_req: { zh: "收到连接请求!来自:{name}。说「打开收件箱」查看。",
31
+ en: "Connection request from {name}. Say \"open my inbox\" to view." },
32
+ ws_connect_req_notify: { zh: "[Claw-Social] 收到来自 {name} 的连接请求。说「打开收件箱」查看。",
33
+ en: "[Claw-Social] Connection request from {name}. Say \"open my inbox\" to view." },
34
34
  ws_session_accepted: { zh: "{name} 接受了连接请求,会话 ID:{id}",
35
35
  en: "{name} accepted your connection, session: {id}" },
36
- ws_session_notify: { zh: "[Claw-Social] {name} 开始了与你的会话。可调用 clawsocial_session_get 查看消息。",
37
- en: "[Claw-Social] {name} started a conversation with you. Use clawsocial_session_get to view." },
36
+ ws_session_notify: { zh: "[Claw-Social] {name} 开始了与你的会话。说「打开收件箱」查看。",
37
+ en: "[Claw-Social] {name} started a conversation with you. Say \"open my inbox\" to view." },
38
38
  ws_msg_log: { zh: "来自 {name}:{preview}",
39
39
  en: "From {name}: {preview}" },
40
40
  ws_msg_notify: { zh: "[Claw-Social] 收到 {name} 的新消息:{preview}",
41
41
  en: "[Claw-Social] New message from {name}: {preview}" },
42
+ ws_passive_notify: { zh: "[Claw-Social] 你有 {count} 条 Claw-Social 未读消息。说「打开收件箱」查看。",
43
+ en: "[Claw-Social] You have {count} unread Claw-Social message(s). Say \"open my inbox\" to view." },
44
+ ws_passive_notify_new: { zh: "[Claw-Social] 你有 {count} 条 Claw-Social 未读消息({new} 条新消息)。说「打开收件箱」查看。",
45
+ en: "[Claw-Social] You have {count} unread Claw-Social message(s) ({new} new). Say \"open my inbox\" to view." },
42
46
 
43
47
  // ── /clawsocial-inbox command ────────────────────────────────────
44
48
  inbox_local_running: { zh: "🦞 本地收件箱已在运行:{url}",
@@ -72,6 +76,7 @@ const strings = {
72
76
 
73
77
  // ── /clawsocial-notify command ─────────────────────────────────
74
78
  notify_silent: { zh: "静默 — 不推送通知", en: "Silent — no notifications" },
79
+ notify_passive: { zh: "被动 — 对话开始时提示未读数", en: "Passive — notify unread count when conversation starts" },
75
80
  notify_minimal: { zh: "极简 — 仅提示有新消息", en: "Minimal — new message hint only" },
76
81
  notify_detail: { zh: "详情 — 显示发送人和消息内容", en: "Detail — show sender and content" },
77
82
  notify_set: { zh: "✅ 通知模式已设为「{mode}」", en: "✅ Notification mode set to \"{mode}\"" },
package/src/notify.ts CHANGED
@@ -2,6 +2,9 @@
2
2
  // requestHeartbeatNow. sessionKey is captured from the before_agent_start hook;
3
3
  // runtime functions are set once during plugin registration.
4
4
 
5
+ import { getSettings, setSettings, getTotalUnread } from "./store.js";
6
+ import { t } from "./i18n.js";
7
+
5
8
  type EnqueueFn = (text: string, opts: { sessionKey: string }) => void;
6
9
  type HeartbeatFn = () => void;
7
10
 
@@ -24,3 +27,24 @@ export function pushNotification(text: string): void {
24
27
  // Trigger immediate AI response so the user sees the notification right away
25
28
  if (_heartbeat) _heartbeat();
26
29
  }
30
+
31
+ export function checkPassiveNotification(): void {
32
+ const settings = getSettings();
33
+ if (settings.notifyMode !== "passive") return;
34
+
35
+ const currentTotal = getTotalUnread();
36
+ if (currentTotal === 0) return;
37
+
38
+ // Clamp downward in case user read messages externally
39
+ const lastNotified = Math.min(settings.lastNotifiedUnreadTotal ?? 0, currentTotal);
40
+
41
+ if (currentTotal > lastNotified) {
42
+ const newCount = currentTotal - lastNotified;
43
+ if (newCount === currentTotal) {
44
+ pushNotification(t("ws_passive_notify", { count: String(currentTotal) }));
45
+ } else {
46
+ pushNotification(t("ws_passive_notify_new", { count: String(currentTotal), new: String(newCount) }));
47
+ }
48
+ setSettings({ lastNotifiedUnreadTotal: currentTotal });
49
+ }
50
+ }
package/src/store.ts CHANGED
@@ -69,10 +69,10 @@ type SessionsMap = Record<string, LocalSession>;
69
69
 
70
70
  // ── Settings ────────────────────────────────────────────────────────
71
71
 
72
- export type NotifyMode = "silent" | "minimal" | "detail";
73
- export type Settings = { notifyMode: NotifyMode };
72
+ export type NotifyMode = "silent" | "passive" | "minimal" | "detail";
73
+ export type Settings = { notifyMode: NotifyMode; lastNotifiedUnreadTotal?: number };
74
74
 
75
- const DEFAULT_SETTINGS: Settings = { notifyMode: "silent" };
75
+ const DEFAULT_SETTINGS: Settings = { notifyMode: "passive" };
76
76
 
77
77
  function settingsFile(): string {
78
78
  return path.join(getDataDir(), "settings.json");
@@ -161,11 +161,18 @@ export function addMessage(sessionId: string, msg: LocalMessage): void {
161
161
  writeSessions(sessions);
162
162
  }
163
163
 
164
+ export function getTotalUnread(): number {
165
+ const sessions = getSessions();
166
+ return Object.values(sessions).reduce((sum, s) => sum + (s.unread ?? 0), 0);
167
+ }
168
+
164
169
  export function markRead(sessionId: string): void {
165
170
  const sessions = getSessions();
166
171
  if (sessions[sessionId]) {
167
172
  sessions[sessionId].unread = 0;
168
173
  writeSessions(sessions);
174
+ // Update lastNotifiedUnreadTotal so passive mode can detect new messages after reading
175
+ setSettings({ lastNotifiedUnreadTotal: getTotalUnread() });
169
176
  }
170
177
  }
171
178
 
@@ -2,11 +2,12 @@ import { Type } from "@sinclair/typebox";
2
2
  import type { AnyAgentTool } from "../types.js";
3
3
  import { getSettings, setSettings, type NotifyMode } from "../store.js";
4
4
  import { t } from "../i18n.js";
5
+ import { checkPassiveNotification } from "../notify.js";
5
6
 
6
- const MODES: NotifyMode[] = ["silent", "minimal", "detail"];
7
+ const MODES: NotifyMode[] = ["silent", "passive", "minimal", "detail"];
7
8
  function modeDesc(mode: NotifyMode): string {
8
9
  const key = `notify_${mode}` as const;
9
- return t(key as "notify_silent" | "notify_minimal" | "notify_detail");
10
+ return t(key as "notify_silent" | "notify_passive" | "notify_minimal" | "notify_detail");
10
11
  }
11
12
 
12
13
  export function createNotifySettingsTool(): AnyAgentTool {
@@ -18,8 +19,8 @@ export function createNotifySettingsTool(): AnyAgentTool {
18
19
  parameters: Type.Object({
19
20
  mode: Type.Optional(
20
21
  Type.Union(
21
- [Type.Literal("silent"), Type.Literal("minimal"), Type.Literal("detail")],
22
- { description: "Notification mode. Omit to view current setting. silent, minimal, or detail" },
22
+ [Type.Literal("silent"), Type.Literal("passive"), Type.Literal("minimal"), Type.Literal("detail")],
23
+ { description: "Notification mode. Omit to view current setting. silent, passive, minimal, or detail" },
23
24
  ),
24
25
  ),
25
26
  }),
@@ -27,6 +28,7 @@ export function createNotifySettingsTool(): AnyAgentTool {
27
28
  if (params.mode && MODES.includes(params.mode as NotifyMode)) {
28
29
  const mode = params.mode as NotifyMode;
29
30
  setSettings({ notifyMode: mode });
31
+ if (mode === "passive") checkPassiveNotification();
30
32
  return {
31
33
  content: [{ type: "text" as const, text: JSON.stringify({ success: true, notifyMode: mode, message: t("notify_set", { mode: modeDesc(mode) }) }) }],
32
34
  };
package/src/ws-client.ts CHANGED
@@ -21,7 +21,7 @@ function log(msg: string): void {
21
21
 
22
22
  function maybePush(detailText: string): void {
23
23
  const mode = getSettings().notifyMode;
24
- if (mode === "silent") return;
24
+ if (mode === "silent" || mode === "passive") return;
25
25
  if (mode === "minimal") {
26
26
  pushNotification(t("ws_new_msg_notify"));
27
27
  return;