@wzfukui/ani 2026.3.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.
package/src/tools.ts ADDED
@@ -0,0 +1,602 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { ChannelAgentTool } from "./sdk-compat.js";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+
6
+ import {
7
+ createAniTask,
8
+ deleteAniTask,
9
+ getAniTask,
10
+ listAniTasks,
11
+ sendAniMessage,
12
+ updateAniTask,
13
+ uploadAniFile,
14
+ type AniTask,
15
+ } from "./monitor/send.js";
16
+ import type { AniAttachment } from "./monitor/send.js";
17
+ import { resolveAniCredentials, getMimeType } from "./utils.js";
18
+
19
+ const TASK_STATUS_VALUES = ["pending", "in_progress", "done", "cancelled", "handed_over"] as const;
20
+ const TASK_PRIORITY_VALUES = ["low", "medium", "high"] as const;
21
+
22
+ function formatTask(task: AniTask): string {
23
+ const assignee = task.assignee?.display_name ?? (task.assignee_id != null ? `#${task.assignee_id}` : "unassigned");
24
+ const creator = task.creator?.display_name ?? `#${task.created_by}`;
25
+ const due = task.due_date ? `, due ${task.due_date}` : "";
26
+ const parent = task.parent_task_id != null ? `, parent #${task.parent_task_id}` : "";
27
+ return [
28
+ `#${task.id} ${task.title}`,
29
+ `status=${task.status}, priority=${task.priority}, assignee=${assignee}, creator=${creator}${due}${parent}`,
30
+ task.description?.trim() ? `description: ${task.description.trim()}` : "",
31
+ ].filter(Boolean).join("\n");
32
+ }
33
+
34
+ function validateTaskStatus(status: string | undefined): string | null {
35
+ if (!status) return null;
36
+ return TASK_STATUS_VALUES.includes(status as typeof TASK_STATUS_VALUES[number])
37
+ ? status
38
+ : `Error: status must be one of ${TASK_STATUS_VALUES.join(", ")}`;
39
+ }
40
+
41
+ function validateTaskPriority(priority: string | undefined): string | null {
42
+ if (!priority) return null;
43
+ return TASK_PRIORITY_VALUES.includes(priority as typeof TASK_PRIORITY_VALUES[number])
44
+ ? priority
45
+ : `Error: priority must be one of ${TASK_PRIORITY_VALUES.join(", ")}`;
46
+ }
47
+
48
+ /**
49
+ * Agent tool: ani_send_file
50
+ *
51
+ * Unified file sending tool — handles both generated text content and existing
52
+ * files on disk. The tool auto-detects the mode based on which parameter is provided:
53
+ *
54
+ * - `file_path`: Read binary/text file from disk and send (for screenshots, PDFs, etc.)
55
+ * - `content`: Create a new file from text content and send (for generated .md, .csv, etc.)
56
+ *
57
+ * If both are provided, `file_path` takes precedence.
58
+ */
59
+ export function createSendFileTool(): ChannelAgentTool {
60
+ return {
61
+ label: "Send File to ANI",
62
+ name: "ani_send_file",
63
+ description: [
64
+ "Send a file to the current ANI conversation as a downloadable attachment.",
65
+ "Two modes: (1) provide file_path to send an existing file from disk (screenshot, PDF, image, etc.),",
66
+ "or (2) provide content to create and send a new text file (.md, .csv, .json, etc.).",
67
+ "You MUST provide conversation_id. Provide either file_path OR content (not both).",
68
+ ].join(" "),
69
+ parameters: Type.Object({
70
+ conversation_id: Type.Number({
71
+ description: "The ANI conversation ID to send the file to",
72
+ }),
73
+ file_path: Type.Optional(
74
+ Type.String({
75
+ description: "Absolute path to an existing file on disk to send",
76
+ }),
77
+ ),
78
+ filename: Type.Optional(
79
+ Type.String({
80
+ description: "Filename with extension (required when using content mode, optional for file_path mode)",
81
+ }),
82
+ ),
83
+ content: Type.Optional(
84
+ Type.String({
85
+ description: "Text content to create as a new file (for .md, .csv, .json, .txt, etc.)",
86
+ }),
87
+ ),
88
+ caption: Type.Optional(
89
+ Type.String({
90
+ description: "Optional message text to accompany the file",
91
+ }),
92
+ ),
93
+ }),
94
+ execute: async (_toolCallId, args) => {
95
+ const params = args as {
96
+ conversation_id?: number;
97
+ file_path?: string;
98
+ filename?: string;
99
+ content?: string;
100
+ caption?: string;
101
+ };
102
+
103
+ const conversationId = params.conversation_id;
104
+ if (!conversationId) {
105
+ return { content: [{ type: "text" as const, text: "Error: conversation_id is required" }] };
106
+ }
107
+
108
+ const filePath = params.file_path?.trim();
109
+ const textContent = params.content;
110
+ const caption = params.caption?.trim() ?? "";
111
+
112
+ if (!filePath && !textContent) {
113
+ return { content: [{ type: "text" as const, text: "Error: provide either file_path or content" }] };
114
+ }
115
+
116
+ try {
117
+ const { serverUrl, apiKey } = resolveAniCredentials();
118
+ let buffer: Buffer;
119
+ let filename: string;
120
+
121
+ if (filePath) {
122
+ // Mode 1: Read existing file from disk
123
+ // Path traversal protection: restrict file access to the workspace directory
124
+ const resolved = path.resolve(filePath);
125
+ const workspace = process.cwd();
126
+ if (!resolved.startsWith(workspace + path.sep) && resolved !== workspace) {
127
+ return { content: [{ type: "text" as const, text: `Access denied: file must be within workspace (${workspace})` }] };
128
+ }
129
+ const stat = await fs.stat(resolved);
130
+ if (!stat.isFile()) {
131
+ return { content: [{ type: "text" as const, text: `Error: ${resolved} is not a file` }] };
132
+ }
133
+ if (stat.size > 32 * 1024 * 1024) {
134
+ return { content: [{ type: "text" as const, text: `Error: file too large (${(stat.size / 1024 / 1024).toFixed(1)}MB, max 32MB)` }] };
135
+ }
136
+ buffer = await fs.readFile(resolved);
137
+ filename = params.filename?.trim() || path.basename(filePath);
138
+ } else {
139
+ // Mode 2: Create file from text content
140
+ filename = params.filename?.trim() || "file.txt";
141
+ buffer = Buffer.from(textContent!, "utf-8");
142
+ }
143
+
144
+ const mimeType = getMimeType(filename);
145
+
146
+ // Upload to ANI backend
147
+ const uploaded = await uploadAniFile({ serverUrl, apiKey, buffer, filename });
148
+
149
+ // Determine attachment type from MIME
150
+ let attachType = "file";
151
+ if (mimeType.startsWith("image/")) attachType = "image";
152
+ else if (mimeType.startsWith("audio/")) attachType = "audio";
153
+ else if (mimeType.startsWith("video/")) attachType = "video";
154
+
155
+ // Send message with attachment
156
+ const attachments: AniAttachment[] = [{
157
+ type: attachType,
158
+ url: uploaded.url,
159
+ filename: uploaded.filename,
160
+ mime_type: mimeType,
161
+ size: buffer.length,
162
+ }];
163
+
164
+ const result = await sendAniMessage({
165
+ serverUrl,
166
+ apiKey,
167
+ conversationId,
168
+ text: caption || `📎 ${filename}`,
169
+ attachments,
170
+ contentType: attachType,
171
+ });
172
+
173
+ return {
174
+ content: [{
175
+ type: "text" as const,
176
+ text: `File "${filename}" (${(buffer.length / 1024).toFixed(1)}KB, ${mimeType}) sent to conversation ${conversationId}. Message ID: ${result.messageId}`,
177
+ }],
178
+ };
179
+ } catch (err) {
180
+ return {
181
+ content: [{
182
+ type: "text" as const,
183
+ text: `Error sending file: ${err instanceof Error ? err.message : String(err)}`,
184
+ }],
185
+ };
186
+ }
187
+ },
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Agent tool: ani_fetch_chat_history_messages
193
+ *
194
+ * Fetch the FULL conversation history directly from the ANI platform.
195
+ * This is different from sessions_history which only shows messages YOU received.
196
+ * This tool returns ALL messages — including those between other participants,
197
+ * messages sent while you were offline, and messages you were not @mentioned in.
198
+ */
199
+ export function createGetHistoryTool(): ChannelAgentTool {
200
+ return {
201
+ label: "Fetch Chat Messages from ANI",
202
+ name: "ani_fetch_chat_history_messages",
203
+ description: [
204
+ "Retrieve the full message history of an ANI conversation directly from the platform.",
205
+ "Returns ALL messages including those you were NOT @mentioned in — human-to-human messages, other bots' replies, files shared while you were offline, etc.",
206
+ "Use this when:",
207
+ "- A user says 'look at what I sent earlier' or 'check the file I shared'",
208
+ "- You need context about what happened in the group before you were @mentioned",
209
+ "- You want to summarize the entire conversation, not just your interactions",
210
+ "- sessions_history is missing messages you know exist",
211
+ "Default: returns the 5 most recent messages. Use limit to get more (max 50).",
212
+ ].join(" "),
213
+ parameters: Type.Object({
214
+ conversation_id: Type.Number({
215
+ description: "The conversation ID to fetch messages from. You can find this in the system prompt under 'Current Conversation'.",
216
+ }),
217
+ limit: Type.Optional(
218
+ Type.Number({
219
+ description: "Number of messages to return. Default: 5, max: 50. Use 50 to get a fuller picture.",
220
+ default: 5,
221
+ }),
222
+ ),
223
+ since_id: Type.Optional(
224
+ Type.Number({
225
+ description: "Only return messages with ID greater than this value. Useful for incremental fetching — pass the ID of the last message you already have.",
226
+ }),
227
+ ),
228
+ }),
229
+ execute: async (_toolCallId, args) => {
230
+ const params = args as {
231
+ conversation_id?: number;
232
+ limit?: number;
233
+ since_id?: number;
234
+ };
235
+
236
+ const conversationId = params.conversation_id;
237
+ if (!conversationId) {
238
+ return { content: [{ type: "text" as const, text: "Error: conversation_id is required" }] };
239
+ }
240
+
241
+ const limit = Math.min(Math.max(params.limit ?? 5, 1), 50);
242
+
243
+ try {
244
+ const { serverUrl, apiKey } = resolveAniCredentials();
245
+ let url = `${serverUrl}/api/v1/conversations/${conversationId}/messages?limit=${limit}`;
246
+ if (params.since_id) {
247
+ url += `&since_id=${params.since_id}`;
248
+ }
249
+
250
+ const res = await fetch(url, {
251
+ headers: { Authorization: `Bearer ${apiKey}` },
252
+ signal: AbortSignal.timeout(15_000),
253
+ });
254
+
255
+ if (!res.ok) {
256
+ return { content: [{ type: "text" as const, text: `Error fetching history: HTTP ${res.status}` }] };
257
+ }
258
+
259
+ const json = await res.json() as { data?: { messages?: Array<Record<string, unknown>> } };
260
+ const messages = json.data?.messages ?? [];
261
+
262
+ // Format messages for LLM readability
263
+ const formatted = messages.map((m: Record<string, unknown>) => {
264
+ const sender = (m.sender as Record<string, unknown>)?.display_name ?? `entity-${m.sender_id}`;
265
+ const text = ((m.layers as Record<string, unknown>)?.summary as string) ?? "";
266
+ const time = m.created_at as string;
267
+ const atts = (m.attachments as Array<Record<string, unknown>>) ?? [];
268
+ const attDesc = atts.length > 0
269
+ ? ` [${atts.length} attachment(s): ${atts.map((a) => a.filename ?? a.type).join(", ")}]`
270
+ : "";
271
+ return `[${time}] ${sender}: ${text}${attDesc}`;
272
+ }).reverse(); // oldest first for readability
273
+
274
+ return {
275
+ content: [{
276
+ type: "text" as const,
277
+ text: `Conversation ${conversationId} — last ${messages.length} messages:\n\n${formatted.join("\n")}`,
278
+ }],
279
+ };
280
+ } catch (err) {
281
+ return {
282
+ content: [{
283
+ type: "text" as const,
284
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
285
+ }],
286
+ };
287
+ }
288
+ },
289
+ };
290
+ }
291
+
292
+ export function createListTasksTool(): ChannelAgentTool {
293
+ return {
294
+ label: "List ANI Conversation Tasks",
295
+ name: "ani_list_conversation_tasks",
296
+ description: [
297
+ "List the current task roadmap for an ANI conversation.",
298
+ "Use this to inspect task titles, assignees, priorities, parent dependencies, and statuses",
299
+ "before planning work or reporting progress.",
300
+ ].join(" "),
301
+ parameters: Type.Object({
302
+ conversation_id: Type.Number({
303
+ description: "The ANI conversation ID whose tasks should be listed.",
304
+ }),
305
+ status: Type.Optional(
306
+ Type.String({
307
+ description: "Optional status filter: pending, in_progress, done, cancelled, handed_over.",
308
+ }),
309
+ ),
310
+ }),
311
+ execute: async (_toolCallId, args) => {
312
+ const params = args as { conversation_id?: number; status?: string };
313
+ if (!params.conversation_id) {
314
+ return { content: [{ type: "text" as const, text: "Error: conversation_id is required" }] };
315
+ }
316
+ const statusErr = validateTaskStatus(params.status);
317
+ if (statusErr) {
318
+ return { content: [{ type: "text" as const, text: statusErr }] };
319
+ }
320
+
321
+ try {
322
+ const { serverUrl, apiKey } = resolveAniCredentials();
323
+ const tasks = await listAniTasks({
324
+ serverUrl,
325
+ apiKey,
326
+ conversationId: params.conversation_id,
327
+ status: params.status,
328
+ });
329
+
330
+ if (tasks.length === 0) {
331
+ return {
332
+ content: [{
333
+ type: "text" as const,
334
+ text: `Conversation ${params.conversation_id} has no tasks${params.status ? ` with status ${params.status}` : ""}.`,
335
+ }],
336
+ };
337
+ }
338
+
339
+ return {
340
+ content: [{
341
+ type: "text" as const,
342
+ text: `Conversation ${params.conversation_id} tasks (${tasks.length}):\n\n${tasks.map(formatTask).join("\n\n")}`,
343
+ }],
344
+ };
345
+ } catch (err) {
346
+ return {
347
+ content: [{
348
+ type: "text" as const,
349
+ text: `Error listing tasks: ${err instanceof Error ? err.message : String(err)}`,
350
+ }],
351
+ };
352
+ }
353
+ },
354
+ };
355
+ }
356
+
357
+ export function createGetTaskTool(): ChannelAgentTool {
358
+ return {
359
+ label: "Get ANI Task Details",
360
+ name: "ani_get_task",
361
+ description: "Get the full details and current status for a single ANI task by task ID.",
362
+ parameters: Type.Object({
363
+ task_id: Type.Number({
364
+ description: "The ANI task ID to retrieve.",
365
+ }),
366
+ }),
367
+ execute: async (_toolCallId, args) => {
368
+ const params = args as { task_id?: number };
369
+ if (!params.task_id) {
370
+ return { content: [{ type: "text" as const, text: "Error: task_id is required" }] };
371
+ }
372
+
373
+ try {
374
+ const { serverUrl, apiKey } = resolveAniCredentials();
375
+ const task = await getAniTask({ serverUrl, apiKey, taskId: params.task_id });
376
+ return {
377
+ content: [{ type: "text" as const, text: formatTask(task) }],
378
+ };
379
+ } catch (err) {
380
+ return {
381
+ content: [{
382
+ type: "text" as const,
383
+ text: `Error getting task: ${err instanceof Error ? err.message : String(err)}`,
384
+ }],
385
+ };
386
+ }
387
+ },
388
+ };
389
+ }
390
+
391
+ export function createCreateTaskTool(): ChannelAgentTool {
392
+ return {
393
+ label: "Create ANI Task",
394
+ name: "ani_create_task",
395
+ description: [
396
+ "Create a new task in the current ANI conversation roadmap.",
397
+ "The ANI backend will enforce conversation membership and any existing task permissions.",
398
+ ].join(" "),
399
+ parameters: Type.Object({
400
+ conversation_id: Type.Number({
401
+ description: "The ANI conversation ID where the task should be created.",
402
+ }),
403
+ title: Type.String({
404
+ description: "Short task title.",
405
+ }),
406
+ description: Type.Optional(
407
+ Type.String({
408
+ description: "Optional detailed description.",
409
+ }),
410
+ ),
411
+ assignee_id: Type.Optional(
412
+ Type.Number({
413
+ description: "Optional entity ID to assign the task to.",
414
+ }),
415
+ ),
416
+ priority: Type.Optional(
417
+ Type.String({
418
+ description: "Optional priority: low, medium, or high.",
419
+ }),
420
+ ),
421
+ due_date: Type.Optional(
422
+ Type.String({
423
+ description: "Optional due date as RFC3339 timestamp.",
424
+ }),
425
+ ),
426
+ parent_task_id: Type.Optional(
427
+ Type.Number({
428
+ description: "Optional parent task ID for dependency trees.",
429
+ }),
430
+ ),
431
+ }),
432
+ execute: async (_toolCallId, args) => {
433
+ const params = args as {
434
+ conversation_id?: number;
435
+ title?: string;
436
+ description?: string;
437
+ assignee_id?: number;
438
+ priority?: string;
439
+ due_date?: string;
440
+ parent_task_id?: number;
441
+ };
442
+
443
+ if (!params.conversation_id) {
444
+ return { content: [{ type: "text" as const, text: "Error: conversation_id is required" }] };
445
+ }
446
+ if (!params.title?.trim()) {
447
+ return { content: [{ type: "text" as const, text: "Error: title is required" }] };
448
+ }
449
+ const priorityErr = validateTaskPriority(params.priority);
450
+ if (priorityErr) {
451
+ return { content: [{ type: "text" as const, text: priorityErr }] };
452
+ }
453
+
454
+ try {
455
+ const { serverUrl, apiKey } = resolveAniCredentials();
456
+ const task = await createAniTask({
457
+ serverUrl,
458
+ apiKey,
459
+ conversationId: params.conversation_id,
460
+ title: params.title.trim(),
461
+ description: params.description?.trim(),
462
+ assignee_id: params.assignee_id,
463
+ priority: params.priority,
464
+ due_date: params.due_date,
465
+ parent_task_id: params.parent_task_id,
466
+ });
467
+ return {
468
+ content: [{ type: "text" as const, text: `Task created:\n${formatTask(task)}` }],
469
+ };
470
+ } catch (err) {
471
+ return {
472
+ content: [{
473
+ type: "text" as const,
474
+ text: `Error creating task: ${err instanceof Error ? err.message : String(err)}`,
475
+ }],
476
+ };
477
+ }
478
+ },
479
+ };
480
+ }
481
+
482
+ export function createUpdateTaskTool(): ChannelAgentTool {
483
+ return {
484
+ label: "Update ANI Task",
485
+ name: "ani_update_task",
486
+ description: [
487
+ "Update an existing ANI task.",
488
+ "Use this to change status, title, description, assignee, priority, due date, or sort order.",
489
+ "The ANI backend enforces existing permissions for creator, assignee, and admin roles.",
490
+ ].join(" "),
491
+ parameters: Type.Object({
492
+ task_id: Type.Number({
493
+ description: "The ANI task ID to update.",
494
+ }),
495
+ title: Type.Optional(Type.String({ description: "Optional new task title." })),
496
+ description: Type.Optional(Type.String({ description: "Optional new description." })),
497
+ assignee_id: Type.Optional(Type.Number({ description: "Optional new assignee entity ID." })),
498
+ status: Type.Optional(Type.String({ description: "Optional new status: pending, in_progress, done, cancelled, handed_over." })),
499
+ priority: Type.Optional(Type.String({ description: "Optional new priority: low, medium, high." })),
500
+ due_date: Type.Optional(Type.String({ description: "Optional new due date as RFC3339 timestamp." })),
501
+ sort_order: Type.Optional(Type.Number({ description: "Optional new sort order integer." })),
502
+ }),
503
+ execute: async (_toolCallId, args) => {
504
+ const params = args as {
505
+ task_id?: number;
506
+ title?: string;
507
+ description?: string;
508
+ assignee_id?: number;
509
+ status?: string;
510
+ priority?: string;
511
+ due_date?: string;
512
+ sort_order?: number;
513
+ };
514
+
515
+ if (!params.task_id) {
516
+ return { content: [{ type: "text" as const, text: "Error: task_id is required" }] };
517
+ }
518
+ if (
519
+ params.title === undefined &&
520
+ params.description === undefined &&
521
+ params.assignee_id === undefined &&
522
+ params.status === undefined &&
523
+ params.priority === undefined &&
524
+ params.due_date === undefined &&
525
+ params.sort_order === undefined
526
+ ) {
527
+ return { content: [{ type: "text" as const, text: "Error: provide at least one field to update" }] };
528
+ }
529
+ const statusErr = validateTaskStatus(params.status);
530
+ if (statusErr) {
531
+ return { content: [{ type: "text" as const, text: statusErr }] };
532
+ }
533
+ const priorityErr = validateTaskPriority(params.priority);
534
+ if (priorityErr) {
535
+ return { content: [{ type: "text" as const, text: priorityErr }] };
536
+ }
537
+
538
+ try {
539
+ const { serverUrl, apiKey } = resolveAniCredentials();
540
+ const task = await updateAniTask({
541
+ serverUrl,
542
+ apiKey,
543
+ taskId: params.task_id,
544
+ title: params.title?.trim(),
545
+ description: params.description?.trim(),
546
+ assignee_id: params.assignee_id,
547
+ status: params.status,
548
+ priority: params.priority,
549
+ due_date: params.due_date,
550
+ sort_order: params.sort_order,
551
+ });
552
+ return {
553
+ content: [{ type: "text" as const, text: `Task updated:\n${formatTask(task)}` }],
554
+ };
555
+ } catch (err) {
556
+ return {
557
+ content: [{
558
+ type: "text" as const,
559
+ text: `Error updating task: ${err instanceof Error ? err.message : String(err)}`,
560
+ }],
561
+ };
562
+ }
563
+ },
564
+ };
565
+ }
566
+
567
+ export function createDeleteTaskTool(): ChannelAgentTool {
568
+ return {
569
+ label: "Delete ANI Task",
570
+ name: "ani_delete_task",
571
+ description: [
572
+ "Delete an ANI task by task ID.",
573
+ "Use with care; the ANI backend still enforces creator/admin permissions.",
574
+ ].join(" "),
575
+ parameters: Type.Object({
576
+ task_id: Type.Number({
577
+ description: "The ANI task ID to delete.",
578
+ }),
579
+ }),
580
+ execute: async (_toolCallId, args) => {
581
+ const params = args as { task_id?: number };
582
+ if (!params.task_id) {
583
+ return { content: [{ type: "text" as const, text: "Error: task_id is required" }] };
584
+ }
585
+
586
+ try {
587
+ const { serverUrl, apiKey } = resolveAniCredentials();
588
+ await deleteAniTask({ serverUrl, apiKey, taskId: params.task_id });
589
+ return {
590
+ content: [{ type: "text" as const, text: `Task #${params.task_id} deleted.` }],
591
+ };
592
+ } catch (err) {
593
+ return {
594
+ content: [{
595
+ type: "text" as const,
596
+ text: `Error deleting task: ${err instanceof Error ? err.message : String(err)}`,
597
+ }],
598
+ };
599
+ }
600
+ },
601
+ };
602
+ }
package/src/types.ts ADDED
@@ -0,0 +1,50 @@
1
+ /** ANI channel configuration stored in openclaw config under channels.ani */
2
+ export type AniConfig = {
3
+ enabled?: boolean;
4
+ name?: string;
5
+
6
+ /** ANI server base URL, e.g. "https://agent-native.im" */
7
+ serverUrl?: string;
8
+
9
+ /** Permanent API key (aim_ prefix). Legacy aimb_ keys are not supported. */
10
+ apiKey?: string;
11
+
12
+ /** Legacy numeric entity ID override. Usually leave empty and let ANI auto-detect. */
13
+ entityId?: number;
14
+
15
+ /** DM policy — ANI routes all messages through conversations, so "open" is default */
16
+ dm?: {
17
+ policy?: "open" | "disabled";
18
+ };
19
+
20
+ /** Max text chunk length for outbound messages */
21
+ textChunkLimit?: number;
22
+ };
23
+
24
+ export type ResolvedAniAccount = {
25
+ accountId: string;
26
+ enabled: boolean;
27
+ name?: string;
28
+ configured: boolean;
29
+ serverUrl?: string;
30
+ entityId?: number;
31
+ config: AniConfig;
32
+ };
33
+
34
+ export type CoreConfig = {
35
+ channels?: {
36
+ ani?: AniConfig;
37
+ defaults?: {
38
+ groupPolicy?: string;
39
+ };
40
+ [key: string]: unknown;
41
+ };
42
+ session?: {
43
+ store?: string;
44
+ };
45
+ messages?: {
46
+ ackReaction?: string;
47
+ ackReactionScope?: string;
48
+ };
49
+ [key: string]: unknown;
50
+ };