clawgram 2.22.0 → 2.24.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.
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ // Имена действий: что принимается и во что разрешается.
3
+ //
4
+ // Вынесено из channel.ts, который был единственным файлом на 3274 строки при
5
+ // следующем по величине 1165 (находка A6-11). Здесь нет ни рантайма, ни
6
+ // конфига — только словарь, поэтому читать и править его можно, не открывая
7
+ // диспетчер.
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.MANAGE_ACTIONS = exports.CORE_ACTION_SYNONYMS = void 0;
10
+ exports.canonicalAction = canonicalAction;
11
+ /**
12
+ * Every accepted spelling of an action, mapped to its canonical name.
13
+ *
14
+ * One table, not three. The synonyms used to live in
15
+ * `CORE_ACTION_SYNONYMS`, again in `MANAGE_ACTION_ALIASES`, and a third time
16
+ * as `action === "…" || …` chains inside the dispatcher — and the dispatcher
17
+ * read only the chains. A name could therefore be added to a table and to the
18
+ * advertised list and still reach nothing, with the suite none the wiser:
19
+ * it only ever dispatched the native spellings (finding A6-10).
20
+ *
21
+ * `canonicalAction` is now the only place a name is resolved, and
22
+ * `CORE_ACTION_SYNONYMS` below is derived from this table rather than kept
23
+ * beside it.
24
+ */
25
+ const ACTION_ALIASES = {
26
+ send: "send",
27
+ read: "read",
28
+ // `list` is accepted so a caller that guessed the other obvious name is not
29
+ // silently refused.
30
+ list: "read",
31
+ react: "react",
32
+ joins: "joins",
33
+ "upload-file": "upload-file",
34
+ sendAttachment: "upload-file",
35
+ "fetch-media": "fetch-media",
36
+ fetchMedia: "fetch-media",
37
+ "download-media": "fetch-media",
38
+ downloadMedia: "fetch-media",
39
+ getMedia: "fetch-media",
40
+ "download-file": "fetch-media",
41
+ participants: "participants",
42
+ members: "participants",
43
+ "member-info": "participants",
44
+ topics: "topics",
45
+ forumTopics: "topics",
46
+ "thread-list": "topics",
47
+ dialogs: "dialogs",
48
+ chats: "dialogs",
49
+ "channel-list": "dialogs",
50
+ chatInfo: "chatInfo",
51
+ getChatInfo: "chatInfo",
52
+ "channel-info": "chatInfo",
53
+ chatMetadata: "chatInfo",
54
+ getChatMetadata: "chatInfo",
55
+ // Chat management. `kick` was already accepted; the rest were advertised
56
+ // under names core does not know and were therefore never callable from the
57
+ // tool at all — 2.19.4 gives them core's nearest name. `transferOwnership`
58
+ // and `inviteLink` have no counterpart in that vocabulary and stay
59
+ // gateway-only, as does `joins`.
60
+ createGroup: "createGroup",
61
+ createChat: "createGroup",
62
+ "create-group": "createGroup",
63
+ "channel-create": "createGroup",
64
+ addMembers: "addMembers",
65
+ addMember: "addMembers",
66
+ "add-members": "addMembers",
67
+ addParticipant: "addMembers",
68
+ removeMember: "removeMember",
69
+ removeMembers: "removeMember",
70
+ "remove-member": "removeMember",
71
+ kick: "removeMember",
72
+ promoteAdmin: "promoteAdmin",
73
+ promote: "promoteAdmin",
74
+ "promote-admin": "promoteAdmin",
75
+ setAdmin: "promoteAdmin",
76
+ "role-add": "promoteAdmin",
77
+ demoteAdmin: "demoteAdmin",
78
+ demote: "demoteAdmin",
79
+ "demote-admin": "demoteAdmin",
80
+ "role-remove": "demoteAdmin",
81
+ transferOwnership: "transferOwnership",
82
+ transferOwner: "transferOwnership",
83
+ "transfer-ownership": "transferOwnership",
84
+ inviteLink: "inviteLink",
85
+ exportInviteLink: "inviteLink",
86
+ "invite-link": "inviteLink",
87
+ };
88
+ /** The canonical action for a spelling; an unknown name stays itself. */
89
+ function canonicalAction(action) {
90
+ return ACTION_ALIASES[action] ?? action;
91
+ }
92
+ /**
93
+ * Core's own name for a clawgram action, and the only thing that makes the
94
+ * action reachable from the agent's `message` tool.
95
+ *
96
+ * Core keys its target policy by `CHANNEL_MESSAGE_ACTION_NAMES`, and an action
97
+ * outside that vocabulary is simultaneously "requires a target" and "does not
98
+ * accept a target" — there is no call that satisfies both. Declaring `chatId`
99
+ * through `messageActionTargetAliases` looks like the fix and is not: core
100
+ * resolves the channel with `getBootstrapChannelPlugin`, which only knows
101
+ * bundled channels, so a plugin channel's declaration is never read. Measured
102
+ * on 2026-08-30 — `thread-list` reached `handleAction` and `topics` did not,
103
+ * from the same caller, on the same chat.
104
+ *
105
+ * Every name on the right maps to core target mode `"none"` except
106
+ * `channel-info`, which is `"channelId"`: the chat arrives in
107
+ * `params.channelId`, a spelling no parser here read until 2.21.0 — so the
108
+ * call fell through to the current chat and answered about the wrong one.
109
+ * `readChatTargetParam` is the single list of accepted spellings now.
110
+ *
111
+ * These spellings are derived from `ACTION_ALIASES` rather than kept beside
112
+ * it; that core actually knows each of them is asserted against the installed
113
+ * core in `core-action-synonyms.test.ts`.
114
+ */
115
+ const CORE_VOCABULARY_SPELLINGS = [
116
+ "thread-list", "channel-list", "channel-info", "member-info", "download-file",
117
+ "channel-create", "addParticipant", "kick", "role-add", "role-remove",
118
+ ];
119
+ exports.CORE_ACTION_SYNONYMS = Object.fromEntries(CORE_VOCABULARY_SPELLINGS.map((name) => [name, ACTION_ALIASES[name]]));
120
+ /** Canonical actions that go through the chat-management gate. */
121
+ exports.MANAGE_ACTIONS = new Set([
122
+ "createGroup", "addMembers", "removeMember",
123
+ "promoteAdmin", "demoteAdmin", "transferOwnership", "inviteLink",
124
+ ]);
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ // Входящее вложение: скачать, понять, отдать текстом.
3
+ //
4
+ // Вынесено из channel.ts — файла на 3145 строк при следующем по величине
5
+ // 1165 (находка A6-11). Ни диспетчера, ни конфигурации канала здесь нет:
6
+ // только путь «файл → текст», общий у входящего контура и у `fetch-media`.
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.INBOUND_MEDIA_MAX_BYTES = void 0;
12
+ exports.understandAttachmentFile = understandAttachmentFile;
13
+ exports.readInboundAttachment = readInboundAttachment;
14
+ const node_os_1 = __importDefault(require("node:os"));
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ const node_fs_1 = require("node:fs");
17
+ const media_1 = require("./media");
18
+ const state_dir_1 = require("./state-dir");
19
+ /** Attachments above this are left unread: a long recording or a huge image is
20
+ * a different conversation from a spoken line or a screenshot, and the
21
+ * transfer is not free. */
22
+ exports.INBOUND_MEDIA_MAX_BYTES = 25 * 1024 * 1024;
23
+ /**
24
+ * Locates the agent directory that image understanding needs.
25
+ *
26
+ * Image models are called with the agent's own credentials, so the pipeline
27
+ * refuses to run without this path — audio does not need it, which is why
28
+ * voice notes worked before images did. The platform exposes no resolver to
29
+ * plugins, so the documented layout is reconstructed here and checked before
30
+ * use: a wrong guess would fail the read anyway, and returning undefined lets
31
+ * the caller degrade instead of throwing.
32
+ */
33
+ function resolveAgentDirForMedia(cfg) {
34
+ const stateDir = (0, state_dir_1.resolveStateDir)();
35
+ const configuredId = cfg?.agents?.defaults?.id;
36
+ const agentId = typeof configuredId === "string" && configuredId.trim() ? configuredId.trim() : "main";
37
+ const dir = node_path_1.default.join(stateDir, "agents", agentId, "agent");
38
+ return (0, node_fs_1.existsSync)(dir) ? dir : undefined;
39
+ }
40
+ /**
41
+ * Turns a downloaded attachment into text.
42
+ *
43
+ * Shared by the inbound path and by `fetch-media`: the backend choice lives in
44
+ * `runtime.mediaUnderstanding`, and both callers have to make exactly the same
45
+ * call — an image read on arrival and the same image read on request must not
46
+ * become two different readings because two call sites drifted.
47
+ */
48
+ async function understandAttachmentFile(params) {
49
+ const media = params.runtime?.mediaUnderstanding;
50
+ if (!media)
51
+ return undefined;
52
+ const result = params.understanding === "transcript"
53
+ ? await media.transcribeAudioFile({
54
+ filePath: params.filePath,
55
+ cfg: params.cfg,
56
+ mime: params.mimeType,
57
+ })
58
+ : await media.describeImageFile({
59
+ filePath: params.filePath,
60
+ cfg: params.cfg,
61
+ mime: params.mimeType,
62
+ agentDir: resolveAgentDirForMedia(params.cfg),
63
+ });
64
+ const text = typeof result?.text === "string" ? result.text.trim() : "";
65
+ return text || undefined;
66
+ }
67
+ async function readInboundAttachment(params) {
68
+ const media = params.runtime?.mediaUnderstanding;
69
+ const message = params.event?.message;
70
+ if (!media || !message) {
71
+ return undefined;
72
+ }
73
+ let downloaded;
74
+ try {
75
+ downloaded = await (0, media_1.downloadInboundMediaToTempFile)({
76
+ client: params.gram.getClient(),
77
+ message,
78
+ maxBytes: exports.INBOUND_MEDIA_MAX_BYTES,
79
+ tmpDir: node_os_1.default.tmpdir(),
80
+ });
81
+ }
82
+ catch (err) {
83
+ params.log?.info?.("clawgram attachment download failed", {
84
+ accountId: params.accountId,
85
+ chatId: params.chatId,
86
+ messageId: params.messageId,
87
+ error: String(err),
88
+ });
89
+ return undefined;
90
+ }
91
+ if (!downloaded) {
92
+ return undefined;
93
+ }
94
+ try {
95
+ const read = await understandAttachmentFile({
96
+ runtime: params.runtime,
97
+ cfg: params.cfg,
98
+ filePath: downloaded.path,
99
+ mimeType: downloaded.mimeType,
100
+ understanding: downloaded.understanding,
101
+ });
102
+ if (!read) {
103
+ params.log?.info?.("clawgram attachment read empty", {
104
+ accountId: params.accountId,
105
+ chatId: params.chatId,
106
+ messageId: params.messageId,
107
+ understanding: downloaded.understanding,
108
+ });
109
+ return undefined;
110
+ }
111
+ params.log?.info?.("clawgram attachment read", {
112
+ accountId: params.accountId,
113
+ chatId: params.chatId,
114
+ messageId: params.messageId,
115
+ understanding: downloaded.understanding,
116
+ characters: read.length,
117
+ });
118
+ return { text: read, understanding: downloaded.understanding };
119
+ }
120
+ catch (err) {
121
+ params.log?.info?.("clawgram attachment read failed", {
122
+ accountId: params.accountId,
123
+ chatId: params.chatId,
124
+ messageId: params.messageId,
125
+ understanding: downloaded.understanding,
126
+ error: String(err),
127
+ });
128
+ return undefined;
129
+ }
130
+ finally {
131
+ void (async () => {
132
+ try {
133
+ const { rm } = await import("node:fs/promises");
134
+ const { dirname } = await import("node:path");
135
+ await rm(dirname(downloaded.path), { recursive: true, force: true });
136
+ }
137
+ catch {
138
+ // Leaving a temp file behind is not worth failing a delivered message.
139
+ }
140
+ })();
141
+ }
142
+ }