@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.
- package/README.md +10 -7
- package/dist/channels/channel-manager.d.ts +35 -0
- package/dist/channels/channel-manager.js +94 -0
- package/dist/channels/channel-manager.js.map +1 -0
- package/dist/channels/discord/discord-channel.d.ts +41 -0
- package/dist/channels/discord/discord-channel.js +203 -0
- package/dist/channels/discord/discord-channel.js.map +1 -0
- package/dist/channels/discord/message-formatter.d.ts +95 -0
- package/dist/channels/discord/message-formatter.js +482 -0
- package/dist/channels/discord/message-formatter.js.map +1 -0
- package/dist/channels/types.d.ts +36 -0
- package/dist/channels/types.js +2 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/cli/commands/setup.js +92 -136
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/commands/start.d.ts +4 -1
- package/dist/cli/commands/start.js +47 -37
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.js +19 -0
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/config/config.d.ts +11 -2
- package/dist/config/config.js +34 -13
- package/dist/config/config.js.map +1 -1
- package/dist/scripts/debug-cli.js +1 -1
- package/dist/scripts/debug-cli.js.map +1 -1
- package/dist/supervisor/cron-service.d.ts +3 -3
- package/dist/supervisor/cron-service.js +4 -4
- package/dist/supervisor/cron-service.js.map +1 -1
- package/dist/supervisor/gemini-engine.d.ts +4 -4
- package/dist/supervisor/gemini-engine.js +9 -9
- package/dist/supervisor/gemini-engine.js.map +1 -1
- package/dist/supervisor/heartbeat-service.js +1 -1
- package/dist/supervisor/heartbeat-service.js.map +1 -1
- package/dist/supervisor/main.js +59 -60
- package/dist/supervisor/main.js.map +1 -1
- package/dist/tools/send-discord-message.d.ts +9 -6
- package/dist/tools/send-discord-message.js +14 -21
- package/dist/tools/send-discord-message.js.map +1 -1
- package/dist/tools/send-notification.d.ts +15 -0
- package/dist/tools/send-notification.js +51 -0
- package/dist/tools/send-notification.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +12 -2
- 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
|
|
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
|
|
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
|
-
>
|
|
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
|
|
101
|
-
2. **
|
|
102
|
-
3. **
|
|
103
|
-
4. **
|
|
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":"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"}
|
|
@@ -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
|
+
}
|