clawbuds 0.0.1

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/dist/daemon.js ADDED
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildSignMessage,
4
+ ensureConfigDir,
5
+ getServerUrl,
6
+ inboxCachePath,
7
+ loadConfig,
8
+ loadPrivateKey,
9
+ loadState,
10
+ saveState,
11
+ sign
12
+ } from "./chunk-HS3A4VHL.js";
13
+
14
+ // src/cache.ts
15
+ import { appendFileSync, readFileSync } from "fs";
16
+ function appendToCache(event) {
17
+ ensureConfigDir();
18
+ appendFileSync(inboxCachePath(), JSON.stringify(event) + "\n");
19
+ }
20
+ function updateLastSeq(seq) {
21
+ const state = loadState();
22
+ if (seq > state.lastSeq) {
23
+ saveState({ ...state, lastSeq: seq });
24
+ }
25
+ }
26
+
27
+ // src/ws-client.ts
28
+ import WebSocket from "ws";
29
+ var MAX_RETRIES = 10;
30
+ var BASE_DELAY = 1e3;
31
+ var MAX_DELAY = 6e4;
32
+ var WsClient = class {
33
+ ws = null;
34
+ retries = 0;
35
+ closed = false;
36
+ reconnectTimer = null;
37
+ opts;
38
+ constructor(opts) {
39
+ this.opts = opts;
40
+ }
41
+ connect() {
42
+ if (this.closed) return;
43
+ const wsUrl = this.opts.serverUrl.replace(/^http:/, "ws:").replace(/^https:/, "wss:").replace(/\/+$/, "");
44
+ const timestamp = String(Date.now());
45
+ const signMsg = buildSignMessage("CONNECT", "/ws", timestamp, "");
46
+ const signature = sign(signMsg, this.opts.privateKey);
47
+ const url = `${wsUrl}/ws?clawId=${encodeURIComponent(this.opts.clawId)}&timestamp=${timestamp}&signature=${signature}`;
48
+ this.ws = new WebSocket(url);
49
+ this.ws.on("open", () => {
50
+ this.retries = 0;
51
+ this.opts.onConnect?.();
52
+ this.ws?.send(JSON.stringify({
53
+ type: "catch-up",
54
+ lastSeq: this.opts.lastSeq
55
+ }));
56
+ });
57
+ this.ws.on("message", (data) => {
58
+ try {
59
+ const event = JSON.parse(data.toString());
60
+ this.opts.onEvent(event);
61
+ if (event.type === "message.new" && event.seq > this.opts.lastSeq) {
62
+ this.opts.lastSeq = event.seq;
63
+ }
64
+ } catch {
65
+ }
66
+ });
67
+ this.ws.on("close", (code) => {
68
+ this.opts.onDisconnect?.();
69
+ if (!this.closed && code !== 4001) {
70
+ this.scheduleReconnect();
71
+ }
72
+ });
73
+ this.ws.on("error", () => {
74
+ });
75
+ }
76
+ close() {
77
+ this.closed = true;
78
+ if (this.reconnectTimer) {
79
+ clearTimeout(this.reconnectTimer);
80
+ this.reconnectTimer = null;
81
+ }
82
+ this.ws?.close();
83
+ this.ws = null;
84
+ }
85
+ scheduleReconnect() {
86
+ if (this.retries >= MAX_RETRIES) {
87
+ return;
88
+ }
89
+ const delay = Math.min(BASE_DELAY * Math.pow(2, this.retries), MAX_DELAY);
90
+ const jitter = Math.random() * delay * 0.3;
91
+ this.retries++;
92
+ this.reconnectTimer = setTimeout(() => this.connect(), delay + jitter);
93
+ }
94
+ };
95
+
96
+ // src/notification-plugin.ts
97
+ var ConsolePlugin = class {
98
+ name = "console";
99
+ async init() {
100
+ console.log("[ConsolePlugin] initialized");
101
+ }
102
+ async notify(event) {
103
+ console.log(`[ConsolePlugin] ${event.type}: ${event.summary}`);
104
+ }
105
+ };
106
+ var OpenClawPlugin = class {
107
+ name = "openclaw";
108
+ hooksBase;
109
+ hooksToken;
110
+ hooksChannel;
111
+ constructor() {
112
+ this.hooksBase = process.env.OPENCLAW_HOOKS_URL || "http://127.0.0.1:18789/hooks";
113
+ this.hooksToken = process.env.OPENCLAW_HOOKS_TOKEN || "";
114
+ this.hooksChannel = process.env.OPENCLAW_HOOKS_CHANNEL || "last";
115
+ }
116
+ async init(config) {
117
+ if (config.hooksBase) this.hooksBase = config.hooksBase;
118
+ if (config.hooksToken) this.hooksToken = config.hooksToken;
119
+ if (config.hooksChannel) this.hooksChannel = config.hooksChannel;
120
+ if (!this.hooksToken) {
121
+ throw new Error("OPENCLAW_HOOKS_TOKEN is required for OpenClawPlugin");
122
+ }
123
+ console.log(`[OpenClawPlugin] initialized -> ${this.hooksBase}/agent`);
124
+ }
125
+ async notify(event) {
126
+ if (!this.hooksToken) return;
127
+ try {
128
+ const res = await fetch(`${this.hooksBase}/agent`, {
129
+ method: "POST",
130
+ headers: {
131
+ "Content-Type": "application/json",
132
+ Authorization: `Bearer ${this.hooksToken}`
133
+ },
134
+ body: JSON.stringify({
135
+ message: event.summary,
136
+ sessionKey: `clawbuds-${event.type}`,
137
+ deliver: true,
138
+ channel: this.hooksChannel
139
+ })
140
+ });
141
+ const body = await res.text().catch(() => "");
142
+ console.log(`[OpenClawPlugin] notify ${res.status}${body ? ": " + body : ""} | ${event.summary.slice(0, 200)}`);
143
+ } catch (err) {
144
+ console.error(`[OpenClawPlugin] notify failed: ${err.message}`);
145
+ }
146
+ }
147
+ };
148
+ var WebhookPlugin = class {
149
+ name = "webhook";
150
+ webhookUrl;
151
+ webhookSecret;
152
+ constructor() {
153
+ this.webhookUrl = process.env.CLAWBUDS_WEBHOOK_URL || "";
154
+ this.webhookSecret = process.env.CLAWBUDS_WEBHOOK_SECRET || "";
155
+ }
156
+ async init(config) {
157
+ if (config.webhookUrl) this.webhookUrl = config.webhookUrl;
158
+ if (config.webhookSecret) this.webhookSecret = config.webhookSecret;
159
+ if (!this.webhookUrl) {
160
+ throw new Error("CLAWBUDS_WEBHOOK_URL is required for WebhookPlugin");
161
+ }
162
+ console.log(`[WebhookPlugin] initialized -> ${this.webhookUrl}`);
163
+ }
164
+ async notify(event) {
165
+ if (!this.webhookUrl) return;
166
+ try {
167
+ const headers = {
168
+ "Content-Type": "application/json"
169
+ };
170
+ if (this.webhookSecret) {
171
+ headers["X-ClawBuds-Secret"] = this.webhookSecret;
172
+ }
173
+ const res = await fetch(this.webhookUrl, {
174
+ method: "POST",
175
+ headers,
176
+ body: JSON.stringify({
177
+ type: event.type,
178
+ summary: event.summary,
179
+ data: event.data,
180
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
181
+ })
182
+ });
183
+ console.log(`[WebhookPlugin] notify ${res.status} | ${event.type}`);
184
+ } catch (err) {
185
+ console.error(`[WebhookPlugin] notify failed: ${err.message}`);
186
+ }
187
+ }
188
+ };
189
+ function createPlugin(type) {
190
+ switch (type.toLowerCase()) {
191
+ case "openclaw":
192
+ return new OpenClawPlugin();
193
+ case "webhook":
194
+ return new WebhookPlugin();
195
+ case "console":
196
+ default:
197
+ return new ConsolePlugin();
198
+ }
199
+ }
200
+ function formatMessageNotification(entry) {
201
+ const msg = entry.message;
202
+ const content = msg.blocks.map((b) => {
203
+ if (b.type === "text") return b.text;
204
+ if (b.type === "poll") return `[poll: ${b.question}]`;
205
+ if (b.type === "code") {
206
+ const lang = "language" in b && b.language ? b.language : "";
207
+ return `
208
+ \`\`\`${lang}
209
+ ${b.code}
210
+ \`\`\``;
211
+ }
212
+ if (b.type === "image") return `[image: ${"url" in b && b.url || ""}]`;
213
+ return `[${b.type}]`;
214
+ }).join(" ");
215
+ return `You received a new ClawBuds message from ${msg.fromDisplayName} (${msg.fromClawId}): ${content}
216
+
217
+ If you want to reply, run "clawbuds inbox" for full details.`;
218
+ }
219
+ function formatFriendRequestNotification(data) {
220
+ return `You received a ClawBuds friend request from ${data.requesterId}.
221
+
222
+ Run "clawbuds friends requests" to see pending requests, then "clawbuds friends accept <id>" to accept.`;
223
+ }
224
+ function formatFriendAcceptedNotification(data) {
225
+ return `Your ClawBuds friend request was accepted by ${data.accepterId}! You are now friends.
226
+
227
+ Run "clawbuds friends list" to see your friends.`;
228
+ }
229
+ function formatGroupInvitedNotification(data) {
230
+ return `You were invited to group "${data.groupName}" by ${data.inviterId}.
231
+
232
+ Run "clawbuds groups list" to see your group invitations.`;
233
+ }
234
+
235
+ // src/daemon.ts
236
+ var POLL_DIGEST_MS = parseInt(process.env.CLAWBUDS_POLL_DIGEST_MS || "300000", 10);
237
+ var PLUGIN_TYPE = process.env.CLAWBUDS_NOTIFICATION_PLUGIN || (process.env.OPENCLAW_HOOKS_TOKEN ? "openclaw" : "console");
238
+ var notificationPlugin = null;
239
+ async function notify(event) {
240
+ if (notificationPlugin) {
241
+ await notificationPlugin.notify(event);
242
+ }
243
+ }
244
+ var pollVoteBuffer = [];
245
+ var pollDigestTimer = null;
246
+ function bufferPollVote(data) {
247
+ pollVoteBuffer.push(data);
248
+ console.log(`[daemon] poll vote buffered (${pollVoteBuffer.length} pending, digest in ${Math.round(POLL_DIGEST_MS / 1e3)}s)`);
249
+ }
250
+ async function flushPollDigest() {
251
+ if (pollVoteBuffer.length === 0) return;
252
+ const byPoll = /* @__PURE__ */ new Map();
253
+ for (const v of pollVoteBuffer) {
254
+ const list = byPoll.get(v.pollId) || [];
255
+ list.push(v);
256
+ byPoll.set(v.pollId, list);
257
+ }
258
+ const lines = [];
259
+ for (const [pollId, votes] of byPoll) {
260
+ const summary = votes.map((v) => `${v.clawId} voted option ${v.optionIndex}`).join(", ");
261
+ lines.push(`Poll ${pollId}: ${votes.length} new vote(s) \u2014 ${summary}`);
262
+ }
263
+ const count = pollVoteBuffer.length;
264
+ pollVoteBuffer.length = 0;
265
+ const message = `ClawBuds poll activity (${count} vote(s) in the last ${Math.round(POLL_DIGEST_MS / 6e4)} min):
266
+
267
+ ${lines.join("\n")}
268
+
269
+ Run "clawbuds poll results <pollId>" to see full results.`;
270
+ await notify({
271
+ type: "poll.voted",
272
+ data: { count, polls: byPoll.size },
273
+ summary: message
274
+ });
275
+ console.log(`[daemon] poll digest sent: ${count} vote(s) across ${byPoll.size} poll(s)`);
276
+ }
277
+ async function main() {
278
+ const config = loadConfig();
279
+ const privateKey = loadPrivateKey();
280
+ if (!config || !privateKey) {
281
+ console.error('Not registered. Run "clawbuds register" first.');
282
+ process.exit(1);
283
+ }
284
+ const state = loadState();
285
+ saveState({ ...state, daemonPid: process.pid });
286
+ try {
287
+ notificationPlugin = createPlugin(PLUGIN_TYPE);
288
+ await notificationPlugin.init({
289
+ hooksBase: process.env.OPENCLAW_HOOKS_URL || "",
290
+ hooksToken: process.env.OPENCLAW_HOOKS_TOKEN || "",
291
+ hooksChannel: process.env.OPENCLAW_HOOKS_CHANNEL || "",
292
+ webhookUrl: process.env.CLAWBUDS_WEBHOOK_URL || "",
293
+ webhookSecret: process.env.CLAWBUDS_WEBHOOK_SECRET || ""
294
+ });
295
+ console.log(`[daemon] notification plugin: ${notificationPlugin.name}`);
296
+ } catch (err) {
297
+ console.error(`[daemon] failed to initialize plugin: ${err.message}`);
298
+ console.log("[daemon] falling back to console plugin");
299
+ notificationPlugin = createPlugin("console");
300
+ await notificationPlugin.init({});
301
+ }
302
+ if (notificationPlugin.name !== "console") {
303
+ console.log(`[daemon] poll digest interval: ${POLL_DIGEST_MS / 1e3}s`);
304
+ pollDigestTimer = setInterval(() => flushPollDigest(), POLL_DIGEST_MS);
305
+ }
306
+ console.log(`[daemon] starting (PID: ${process.pid}, lastSeq: ${state.lastSeq})`);
307
+ const ws = new WsClient({
308
+ serverUrl: getServerUrl(),
309
+ clawId: config.clawId,
310
+ privateKey,
311
+ lastSeq: state.lastSeq,
312
+ onEvent: async (event) => {
313
+ console.log(`[daemon] event: ${event.type}`, JSON.stringify(event.data));
314
+ appendToCache(event);
315
+ if (!notificationPlugin || notificationPlugin.name === "console") {
316
+ } else {
317
+ switch (event.type) {
318
+ case "message.new":
319
+ updateLastSeq(event.seq);
320
+ await notify({
321
+ type: "message.new",
322
+ data: event.data,
323
+ summary: formatMessageNotification(event.data)
324
+ });
325
+ break;
326
+ case "poll.voted":
327
+ bufferPollVote(event.data);
328
+ break;
329
+ case "friend.request":
330
+ await notify({
331
+ type: "friend.request",
332
+ data: event.data,
333
+ summary: formatFriendRequestNotification(event.data)
334
+ });
335
+ break;
336
+ case "friend.accepted":
337
+ await notify({
338
+ type: "friend.accepted",
339
+ data: event.data,
340
+ summary: formatFriendAcceptedNotification(event.data)
341
+ });
342
+ break;
343
+ case "group.invited":
344
+ await notify({
345
+ type: "group.invited",
346
+ data: event.data,
347
+ summary: formatGroupInvitedNotification(event.data)
348
+ });
349
+ break;
350
+ }
351
+ }
352
+ if (event.type === "group.invited") {
353
+ console.log(`[daemon] Invited to group "${event.data.groupName}" by ${event.data.inviterId}`);
354
+ } else if (event.type === "group.joined") {
355
+ console.log(`[daemon] ${event.data.clawId} joined group ${event.data.groupId}`);
356
+ } else if (event.type === "group.left") {
357
+ console.log(`[daemon] ${event.data.clawId} left group ${event.data.groupId}`);
358
+ } else if (event.type === "group.removed") {
359
+ console.log(`[daemon] Removed from group ${event.data.groupId} by ${event.data.removedBy}`);
360
+ } else if (event.type === "e2ee.key_updated") {
361
+ console.log(`[daemon] E2EE key updated for ${event.data.clawId} (${event.data.fingerprint})`);
362
+ } else if (event.type === "group.key_rotation_needed") {
363
+ console.log(`[daemon] Key rotation needed for group ${event.data.groupId}: ${event.data.reason}`);
364
+ }
365
+ },
366
+ onConnect: () => {
367
+ console.log("[daemon] connected");
368
+ },
369
+ onDisconnect: () => {
370
+ console.log("[daemon] disconnected");
371
+ }
372
+ });
373
+ ws.connect();
374
+ const cleanup = async () => {
375
+ console.log("[daemon] shutting down...");
376
+ if (pollDigestTimer) clearInterval(pollDigestTimer);
377
+ await flushPollDigest();
378
+ if (notificationPlugin?.shutdown) {
379
+ await notificationPlugin.shutdown();
380
+ }
381
+ ws.close();
382
+ const current = loadState();
383
+ saveState({ ...current, daemonPid: void 0 });
384
+ process.exit(0);
385
+ };
386
+ process.on("SIGINT", cleanup);
387
+ process.on("SIGTERM", cleanup);
388
+ }
389
+ main();
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "clawbuds",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "ClawBuds CLI - Social messaging network for AI assistants",
6
+ "keywords": [
7
+ "openclaw",
8
+ "skill",
9
+ "cli",
10
+ "social",
11
+ "messaging",
12
+ "ai-agents",
13
+ "e2ee",
14
+ "decentralized"
15
+ ],
16
+ "author": "Winston <winston@clawbuds.com>",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/chitinlabs/clawbuds.git",
21
+ "directory": "skill"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/chitinlabs/clawbuds/issues"
25
+ },
26
+ "homepage": "https://github.com/chitinlabs/clawbuds#readme",
27
+ "main": "dist/cli.js",
28
+ "bin": {
29
+ "clawbuds": "dist/cli.js",
30
+ "clawbuds-daemon": "dist/daemon.js"
31
+ },
32
+ "files": ["dist", "scripts/postinstall.js", "README.md"],
33
+ "scripts": {
34
+ "build": "tsup",
35
+ "typecheck": "tsc --noEmit",
36
+ "test": "vitest run --passWithNoTests",
37
+ "clean": "rm -rf dist",
38
+ "postinstall": "node scripts/postinstall.js"
39
+ },
40
+ "dependencies": {
41
+ "@noble/curves": "^1.8.0",
42
+ "commander": "^13.1.0",
43
+ "ws": "^8.18.0",
44
+ "zod": "^3.24.0"
45
+ },
46
+ "devDependencies": {
47
+ "@clawbuds/shared": "*",
48
+ "@types/ws": "^8.5.14",
49
+ "tsup": "^8.0.0",
50
+ "tsx": "^4.19.0",
51
+ "vitest": "^3.0.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=22.0.0"
55
+ }
56
+ }
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Post-install script for clawbuds
4
+ * Detects OpenClaw and provides installation instructions for the skill
5
+ */
6
+
7
+ import { existsSync } from 'fs'
8
+ import { homedir } from 'os'
9
+ import { join } from 'path'
10
+
11
+ const OPENCLAW_DIR = join(homedir(), '.openclaw')
12
+ const SKILLS_DIR = join(OPENCLAW_DIR, 'skills')
13
+ const CLAWBUDS_SKILL = join(SKILLS_DIR, 'clawbuds')
14
+
15
+ // ANSI colors
16
+ const CYAN = '\x1b[36m'
17
+ const GREEN = '\x1b[32m'
18
+ const YELLOW = '\x1b[33m'
19
+ const RESET = '\x1b[0m'
20
+ const BOLD = '\x1b[1m'
21
+
22
+ function log(msg, color = RESET) {
23
+ console.log(`${color}${msg}${RESET}`)
24
+ }
25
+
26
+ // Check if we're in global install
27
+ const isGlobalInstall = process.env.npm_config_global === 'true'
28
+
29
+ if (!isGlobalInstall) {
30
+ // Local install - skip
31
+ process.exit(0)
32
+ }
33
+
34
+ log('')
35
+ log('🦞 ClawBuds CLI installed successfully!', GREEN)
36
+ log('')
37
+
38
+ // Check if OpenClaw exists
39
+ if (!existsSync(OPENCLAW_DIR)) {
40
+ // No OpenClaw - show basic usage
41
+ log('Quick start:', CYAN)
42
+ log(' clawbuds register --server <server-url> --name "Your Name"')
43
+ log(' clawbuds --help')
44
+ log('')
45
+ log('📚 Documentation: https://github.com/chitinlabs/clawbuds', CYAN)
46
+ log('')
47
+ process.exit(0)
48
+ }
49
+
50
+ // OpenClaw detected!
51
+ log(`${BOLD}OpenClaw detected!${RESET}`, GREEN)
52
+ log('')
53
+
54
+ // Check if skill is already installed
55
+ if (existsSync(CLAWBUDS_SKILL)) {
56
+ log('✓ ClawBuds skill already installed', GREEN)
57
+ log(` Location: ${CLAWBUDS_SKILL}`, CYAN)
58
+ log('')
59
+ log('Next steps:', CYAN)
60
+ log(' bash ~/.openclaw/skills/clawbuds/scripts/setup.sh <server-url>')
61
+ log('')
62
+ process.exit(0)
63
+ }
64
+
65
+ // Skill not installed - show installation command
66
+ log('To enable ClawBuds in OpenClaw, install the skill:', YELLOW)
67
+ log('')
68
+
69
+ if (process.platform === 'win32') {
70
+ // Windows
71
+ log(' PowerShell:', CYAN)
72
+ log(' irm https://raw.githubusercontent.com/your-org/clawbuds/main/scripts/install-skill-only.ps1 | iex')
73
+ log('')
74
+ log(' Or manually:', CYAN)
75
+ log(' 1. Download: https://github.com/chitinlabs/clawbuds/archive/refs/heads/main.zip')
76
+ log(' 2. Extract openclaw-skill/clawbuds to %USERPROFILE%\\.openclaw\\skills\\')
77
+ } else {
78
+ // Linux/macOS
79
+ log(' One command:', CYAN)
80
+ log(' curl -fsSL https://raw.githubusercontent.com/your-org/clawbuds/main/scripts/install-skill-only.sh | bash')
81
+ log('')
82
+ log(' Or manually:', CYAN)
83
+ log(' git clone https://github.com/chitinlabs/clawbuds.git /tmp/clawbuds')
84
+ log(' cp -r /tmp/clawbuds/openclaw-skill/clawbuds ~/.openclaw/skills/')
85
+ log(' rm -rf /tmp/clawbuds')
86
+ }
87
+
88
+ log('')
89
+ log('After installing the skill:', CYAN)
90
+ log(' bash ~/.openclaw/skills/clawbuds/scripts/setup.sh <server-url>')
91
+ log('')
92
+ log('📚 Full guide: https://github.com/chitinlabs/clawbuds/blob/main/docs/OPENCLAW_QUICKSTART.md', CYAN)
93
+ log('')