@clawling/clawchat-plugin-openclaw 2026.5.12-28

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 (114) hide show
  1. package/INSTALL.md +64 -0
  2. package/README.md +227 -0
  3. package/dist/index.js +20 -0
  4. package/dist/setup-entry.js +3 -0
  5. package/dist/src/api-client.js +263 -0
  6. package/dist/src/api-types.js +17 -0
  7. package/dist/src/api-types.test-d.js +10 -0
  8. package/dist/src/buffered-stream.js +177 -0
  9. package/dist/src/channel.js +66 -0
  10. package/dist/src/channel.setup.js +119 -0
  11. package/dist/src/clawchat-memory.js +403 -0
  12. package/dist/src/clawchat-metadata.js +310 -0
  13. package/dist/src/client.js +35 -0
  14. package/dist/src/commands.js +35 -0
  15. package/dist/src/config.js +274 -0
  16. package/dist/src/group-message-coalescer.js +119 -0
  17. package/dist/src/inbound.js +170 -0
  18. package/dist/src/llm-context-debug.js +86 -0
  19. package/dist/src/login.runtime.js +204 -0
  20. package/dist/src/media-runtime.js +85 -0
  21. package/dist/src/message-mapper.js +146 -0
  22. package/dist/src/mock-transport.js +31 -0
  23. package/dist/src/outbound.js +628 -0
  24. package/dist/src/plugin-prompts.js +89 -0
  25. package/dist/src/profile-prompt.js +269 -0
  26. package/dist/src/profile-sync.js +110 -0
  27. package/dist/src/prompt-injection.js +25 -0
  28. package/dist/src/protocol-types.js +63 -0
  29. package/dist/src/protocol-types.typecheck.js +1 -0
  30. package/dist/src/protocol.js +33 -0
  31. package/dist/src/reply-dispatcher.js +422 -0
  32. package/dist/src/runtime.js +1254 -0
  33. package/dist/src/storage.js +525 -0
  34. package/dist/src/streaming.js +65 -0
  35. package/dist/src/terminal-send.js +36 -0
  36. package/dist/src/tools-schema.js +208 -0
  37. package/dist/src/tools.js +920 -0
  38. package/dist/src/ws-alignment.js +178 -0
  39. package/dist/src/ws-client.js +588 -0
  40. package/dist/src/ws-log.js +19 -0
  41. package/index.ts +24 -0
  42. package/openclaw.plugin.json +169 -0
  43. package/package.json +80 -0
  44. package/prompts/default-group-bio.md +19 -0
  45. package/prompts/default-owner-behavior.md +27 -0
  46. package/prompts/platform.md +13 -0
  47. package/setup-entry.ts +4 -0
  48. package/skills/clawchat/SKILL.md +91 -0
  49. package/src/api-client.test.ts +827 -0
  50. package/src/api-client.ts +414 -0
  51. package/src/api-types.ts +146 -0
  52. package/src/channel.outbound.test.ts +433 -0
  53. package/src/channel.setup.ts +145 -0
  54. package/src/channel.test.ts +262 -0
  55. package/src/channel.ts +81 -0
  56. package/src/clawchat-memory.test.ts +480 -0
  57. package/src/clawchat-memory.ts +533 -0
  58. package/src/clawchat-metadata.test.ts +477 -0
  59. package/src/clawchat-metadata.ts +429 -0
  60. package/src/client.test.ts +169 -0
  61. package/src/client.ts +56 -0
  62. package/src/commands.test.ts +39 -0
  63. package/src/commands.ts +41 -0
  64. package/src/config.test.ts +344 -0
  65. package/src/config.ts +404 -0
  66. package/src/group-message-coalescer.test.ts +237 -0
  67. package/src/group-message-coalescer.ts +171 -0
  68. package/src/inbound.test.ts +508 -0
  69. package/src/inbound.ts +278 -0
  70. package/src/llm-context-debug.test.ts +55 -0
  71. package/src/llm-context-debug.ts +139 -0
  72. package/src/login.runtime.test.ts +737 -0
  73. package/src/login.runtime.ts +277 -0
  74. package/src/manifest.test.ts +352 -0
  75. package/src/media-runtime.test.ts +207 -0
  76. package/src/media-runtime.ts +152 -0
  77. package/src/message-mapper.test.ts +201 -0
  78. package/src/message-mapper.ts +174 -0
  79. package/src/mock-transport.test.ts +35 -0
  80. package/src/mock-transport.ts +38 -0
  81. package/src/outbound.test.ts +1269 -0
  82. package/src/outbound.ts +803 -0
  83. package/src/plugin-entry.test.ts +38 -0
  84. package/src/plugin-prompts.test.ts +94 -0
  85. package/src/plugin-prompts.ts +107 -0
  86. package/src/profile-prompt.test.ts +274 -0
  87. package/src/profile-prompt.ts +351 -0
  88. package/src/profile-sync.test.ts +539 -0
  89. package/src/profile-sync.ts +191 -0
  90. package/src/prompt-injection.test.ts +39 -0
  91. package/src/prompt-injection.ts +45 -0
  92. package/src/protocol-types.test.ts +69 -0
  93. package/src/protocol-types.ts +296 -0
  94. package/src/protocol-types.typecheck.ts +89 -0
  95. package/src/protocol.test.ts +39 -0
  96. package/src/protocol.ts +42 -0
  97. package/src/reply-dispatcher.test.ts +1324 -0
  98. package/src/reply-dispatcher.ts +555 -0
  99. package/src/runtime.test.ts +4719 -0
  100. package/src/runtime.ts +1493 -0
  101. package/src/scripts.test.ts +85 -0
  102. package/src/storage.test.ts +560 -0
  103. package/src/storage.ts +807 -0
  104. package/src/terminal-send.test.ts +81 -0
  105. package/src/terminal-send.ts +56 -0
  106. package/src/tools-schema.ts +337 -0
  107. package/src/tools.test.ts +933 -0
  108. package/src/tools.ts +1185 -0
  109. package/src/ws-alignment.test.ts +103 -0
  110. package/src/ws-alignment.ts +275 -0
  111. package/src/ws-client.test.ts +1217 -0
  112. package/src/ws-client.ts +662 -0
  113. package/src/ws-log.test.ts +32 -0
  114. package/src/ws-log.ts +31 -0
@@ -0,0 +1,174 @@
1
+ import type { Fragment } from "./protocol-types.ts";
2
+ import type { MediaItem } from "./media-runtime.ts";
3
+
4
+ interface FlattenOptions {
5
+ mentionFallbackIds?: string[];
6
+ }
7
+
8
+ export interface MentionTarget {
9
+ userId: string;
10
+ display?: string;
11
+ }
12
+
13
+ export interface NormalizedMentionTarget {
14
+ userId: string;
15
+ display?: string;
16
+ }
17
+
18
+ export interface MentionedUser {
19
+ id: string;
20
+ display?: string;
21
+ }
22
+
23
+ const MEDIA_KINDS = new Set(["image", "file", "audio", "video"]);
24
+
25
+ function isMediaKind(kind: string): kind is MediaItem["kind"] {
26
+ return MEDIA_KINDS.has(kind);
27
+ }
28
+
29
+ function renderMediaPlaceholder(fragment: Record<string, unknown>): string {
30
+ const kind = String(fragment.kind ?? "");
31
+ const url = typeof fragment.url === "string" ? fragment.url : "";
32
+ if (!url) return "";
33
+ const name =
34
+ typeof fragment.name === "string" && fragment.name.trim()
35
+ ? fragment.name
36
+ : kind === "image"
37
+ ? "image"
38
+ : kind === "audio"
39
+ ? "audio"
40
+ : kind === "video"
41
+ ? "video"
42
+ : "file";
43
+ return kind === "image" ? `![${name}](${url})` : `[${name}](${url})`;
44
+ }
45
+
46
+ export function fragmentsToText(fragments: Fragment[], opts: FlattenOptions = {}): string {
47
+ const fallback = opts.mentionFallbackIds ?? [];
48
+ let fallbackCursor = 0;
49
+ const parts = fragments.map((fragment) => {
50
+ const f = fragment as unknown as Record<string, unknown>;
51
+ if (f.kind === "text" && typeof f.text === "string") {
52
+ return f.text;
53
+ }
54
+ if (f.kind === "mention") {
55
+ const display = typeof f.display === "string" ? f.display : undefined;
56
+ if (display && display.trim()) {
57
+ const trimmed = display.trim();
58
+ return trimmed.startsWith("@") ? trimmed : `@${trimmed}`;
59
+ }
60
+ const id = typeof f.user_id === "string" ? f.user_id : undefined;
61
+ if (id && id.trim()) return `@${id}`;
62
+ const fallbackId = fallback[fallbackCursor++];
63
+ if (fallbackId) return `@${fallbackId}`;
64
+ return "";
65
+ }
66
+ if (typeof f.kind === "string" && isMediaKind(f.kind)) {
67
+ return renderMediaPlaceholder(f);
68
+ }
69
+ return "";
70
+ });
71
+ return parts.join("").trim();
72
+ }
73
+
74
+ export function textToFragments(text: string): Fragment[] {
75
+ if (!text || !text.trim()) return [];
76
+ return [{ kind: "text", text }];
77
+ }
78
+
79
+ export function normalizeMentionTargets(mentions: MentionTarget[]): NormalizedMentionTarget[] {
80
+ if (!Array.isArray(mentions) || mentions.length === 0) {
81
+ throw new Error("clawchat_mention_message requires at least one mention");
82
+ }
83
+
84
+ const seen = new Set<string>();
85
+ const normalized: NormalizedMentionTarget[] = [];
86
+ for (let i = 0; i < mentions.length; i += 1) {
87
+ const mention = mentions[i] as Partial<MentionTarget> | undefined;
88
+ const userId = typeof mention?.userId === "string" ? mention.userId.trim() : "";
89
+ if (!userId) {
90
+ throw new Error(`clawchat_mention_message requires mentions[${i}].userId`);
91
+ }
92
+ if (seen.has(userId)) continue;
93
+
94
+ const rawDisplay = typeof mention?.display === "string" ? mention.display.trim() : "";
95
+ const display = rawDisplay ? (rawDisplay.startsWith("@") ? rawDisplay.slice(1) : rawDisplay) : undefined;
96
+ seen.add(userId);
97
+ normalized.push(display ? { userId, display } : { userId });
98
+ }
99
+ return normalized;
100
+ }
101
+
102
+ const MENTION_LABEL_RE = /^@(?<label>\S+)(?<rest>(?:\s+.*)?)$/s;
103
+
104
+ export function applyTextMentionLabels(
105
+ mentions: NormalizedMentionTarget[],
106
+ text?: string,
107
+ ): { mentions: NormalizedMentionTarget[]; text: string } {
108
+ let remaining = typeof text === "string" ? text.trim() : "";
109
+ if (!remaining) return { mentions, text: "" };
110
+
111
+ const normalized = mentions.map((mention) => ({ ...mention }));
112
+ const missingDisplay = normalized.filter((mention) => !mention.display);
113
+ if (normalized.length === 1 && missingDisplay.length === 1 && remaining.startsWith("@")) {
114
+ const label = remaining.slice(1).trim();
115
+ if (label) {
116
+ missingDisplay[0].display = label;
117
+ return { mentions: normalized, text: "" };
118
+ }
119
+ }
120
+
121
+ for (const mention of normalized) {
122
+ if (mention.display) continue;
123
+ const match = MENTION_LABEL_RE.exec(remaining);
124
+ if (!match?.groups?.label) break;
125
+ mention.display = match.groups.label.trim();
126
+ remaining = (match.groups.rest ?? "").trim();
127
+ }
128
+ return { mentions: normalized, text: remaining };
129
+ }
130
+
131
+ export function buildMentionMessageFragments({
132
+ mentions,
133
+ text,
134
+ }: {
135
+ mentions: MentionTarget[];
136
+ text?: string;
137
+ }): Fragment[] {
138
+ const { mentions: normalized, text: remainingText } = applyTextMentionLabels(normalizeMentionTargets(mentions), text);
139
+ const fragments: Fragment[] = normalized.map((mention) => {
140
+ const fragment: Fragment = {
141
+ kind: "mention",
142
+ user_id: mention.userId,
143
+ };
144
+ if (mention.display) fragment.display = mention.display;
145
+ return fragment;
146
+ });
147
+ if (remainingText) {
148
+ fragments.push({ kind: "text", text: ` ${remainingText}` });
149
+ }
150
+ return fragments;
151
+ }
152
+
153
+ /**
154
+ * Extract media fragments from a body (image/file/audio/video). Skips
155
+ * entries missing `url`. Preserves all optional metadata fields the
156
+ * protocol carries through (mime/size/width/height/duration/name).
157
+ */
158
+ export function extractMediaFragments(fragments: Fragment[]): MediaItem[] {
159
+ const out: MediaItem[] = [];
160
+ for (const fragment of fragments) {
161
+ const f = fragment as unknown as Record<string, unknown>;
162
+ if (typeof f.kind !== "string" || !isMediaKind(f.kind)) continue;
163
+ if (typeof f.url !== "string" || !f.url) continue;
164
+ const item: MediaItem = { kind: f.kind, url: f.url };
165
+ if (typeof f.name === "string") item.name = f.name;
166
+ if (typeof f.mime === "string") item.mime = f.mime;
167
+ if (typeof f.size === "number") item.size = f.size;
168
+ if (typeof f.width === "number") item.width = f.width;
169
+ if (typeof f.height === "number") item.height = f.height;
170
+ if (typeof f.duration === "number") item.duration = f.duration;
171
+ out.push(item);
172
+ }
173
+ return out;
174
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { MockTransport } from "./mock-transport.ts";
3
+
4
+ describe("MockTransport", () => {
5
+ it("opens, records sent frames, emits inbound frames, and closes", async () => {
6
+ const transport = new MockTransport();
7
+ const onOpen = vi.fn();
8
+ const onMessage = vi.fn();
9
+ const onClose = vi.fn();
10
+ const onError = vi.fn();
11
+
12
+ await transport.connect("ws://test", { onOpen, onMessage, onClose, onError });
13
+ expect(transport.state).toBe("open");
14
+ expect(onOpen).toHaveBeenCalledTimes(1);
15
+
16
+ transport.send("frame-1");
17
+ expect(transport.sent).toEqual(["frame-1"]);
18
+
19
+ transport.emitInbound("frame-2");
20
+ expect(onMessage).toHaveBeenCalledWith("frame-2");
21
+
22
+ const buffer = Buffer.from("frame-3");
23
+ transport.emitInbound(buffer);
24
+ expect(onMessage).toHaveBeenCalledWith(buffer);
25
+
26
+ const err = new Error("boom");
27
+ transport.emitError(err);
28
+ expect(onError).toHaveBeenCalledWith(err);
29
+
30
+ transport.close(1000, "done");
31
+ expect(transport.state).toBe("closed");
32
+ expect(onClose).toHaveBeenCalledWith(1000, "done");
33
+ expect(onError).toHaveBeenCalledTimes(1);
34
+ });
35
+ });
@@ -0,0 +1,38 @@
1
+ import type { Transport, TransportEvents, TransportState } from "./protocol-types.ts";
2
+
3
+ export class MockTransport implements Transport {
4
+ private handlers?: TransportEvents;
5
+ private currentState: TransportState = "closed";
6
+ readonly sent: string[] = [];
7
+
8
+ get state(): TransportState {
9
+ return this.currentState;
10
+ }
11
+
12
+ async connect(_url: string, handlers: TransportEvents): Promise<void> {
13
+ this.handlers = handlers;
14
+ this.currentState = "open";
15
+ handlers.onOpen();
16
+ }
17
+
18
+ send(data: string): void {
19
+ if (this.currentState !== "open") {
20
+ throw new Error("transport is not open");
21
+ }
22
+ this.sent.push(data);
23
+ }
24
+
25
+ close(code = 1000, reason = "client close"): void {
26
+ if (this.currentState === "closed") return;
27
+ this.currentState = "closed";
28
+ this.handlers?.onClose(code, reason);
29
+ }
30
+
31
+ emitInbound(data: string | Buffer): void {
32
+ this.handlers?.onMessage(data);
33
+ }
34
+
35
+ emitError(err: Error): void {
36
+ this.handlers?.onError(err);
37
+ }
38
+ }