@clawling/clawchat-plugin-openclaw 2026.10.9-2 → 2026.10.9-4

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.
@@ -0,0 +1,79 @@
1
+ // Who started this turn? The owner's turns are exactly the ones in the owner's
2
+ // activation direct chat (whoever the sender is — plugin-started turns there
3
+ // count). Every group turn and every other direct chat is someone else's.
4
+ // Anything we cannot place is treated as someone else's (fail closed).
5
+ import { CHANNEL_ID, normalizeOpenclawClawlingAccountId } from "./config.ts";
6
+
7
+ export type TurnClass = "owner" | "non-owner";
8
+ export type ParsedClawChatSession = { kind: "direct" | "group"; chatId: string } | null;
9
+
10
+ /** Tools a turn the owner did not start may still see and call. */
11
+ export const NON_OWNER_TOOL_ALLOWLIST: readonly string[] = Object.freeze([
12
+ "clawchat_memory_search", "clawchat_memory_read", "clawchat_memory_write", "clawchat_memory_edit",
13
+ "clawchat_mention_message", "clawchat_react_message", "clawchat_get_user_profile", "clawchat_metadata_sync",
14
+ "web_search",
15
+ ]);
16
+
17
+ const norm = (v: string | undefined | null) => (typeof v === "string" ? v.trim().toLowerCase() : "");
18
+
19
+ // `<channel>:` then `direct:`/`group:`, each optional and stripped at most once.
20
+ const TOOL_CHAT_ID_TARGET_PREFIX = new RegExp(`^(?:${CHANNEL_ID}:)?(?:(?:direct|group):)?`, "i");
21
+
22
+ /**
23
+ * The bare ClawChat chat id a tool argument names. The host shows the model
24
+ * this chat's `chat_id` as the channel-prefixed origin target
25
+ * (`clawchat-plugin-openclaw:cnv_…` / `clawchat-plugin-openclaw:group:cnv_…`)
26
+ * and a group's label as `group:cnv_…`; models copy either form into tool
27
+ * arguments. Only that form is stripped; case is kept, and whatever remains is
28
+ * still validated by the send path (`isValidChatId`) or the server. Every tool
29
+ * that takes a ClawChat chat id, and the non-owner current-chat check, use
30
+ * this one helper so they agree on which chat an argument names.
31
+ */
32
+ export function normalizeToolChatId(value: unknown): string {
33
+ if (typeof value !== "string") return "";
34
+ return value.trim().replace(TOOL_CHAT_ID_TARGET_PREFIX, "").trim();
35
+ }
36
+
37
+ /**
38
+ * Host session keys (dmScope per-account-channel-peer):
39
+ * agent:<agent>:<channel>:<account>:direct:<chat>
40
+ * agent:<agent>:<channel>:group:<chat>
41
+ */
42
+ export function parseClawChatSessionKey(sessionKey: string | undefined | null): ParsedClawChatSession {
43
+ const parts = norm(sessionKey).split(":");
44
+ if (parts.length < 5 || parts[0] !== "agent" || parts[2] !== CHANNEL_ID) return null;
45
+ if (parts[3] === "group" && parts.length === 5 && parts[4]) return { kind: "group", chatId: parts[4] };
46
+ if (parts[4] === "direct" && parts.length === 6 && parts[5]) return { kind: "direct", chatId: parts[5] };
47
+ return null;
48
+ }
49
+
50
+ /**
51
+ * Account id carried by a ClawChat direct session key, normalized exactly once
52
+ * with the plugin's account-id normalizer. Group keys carry no account
53
+ * segment, and a blank segment is missing (never silently "default").
54
+ */
55
+ export function clawChatSessionAccountId(sessionKey: string | undefined | null): string | undefined {
56
+ if (parseClawChatSessionKey(sessionKey)?.kind !== "direct") return undefined;
57
+ const segment = (sessionKey ?? "").trim().split(":")[3]?.trim();
58
+ return segment ? normalizeOpenclawClawlingAccountId(segment) : undefined;
59
+ }
60
+
61
+ export function classifyChat(input: {
62
+ kind: "direct" | "group" | string;
63
+ chatId?: string | undefined | null;
64
+ ownerDirectConversationId?: string | undefined | null;
65
+ }): TurnClass {
66
+ const owner = norm(input.ownerDirectConversationId);
67
+ const chat = norm(input.chatId);
68
+ if (input.kind !== "direct" || !owner || !chat) return "non-owner";
69
+ return chat === owner ? "owner" : "non-owner";
70
+ }
71
+
72
+ export function classifyTurn(input: {
73
+ sessionKey?: string | null;
74
+ ownerDirectConversationId?: string | null;
75
+ }): TurnClass {
76
+ const parsed = parseClawChatSessionKey(input.sessionKey);
77
+ if (!parsed) return "non-owner";
78
+ return classifyChat({ ...parsed, ownerDirectConversationId: input.ownerDirectConversationId });
79
+ }