@saccolabs/tars 1.7.0 → 1.7.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.
Files changed (46) hide show
  1. package/README.md +10 -7
  2. package/dist/channels/channel-manager.d.ts +35 -0
  3. package/dist/channels/channel-manager.js +94 -0
  4. package/dist/channels/channel-manager.js.map +1 -0
  5. package/dist/channels/discord/discord-channel.d.ts +41 -0
  6. package/dist/channels/discord/discord-channel.js +203 -0
  7. package/dist/channels/discord/discord-channel.js.map +1 -0
  8. package/dist/channels/discord/message-formatter.d.ts +95 -0
  9. package/dist/channels/discord/message-formatter.js +482 -0
  10. package/dist/channels/discord/message-formatter.js.map +1 -0
  11. package/dist/channels/types.d.ts +36 -0
  12. package/dist/channels/types.js +2 -0
  13. package/dist/channels/types.js.map +1 -0
  14. package/dist/cli/commands/setup.js +92 -136
  15. package/dist/cli/commands/setup.js.map +1 -1
  16. package/dist/cli/commands/start.d.ts +4 -1
  17. package/dist/cli/commands/start.js +47 -37
  18. package/dist/cli/commands/start.js.map +1 -1
  19. package/dist/cli/commands/status.js +19 -0
  20. package/dist/cli/commands/status.js.map +1 -1
  21. package/dist/cli/index.js +2 -0
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/config/config.d.ts +11 -2
  24. package/dist/config/config.js +34 -13
  25. package/dist/config/config.js.map +1 -1
  26. package/dist/scripts/debug-cli.js +1 -1
  27. package/dist/scripts/debug-cli.js.map +1 -1
  28. package/dist/supervisor/cron-service.d.ts +3 -3
  29. package/dist/supervisor/cron-service.js +4 -4
  30. package/dist/supervisor/cron-service.js.map +1 -1
  31. package/dist/supervisor/gemini-engine.d.ts +4 -4
  32. package/dist/supervisor/gemini-engine.js +9 -9
  33. package/dist/supervisor/gemini-engine.js.map +1 -1
  34. package/dist/supervisor/heartbeat-service.js +1 -1
  35. package/dist/supervisor/heartbeat-service.js.map +1 -1
  36. package/dist/supervisor/main.js +59 -60
  37. package/dist/supervisor/main.js.map +1 -1
  38. package/dist/tools/send-discord-message.d.ts +9 -6
  39. package/dist/tools/send-discord-message.js +14 -21
  40. package/dist/tools/send-discord-message.js.map +1 -1
  41. package/dist/tools/send-notification.d.ts +15 -0
  42. package/dist/tools/send-notification.js +51 -0
  43. package/dist/tools/send-notification.js.map +1 -0
  44. package/dist/types/index.d.ts +1 -0
  45. package/package.json +12 -2
  46. package/src/prompts/system.md +4 -0
package/README.md CHANGED
@@ -66,7 +66,7 @@ npm install -g @saccolabs/tars
66
66
 
67
67
  ### Initial Setup
68
68
 
69
- Run the setup wizard to authorize Gemini and connect your Discord bot:
69
+ Run the setup wizard to authorize Gemini and connect your communication channels (Discord, WhatsApp, or both):
70
70
 
71
71
  ```bash
72
72
  tars setup
@@ -87,9 +87,11 @@ tars setup
87
87
 
88
88
  ### Interaction
89
89
 
90
- Tars communicates primarily through a private Discord channel. It supports file uploads, long-running task management, and complex multi-step instructions.
90
+ Tars communicates through a multi-channel architecture. You can interact via **Discord**, **WhatsApp**, or both simultaneously. It supports file uploads, long-running task management, and complex multi-step instructions.
91
91
 
92
- > **!tars** "Analyze the logs in /var/log/syslog and summarize any critical errors."
92
+ > **Discord**: `!tars Analyze the logs in /var/log/syslog and summarize any critical errors.`
93
+ >
94
+ > **WhatsApp**: Just send a message directly — no prefix needed.
93
95
 
94
96
  ---
95
97
 
@@ -97,10 +99,11 @@ Tars communicates primarily through a private Discord channel. It supports file
97
99
 
98
100
  Tars utilizes a Supervisor-Orchestrator model:
99
101
 
100
- 1. **Supervisor**: Manages persistent sessions and Discord communication.
101
- 2. **Subagents**: Specialized expert agents invoked dynamically for specific technical domains.
102
- 3. **Heartbeat**: Cron-based engine for autonomous execution and cleanup.
103
- 4. **Extensions**: MCP servers that provide tool-level capabilities to the intelligence core.
102
+ 1. **Supervisor**: Manages persistent sessions and multi-channel communication.
103
+ 2. **Channel Manager**: Orchestrates communication across Discord and WhatsApp with automatic notification routing.
104
+ 3. **Subagents**: Specialized expert agents invoked dynamically for specific technical domains.
105
+ 4. **Heartbeat**: Cron-based engine for autonomous execution and cleanup.
106
+ 5. **Extensions**: MCP servers that provide tool-level capabilities to the intelligence core.
104
107
 
105
108
  ---
106
109
 
@@ -0,0 +1,35 @@
1
+ import { CommunicationChannel, ChannelMessage } from './types.js';
2
+ /**
3
+ * Orchestrates all communication channels (Discord, WhatsApp, etc.)
4
+ */
5
+ export declare class ChannelManager {
6
+ private readonly channels;
7
+ private readonly config;
8
+ private messageHandler?;
9
+ private lastActiveChannelId?;
10
+ constructor();
11
+ /**
12
+ * Initialize enabled channels based on configuration
13
+ */
14
+ private initializeChannels;
15
+ /**
16
+ * Start all enabled channels
17
+ */
18
+ start(): Promise<void>;
19
+ /**
20
+ * Stop all channels
21
+ */
22
+ stop(): Promise<void>;
23
+ /**
24
+ * Register a global handler for messages from any channel
25
+ */
26
+ onMessage(handler: (message: ChannelMessage) => Promise<void>): void;
27
+ /**
28
+ * Send a proactive notification
29
+ */
30
+ notify(content: string, attachments?: string[]): Promise<void>;
31
+ /**
32
+ * Get a specific channel by ID
33
+ */
34
+ getChannel(id: string): CommunicationChannel | undefined;
35
+ }
@@ -0,0 +1,94 @@
1
+ import { DiscordChannel } from './discord/discord-channel.js';
2
+ import logger from '../utils/logger.js';
3
+ import { Config } from '../config/config.js';
4
+ /**
5
+ * Orchestrates all communication channels (Discord, WhatsApp, etc.)
6
+ */
7
+ export class ChannelManager {
8
+ channels = new Map();
9
+ config;
10
+ messageHandler;
11
+ lastActiveChannelId;
12
+ constructor() {
13
+ this.config = Config.getInstance();
14
+ this.initializeChannels();
15
+ }
16
+ /**
17
+ * Initialize enabled channels based on configuration
18
+ */
19
+ initializeChannels() {
20
+ // 1. Initialize Discord
21
+ const discord = new DiscordChannel();
22
+ if (discord.isEnabled) {
23
+ this.channels.set(discord.id, discord);
24
+ }
25
+ }
26
+ /**
27
+ * Start all enabled channels
28
+ */
29
+ async start() {
30
+ if (this.channels.size === 0) {
31
+ logger.warn('⚠️ No communication channels enabled. Tars will be unreachable.');
32
+ return;
33
+ }
34
+ logger.info(`🚀 Starting ${this.channels.size} communication channel(s)...`);
35
+ for (const channel of this.channels.values()) {
36
+ try {
37
+ channel.onMessage(async (message) => {
38
+ this.lastActiveChannelId = channel.id;
39
+ if (this.messageHandler) {
40
+ await this.messageHandler(message);
41
+ }
42
+ });
43
+ await channel.start();
44
+ }
45
+ catch (error) {
46
+ logger.error(`Failed to start channel ${channel.id}: ${error.message}`);
47
+ }
48
+ }
49
+ }
50
+ /**
51
+ * Stop all channels
52
+ */
53
+ async stop() {
54
+ logger.info('Stopping all communication channels...');
55
+ for (const channel of this.channels.values()) {
56
+ await channel
57
+ .stop()
58
+ .catch((e) => logger.error(`Error stopping ${channel.id}: ${e.message}`));
59
+ }
60
+ }
61
+ /**
62
+ * Register a global handler for messages from any channel
63
+ */
64
+ onMessage(handler) {
65
+ this.messageHandler = handler;
66
+ }
67
+ /**
68
+ * Send a proactive notification
69
+ */
70
+ async notify(content, attachments) {
71
+ const primaryChannelId = this.lastActiveChannelId || this.config.primaryChannel || 'discord';
72
+ const channel = this.channels.get(primaryChannelId);
73
+ if (channel) {
74
+ await channel.notify(content, attachments);
75
+ }
76
+ else {
77
+ // Fallback to the first available channel if primary is not found
78
+ const fallback = Array.from(this.channels.values())[0];
79
+ if (fallback) {
80
+ await fallback.notify(content, attachments);
81
+ }
82
+ else {
83
+ logger.warn(`No active channels available for notification.`);
84
+ }
85
+ }
86
+ }
87
+ /**
88
+ * Get a specific channel by ID
89
+ */
90
+ getChannel(id) {
91
+ return this.channels.get(id);
92
+ }
93
+ }
94
+ //# sourceMappingURL=channel-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-manager.js","sourceRoot":"","sources":["../../src/channels/channel-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import { CommunicationChannel, ChannelMessage } from '../types.js';
2
+ /**
3
+ * Discord Channel Implementation for Tars
4
+ */
5
+ export declare class DiscordChannel implements CommunicationChannel {
6
+ readonly id = "discord";
7
+ private readonly config;
8
+ private readonly client;
9
+ private readonly processor;
10
+ private messageHandler?;
11
+ constructor();
12
+ get isEnabled(): boolean;
13
+ /**
14
+ * Start the Discord bot
15
+ */
16
+ start(): Promise<void>;
17
+ /**
18
+ * Stop the Discord bot
19
+ */
20
+ stop(): Promise<void>;
21
+ /**
22
+ * Send a proactive notification to the primary contact
23
+ */
24
+ notify(content: string, attachments?: string[]): Promise<void>;
25
+ /**
26
+ * Register message handler
27
+ */
28
+ onMessage(handler: (message: ChannelMessage) => Promise<void>): void;
29
+ /**
30
+ * Setup event handlers
31
+ */
32
+ private setupEventHandlers;
33
+ /**
34
+ * Internal handler for Discord-specific messages
35
+ */
36
+ private handleIncomingMessage;
37
+ /**
38
+ * Extract prompt and handle prefix
39
+ */
40
+ private extractPrompt;
41
+ }
@@ -0,0 +1,203 @@
1
+ import { Client, GatewayIntentBits, ChannelType, Partials } from 'discord.js';
2
+ import { Config } from '../../config/config.js';
3
+ import logger from '../../utils/logger.js';
4
+ import { MessageFormatter } from './message-formatter.js';
5
+ import { AttachmentProcessor } from '../../utils/attachment-processor.js';
6
+ /**
7
+ * Discord Channel Implementation for Tars
8
+ */
9
+ export class DiscordChannel {
10
+ id = 'discord';
11
+ config;
12
+ client;
13
+ processor;
14
+ messageHandler;
15
+ constructor() {
16
+ this.config = Config.getInstance();
17
+ this.processor = new AttachmentProcessor(this.config);
18
+ this.client = new Client({
19
+ intents: [
20
+ GatewayIntentBits.Guilds,
21
+ GatewayIntentBits.GuildMessages,
22
+ GatewayIntentBits.MessageContent,
23
+ GatewayIntentBits.DirectMessages
24
+ ],
25
+ partials: [Partials.Channel, Partials.Message]
26
+ });
27
+ this.setupEventHandlers();
28
+ }
29
+ get isEnabled() {
30
+ // For backward compatibility, if no explicit channels are configured but discordToken exists, it's enabled.
31
+ // Once WhatsApp is added, we'll use structured config.
32
+ return !!this.config.discordToken;
33
+ }
34
+ /**
35
+ * Start the Discord bot
36
+ */
37
+ async start() {
38
+ if (!this.isEnabled) {
39
+ logger.info('Discord channel is disabled.');
40
+ return;
41
+ }
42
+ await this.client.login(this.config.discordToken);
43
+ }
44
+ /**
45
+ * Stop the Discord bot
46
+ */
47
+ async stop() {
48
+ this.client.destroy();
49
+ }
50
+ /**
51
+ * Send a proactive notification to the primary contact
52
+ */
53
+ async notify(content, attachments) {
54
+ if (!this.config.discordOwnerId || !content.trim())
55
+ return;
56
+ try {
57
+ const user = await this.client.users.fetch(this.config.discordOwnerId);
58
+ if (user) {
59
+ const formatted = MessageFormatter.format(content);
60
+ const files = attachments || [];
61
+ if (formatted.length > 8000) {
62
+ const filePath = this.processor.saveResponse(content, 'md');
63
+ await user.send({
64
+ content: `🔔 **Task Notification** (Response too long, see attached):`,
65
+ files: [filePath, ...files]
66
+ });
67
+ }
68
+ else {
69
+ const chunks = MessageFormatter.split(formatted);
70
+ for (let i = 0; i < chunks.length; i++) {
71
+ const prefix = i === 0 ? `🔔 **Task Notification:**\n` : ``;
72
+ await user.send({
73
+ content: prefix + chunks[i],
74
+ files: i === chunks.length - 1 ? files : []
75
+ });
76
+ }
77
+ }
78
+ }
79
+ }
80
+ catch (e) {
81
+ logger.error(`Failed to send proactive notification via Discord: ${e.message}`);
82
+ }
83
+ }
84
+ /**
85
+ * Register message handler
86
+ */
87
+ onMessage(handler) {
88
+ this.messageHandler = handler;
89
+ }
90
+ /**
91
+ * Setup event handlers
92
+ */
93
+ setupEventHandlers() {
94
+ this.client.once('clientReady', (c) => {
95
+ logger.info(`🚀 Discord channel online as ${c.user.tag}`);
96
+ if (this.config.discordOwnerId) {
97
+ logger.info(`👤 Primary Discord Contact ID: ${this.config.discordOwnerId}`);
98
+ }
99
+ else {
100
+ logger.warn(`⚠️ No Primary Discord Contact ID set. Will bind to the first user who sends a message.`);
101
+ }
102
+ });
103
+ this.client.on('messageCreate', this.handleIncomingMessage.bind(this));
104
+ }
105
+ /**
106
+ * Internal handler for Discord-specific messages
107
+ */
108
+ async handleIncomingMessage(message) {
109
+ if (message.author.bot || !this.messageHandler)
110
+ return;
111
+ const userPrompt = this.extractPrompt(message);
112
+ if (userPrompt === null)
113
+ return;
114
+ // Auto-Bind on first interaction if not set
115
+ const wasAutoBound = !this.config.discordOwnerId;
116
+ if (wasAutoBound) {
117
+ this.config.discordOwnerId = message.author.id;
118
+ if (this.config.channels.discord) {
119
+ this.config.channels.discord.ownerId = message.author.id;
120
+ }
121
+ this.config.saveSettings();
122
+ logger.info(`🔒 Automatically bound Primary Contact to Discord user: ${message.author.id} (Channel: ${message.channelId})`);
123
+ }
124
+ if (!userPrompt && message.attachments.size === 0)
125
+ return;
126
+ // Handle Attachments
127
+ const attachments = [];
128
+ if (message.attachments.size > 0) {
129
+ for (const [id, attachment] of message.attachments) {
130
+ try {
131
+ const filePath = await this.processor.download(attachment);
132
+ if (attachment.contentType) {
133
+ attachments.push({
134
+ path: filePath,
135
+ mimeType: attachment.contentType
136
+ });
137
+ }
138
+ }
139
+ catch (err) {
140
+ logger.error(`Failed to download Discord attachment: ${err.message}`);
141
+ }
142
+ }
143
+ }
144
+ // Map to common ChannelMessage
145
+ const channelMessage = {
146
+ content: userPrompt,
147
+ senderId: message.author.id,
148
+ senderName: message.author.username,
149
+ channelId: message.channelId,
150
+ attachments,
151
+ metadata: { wasAutoBound },
152
+ reply: async (response, outAttachments) => {
153
+ const formatted = MessageFormatter.format(response);
154
+ if (formatted.length > 8000) {
155
+ const filePath = this.processor.saveResponse(response, 'md');
156
+ await message.reply({
157
+ content: `📄 **Response too long** (${formatted.length} chars). See attached file:`,
158
+ files: [filePath, ...(outAttachments || [])]
159
+ });
160
+ }
161
+ else {
162
+ const chunks = MessageFormatter.split(formatted);
163
+ for (let i = 0; i < chunks.length; i++) {
164
+ await message.reply({
165
+ content: chunks[i],
166
+ files: i === chunks.length - 1 ? outAttachments || [] : []
167
+ });
168
+ }
169
+ }
170
+ }
171
+ };
172
+ // Show typing indicator
173
+ if ('sendTyping' in message.channel) {
174
+ await message.channel.sendTyping().catch(() => { });
175
+ }
176
+ // Forward to the registered handler (Supervisor via ChannelManager)
177
+ await this.messageHandler(channelMessage);
178
+ }
179
+ /**
180
+ * Extract prompt and handle prefix
181
+ */
182
+ extractPrompt(message) {
183
+ const isDM = message.channel.type === ChannelType.DM;
184
+ const isMentioned = this.client.user && message.mentions.has(this.client.user);
185
+ const customPrefix = `!${this.config.assistantName.toLowerCase()}`;
186
+ const hasCustomCommand = message.content.toLowerCase().startsWith(customPrefix);
187
+ const hasLegacyCommand = message.content.toLowerCase().startsWith('!tars');
188
+ if (!isDM && !isMentioned && !hasCustomCommand && !hasLegacyCommand)
189
+ return null;
190
+ let prompt = message.content;
191
+ if (hasCustomCommand) {
192
+ prompt = prompt.substring(customPrefix.length);
193
+ }
194
+ else if (hasLegacyCommand) {
195
+ prompt = prompt.substring(6); // length of '!tars'
196
+ }
197
+ if (isMentioned && this.client.user) {
198
+ prompt = prompt.replace(new RegExp(`<@!?${this.client.user.id}>`, 'g'), '');
199
+ }
200
+ return prompt.trim();
201
+ }
202
+ }
203
+ //# sourceMappingURL=discord-channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord-channel.js","sourceRoot":"","sources":["../../../src/channels/discord/discord-channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAW,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACvF,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAChD,OAAO,MAAM,MAAM,uBAAuB,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAG1E;;GAEG;AACH,MAAM,OAAO,cAAc;IACP,EAAE,GAAG,SAAS,CAAC;IACd,MAAM,CAAS;IACf,MAAM,CAAS;IACf,SAAS,CAAsB;IACxC,cAAc,CAA8C;IAEpE;QACI,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;QACnC,IAAI,CAAC,SAAS,GAAG,IAAI,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAEtD,IAAI,CAAC,MAAM,GAAG,IAAI,MAAM,CAAC;YACrB,OAAO,EAAE;gBACL,iBAAiB,CAAC,MAAM;gBACxB,iBAAiB,CAAC,aAAa;gBAC/B,iBAAiB,CAAC,cAAc;gBAChC,iBAAiB,CAAC,cAAc;aACnC;YACD,QAAQ,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC;SACjD,CAAC,CAAC;QAEH,IAAI,CAAC,kBAAkB,EAAE,CAAC;IAC9B,CAAC;IAED,IAAI,SAAS;QACT,4GAA4G;QAC5G,uDAAuD;QACvD,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC;IACtC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,KAAK;QACP,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YAClB,MAAM,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;YAC5C,OAAO;QACX,CAAC;QACD,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IACtD,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,IAAI;QACN,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;IAC1B,CAAC;IAED;;OAEG;IACI,KAAK,CAAC,MAAM,CAAC,OAAe,EAAE,WAAsB;QACvD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE;YAAE,OAAO;QAC3D,IAAI,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;YACvE,IAAI,IAAI,EAAE,CAAC;gBACP,MAAM,SAAS,GAAG,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBACnD,MAAM,KAAK,GAAG,WAAW,IAAI,EAAE,CAAC;gBAEhC,IAAI,SAAS,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;oBAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;oBAC5D,MAAM,IAAI,CAAC,IAAI,CAAC;wBACZ,OAAO,EAAE,6DAA6D;wBACtE,KAAK,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,CAAC;qBAC9B,CAAC,CAAC;gBACP,CAAC;qBAAM,CAAC;oBACJ,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;oBACjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;wBACrC,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,6BAA6B,CAAC,CAAC,CAAC,EAAE,CAAC;wBAC5D,MAAM,IAAI,CAAC,IAAI,CAAC;4BACZ,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;4BAC3B,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;yBAC9C,CAAC,CAAC;oBACP,CAAC;gBACL,CAAC;YACL,CAAC;QACL,CAAC;QAAC,OAAO,CAAM,EAAE,CAAC;YACd,MAAM,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACpF,CAAC;IACL,CAAC;IAED;;OAEG;IACI,SAAS,CAAC,OAAmD;QAChE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC;IAClC,CAAC;IAED;;OAEG;IACK,kBAAkB;QACtB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC,EAAE,EAAE;YAClC,MAAM,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;YAC1D,IAAI,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;gBAC7B,MAAM,CAAC,IAAI,CAAC,kCAAkC,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC;YAChF,CAAC;iBAAM,CAAC;gBACJ,MAAM,CAAC,IAAI,CACP,wFAAwF,CAC3F,CAAC;YACN,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,eAAe,EAAE,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3E,CAAC;IAED;;OAEG;IACK,KAAK,CAAC,qBAAqB,CAAC,OAAgB;QAChD,IAAI,OAAO,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,cAAc;YAAE,OAAO;QAEvD,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAC/C,IAAI,UAAU,KAAK,IAAI;YAAE,OAAO;QAEhC,4CAA4C;QAC5C,MAAM,YAAY,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC;QACjD,IAAI,YAAY,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC/C,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC;gBAC/B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7D,CAAC;YACD,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,CACP,2DAA2D,OAAO,CAAC,MAAM,CAAC,EAAE,cAAc,OAAO,CAAC,SAAS,GAAG,CACjH,CAAC;QACN,CAAC;QAED,IAAI,CAAC,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO;QAE1D,qBAAqB;QACrB,MAAM,WAAW,GAAwB,EAAE,CAAC;QAC5C,IAAI,OAAO,CAAC,WAAW,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC/B,KAAK,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;gBACjD,IAAI,CAAC;oBACD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;oBAC3D,IAAI,UAAU,CAAC,WAAW,EAAE,CAAC;wBACzB,WAAW,CAAC,IAAI,CAAC;4BACb,IAAI,EAAE,QAAQ;4BACd,QAAQ,EAAE,UAAU,CAAC,WAAW;yBACnC,CAAC,CAAC;oBACP,CAAC;gBACL,CAAC;gBAAC,OAAO,GAAQ,EAAE,CAAC;oBAChB,MAAM,CAAC,KAAK,CAAC,0CAA0C,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;gBAC1E,CAAC;YACL,CAAC;QACL,CAAC;QAED,+BAA+B;QAC/B,MAAM,cAAc,GAAmB;YACnC,OAAO,EAAE,UAAU;YACnB,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE;YAC3B,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ;YACnC,SAAS,EAAE,OAAO,CAAC,SAAS;YAC5B,WAAW;YACX,QAAQ,EAAE,EAAE,YAAY,EAAE;YAC1B,KAAK,EAAE,KAAK,EAAE,QAAgB,EAAE,cAAyB,EAAE,EAAE;gBACzD,MAAM,SAAS,GAAG,gBAAgB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBACpD,IAAI,SAAS,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC;oBAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;oBAC7D,MAAM,OAAO,CAAC,KAAK,CAAC;wBAChB,OAAO,EAAE,6BAA6B,SAAS,CAAC,MAAM,6BAA6B;wBACnF,KAAK,EAAE,CAAC,QAAQ,EAAE,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC;qBAC/C,CAAC,CAAC;gBACP,CAAC;qBAAM,CAAC;oBACJ,MAAM,MAAM,GAAG,gBAAgB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;oBACjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;wBACrC,MAAM,OAAO,CAAC,KAAK,CAAC;4BAChB,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;4BAClB,KAAK,EAAE,CAAC,KAAK,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE;yBAC7D,CAAC,CAAC;oBACP,CAAC;gBACL,CAAC;YACL,CAAC;SACJ,CAAC;QAEF,wBAAwB;QACxB,IAAI,YAAY,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;YAClC,MAAM,OAAO,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QACvD,CAAC;QAED,oEAAoE;QACpE,MAAM,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC;IAC9C,CAAC;IAED;;OAEG;IACK,aAAa,CAAC,OAAgB;QAClC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,KAAK,WAAW,CAAC,EAAE,CAAC;QACrD,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAE/E,MAAM,YAAY,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,WAAW,EAAE,EAAE,CAAC;QACnE,MAAM,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;QAChF,MAAM,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAE3E,IAAI,CAAC,IAAI,IAAI,CAAC,WAAW,IAAI,CAAC,gBAAgB,IAAI,CAAC,gBAAgB;YAAE,OAAO,IAAI,CAAC;QAEjF,IAAI,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;QAC7B,IAAI,gBAAgB,EAAE,CAAC;YACnB,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACnD,CAAC;aAAM,IAAI,gBAAgB,EAAE,CAAC;YAC1B,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,oBAAoB;QACtD,CAAC;QAED,IAAI,WAAW,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAClC,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;QAChF,CAAC;QAED,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC;CACJ"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Discord Message Formatter
3
+ *
4
+ * Transforms Gemini CLI output (GitHub Flavored Markdown)
5
+ * into Discord-compatible formatting.
6
+ *
7
+ * Discord supports:
8
+ * - Bold: **text**
9
+ * - Italic: *text* or _text_
10
+ * - Underline: __text__
11
+ * - Strikethrough: ~~text~~
12
+ * - Code inline: `text`
13
+ * - Code block: ```lang\ncode\n```
14
+ * - Blockquotes: > text
15
+ * - Headers: # (only #, ##, ###)
16
+ *
17
+ * Discord does NOT support:
18
+ * - Markdown tables (we instruct the LLM to avoid these)
19
+ * - #### or deeper headers
20
+ * - Small text (-#)
21
+ */
22
+ export declare class MessageFormatter {
23
+ private static readonly MAX_MESSAGE_LENGTH;
24
+ /**
25
+ * Format text for Discord
26
+ */
27
+ static format(text: string): string;
28
+ /**
29
+ * Fix critical spacing issues from LLM output
30
+ * These are the most common formatting bugs that break Discord rendering
31
+ */
32
+ private static fixSpacing;
33
+ /**
34
+ * Fix broken asterisks patterns
35
+ * Only fixes obviously broken patterns, avoids aggressive matching
36
+ */
37
+ private static fixAsterisks;
38
+ /**
39
+ * Normalize bullet points for Discord
40
+ */
41
+ private static normalizeBullets;
42
+ /**
43
+ * Normalize markdown headers to Discord-friendly format
44
+ * Discord supports #, ##, ### natively now.
45
+ */
46
+ private static normalizeHeaders;
47
+ /**
48
+ * Fix blockquote formatting
49
+ */
50
+ private static fixBlockquotes;
51
+ /**
52
+ * Detect and wrap JSON-like content in code blocks
53
+ */
54
+ private static formatJsonBlocks;
55
+ /**
56
+ * Strip markdown tables - they don't render well on mobile Discord
57
+ * This is a fallback; the LLM should be instructed not to generate tables
58
+ */
59
+ private static stripTables;
60
+ /**
61
+ * Fix small text markers (-#) that Discord doesn't support
62
+ */
63
+ private static fixSmallText;
64
+ /**
65
+ * Split long messages into Discord-safe chunks intelligently
66
+ * Respects semantic boundaries: headers, code blocks, paragraphs
67
+ */
68
+ static split(text: string, maxLength?: number): string[];
69
+ /**
70
+ * Find the optimal split point respecting semantic boundaries
71
+ * Priority: Header > Code block boundary > Paragraph > Sentence > Hard cut
72
+ */
73
+ private static findSemanticSplitPoint;
74
+ /**
75
+ * Format and split in one operation
76
+ * Ensures summary line (first line with actionable emoji) stays at the top
77
+ */
78
+ static formatAndSplit(text: string): string[];
79
+ /**
80
+ * Extract the summary line from the beginning of formatted text
81
+ * Summary lines start with key actionable emojis: 🎯 ⚖️ 📊 ⚠️ ✅ ❓
82
+ */
83
+ private static extractSummaryLine;
84
+ /**
85
+ * Parse markdown into sections based on headers (##)
86
+ */
87
+ static parseSections(text: string): {
88
+ title: string;
89
+ content: string;
90
+ }[];
91
+ /**
92
+ * Format a data object as a clean Discord-friendly list
93
+ */
94
+ static formatDataAsEmbed(title: string, data: Record<string, unknown>): string;
95
+ }