style: auto-fix ESLint issues (curly braces and formatting)

- Add curly braces to all if/else/for/while statements
- Fix indentation and trailing spaces
- Auto-fixed 372 linting errors using eslint --fix
- Remaining issues are warnings only (non-null assertions, explicit any types)
This commit is contained in:
William Valentin
2026-02-11 10:30:24 -08:00
parent 0578a87d85
commit 6090508bad
99 changed files with 418 additions and 418 deletions
+1 -1
View File
@@ -140,7 +140,7 @@ export function storeToken(token: string): void {
export function getGitHubToken(): string | null { export function getGitHubToken(): string | null {
// 1. Environment variable // 1. Environment variable
const envToken = process.env.GITHUB_TOKEN; const envToken = process.env.GITHUB_TOKEN;
if (envToken) return envToken; if (envToken) {return envToken;}
// 2. Stored OAuth token // 2. Stored OAuth token
return loadStoredToken(); return loadStoredToken();
+2 -2
View File
@@ -31,7 +31,7 @@ export class CronScheduler implements ChannelAdapter {
this._status = 'connected'; this._status = 'connected';
for (const job of this.jobConfigs) { for (const job of this.jobConfigs) {
if (!job.enabled) continue; if (!job.enabled) {continue;}
const cronInstance = new Cron(job.schedule, { const cronInstance = new Cron(job.schedule, {
timezone: job.timezone, timezone: job.timezone,
@@ -81,7 +81,7 @@ export class CronScheduler implements ChannelAdapter {
/** Manually trigger a job (also called by cron on schedule). */ /** Manually trigger a job (also called by cron on schedule). */
triggerJob(jobName: string): void { triggerJob(jobName: string): void {
const job = this.jobs.get(jobName); const job = this.jobs.get(jobName);
if (!job) return; if (!job) {return;}
const msg: InboundMessage = { const msg: InboundMessage = {
id: `cron-${jobName}-${Date.now()}`, id: `cron-${jobName}-${Date.now()}`,
+2 -2
View File
@@ -181,8 +181,8 @@ describe('GmailWatcher', () => {
// credentials file exists but token file does not // credentials file exists but token file does not
mockExistsSync.mockImplementation((path: unknown) => { mockExistsSync.mockImplementation((path: unknown) => {
const p = String(path); const p = String(path);
if (p.includes('credentials')) return true; if (p.includes('credentials')) {return true;}
if (p.includes('token')) return false; if (p.includes('token')) {return false;}
return true; return true;
}); });
+7 -7
View File
@@ -212,7 +212,7 @@ export class GmailWatcher implements ChannelAdapter {
* Calls gmail.users.watch() and schedules renewal before expiry. * Calls gmail.users.watch() and schedules renewal before expiry.
*/ */
private async setupWatch(): Promise<void> { private async setupWatch(): Promise<void> {
if (!this.oauth2Client) return; if (!this.oauth2Client) {return;}
const gmail = google.gmail({ version: 'v1', auth: this.oauth2Client }); const gmail = google.gmail({ version: 'v1', auth: this.oauth2Client });
@@ -243,7 +243,7 @@ export class GmailWatcher implements ChannelAdapter {
* Fallback mechanism when Pub/Sub push is not available. * Fallback mechanism when Pub/Sub push is not available.
*/ */
private async pollForNewMessages(): Promise<void> { private async pollForNewMessages(): Promise<void> {
if (!this.oauth2Client) return; if (!this.oauth2Client) {return;}
const gmail = google.gmail({ version: 'v1', auth: this.oauth2Client }); const gmail = google.gmail({ version: 'v1', auth: this.oauth2Client });
@@ -268,7 +268,7 @@ export class GmailWatcher implements ChannelAdapter {
* Updates lastHistoryId to the latest value from the response. * Updates lastHistoryId to the latest value from the response.
*/ */
private async processHistoryChanges(startHistoryId: string): Promise<void> { private async processHistoryChanges(startHistoryId: string): Promise<void> {
if (!this.oauth2Client) return; if (!this.oauth2Client) {return;}
const gmail = google.gmail({ version: 'v1', auth: this.oauth2Client }); const gmail = google.gmail({ version: 'v1', auth: this.oauth2Client });
@@ -287,17 +287,17 @@ export class GmailWatcher implements ChannelAdapter {
const addedMessages = record.messagesAdded ?? []; const addedMessages = record.messagesAdded ?? [];
for (const added of addedMessages) { for (const added of addedMessages) {
const messageId = added.message?.id; const messageId = added.message?.id;
if (!messageId || processedIds.has(messageId)) continue; if (!messageId || processedIds.has(messageId)) {continue;}
processedIds.add(messageId); processedIds.add(messageId);
const email = await this.getMessageDetails(messageId); const email = await this.getMessageDetails(messageId);
if (!email) continue; if (!email) {continue;}
// Skip messages before history_start if configured // Skip messages before history_start if configured
if (this.config.history_start) { if (this.config.history_start) {
const emailDate = new Date(email.date); const emailDate = new Date(email.date);
const startDate = new Date(this.config.history_start); const startDate = new Date(this.config.history_start);
if (emailDate < startDate) continue; if (emailDate < startDate) {continue;}
} }
const text = this.renderTemplate(email); const text = this.renderTemplate(email);
@@ -348,7 +348,7 @@ export class GmailWatcher implements ChannelAdapter {
* Fetch full message details by ID and extract relevant headers. * Fetch full message details by ID and extract relevant headers.
*/ */
private async getMessageDetails(messageId: string): Promise<EmailInfo | null> { private async getMessageDetails(messageId: string): Promise<EmailInfo | null> {
if (!this.oauth2Client) return null; if (!this.oauth2Client) {return null;}
const gmail = google.gmail({ version: 'v1', auth: this.oauth2Client }); const gmail = google.gmail({ version: 'v1', auth: this.oauth2Client });
+2 -2
View File
@@ -73,7 +73,7 @@ export class HeartbeatMonitor {
/** Start the heartbeat monitor. Does nothing if disabled. */ /** Start the heartbeat monitor. Does nothing if disabled. */
start(): void { start(): void {
if (!this.deps.config.enabled) return; if (!this.deps.config.enabled) {return;}
const intervalMs = parseInterval(this.deps.config.interval); const intervalMs = parseInterval(this.deps.config.interval);
console.log(`HeartbeatMonitor: starting (interval=${this.deps.config.interval}, checks=[${this.deps.config.checks.join(', ')}])`); console.log(`HeartbeatMonitor: starting (interval=${this.deps.config.interval}, checks=[${this.deps.config.checks.join(', ')}])`);
@@ -290,7 +290,7 @@ export class HeartbeatMonitor {
private async notify(text: string): Promise<void> { private async notify(text: string): Promise<void> {
const notifyConfig = this.deps.config.notify; const notifyConfig = this.deps.config.notify;
if (!notifyConfig) return; if (!notifyConfig) {return;}
const adapter = this.deps.channelLookup.get(notifyConfig.channel); const adapter = this.deps.channelLookup.get(notifyConfig.channel);
if (!adapter) { if (!adapter) {
+1 -1
View File
@@ -36,7 +36,7 @@ function mockResponse(): ServerResponse & { statusCode_: number; body_: string;
headers_: {}, headers_: {},
writeHead(code: number, headers?: Record<string, string>) { writeHead(code: number, headers?: Record<string, string>) {
res.statusCode_ = code; res.statusCode_ = code;
if (headers) res.headers_ = headers; if (headers) {res.headers_ = headers;}
return res; return res;
}, },
end(body?: string) { end(body?: string) {
+2 -2
View File
@@ -23,7 +23,7 @@ function verifyHmac(body: string, secret: string, signature: string): boolean {
const expected = createHmac('sha256', secret).update(body).digest('hex'); const expected = createHmac('sha256', secret).update(body).digest('hex');
const sig = signature.startsWith('sha256=') ? signature.slice(7) : signature; const sig = signature.startsWith('sha256=') ? signature.slice(7) : signature;
if (expected.length !== sig.length) return false; if (expected.length !== sig.length) {return false;}
try { try {
return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(sig, 'hex')); return timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(sig, 'hex'));
@@ -51,7 +51,7 @@ function renderTemplate(template: string, body: string): string {
} }
} }
const value = parsed[field]; const value = parsed[field];
if (value === undefined || value === null) return ''; if (value === undefined || value === null) {return '';}
return typeof value === 'string' ? value : JSON.stringify(value); return typeof value === 'string' ? value : JSON.stringify(value);
}); });
+1 -1
View File
@@ -243,7 +243,7 @@ describe('AgentOrchestrator', () => {
}); });
expect(consoleSpy).toHaveBeenCalledWith( expect(consoleSpy).toHaveBeenCalledWith(
'[Flynn:delegate] tier=fast tokens=50+25' '[Flynn:delegate] tier=fast tokens=50+25',
); );
consoleSpy.mockRestore(); consoleSpy.mockRestore();
+3 -3
View File
@@ -364,10 +364,10 @@ export class AgentOrchestrator {
* Called before each `process()` call when compaction is configured. * Called before each `process()` call when compaction is configured.
*/ */
private async compactIfNeeded(): Promise<void> { private async compactIfNeeded(): Promise<void> {
if (!this._compactionConfig) return; if (!this._compactionConfig) {return;}
const messages = this.getHistory(); const messages = this.getHistory();
if (messages.length === 0) return; if (messages.length === 0) {return;}
const model = this._modelName ?? 'unknown'; const model = this._modelName ?? 'unknown';
const needs = shouldCompact({ const needs = shouldCompact({
@@ -377,7 +377,7 @@ export class AgentOrchestrator {
thresholdPct: this._compactionConfig.thresholdPct, thresholdPct: this._compactionConfig.thresholdPct,
}); });
if (!needs) return; if (!needs) {return;}
await this.compact(); await this.compact();
} }
+2 -2
View File
@@ -14,7 +14,7 @@ function createMockClient() {
_handlers: handlers, _handlers: handlers,
user: null as { id: string; tag: string } | null, user: null as { id: string; tag: string } | null,
on: vi.fn((event: string, handler: (...args: unknown[]) => void) => { on: vi.fn((event: string, handler: (...args: unknown[]) => void) => {
if (!handlers.has(event)) handlers.set(event, []); if (!handlers.has(event)) {handlers.set(event, []);}
handlers.get(event)!.push(handler); handlers.get(event)!.push(handler);
}), }),
login: vi.fn(async (_token: string) => { login: vi.fn(async (_token: string) => {
@@ -23,7 +23,7 @@ function createMockClient() {
// Trigger ready event asynchronously // Trigger ready event asynchronously
setTimeout(() => { setTimeout(() => {
const readyHandlers = handlers.get('ready') ?? []; const readyHandlers = handlers.get('ready') ?? [];
for (const h of readyHandlers) h(); for (const h of readyHandlers) {h();}
}, 0); }, 0);
}), }),
destroy: vi.fn(), destroy: vi.fn(),
+3 -3
View File
@@ -117,7 +117,7 @@ export class DiscordAdapter implements ChannelAdapter {
/** Send an outbound message, automatically chunking if it exceeds Discord's 2000-char limit. */ /** Send an outbound message, automatically chunking if it exceeds Discord's 2000-char limit. */
async send(peerId: string, message: OutboundMessage): Promise<void> { async send(peerId: string, message: OutboundMessage): Promise<void> {
if (!this.client) throw new Error('Discord adapter not connected'); if (!this.client) {throw new Error('Discord adapter not connected');}
const channel = await this.client.channels.fetch(peerId); const channel = await this.client.channels.fetch(peerId);
if (!channel || !('send' in channel)) { if (!channel || !('send' in channel)) {
@@ -163,10 +163,10 @@ export class DiscordAdapter implements ChannelAdapter {
/** Internal: process an inbound Discord message. */ /** Internal: process an inbound Discord message. */
private handleMessage(message: DiscordMessage): void { private handleMessage(message: DiscordMessage): void {
if (!this.messageHandler) return; if (!this.messageHandler) {return;}
// Ignore bot messages // Ignore bot messages
if (message.author.bot) return; if (message.author.bot) {return;}
const isDM = !message.guild; const isDM = !message.guild;
+1 -1
View File
@@ -83,7 +83,7 @@ export class PairingManager {
const normalizedCode = code.trim().toUpperCase(); const normalizedCode = code.trim().toUpperCase();
const pending = this.pendingCodes.get(normalizedCode); const pending = this.pendingCodes.get(normalizedCode);
if (!pending) return false; if (!pending) {return false;}
if (Date.now() > pending.expiresAt) { if (Date.now() > pending.expiresAt) {
this.pendingCodes.delete(normalizedCode); this.pendingCodes.delete(normalizedCode);
return false; return false;
+1 -1
View File
@@ -32,7 +32,7 @@ export class ChannelRegistry {
/** Unregister an adapter by name. Calls disconnect() if connected. */ /** Unregister an adapter by name. Calls disconnect() if connected. */
async unregister(name: string): Promise<void> { async unregister(name: string): Promise<void> {
const adapter = this.adapters.get(name); const adapter = this.adapters.get(name);
if (!adapter) return; if (!adapter) {return;}
if (adapter.status === 'connected' || adapter.status === 'connecting') { if (adapter.status === 'connected' || adapter.status === 'connecting') {
await adapter.disconnect(); await adapter.disconnect();
+1 -1
View File
@@ -50,7 +50,7 @@ const baseConfig: SlackAdapterConfig = {
/** Helper: simulate a Slack message event through the captured handler. */ /** Helper: simulate a Slack message event through the captured handler. */
async function simulateMessage(message: Record<string, unknown>) { async function simulateMessage(message: Record<string, unknown>) {
if (!capturedMessageHandler) throw new Error('No message handler captured — call connect() first'); if (!capturedMessageHandler) {throw new Error('No message handler captured — call connect() first');}
await capturedMessageHandler({ message }); await capturedMessageHandler({ message });
} }
+11 -11
View File
@@ -128,15 +128,15 @@ export class SlackAdapter implements ChannelAdapter {
/** Send an outbound message, automatically chunking if it exceeds 4000 chars. */ /** Send an outbound message, automatically chunking if it exceeds 4000 chars. */
async send(peerId: string, message: OutboundMessage): Promise<void> { async send(peerId: string, message: OutboundMessage): Promise<void> {
if (!this.app) throw new Error('Slack adapter not connected'); if (!this.app) {throw new Error('Slack adapter not connected');}
// Parse peerId: "channelId:threadTs" // Parse peerId: "channelId:threadTs"
const colonIndex = peerId.indexOf(':'); const colonIndex = peerId.indexOf(':');
if (colonIndex === -1) throw new Error(`Invalid peer ID format: ${peerId}`); if (colonIndex === -1) {throw new Error(`Invalid peer ID format: ${peerId}`);}
const channel = peerId.slice(0, colonIndex); const channel = peerId.slice(0, colonIndex);
const threadTs = peerId.slice(colonIndex + 1); const threadTs = peerId.slice(colonIndex + 1);
if (!channel || !threadTs) throw new Error(`Invalid peer ID format: ${peerId}`); if (!channel || !threadTs) {throw new Error(`Invalid peer ID format: ${peerId}`);}
const text = message.text; const text = message.text;
@@ -171,7 +171,7 @@ export class SlackAdapter implements ChannelAdapter {
threadTs: string, threadTs: string,
attachment: OutboundAttachment, attachment: OutboundAttachment,
): Promise<void> { ): Promise<void> {
if (!this.app) return; if (!this.app) {return;}
try { try {
if (attachment.data) { if (attachment.data) {
@@ -200,7 +200,7 @@ export class SlackAdapter implements ChannelAdapter {
/** Resolve a Slack user ID to a display name, with caching. */ /** Resolve a Slack user ID to a display name, with caching. */
private async resolveUserName(userId: string): Promise<string> { private async resolveUserName(userId: string): Promise<string> {
const cached = this.userNameCache.get(userId); const cached = this.userNameCache.get(userId);
if (cached) return cached; if (cached) {return cached;}
try { try {
const result = await this.app!.client.users.info({ user: userId }); const result = await this.app!.client.users.info({ user: userId });
@@ -219,16 +219,16 @@ export class SlackAdapter implements ChannelAdapter {
private async extractMediaAttachments( private async extractMediaAttachments(
files?: SlackMessageEvent['files'], files?: SlackMessageEvent['files'],
): Promise<Attachment[]> { ): Promise<Attachment[]> {
if (!files || files.length === 0) return []; if (!files || files.length === 0) {return [];}
const attachments: Attachment[] = []; const attachments: Attachment[] = [];
for (const file of files) { for (const file of files) {
// Only process image and audio files // Only process image and audio files
if (!file.mimetype?.startsWith('image/') && !file.mimetype?.startsWith('audio/')) continue; if (!file.mimetype?.startsWith('image/') && !file.mimetype?.startsWith('audio/')) {continue;}
const downloadUrl = file.url_private_download || file.url_private; const downloadUrl = file.url_private_download || file.url_private;
if (!downloadUrl) continue; if (!downloadUrl) {continue;}
try { try {
const response = await fetch(downloadUrl, { const response = await fetch(downloadUrl, {
@@ -264,13 +264,13 @@ export class SlackAdapter implements ChannelAdapter {
/** Internal: process an inbound Slack message event. */ /** Internal: process an inbound Slack message event. */
private async handleMessage(message: SlackMessageEvent): Promise<void> { private async handleMessage(message: SlackMessageEvent): Promise<void> {
if (!this.messageHandler) return; if (!this.messageHandler) {return;}
// Ignore bot messages // Ignore bot messages
if (message.bot_id || message.subtype === 'bot_message') return; if (message.bot_id || message.subtype === 'bot_message') {return;}
const channelId = message.channel; const channelId = message.channel;
if (!channelId) return; if (!channelId) {return;}
// Check allowed channel IDs // Check allowed channel IDs
if ( if (
+15 -15
View File
@@ -53,13 +53,13 @@ export class TelegramAdapter implements ChannelAdapter {
private async downloadFileToBase64(fileId: string): Promise<string | null> { private async downloadFileToBase64(fileId: string): Promise<string | null> {
try { try {
const file = await this.bot?.api.getFile(fileId); const file = await this.bot?.api.getFile(fileId);
if (!file || !file.file_path) return null; if (!file || !file.file_path) {return null;}
const token = this.config.botToken; const token = this.config.botToken;
const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`; const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) return null; if (!response.ok) {return null;}
const buffer = Buffer.from(await response.arrayBuffer()); const buffer = Buffer.from(await response.arrayBuffer());
return buffer.toString('base64'); return buffer.toString('base64');
@@ -82,7 +82,7 @@ export class TelegramAdapter implements ChannelAdapter {
// ── Auth middleware — reject messages from unknown chats (with pairing fallback) ── // ── Auth middleware — reject messages from unknown chats (with pairing fallback) ──
this.bot.use(async (ctx, next) => { this.bot.use(async (ctx, next) => {
const chatId = ctx.chat?.id; const chatId = ctx.chat?.id;
if (chatId === undefined) return; if (chatId === undefined) {return;}
// Allowlist check // Allowlist check
if (isAllowedChat(chatId, this.config.allowedChatIds)) { if (isAllowedChat(chatId, this.config.allowedChatIds)) {
@@ -166,7 +166,7 @@ export class TelegramAdapter implements ChannelAdapter {
// ── Text message handler ── // ── Text message handler ──
this.bot.on('message:text', async (ctx) => { this.bot.on('message:text', async (ctx) => {
if (!this.messageHandler) return; if (!this.messageHandler) {return;}
// Group chat mention gating // Group chat mention gating
const isGroup = ctx.chat.type === 'group' || ctx.chat.type === 'supergroup'; const isGroup = ctx.chat.type === 'group' || ctx.chat.type === 'supergroup';
@@ -212,10 +212,10 @@ export class TelegramAdapter implements ChannelAdapter {
// ── Photo message handler ── // ── Photo message handler ──
this.bot.on('message:photo', async (ctx) => { this.bot.on('message:photo', async (ctx) => {
if (!this.messageHandler) return; if (!this.messageHandler) {return;}
const photo = ctx.message.photo; const photo = ctx.message.photo;
if (!photo || photo.length === 0) return; if (!photo || photo.length === 0) {return;}
const largestPhoto = photo[photo.length - 1]; const largestPhoto = photo[photo.length - 1];
@@ -250,13 +250,13 @@ export class TelegramAdapter implements ChannelAdapter {
// ── Image document handler ── // ── Image document handler ──
this.bot.on('message:document', async (ctx) => { this.bot.on('message:document', async (ctx) => {
if (!this.messageHandler) return; if (!this.messageHandler) {return;}
const document = ctx.message.document; const document = ctx.message.document;
if (!document) return; if (!document) {return;}
const mimeType = document.mime_type ?? ''; const mimeType = document.mime_type ?? '';
if (!mimeType.startsWith('image/')) return; if (!mimeType.startsWith('image/')) {return;}
await ctx.replyWithChatAction('typing'); await ctx.replyWithChatAction('typing');
@@ -290,10 +290,10 @@ export class TelegramAdapter implements ChannelAdapter {
// ── Voice message handler ── // ── Voice message handler ──
this.bot.on('message:voice', async (ctx) => { this.bot.on('message:voice', async (ctx) => {
if (!this.messageHandler) return; if (!this.messageHandler) {return;}
const voice = ctx.message.voice; const voice = ctx.message.voice;
if (!voice) return; if (!voice) {return;}
await ctx.replyWithChatAction('typing'); await ctx.replyWithChatAction('typing');
@@ -327,10 +327,10 @@ export class TelegramAdapter implements ChannelAdapter {
// ── Audio message handler ── // ── Audio message handler ──
this.bot.on('message:audio', async (ctx) => { this.bot.on('message:audio', async (ctx) => {
if (!this.messageHandler) return; if (!this.messageHandler) {return;}
const audio = ctx.message.audio; const audio = ctx.message.audio;
if (!audio) return; if (!audio) {return;}
await ctx.replyWithChatAction('typing'); await ctx.replyWithChatAction('typing');
@@ -388,7 +388,7 @@ export class TelegramAdapter implements ChannelAdapter {
/** Send an outbound message, automatically chunking if it exceeds Telegram's limit. */ /** Send an outbound message, automatically chunking if it exceeds Telegram's limit. */
async send(peerId: string, message: OutboundMessage): Promise<void> { async send(peerId: string, message: OutboundMessage): Promise<void> {
if (!this.bot) throw new Error('Telegram adapter not connected'); if (!this.bot) {throw new Error('Telegram adapter not connected');}
const chatId = Number(peerId); const chatId = Number(peerId);
const text = message.text; const text = message.text;
@@ -413,7 +413,7 @@ export class TelegramAdapter implements ChannelAdapter {
/** Send a single outbound attachment via the Telegram API. */ /** Send a single outbound attachment via the Telegram API. */
private async sendAttachment(chatId: number, attachment: OutboundAttachment): Promise<void> { private async sendAttachment(chatId: number, attachment: OutboundAttachment): Promise<void> {
if (!this.bot) return; if (!this.bot) {return;}
try { try {
const file = attachment.data const file = attachment.data
+5 -5
View File
@@ -146,7 +146,7 @@ export class WhatsAppAdapter implements ChannelAdapter {
/** Send an outbound message, automatically chunking if it exceeds 4096 chars. */ /** Send an outbound message, automatically chunking if it exceeds 4096 chars. */
async send(peerId: string, message: OutboundMessage): Promise<void> { async send(peerId: string, message: OutboundMessage): Promise<void> {
if (!this.client) throw new Error('WhatsApp adapter not connected'); if (!this.client) {throw new Error('WhatsApp adapter not connected');}
const text = message.text; const text = message.text;
@@ -169,7 +169,7 @@ export class WhatsAppAdapter implements ChannelAdapter {
/** Send a single outbound attachment via WhatsApp using MessageMedia. */ /** Send a single outbound attachment via WhatsApp using MessageMedia. */
private async sendAttachment(peerId: string, attachment: OutboundAttachment): Promise<void> { private async sendAttachment(peerId: string, attachment: OutboundAttachment): Promise<void> {
if (!this.client) return; if (!this.client) {return;}
try { try {
if (attachment.data) { if (attachment.data) {
@@ -194,10 +194,10 @@ export class WhatsAppAdapter implements ChannelAdapter {
/** Internal: process an inbound WhatsApp message. */ /** Internal: process an inbound WhatsApp message. */
private async handleMessage(message: WhatsAppMessage): Promise<void> { private async handleMessage(message: WhatsAppMessage): Promise<void> {
if (!this.messageHandler) return; if (!this.messageHandler) {return;}
// Ignore messages from the bot itself // Ignore messages from the bot itself
if (message.fromMe) return; if (message.fromMe) {return;}
const from = message.from; const from = message.from;
@@ -223,7 +223,7 @@ export class WhatsAppAdapter implements ChannelAdapter {
? message.body?.includes(`@${this.botId.replace(/@c\.us$/, '')}`) || ? message.body?.includes(`@${this.botId.replace(/@c\.us$/, '')}`) ||
(message as any).mentionedIds?.some((id: string) => id === this.botId) (message as any).mentionedIds?.some((id: string) => id === this.botId)
: false; : false;
if (!mentionsBot) return; if (!mentionsBot) {return;}
} }
} }
+9 -9
View File
@@ -74,18 +74,18 @@ compdef _flynn flynn
export function generateFishCompletion(): string { export function generateFishCompletion(): string {
const lines = [ const lines = [
`# Flynn fish completion — generated by 'flynn completion fish'`, '# Flynn fish completion — generated by \'flynn completion fish\'',
`# Disable file completions by default`, '# Disable file completions by default',
`complete -c flynn -f`, 'complete -c flynn -f',
``, '',
`# Subcommands`, '# Subcommands',
]; ];
for (const cmd of SUBCOMMANDS) { for (const cmd of SUBCOMMANDS) {
lines.push(`complete -c flynn -n '__fish_use_subcommand' -a '${cmd}' -d '${cmd} subcommand'`); lines.push(`complete -c flynn -n '__fish_use_subcommand' -a '${cmd}' -d '${cmd} subcommand'`);
} }
lines.push(`complete -c flynn -n '__fish_use_subcommand' -l help -d 'Show help'`); lines.push('complete -c flynn -n \'__fish_use_subcommand\' -l help -d \'Show help\'');
lines.push(`complete -c flynn -n '__fish_use_subcommand' -l version -d 'Show version'`); lines.push('complete -c flynn -n \'__fish_use_subcommand\' -l version -d \'Show version\'');
lines.push(''); lines.push('');
lines.push('# Subcommand options'); lines.push('# Subcommand options');
@@ -98,7 +98,7 @@ export function generateFishCompletion(): string {
} }
} }
lines.push(`complete -c flynn -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish' -d 'Shell type'`); lines.push('complete -c flynn -n \'__fish_seen_subcommand_from completion\' -a \'bash zsh fish\' -d \'Shell type\'');
lines.push(''); lines.push('');
return lines.join('\n'); return lines.join('\n');
@@ -141,7 +141,7 @@ export function registerCompletionCommand(program: Command): void {
writeFileSync(installPath, script, 'utf-8'); writeFileSync(installPath, script, 'utf-8');
console.log(`Completion script installed to: ${installPath}`); console.log(`Completion script installed to: ${installPath}`);
if (shell === 'zsh') { if (shell === 'zsh') {
console.log(`Ensure ~/.zfunc is in your fpath: fpath=(~/.zfunc $fpath)`); console.log('Ensure ~/.zfunc is in your fpath: fpath=(~/.zfunc $fpath)');
} }
} else { } else {
process.stdout.write(script); process.stdout.write(script);
+3 -3
View File
@@ -152,9 +152,9 @@ const checkModelConnectivity: Check = async (ctx) => {
// Build a summary of the model stack // Build a summary of the model stack
const parts = [`default: ${model.provider}/${model.model}`]; const parts = [`default: ${model.provider}/${model.model}`];
if (models.fast) parts.push(`fast: ${models.fast.provider}/${models.fast.model}`); if (models.fast) {parts.push(`fast: ${models.fast.provider}/${models.fast.model}`);}
if (models.complex) parts.push(`complex: ${models.complex.provider}/${models.complex.model}`); if (models.complex) {parts.push(`complex: ${models.complex.provider}/${models.complex.model}`);}
if (models.local) parts.push(`local: ${models.local.provider}/${models.local.model}`); if (models.local) {parts.push(`local: ${models.local.provider}/${models.local.model}`);}
parts.push(`fallback: [${models.fallback_chain.join(', ')}]`); parts.push(`fallback: [${models.fallback_chain.join(', ')}]`);
return { status: 'pass', label: 'Model connectivity', detail: parts.join(', ') }; return { status: 'pass', label: 'Model connectivity', detail: parts.join(', ') };
+1 -1
View File
@@ -38,7 +38,7 @@ function loadSystemPrompt(): string {
resolve(import.meta.dirname, '../../SOUL.md'), resolve(import.meta.dirname, '../../SOUL.md'),
]; ];
for (const p of paths) { for (const p of paths) {
if (existsSync(p)) return readFileSync(p, 'utf-8'); if (existsSync(p)) {return readFileSync(p, 'utf-8');}
} }
return 'You are Flynn, a helpful personal AI assistant.'; return 'You are Flynn, a helpful personal AI assistant.';
} }
+1 -1
View File
@@ -11,7 +11,7 @@ describe('sessions command', () => {
afterEach(() => { afterEach(() => {
store?.close(); store?.close();
if (existsSync(dbPath)) unlinkSync(dbPath); if (existsSync(dbPath)) {unlinkSync(dbPath);}
}); });
it('returns empty list when no sessions', () => { it('returns empty list when no sessions', () => {
+3 -3
View File
@@ -74,7 +74,7 @@ export async function setupAutomation(p: Prompter, builder: ConfigBuilder): Prom
// Google services // Google services
const wantGoogle = await p.confirm('Configure Google services (Gmail, Calendar, Docs, Drive, Tasks)?', false); const wantGoogle = await p.confirm('Configure Google services (Gmail, Calendar, Docs, Drive, Tasks)?', false);
if (!wantGoogle) return; if (!wantGoogle) {return;}
p.println(); p.println();
for (const line of GOOGLE_SETUP_INSTRUCTIONS) { for (const line of GOOGLE_SETUP_INSTRUCTIONS) {
@@ -112,7 +112,7 @@ export async function setupAutomation(p: Prompter, builder: ConfigBuilder): Prom
*/ */
export async function runGoogleAuth(p: Prompter, config: Record<string, any>): Promise<void> { export async function runGoogleAuth(p: Prompter, config: Record<string, any>): Promise<void> {
const automation = config.automation as Record<string, any> | undefined; const automation = config.automation as Record<string, any> | undefined;
if (!automation) return; if (!automation) {return;}
const pending: { name: string; authCmd: string }[] = []; const pending: { name: string; authCmd: string }[] = [];
for (const svc of GOOGLE_SERVICES) { for (const svc of GOOGLE_SERVICES) {
@@ -122,7 +122,7 @@ export async function runGoogleAuth(p: Prompter, config: Record<string, any>): P
} }
} }
if (pending.length === 0) return; if (pending.length === 0) {return;}
p.println(); p.println();
const runAuth = await p.confirm(`Run OAuth authentication for ${pending.map(s => s.name).join(', ')}?`, true); const runAuth = await p.confirm(`Run OAuth authentication for ${pending.map(s => s.name).join(', ')}?`, true);
+2 -2
View File
@@ -87,10 +87,10 @@ export async function setupChannels(p: Prompter, builder: ConfigBuilder): Promis
if (choice === 'more') { if (choice === 'more') {
const moreChoice = await p.choose('Channel:', MORE_CHANNEL_OPTIONS); const moreChoice = await p.choose('Channel:', MORE_CHANNEL_OPTIONS);
const setup = CHANNEL_SETUP[moreChoice]; const setup = CHANNEL_SETUP[moreChoice];
if (setup) await setup(p, builder); if (setup) {await setup(p, builder);}
} else { } else {
const setup = CHANNEL_SETUP[choice]; const setup = CHANNEL_SETUP[choice];
if (setup) await setup(p, builder); if (setup) {await setup(p, builder);}
} }
p.println(); p.println();
+6 -6
View File
@@ -39,9 +39,9 @@ export class ConfigBuilder {
setProvider(tier: 'default' | 'fast' | 'complex' | 'local', cfg: ProviderConfig): void { setProvider(tier: 'default' | 'fast' | 'complex' | 'local', cfg: ProviderConfig): void {
const models = (this.config.models ?? {}) as Record<string, unknown>; const models = (this.config.models ?? {}) as Record<string, unknown>;
const entry: Record<string, unknown> = { provider: cfg.provider, model: cfg.model }; const entry: Record<string, unknown> = { provider: cfg.provider, model: cfg.model };
if (cfg.api_key) entry.api_key = cfg.api_key; if (cfg.api_key) {entry.api_key = cfg.api_key;}
if (cfg.auth_token) entry.auth_token = cfg.auth_token; if (cfg.auth_token) {entry.auth_token = cfg.auth_token;}
if (cfg.endpoint) entry.endpoint = cfg.endpoint; if (cfg.endpoint) {entry.endpoint = cfg.endpoint;}
models[tier] = entry; models[tier] = entry;
this.config.models = models; this.config.models = models;
} }
@@ -91,8 +91,8 @@ export class ConfigBuilder {
setMemoryEmbedding(cfg: EmbeddingConfig): void { setMemoryEmbedding(cfg: EmbeddingConfig): void {
const memory = (this.config.memory ?? {}) as Record<string, unknown>; const memory = (this.config.memory ?? {}) as Record<string, unknown>;
const embedding: Record<string, unknown> = { enabled: true, provider: cfg.provider }; const embedding: Record<string, unknown> = { enabled: true, provider: cfg.provider };
if (cfg.api_key) embedding.api_key = cfg.api_key; if (cfg.api_key) {embedding.api_key = cfg.api_key;}
if (cfg.endpoint) embedding.endpoint = cfg.endpoint; if (cfg.endpoint) {embedding.endpoint = cfg.endpoint;}
memory.embedding = embedding; memory.embedding = embedding;
this.config.memory = memory; this.config.memory = memory;
} }
@@ -151,7 +151,7 @@ export class ConfigBuilder {
setCronEnabled(): void { setCronEnabled(): void {
const automation = (this.config.automation ?? {}) as Record<string, unknown>; const automation = (this.config.automation ?? {}) as Record<string, unknown>;
if (!automation.cron) automation.cron = []; if (!automation.cron) {automation.cron = [];}
this.config.automation = automation; this.config.automation = automation;
} }
+2 -2
View File
@@ -14,7 +14,7 @@ export async function setupMemory(p: Prompter, builder: ConfigBuilder): Promise<
p.println(' Vector search enables semantic memory — Flynn remembers and retrieves'); p.println(' Vector search enables semantic memory — Flynn remembers and retrieves');
p.println(' information based on meaning, not just keywords.'); p.println(' information based on meaning, not just keywords.');
const enable = await p.confirm('Enable vector search for semantic memory?', false); const enable = await p.confirm('Enable vector search for semantic memory?', false);
if (!enable) return; if (!enable) {return;}
p.println(' Pick a provider to generate embeddings (vector representations of text).'); p.println(' Pick a provider to generate embeddings (vector representations of text).');
p.println(' If you already configured OpenAI or Gemini as a model, you can reuse that key.'); p.println(' If you already configured OpenAI or Gemini as a model, you can reuse that key.');
@@ -43,7 +43,7 @@ function findReusableApiKey(config: Record<string, any>, embeddingProvider: stri
const models = config.models ?? {}; const models = config.models ?? {};
for (const tier of ['default', 'fast', 'complex', 'local']) { for (const tier of ['default', 'fast', 'complex', 'local']) {
const m = models[tier]; const m = models[tier];
if (m?.provider === embeddingProvider && m?.api_key) return m.api_key; if (m?.provider === embeddingProvider && m?.api_key) {return m.api_key;}
} }
return undefined; return undefined;
} }
+1 -1
View File
@@ -41,7 +41,7 @@ export async function runMenu(p: Prompter, builder: ConfigBuilder): Promise<void
const answer = await p.ask('>', '0'); const answer = await p.ask('>', '0');
const idx = parseInt(answer, 10); const idx = parseInt(answer, 10);
if (idx === 0 || isNaN(idx)) break; if (idx === 0 || isNaN(idx)) {break;}
if (idx >= 1 && idx <= MENU_OPTIONS.length) { if (idx >= 1 && idx <= MENU_OPTIONS.length) {
const section = MENU_OPTIONS[idx - 1].value; const section = MENU_OPTIONS[idx - 1].value;
const handler = SECTION_HANDLERS[section]; const handler = SECTION_HANDLERS[section];
+3 -3
View File
@@ -25,7 +25,7 @@ export function createPrompter(rl: ReadlineInterface): Prompter {
const hint = defaultYes ? '[Y/n]' : '[y/N]'; const hint = defaultYes ? '[Y/n]' : '[y/N]';
const answer = await rl.question(`${question} ${hint} `); const answer = await rl.question(`${question} ${hint} `);
const trimmed = answer.trim().toLowerCase(); const trimmed = answer.trim().toLowerCase();
if (trimmed === '') return defaultYes; if (trimmed === '') {return defaultYes;}
return trimmed === 'y' || trimmed === 'yes'; return trimmed === 'y' || trimmed === 'yes';
}, },
@@ -34,9 +34,9 @@ export function createPrompter(rl: ReadlineInterface): Prompter {
for (let i = 0; i < options.length; i++) { for (let i = 0; i < options.length; i++) {
this.println(` ${i + 1}. ${options[i].label}`); this.println(` ${i + 1}. ${options[i].label}`);
} }
const answer = await rl.question(`> `); const answer = await rl.question('> ');
const idx = parseInt(answer.trim(), 10) - 1; const idx = parseInt(answer.trim(), 10) - 1;
if (idx >= 0 && idx < options.length) return options[idx].value; if (idx >= 0 && idx < options.length) {return options[idx].value;}
return options[0].value; return options[0].value;
}, },
+3 -3
View File
@@ -41,11 +41,11 @@ async function configureProvider(p: Prompter, def: ProviderDef): Promise<{
provider: string; model: string; api_key?: string; endpoint?: string; provider: string; model: string; api_key?: string; endpoint?: string;
}> { }> {
const help = PROVIDER_HELP[def.provider]; const help = PROVIDER_HELP[def.provider];
if (help) p.println(` ${help}`); if (help) {p.println(` ${help}`);}
const config: Record<string, string> = { provider: def.provider }; const config: Record<string, string> = { provider: def.provider };
if (def.needsApiKey) config.api_key = await p.password(def.apiKeyLabel ?? 'API key'); if (def.needsApiKey) {config.api_key = await p.password(def.apiKeyLabel ?? 'API key');}
if (def.needsEndpoint) config.endpoint = await p.ask('Host', def.defaultEndpoint); if (def.needsEndpoint) {config.endpoint = await p.ask('Host', def.defaultEndpoint);}
config.model = await p.ask('Model', def.defaultModel); config.model = await p.ask('Model', def.defaultModel);
return config as { provider: string; model: string; api_key?: string; endpoint?: string }; return config as { provider: string; model: string; api_key?: string; endpoint?: string };
} }
+18 -18
View File
@@ -9,11 +9,11 @@ export function renderSummary(config: Record<string, any>): string {
lines.push(` Models: ${tiers || 'none configured'}`); lines.push(` Models: ${tiers || 'none configured'}`);
const channels: string[] = []; const channels: string[] = [];
if (config.server?.port) channels.push('webchat'); if (config.server?.port) {channels.push('webchat');}
if (config.telegram) channels.push('telegram'); if (config.telegram) {channels.push('telegram');}
if (config.discord) channels.push('discord'); if (config.discord) {channels.push('discord');}
if (config.slack) channels.push('slack'); if (config.slack) {channels.push('slack');}
if (config.whatsapp) channels.push('whatsapp'); if (config.whatsapp) {channels.push('whatsapp');}
lines.push(` Channels: ${channels.join(', ') || 'none'}`); lines.push(` Channels: ${channels.join(', ') || 'none'}`);
const embedding = config.memory?.embedding; const embedding = config.memory?.embedding;
@@ -22,27 +22,27 @@ export function renderSummary(config: Record<string, any>): string {
const auto = config.automation ?? {}; const auto = config.automation ?? {};
const autoFeatures: string[] = []; const autoFeatures: string[] = [];
if (auto.cron?.length > 0) autoFeatures.push(`${auto.cron.length} cron jobs`); if (auto.cron?.length > 0) {autoFeatures.push(`${auto.cron.length} cron jobs`);}
if (auto.webhooks?.length > 0) autoFeatures.push('webhooks'); if (auto.webhooks?.length > 0) {autoFeatures.push('webhooks');}
if (auto.gmail?.enabled) autoFeatures.push('gmail'); if (auto.gmail?.enabled) {autoFeatures.push('gmail');}
if (auto.gcal?.enabled) autoFeatures.push('gcal'); if (auto.gcal?.enabled) {autoFeatures.push('gcal');}
if (auto.gdocs?.enabled) autoFeatures.push('gdocs'); if (auto.gdocs?.enabled) {autoFeatures.push('gdocs');}
if (auto.gdrive?.enabled) autoFeatures.push('gdrive'); if (auto.gdrive?.enabled) {autoFeatures.push('gdrive');}
if (auto.gtasks?.enabled) autoFeatures.push('gtasks'); if (auto.gtasks?.enabled) {autoFeatures.push('gtasks');}
if (auto.heartbeat?.enabled) autoFeatures.push('heartbeat'); if (auto.heartbeat?.enabled) {autoFeatures.push('heartbeat');}
lines.push(` Automation: ${autoFeatures.join(', ') || 'none'}`); lines.push(` Automation: ${autoFeatures.join(', ') || 'none'}`);
const secFeatures: string[] = []; const secFeatures: string[] = [];
secFeatures.push(`tools:${config.tools?.profile ?? 'full'}`); secFeatures.push(`tools:${config.tools?.profile ?? 'full'}`);
if (config.sandbox?.enabled) secFeatures.push('sandbox'); if (config.sandbox?.enabled) {secFeatures.push('sandbox');}
if (config.pairing?.enabled) secFeatures.push('pairing'); if (config.pairing?.enabled) {secFeatures.push('pairing');}
lines.push(` Security: ${secFeatures.join(', ')}`); lines.push(` Security: ${secFeatures.join(', ')}`);
const gw: string[] = []; const gw: string[] = [];
gw.push(`port ${config.server?.port ?? 18800}`); gw.push(`port ${config.server?.port ?? 18800}`);
if (config.server?.token) gw.push('auth'); if (config.server?.token) {gw.push('auth');}
if (config.server?.lock) gw.push('locked'); if (config.server?.lock) {gw.push('locked');}
if (config.server?.tailscale?.serve) gw.push('tailscale'); if (config.server?.tailscale?.serve) {gw.push('tailscale');}
lines.push(` Gateway: ${gw.join(', ')}`); lines.push(` Gateway: ${gw.join(', ')}`);
return lines.join('\n'); return lines.join('\n');
+3 -3
View File
@@ -21,7 +21,7 @@ export function getDataDir(): string {
*/ */
export function resolveOverlayPath(basePath: string): string | undefined { export function resolveOverlayPath(basePath: string): string | undefined {
const env = process.env.FLYNN_ENV; const env = process.env.FLYNN_ENV;
if (!env) return undefined; if (!env) {return undefined;}
const configDir = dirname(basePath); const configDir = dirname(basePath);
return join(configDir, `${env}.yaml`); return join(configDir, `${env}.yaml`);
} }
@@ -44,8 +44,8 @@ export function redactSecrets(config: Record<string, unknown>): Record<string, u
const sensitiveKeys = ['bot_token', 'api_key', 'auth_token']; const sensitiveKeys = ['bot_token', 'api_key', 'auth_token'];
function redact(obj: unknown): unknown { function redact(obj: unknown): unknown {
if (obj === null || obj === undefined) return obj; if (obj === null || obj === undefined) {return obj;}
if (Array.isArray(obj)) return obj.map(redact); if (Array.isArray(obj)) {return obj.map(redact);}
if (typeof obj === 'object') { if (typeof obj === 'object') {
const result: Record<string, unknown> = {}; const result: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) { for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
+2 -2
View File
@@ -27,9 +27,9 @@ function formatToolName(name: string): string {
/** Format tool args as a compact, readable summary instead of raw JSON. */ /** Format tool args as a compact, readable summary instead of raw JSON. */
function formatToolArgs(args: unknown): string { function formatToolArgs(args: unknown): string {
if (!args || typeof args !== 'object') return ''; if (!args || typeof args !== 'object') {return '';}
const entries = Object.entries(args as Record<string, unknown>); const entries = Object.entries(args as Record<string, unknown>);
if (entries.length === 0) return ''; if (entries.length === 0) {return '';}
const parts = entries.map(([key, value]) => { const parts = entries.map(([key, value]) => {
if (typeof value === 'string') { if (typeof value === 'string') {
+1 -1
View File
@@ -14,7 +14,7 @@ export class Lifecycle {
} }
async shutdown(): Promise<void> { async shutdown(): Promise<void> {
if (this.shuttingDown) return; if (this.shuttingDown) {return;}
this.shuttingDown = true; this.shuttingDown = true;
this._isRunning = false; this._isRunning = false;
+1 -1
View File
@@ -57,7 +57,7 @@ export async function initMemory(deps: MemoryDeps): Promise<MemoryResult> {
} }
const hash = contentHash(content); const hash = contentHash(content);
if (vectorStore.hasContentHash(ns, hash)) continue; if (vectorStore.hasContentHash(ns, hash)) {continue;}
const chunks = chunkText(content, ns, { const chunks = chunkText(content, ns, {
chunkSize: config.memory.embedding.chunk_size, chunkSize: config.memory.embedding.chunk_size,
+5 -5
View File
@@ -12,7 +12,7 @@ function requireApiKey(cfg: ModelConfig, envVar: string): string {
if (!key) { if (!key) {
throw new Error( throw new Error(
`API key required for ${cfg.provider}. ` + `API key required for ${cfg.provider}. ` +
`Set ${envVar} environment variable or provide api_key in config.` `Set ${envVar} environment variable or provide api_key in config.`,
); );
} }
return key; return key;
@@ -118,7 +118,7 @@ export function anthropicToGitHubModel(anthropicModel: string): string | undefin
'claude-haiku-4-5-20251001': 'claude-haiku-4.5', 'claude-haiku-4-5-20251001': 'claude-haiku-4.5',
}; };
if (MAPPINGS[anthropicModel]) return MAPPINGS[anthropicModel]; if (MAPPINGS[anthropicModel]) {return MAPPINGS[anthropicModel];}
// Generic fallback: strip date suffix, then convert trailing -N to .N // Generic fallback: strip date suffix, then convert trailing -N to .N
// only when preceded by another digit (i.e. "4-5" → "4.5", not "sonnet-5" → "sonnet.5") // only when preceded by another digit (i.e. "4-5" → "4.5", not "sonnet-5" → "sonnet.5")
@@ -140,10 +140,10 @@ export function anthropicToGitHubModel(anthropicModel: string): string | undefin
* Returns undefined if no mapping exists or the tier isn't Anthropic. * Returns undefined if no mapping exists or the tier isn't Anthropic.
*/ */
export function createAutoFallbackClient(tierConfig: { provider: string; model: string }): ModelClient | undefined { export function createAutoFallbackClient(tierConfig: { provider: string; model: string }): ModelClient | undefined {
if (tierConfig.provider !== 'anthropic') return undefined; if (tierConfig.provider !== 'anthropic') {return undefined;}
const githubModel = anthropicToGitHubModel(tierConfig.model); const githubModel = anthropicToGitHubModel(tierConfig.model);
if (!githubModel) return undefined; if (!githubModel) {return undefined;}
return new GitHubModelsClient({ return new GitHubModelsClient({
model: githubModel, model: githubModel,
@@ -202,7 +202,7 @@ export function createModelRouter(config: Config): ModelRouter {
const autoFallbackTiers: string[] = []; const autoFallbackTiers: string[] = [];
for (const { tier, cfg } of tierConfigs) { for (const { tier, cfg } of tierConfigs) {
if (!cfg) continue; if (!cfg) {continue;}
const fallbackList: ModelClient[] = []; const fallbackList: ModelClient[] = [];
+1 -1
View File
@@ -92,7 +92,7 @@ export function loadSystemPrompt(config: Config, skillRegistry: SkillRegistry):
// ── Pairing Manager ───────────────────────────────────────────── // ── Pairing Manager ─────────────────────────────────────────────
export function initPairingManager(config: Config, store?: PairingStore): PairingManager | undefined { export function initPairingManager(config: Config, store?: PairingStore): PairingManager | undefined {
if (!config.pairing.enabled) return undefined; if (!config.pairing.enabled) {return undefined;}
const ttlMatch = config.pairing.code_ttl.match(/^(\d+)(s|m|h)$/); const ttlMatch = config.pairing.code_ttl.match(/^(\d+)(s|m|h)$/);
const codeTtlMs = ttlMatch const codeTtlMs = ttlMatch
+1 -1
View File
@@ -49,7 +49,7 @@ export function createTelegramBot(config: TelegramBotConfig): Bot {
}); });
await ctx.editMessageText( await ctx.editMessageText(
ctx.callbackQuery.message?.text + `\n\n${parsed.approved ? '✅ Approved' : '❌ Denied'}`, ctx.callbackQuery.message?.text + `\n\n${parsed.approved ? '✅ Approved' : '❌ Denied'}`,
{ parse_mode: 'Markdown' } { parse_mode: 'Markdown' },
); );
} else { } else {
await ctx.answerCallbackQuery({ text: 'Confirmation expired or not found' }); await ctx.answerCallbackQuery({ text: 'Confirmation expired or not found' });
+2 -2
View File
@@ -16,7 +16,7 @@ export type Command =
export function parseCommand(input: string): Command | null { export function parseCommand(input: string): Command | null {
const trimmed = input.trim(); const trimmed = input.trim();
if (!trimmed) return null; if (!trimmed) {return null;}
// Quit // Quit
if (trimmed === '/quit' || trimmed === '/exit') { if (trimmed === '/quit' || trimmed === '/exit') {
@@ -261,7 +261,7 @@ export function getCommandTooltip(partial: string): string | null {
if (matches.length === 1) { if (matches.length === 1) {
return `Enter provider/model (e.g. ${matches[0]}/...)`; return `Enter provider/model (e.g. ${matches[0]}/...)`;
} }
return `Enter provider/model (e.g. anthropic/claude-sonnet-4)`; return 'Enter provider/model (e.g. anthropic/claude-sonnet-4)';
} }
} }
+6 -6
View File
@@ -21,9 +21,9 @@ function formatToolName(name: string): string {
/** Format tool args as a compact, readable summary. */ /** Format tool args as a compact, readable summary. */
function formatToolArgs(args: unknown): string { function formatToolArgs(args: unknown): string {
if (!args || typeof args !== 'object') return ''; if (!args || typeof args !== 'object') {return '';}
const entries = Object.entries(args as Record<string, unknown>); const entries = Object.entries(args as Record<string, unknown>);
if (entries.length === 0) return ''; if (entries.length === 0) {return '';}
const parts = entries.map(([key, value]) => { const parts = entries.map(([key, value]) => {
if (typeof value === 'string') { if (typeof value === 'string') {
const display = value.length > 50 ? value.slice(0, 47) + '...' : value; const display = value.length > 50 ? value.slice(0, 47) + '...' : value;
@@ -71,7 +71,7 @@ export function App({
// This replaces the process.stdout.write callback (which corrupts Ink rendering) // This replaces the process.stdout.write callback (which corrupts Ink rendering)
// with one that updates React state to show tool activity in the streaming area. // with one that updates React state to show tool activity in the streaming area.
useEffect(() => { useEffect(() => {
if (!agent) return; if (!agent) {return;}
const handleToolEvent = (event: ToolUseEvent) => { const handleToolEvent = (event: ToolUseEvent) => {
if (event.type === 'start') { if (event.type === 'start') {
@@ -137,7 +137,7 @@ export function App({
const handleSubmit = useCallback(async (value: string) => { const handleSubmit = useCallback(async (value: string) => {
const command = parseCommand(value); const command = parseCommand(value);
if (!command) return; if (!command) {return;}
setInput(''); setInput('');
@@ -212,7 +212,7 @@ export function App({
return; return;
case 'transfer': { case 'transfer': {
const xferMsg: Message = { role: 'assistant', content: `Transfer not supported in fullscreen mode.` }; const xferMsg: Message = { role: 'assistant', content: 'Transfer not supported in fullscreen mode.' };
const xferWithTs = session.addMessage(xferMsg); const xferWithTs = session.addMessage(xferMsg);
setMessages(prev => [...prev, xferWithTs]); setMessages(prev => [...prev, xferWithTs]);
return; return;
@@ -222,7 +222,7 @@ export function App({
break; // Continue to message handling break; // Continue to message handling
} }
if (command.type !== 'message' || isStreaming) return; if (command.type !== 'message' || isStreaming) {return;}
// Add user message to UI (and session if no agent — agent adds it internally) // Add user message to UI (and session if no agent — agent adds it internally)
const userMessage: Message = { role: 'user', content: command.content }; const userMessage: Message = { role: 'user', content: command.content };
+2 -2
View File
@@ -19,12 +19,12 @@ export const InputBar = memo(function InputBar({
placeholder = 'Type a message...', placeholder = 'Type a message...',
}: InputBarProps): React.ReactElement { }: InputBarProps): React.ReactElement {
const completions = useMemo(() => { const completions = useMemo(() => {
if (!value.startsWith('/')) return []; if (!value.startsWith('/')) {return [];}
return getCommandCompletions(value); return getCommandCompletions(value);
}, [value]); }, [value]);
const tooltip = useMemo(() => { const tooltip = useMemo(() => {
if (!value.startsWith('/')) return null; if (!value.startsWith('/')) {return null;}
return getCommandTooltip(value); return getCommandTooltip(value);
}, [value]); }, [value]);
+5 -5
View File
@@ -20,17 +20,17 @@ function formatTimestamp(timestamp: number): string {
const hours = Math.floor(minutes / 60); const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24); const days = Math.floor(hours / 24);
if (seconds < 60) return 'just now'; if (seconds < 60) {return 'just now';}
if (minutes < 60) return `${minutes}m ago`; if (minutes < 60) {return `${minutes}m ago`;}
if (hours < 24) return `${hours}h ago`; if (hours < 24) {return `${hours}h ago`;}
if (days < 7) return `${days}d ago`; if (days < 7) {return `${days}d ago`;}
return new Date(timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' }); return new Date(timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' });
} }
// Individual message component // Individual message component
const MessageItem = memo(function MessageItem({ const MessageItem = memo(function MessageItem({
message, message,
index index,
}: { }: {
message: Message; message: Message;
index: number; index: number;
+1 -1
View File
@@ -31,7 +31,7 @@ export async function startFullscreenTui(config: FullscreenTuiConfig): Promise<v
model: config.model, model: config.model,
agent: config.agent, agent: config.agent,
onExit: config.onExit, onExit: config.onExit,
}) }),
); );
await waitUntilExit(); await waitUntilExit();
+7 -7
View File
@@ -91,10 +91,10 @@ const terminalRenderer: RendererObject = {
table({ header, rows, align }: Tokens.Table): string { table({ header, rows, align }: Tokens.Table): string {
// Render cell contents // Render cell contents
const headerTexts = header.map((cell: Tokens.TableCell) => const headerTexts = header.map((cell: Tokens.TableCell) =>
this.parser.parseInline(cell.tokens) this.parser.parseInline(cell.tokens),
); );
const rowTexts = rows.map((row: Tokens.TableCell[]) => const rowTexts = rows.map((row: Tokens.TableCell[]) =>
row.map((cell: Tokens.TableCell) => this.parser.parseInline(cell.tokens)) row.map((cell: Tokens.TableCell) => this.parser.parseInline(cell.tokens)),
); );
// Calculate column widths (strip ANSI for measurement) // Calculate column widths (strip ANSI for measurement)
@@ -108,8 +108,8 @@ const terminalRenderer: RendererObject = {
const pad = (text: string, width: number, alignment: string | null) => { const pad = (text: string, width: number, alignment: string | null) => {
const visible = stripAnsi(text).length; const visible = stripAnsi(text).length;
const diff = width - visible; const diff = width - visible;
if (diff <= 0) return text; if (diff <= 0) {return text;}
if (alignment === 'right') return ' '.repeat(diff) + text; if (alignment === 'right') {return ' '.repeat(diff) + text;}
if (alignment === 'center') { if (alignment === 'center') {
const left = Math.floor(diff / 2); const left = Math.floor(diff / 2);
return ' '.repeat(left) + text + ' '.repeat(diff - left); return ' '.repeat(left) + text + ' '.repeat(diff - left);
@@ -119,7 +119,7 @@ const terminalRenderer: RendererObject = {
// Build header row // Build header row
const headerRow = ' ' + headerTexts.map((h: string, i: number) => const headerRow = ' ' + headerTexts.map((h: string, i: number) =>
`\x1b[1m${pad(h, colWidths[i], align[i])}\x1b[0m` `\x1b[1m${pad(h, colWidths[i], align[i])}\x1b[0m`,
).join(' │ '); ).join(' │ ');
// Build separator // Build separator
@@ -128,8 +128,8 @@ const terminalRenderer: RendererObject = {
// Build data rows // Build data rows
const dataRows = rowTexts.map((row: string[]) => const dataRows = rowTexts.map((row: string[]) =>
' ' + row.map((cell: string, i: number) => ' ' + row.map((cell: string, i: number) =>
pad(cell, colWidths[i], align[i]) pad(cell, colWidths[i], align[i]),
).join(' │ ') ).join(' │ '),
); );
return [headerRow, separator, ...dataRows].join('\n') + '\n'; return [headerRow, separator, ...dataRows].join('\n') + '\n';
+1 -1
View File
@@ -467,7 +467,7 @@ export class MinimalTui {
fullContent += event.content; fullContent += event.content;
} }
if (event.type === 'fallback_warning' && event.fallbackReason) { if (event.type === 'fallback_warning' && event.fallbackReason) {
console.warn(`\n⚠ Using fallback model`); console.warn('\n⚠ Using fallback model');
} }
if (event.type === 'done' && event.usage) { if (event.type === 'done' && event.usage) {
this.totalUsage.inputTokens += event.usage.inputTokens; this.totalUsage.inputTokens += event.usage.inputTokens;
+1 -1
View File
@@ -80,7 +80,7 @@ function extractQueryToken(req: IncomingMessage): string | undefined {
} }
function extractTailscaleIdentity(req: IncomingMessage, config: AuthConfig): string | undefined { function extractTailscaleIdentity(req: IncomingMessage, config: AuthConfig): string | undefined {
if (!config.tailscaleIdentity) return undefined; if (!config.tailscaleIdentity) {return undefined;}
const header = req.headers['tailscale-user-login']; const header = req.headers['tailscale-user-login'];
if (typeof header === 'string' && header.length > 0) { if (typeof header === 'string' && header.length > 0) {
return header; return header;
+6 -6
View File
@@ -18,9 +18,9 @@ export function redactConfig(config: Config): Record<string, unknown> {
// Helper: redact specified keys on an object if they exist and are non-nullish // Helper: redact specified keys on an object if they exist and are non-nullish
const redact = (obj: Record<string, unknown> | undefined, ...keys: string[]) => { const redact = (obj: Record<string, unknown> | undefined, ...keys: string[]) => {
if (!obj) return; if (!obj) {return;}
for (const key of keys) { for (const key of keys) {
if (obj[key] !== undefined && obj[key] !== null) obj[key] = '***'; if (obj[key] !== undefined && obj[key] !== null) {obj[key] = '***';}
} }
}; };
@@ -102,22 +102,22 @@ export function redactConfig(config: Config): Record<string, unknown> {
/** Keys that are safe to update at runtime via config.patch. */ /** Keys that are safe to update at runtime via config.patch. */
const PATCHABLE_KEYS: Record<string, (config: Config, value: unknown) => boolean> = { const PATCHABLE_KEYS: Record<string, (config: Config, value: unknown) => boolean> = {
'hooks.confirm': (config, value) => { 'hooks.confirm': (config, value) => {
if (!Array.isArray(value) || !value.every((v) => typeof v === 'string')) return false; if (!Array.isArray(value) || !value.every((v) => typeof v === 'string')) {return false;}
config.hooks.confirm = value as string[]; config.hooks.confirm = value as string[];
return true; return true;
}, },
'hooks.log': (config, value) => { 'hooks.log': (config, value) => {
if (!Array.isArray(value) || !value.every((v) => typeof v === 'string')) return false; if (!Array.isArray(value) || !value.every((v) => typeof v === 'string')) {return false;}
config.hooks.log = value as string[]; config.hooks.log = value as string[];
return true; return true;
}, },
'hooks.silent': (config, value) => { 'hooks.silent': (config, value) => {
if (!Array.isArray(value) || !value.every((v) => typeof v === 'string')) return false; if (!Array.isArray(value) || !value.every((v) => typeof v === 'string')) {return false;}
config.hooks.silent = value as string[]; config.hooks.silent = value as string[];
return true; return true;
}, },
'server.localhost': (config, value) => { 'server.localhost': (config, value) => {
if (typeof value !== 'boolean') return false; if (typeof value !== 'boolean') {return false;}
config.server.localhost = value; config.server.localhost = value;
return true; return true;
}, },
+1 -1
View File
@@ -16,7 +16,7 @@ export function createSessionHandlers(deps: SessionHandlerDeps) {
id, id,
messageCount: deps.sessionManager.getSession( messageCount: deps.sessionManager.getSession(
id.split(':')[0], id.split(':')[0],
id.split(':').slice(1).join(':') id.split(':').slice(1).join(':'),
).getHistory().length, ).getHistory().length,
})); }));
return makeResponse(request.id, { sessions }); return makeResponse(request.id, { sessions });
+2 -2
View File
@@ -83,7 +83,7 @@ export class LaneQueue {
*/ */
cancel(laneId: string): void { cancel(laneId: string): void {
const lane = this.lanes.get(laneId); const lane = this.lanes.get(laneId);
if (!lane) return; if (!lane) {return;}
const pending = lane.queue.splice(0); const pending = lane.queue.splice(0);
for (const entry of pending) { for (const entry of pending) {
@@ -102,7 +102,7 @@ export class LaneQueue {
*/ */
private processNext(laneId: string): void { private processNext(laneId: string): void {
const lane = this.lanes.get(laneId); const lane = this.lanes.get(laneId);
if (!lane) return; if (!lane) {return;}
const entry = lane.queue.shift(); const entry = lane.queue.shift();
if (!entry) { if (!entry) {
+2 -2
View File
@@ -109,7 +109,7 @@ export type OutboundMessage = GatewayResponse | GatewayError | GatewayEvent;
// ── Validation helpers ───────────────────────────────────────── // ── Validation helpers ─────────────────────────────────────────
export function isValidRequest(msg: unknown): msg is GatewayRequest { export function isValidRequest(msg: unknown): msg is GatewayRequest {
if (typeof msg !== 'object' || msg === null) return false; if (typeof msg !== 'object' || msg === null) {return false;}
const obj = msg as Record<string, unknown>; const obj = msg as Record<string, unknown>;
return ( return (
typeof obj.id === 'number' && typeof obj.id === 'number' &&
@@ -121,7 +121,7 @@ export function isValidRequest(msg: unknown): msg is GatewayRequest {
export function parseMessage(raw: string): GatewayRequest | null { export function parseMessage(raw: string): GatewayRequest | null {
try { try {
const parsed = JSON.parse(raw); const parsed = JSON.parse(raw);
if (isValidRequest(parsed)) return parsed; if (isValidRequest(parsed)) {return parsed;}
return null; return null;
} catch { } catch {
return null; return null;
+2 -2
View File
@@ -327,7 +327,7 @@ export class GatewayServer {
if (uiDir) { if (uiDir) {
const served = await serveStatic(req, res, uiDir); const served = await serveStatic(req, res, uiDir);
if (served) return; if (served) {return;}
} }
// No UI directory configured, or file not found // No UI directory configured, or file not found
@@ -344,7 +344,7 @@ export class GatewayServer {
} }
// Inject connectionId into params so handlers can identify the client // Inject connectionId into params so handlers can identify the client
if (!request.params) request.params = {}; if (!request.params) {request.params = {};}
request.params.connectionId = connectionId; request.params.connectionId = connectionId;
const send = (msg: OutboundMessage) => this.send(ws, msg); const send = (msg: OutboundMessage) => this.send(ws, msg);
+5 -5
View File
@@ -83,8 +83,8 @@ export class SessionBridge {
/** Switch a connection to a different session (e.g. resuming an old session). */ /** Switch a connection to a different session (e.g. resuming an old session). */
switchSession(connectionId: string, sessionId: string): void { switchSession(connectionId: string, sessionId: string): void {
const client = this.clients.get(connectionId); const client = this.clients.get(connectionId);
if (!client) throw new Error(`Unknown connection: ${connectionId}`); if (!client) {throw new Error(`Unknown connection: ${connectionId}`);}
if (client.busy) throw new Error('Cannot switch session while agent is busy'); if (client.busy) {throw new Error('Cannot switch session while agent is busy');}
const agent = this.getOrCreateAgent(sessionId); const agent = this.getOrCreateAgent(sessionId);
client.sessionId = sessionId; client.sessionId = sessionId;
@@ -109,13 +109,13 @@ export class SessionBridge {
/** Mark a connection's agent as busy/idle. */ /** Mark a connection's agent as busy/idle. */
setBusy(connectionId: string, busy: boolean): void { setBusy(connectionId: string, busy: boolean): void {
const client = this.clients.get(connectionId); const client = this.clients.get(connectionId);
if (client) client.busy = busy; if (client) {client.busy = busy;}
} }
/** Set onToolUse callback for a connection's agent. */ /** Set onToolUse callback for a connection's agent. */
setOnToolUse(connectionId: string, callback: ((event: ToolUseEvent) => void) | undefined): void { setOnToolUse(connectionId: string, callback: ((event: ToolUseEvent) => void) | undefined): void {
const client = this.clients.get(connectionId); const client = this.clients.get(connectionId);
if (client) client.agent.setOnToolUse(callback); if (client) {client.agent.setOnToolUse(callback);}
} }
/** List all active sessions with connection counts. */ /** List all active sessions with connection counts. */
@@ -159,7 +159,7 @@ export class SessionBridge {
const seen = new Set<string>(); const seen = new Set<string>();
for (const client of this.clients.values()) { for (const client of this.clients.values()) {
if (seen.has(client.sessionId)) continue; if (seen.has(client.sessionId)) {continue;}
seen.add(client.sessionId); seen.add(client.sessionId);
const usage = client.agent.getUsage(); const usage = client.agent.getUsage();
+2 -2
View File
@@ -65,12 +65,12 @@ export function initStatusIndicator() {
client.onStatusChange((status) => { client.onStatusChange((status) => {
statusEl.textContent = status === 'connected' ? 'Connected' : statusEl.textContent = status === 'connected' ? 'Connected' :
status === 'connecting' ? 'Connecting...' : 'Disconnected'; status === 'connecting' ? 'Connecting...' : 'Disconnected';
statusEl.className = `conn-status ${status}`; statusEl.className = `conn-status ${status}`;
}); });
// Set initial status // Set initial status
statusEl.textContent = client.status === 'connected' ? 'Connected' : statusEl.textContent = client.status === 'connected' ? 'Connected' :
client.status === 'connecting' ? 'Connecting...' : 'Disconnected'; client.status === 'connecting' ? 'Connecting...' : 'Disconnected';
statusEl.className = `conn-status ${client.status}`; statusEl.className = `conn-status ${client.status}`;
} }
+3 -3
View File
@@ -165,18 +165,18 @@ export class FlynnClient {
const handle = { const handle = {
on(event, callback) { on(event, callback) {
if (!events.has(event)) events.set(event, []); if (!events.has(event)) {events.set(event, []);}
events.get(event).push(callback); events.get(event).push(callback);
return handle; return handle;
}, },
result: new Promise((resolve, reject) => { result: new Promise((resolve, reject) => {
// Auto-wire done/error to resolve/reject the promise // Auto-wire done/error to resolve/reject the promise
if (!events.has('done')) events.set('done', []); if (!events.has('done')) {events.set('done', []);}
events.get('done').push((data) => { events.get('done').push((data) => {
this._listeners.delete(id); this._listeners.delete(id);
resolve(data); resolve(data);
}); });
if (!events.has('error')) events.set('error', []); if (!events.has('error')) {events.set('error', []);}
events.get('error').push((data) => { events.get('error').push((data) => {
this._listeners.delete(id); this._listeners.delete(id);
reject(new Error(data.message || 'Agent error')); reject(new Error(data.message || 'Agent error'));
+22 -22
View File
@@ -133,7 +133,7 @@ function createMessageActions(role) {
copyBtn.innerHTML = COPY_ICON; copyBtn.innerHTML = COPY_ICON;
copyBtn.addEventListener('click', () => { copyBtn.addEventListener('click', () => {
const msg = bar.closest('.message'); const msg = bar.closest('.message');
if (!msg) return; if (!msg) {return;}
const text = getMessageText(msg); const text = getMessageText(msg);
navigator.clipboard.writeText(text).then(() => { navigator.clipboard.writeText(text).then(() => {
copyBtn.innerHTML = CHECK_ICON; copyBtn.innerHTML = CHECK_ICON;
@@ -154,7 +154,7 @@ function createMessageActions(role) {
editBtn.innerHTML = EDIT_ICON; editBtn.innerHTML = EDIT_ICON;
editBtn.addEventListener('click', () => { editBtn.addEventListener('click', () => {
const msg = bar.closest('.message'); const msg = bar.closest('.message');
if (!msg) return; if (!msg) {return;}
const text = getMessageText(msg); const text = getMessageText(msg);
const input = _elements.input; const input = _elements.input;
if (input) { if (input) {
@@ -177,7 +177,7 @@ function setSearchMode(active) {
_searchMode = active; _searchMode = active;
const btn = _elements.searchBtn; const btn = _elements.searchBtn;
const input = _elements.input; const input = _elements.input;
if (!btn || !input) return; if (!btn || !input) {return;}
if (active) { if (active) {
btn.classList.add('active'); btn.classList.add('active');
@@ -198,7 +198,7 @@ function getFilteredCommands(text) {
function showSlashPopup(filtered) { function showSlashPopup(filtered) {
const popup = _elements.slashPopup; const popup = _elements.slashPopup;
if (!popup) return; if (!popup) {return;}
popup.innerHTML = ''; popup.innerHTML = '';
if (filtered.length === 0) { if (filtered.length === 0) {
@@ -225,13 +225,13 @@ function showSlashPopup(filtered) {
function hideSlashPopup() { function hideSlashPopup() {
const popup = _elements.slashPopup; const popup = _elements.slashPopup;
if (popup) popup.classList.add('hidden'); if (popup) {popup.classList.add('hidden');}
_slashPopupIndex = -1; _slashPopupIndex = -1;
} }
function updatePopupSelection(filtered) { function updatePopupSelection(filtered) {
const popup = _elements.slashPopup; const popup = _elements.slashPopup;
if (!popup) return; if (!popup) {return;}
const items = popup.querySelectorAll('.slash-popup-item'); const items = popup.querySelectorAll('.slash-popup-item');
items.forEach((el, i) => { items.forEach((el, i) => {
el.classList.toggle('selected', i === _slashPopupIndex); el.classList.toggle('selected', i === _slashPopupIndex);
@@ -240,7 +240,7 @@ function updatePopupSelection(filtered) {
function selectSlashCommand(name) { function selectSlashCommand(name) {
const input = _elements.input; const input = _elements.input;
if (!input) return; if (!input) {return;}
input.value = name; input.value = name;
hideSlashPopup(); hideSlashPopup();
input.focus(); input.focus();
@@ -248,14 +248,14 @@ function selectSlashCommand(name) {
function handleSlashPopupInput() { function handleSlashPopupInput() {
const input = _elements.input; const input = _elements.input;
if (!input) return; if (!input) {return;}
const text = input.value; const text = input.value;
// Show popup only when text starts with / and is at most a single word (the command itself) // Show popup only when text starts with / and is at most a single word (the command itself)
if (text.startsWith('/') && !text.includes(' ')) { if (text.startsWith('/') && !text.includes(' ')) {
const filtered = getFilteredCommands(text); const filtered = getFilteredCommands(text);
// Clamp selection index // Clamp selection index
if (_slashPopupIndex >= filtered.length) _slashPopupIndex = filtered.length - 1; if (_slashPopupIndex >= filtered.length) {_slashPopupIndex = filtered.length - 1;}
showSlashPopup(filtered); showSlashPopup(filtered);
} else { } else {
hideSlashPopup(); hideSlashPopup();
@@ -266,7 +266,7 @@ function handleSlashPopupInput() {
function parseSlashCommand(text) { function parseSlashCommand(text) {
const trimmed = text.trim(); const trimmed = text.trim();
if (!trimmed.startsWith('/')) return null; if (!trimmed.startsWith('/')) {return null;}
const parts = trimmed.split(/\s+/); const parts = trimmed.split(/\s+/);
const cmd = parts[0].toLowerCase(); const cmd = parts[0].toLowerCase();
@@ -405,7 +405,7 @@ async function handleSlashCommand(cmd, client) {
async function loadSessions(client) { async function loadSessions(client) {
const select = _elements.sessionSelect; const select = _elements.sessionSelect;
if (!select) return; if (!select) {return;}
try { try {
const result = await client.call('sessions.list'); const result = await client.call('sessions.list');
@@ -425,7 +425,7 @@ async function loadSessions(client) {
const opt = document.createElement('option'); const opt = document.createElement('option');
opt.value = s.id; opt.value = s.id;
opt.textContent = `${s.id} (${s.messageCount} msgs)`; opt.textContent = `${s.id} (${s.messageCount} msgs)`;
if (s.id === current) opt.selected = true; if (s.id === current) {opt.selected = true;}
select.appendChild(opt); select.appendChild(opt);
} }
} }
@@ -439,7 +439,7 @@ async function loadSessions(client) {
async function loadHistory(client) { async function loadHistory(client) {
const msgs = _elements.messages; const msgs = _elements.messages;
if (!msgs || !_currentSession) return; if (!msgs || !_currentSession) {return;}
msgs.innerHTML = ''; msgs.innerHTML = '';
@@ -464,21 +464,21 @@ async function loadHistory(client) {
async function sendMessage(client, overrideText) { async function sendMessage(client, overrideText) {
const input = _elements.input; const input = _elements.input;
const rawText = overrideText ?? input?.value?.trim(); const rawText = overrideText ?? input?.value?.trim();
if (!rawText || _sending) return; if (!rawText || _sending) {return;}
// Check for slash commands first // Check for slash commands first
const cmd = parseSlashCommand(rawText); const cmd = parseSlashCommand(rawText);
if (cmd) { if (cmd) {
if (!overrideText) input.value = ''; if (!overrideText) {input.value = '';}
hideSlashPopup(); hideSlashPopup();
const handled = await handleSlashCommand(cmd, client); const handled = await handleSlashCommand(cmd, client);
if (handled) return; if (handled) {return;}
// If not fully handled (e.g. /compact), fall through to send as message // If not fully handled (e.g. /compact), fall through to send as message
} }
_sending = true; _sending = true;
_elements.sendBtn.disabled = true; _elements.sendBtn.disabled = true;
if (!overrideText) input.value = ''; if (!overrideText) {input.value = '';}
// Apply search mode prefix // Apply search mode prefix
let messageText = rawText; let messageText = rawText;
@@ -541,17 +541,17 @@ async function sendMessage(client, overrideText) {
placeholder.textContent = `Error: ${err.message}`; placeholder.textContent = `Error: ${err.message}`;
} finally { } finally {
_sending = false; _sending = false;
if (_elements.sendBtn) _elements.sendBtn.disabled = false; if (_elements.sendBtn) {_elements.sendBtn.disabled = false;}
scrollToBottom(); scrollToBottom();
} }
} }
// ── Search SVG Icon ───────────────────────────────────────── // ── Search SVG Icon ─────────────────────────────────────────
const SEARCH_ICON = `<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M8.5 3a5.5 5.5 0 0 1 4.38 8.82l4.15 4.15a.75.75 0 0 1-1.06 1.06l-4.15-4.15A5.5 5.5 0 1 1 8.5 3zm0 1.5a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" fill="currentColor"/></svg>`; const SEARCH_ICON = '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M8.5 3a5.5 5.5 0 0 1 4.38 8.82l4.15 4.15a.75.75 0 0 1-1.06 1.06l-4.15-4.15A5.5 5.5 0 1 1 8.5 3zm0 1.5a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" fill="currentColor"/></svg>';
const COPY_ICON = `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z" fill="currentColor"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" fill="currentColor"/></svg>`; const COPY_ICON = '<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z" fill="currentColor"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" fill="currentColor"/></svg>';
const CHECK_ICON = `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" fill="currentColor"/></svg>`; const CHECK_ICON = '<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" fill="currentColor"/></svg>';
const EDIT_ICON = `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm1.414 1.06a.25.25 0 0 0-.354 0L3.463 11.1l-.47 1.64 1.64-.47 8.61-8.61a.25.25 0 0 0 0-.354Z" fill="currentColor"/></svg>`; const EDIT_ICON = '<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm1.414 1.06a.25.25 0 0 0-.354 0L3.463 11.1l-.47 1.64 1.64-.47 8.61-8.61a.25.25 0 0 0 0-.354Z" fill="currentColor"/></svg>';
// ── Page Export ────────────────────────────────────────────── // ── Page Export ──────────────────────────────────────────────
+12 -12
View File
@@ -14,17 +14,17 @@ function formatUptime(seconds) {
const m = Math.floor((seconds % 3600) / 60); const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60; const s = seconds % 60;
const parts = []; const parts = [];
if (d > 0) parts.push(`${d}d`); if (d > 0) {parts.push(`${d}d`);}
if (h > 0) parts.push(`${h}h`); if (h > 0) {parts.push(`${h}h`);}
if (m > 0) parts.push(`${m}m`); if (m > 0) {parts.push(`${m}m`);}
parts.push(`${s}s`); parts.push(`${s}s`);
return parts.join(' '); return parts.join(' ');
} }
function timeAgo(timestamp) { function timeAgo(timestamp) {
const secs = Math.floor((Date.now() - timestamp) / 1000); const secs = Math.floor((Date.now() - timestamp) / 1000);
if (secs < 60) return `${secs}s ago`; if (secs < 60) {return `${secs}s ago`;}
if (secs < 3600) return `${Math.floor(secs / 60)}m ago`; if (secs < 3600) {return `${Math.floor(secs / 60)}m ago`;}
return `${Math.floor(secs / 3600)}h ago`; return `${Math.floor(secs / 3600)}h ago`;
} }
@@ -76,7 +76,7 @@ function renderSkeleton(el) {
function updateCounters(metrics, health) { function updateCounters(metrics, health) {
const el = document.getElementById('ops-counters'); const el = document.getElementById('ops-counters');
if (!el) return; if (!el) {return;}
const sessions = health?.sessions ?? 0; const sessions = health?.sessions ?? 0;
const errCount = metrics?.errors ?? 0; const errCount = metrics?.errors ?? 0;
@@ -94,13 +94,13 @@ function updateCounters(metrics, health) {
`<div class="stat-card"> `<div class="stat-card">
<div class="stat-label">${c.label}</div> <div class="stat-label">${c.label}</div>
<div class="stat-value ${c.cls}">${c.value}</div> <div class="stat-value ${c.cls}">${c.value}</div>
</div>` </div>`,
).join(''); ).join('');
} }
function updateModelTable(metrics) { function updateModelTable(metrics) {
const el = document.getElementById('ops-model-table'); const el = document.getElementById('ops-model-table');
if (!el) return; if (!el) {return;}
const mc = metrics?.modelCalls; const mc = metrics?.modelCalls;
const calls = mc?.recentCalls ?? []; const calls = mc?.recentCalls ?? [];
@@ -155,7 +155,7 @@ function updateModelTable(metrics) {
function updateEvents(eventsData) { function updateEvents(eventsData) {
const el = document.getElementById('ops-events'); const el = document.getElementById('ops-events');
if (!el) return; if (!el) {return;}
const events = eventsData?.events ?? []; const events = eventsData?.events ?? [];
@@ -180,7 +180,7 @@ function updateEvents(eventsData) {
function updateActiveRequests(requestsData) { function updateActiveRequests(requestsData) {
const el = document.getElementById('ops-requests'); const el = document.getElementById('ops-requests');
if (!el) return; if (!el) {return;}
const requests = requestsData?.requests ?? []; const requests = requestsData?.requests ?? [];
@@ -219,7 +219,7 @@ function updateActiveRequests(requestsData) {
function updateChannels(channelsData) { function updateChannels(channelsData) {
const el = document.getElementById('ops-channels'); const el = document.getElementById('ops-channels');
if (!el) return; if (!el) {return;}
const channels = channelsData?.channels ?? []; const channels = channelsData?.channels ?? [];
@@ -232,7 +232,7 @@ function updateChannels(channelsData) {
`<div class="channel-card"> `<div class="channel-card">
<span class="channel-dot ${ch.status}"></span> <span class="channel-dot ${ch.status}"></span>
<span class="channel-name">${escapeHtml(ch.name)}</span> <span class="channel-name">${escapeHtml(ch.name)}</span>
</div>` </div>`,
).join(''); ).join('');
} }
+3 -3
View File
@@ -14,11 +14,11 @@ let _client = null;
let _el = null; let _el = null;
async function loadSessionList() { async function loadSessionList() {
if (!_client || !_el) return; if (!_client || !_el) {return;}
const listContainer = _el.querySelector('#sessions-list'); const listContainer = _el.querySelector('#sessions-list');
const detailContainer = _el.querySelector('#session-detail'); const detailContainer = _el.querySelector('#session-detail');
if (detailContainer) detailContainer.innerHTML = ''; if (detailContainer) {detailContainer.innerHTML = '';}
try { try {
const result = await _client.call('sessions.list'); const result = await _client.call('sessions.list');
@@ -78,7 +78,7 @@ async function loadSessionList() {
async function viewSession(sessionId) { async function viewSession(sessionId) {
const detailContainer = _el.querySelector('#session-detail'); const detailContainer = _el.querySelector('#session-detail');
if (!detailContainer) return; if (!detailContainer) {return;}
detailContainer.innerHTML = '<div class="empty-state"><span class="spinner"></span> Loading...</div>'; detailContainer.innerHTML = '<div class="empty-state"><span class="spinner"></span> Loading...</div>';
+2 -2
View File
@@ -15,7 +15,7 @@ let _client = null;
let _el = null; let _el = null;
async function loadSettings() { async function loadSettings() {
if (!_client || !_el) return; if (!_client || !_el) {return;}
let config, tools, channels; let config, tools, channels;
@@ -154,7 +154,7 @@ async function saveHooks() {
// Clear status after 5s // Clear status after 5s
setTimeout(() => { setTimeout(() => {
if (status) status.textContent = ''; if (status) {status.textContent = '';}
}, 5000); }, 5000);
} }
+5 -5
View File
@@ -13,14 +13,14 @@ function formatNumber(n) {
} }
function formatCost(n) { function formatCost(n) {
if (!n || n === 0) return '$0.00'; if (!n || n === 0) {return '$0.00';}
if (n < 0.01) return `$${n.toFixed(4)}`; if (n < 0.01) {return `$${n.toFixed(4)}`;}
return `$${n.toFixed(2)}`; return `$${n.toFixed(2)}`;
} }
function truncateId(id) { function truncateId(id) {
if (!id) return '-'; if (!id) {return '-';}
if (id.length <= 24) return id; if (id.length <= 24) {return id;}
return id.slice(0, 24) + '\u2026'; return id.slice(0, 24) + '\u2026';
} }
@@ -95,7 +95,7 @@ async function loadUsage(el, client) {
let delegationCell = '<span class="text-muted">-</span>'; let delegationCell = '<span class="text-muted">-</span>';
if (delegationEntries.length > 0) { if (delegationEntries.length > 0) {
delegationCell = delegationEntries.map(([tier, stats]) => delegationCell = delegationEntries.map(([tier, stats]) =>
`<span class="badge ok">${tier}</span> ${formatNumber(stats.inputTokens)}/${formatNumber(stats.outputTokens)}` `<span class="badge ok">${tier}</span> ${formatNumber(stats.inputTokens)}/${formatNumber(stats.outputTokens)}`,
).join('<br>'); ).join('<br>');
} }
+4 -4
View File
@@ -30,15 +30,15 @@ function shouldLog(level: LogLevel): boolean {
export const logger = { export const logger = {
debug(...args: unknown[]): void { debug(...args: unknown[]): void {
if (shouldLog('debug')) console.debug(...args); if (shouldLog('debug')) {console.debug(...args);}
}, },
info(...args: unknown[]): void { info(...args: unknown[]): void {
if (shouldLog('info')) console.log(...args); if (shouldLog('info')) {console.log(...args);}
}, },
warn(...args: unknown[]): void { warn(...args: unknown[]): void {
if (shouldLog('warn')) console.warn(...args); if (shouldLog('warn')) {console.warn(...args);}
}, },
error(...args: unknown[]): void { error(...args: unknown[]): void {
if (shouldLog('error')) console.error(...args); if (shouldLog('error')) {console.error(...args);}
}, },
}; };
+1 -1
View File
@@ -25,7 +25,7 @@ export function mcpToolName(serverName: string, toolName: string): string {
*/ */
export function parseMcpToolName(prefixedName: string): { serverName: string; toolName: string } | null { export function parseMcpToolName(prefixedName: string): { serverName: string; toolName: string } | null {
const match = prefixedName.match(/^mcp:([^:]+):(.+)$/); const match = prefixedName.match(/^mcp:([^:]+):(.+)$/);
if (!match) return null; if (!match) {return null;}
return { serverName: match[1], toolName: match[2] }; return { serverName: match[1], toolName: match[2] };
} }
+3 -3
View File
@@ -86,7 +86,7 @@ export class McpManager {
*/ */
async stopServer(name: string): Promise<void> { async stopServer(name: string): Promise<void> {
const client = this.clients.get(name); const client = this.clients.get(name);
if (!client) return; if (!client) {return;}
// Unregister tools from the registry // Unregister tools from the registry
const toolNames = this.registeredToolNames.get(name) ?? []; const toolNames = this.registeredToolNames.get(name) ?? [];
@@ -133,7 +133,7 @@ export class McpManager {
*/ */
getServerState(name: string): McpServerState | undefined { getServerState(name: string): McpServerState | undefined {
const client = this.clients.get(name); const client = this.clients.get(name);
if (!client) return undefined; if (!client) {return undefined;}
const config = this.storedConfigs.get(name) ?? { name, command: '', args: [] }; const config = this.storedConfigs.get(name) ?? { name, command: '', args: [] };
@@ -165,7 +165,7 @@ export class McpManager {
for (const toolNames of this.registeredToolNames.values()) { for (const toolNames of this.registeredToolNames.values()) {
for (const name of toolNames) { for (const name of toolNames) {
const tool = this.toolRegistry.get(name); const tool = this.toolRegistry.get(name);
if (tool) tools.push(tool); if (tool) {tools.push(tool);}
} }
} }
return tools; return tools;
+2 -2
View File
@@ -52,7 +52,7 @@ export function cosineSimilarity(a: number[] | Float32Array, b: number[] | Float
} }
const magnitude = Math.sqrt(normA) * Math.sqrt(normB); const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
if (magnitude === 0) return 0; if (magnitude === 0) {return 0;}
return dotProduct / magnitude; return dotProduct / magnitude;
} }
@@ -129,7 +129,7 @@ export class VectorStore {
throw new Error(`Chunks/embeddings length mismatch: ${chunks.length} vs ${embeddings.length}`); throw new Error(`Chunks/embeddings length mismatch: ${chunks.length} vs ${embeddings.length}`);
} }
if (chunks.length === 0) return; if (chunks.length === 0) {return;}
const namespace = chunks[0].namespace; const namespace = chunks[0].namespace;
+4 -4
View File
@@ -89,10 +89,10 @@ export class BedrockClient implements ModelClient {
// Map stop reason // Map stop reason
let stopReason: string = 'end_turn'; let stopReason: string = 'end_turn';
if (response.stopReason === 'max_tokens') stopReason = 'max_tokens'; if (response.stopReason === 'max_tokens') {stopReason = 'max_tokens';}
else if (response.stopReason === 'tool_use') stopReason = 'tool_use'; else if (response.stopReason === 'tool_use') {stopReason = 'tool_use';}
else if (response.stopReason === 'end_turn') stopReason = 'end_turn'; else if (response.stopReason === 'end_turn') {stopReason = 'end_turn';}
else if (response.stopReason) stopReason = response.stopReason; else if (response.stopReason) {stopReason = response.stopReason;}
return { return {
content, content,
+2 -2
View File
@@ -27,12 +27,12 @@ function makeResponse(parts: unknown[], finishReason = 'STOP', usage = { promptT
usageMetadata: usage, usageMetadata: usage,
text: () => { text: () => {
const textParts = parts.filter((p: unknown) => typeof p === 'object' && p !== null && 'text' in p); const textParts = parts.filter((p: unknown) => typeof p === 'object' && p !== null && 'text' in p);
if (textParts.length === 0) throw new Error('No text parts'); if (textParts.length === 0) {throw new Error('No text parts');}
return textParts.map((p: unknown) => (p as { text: string }).text).join(''); return textParts.map((p: unknown) => (p as { text: string }).text).join('');
}, },
functionCalls: () => { functionCalls: () => {
const fcParts = parts.filter((p: unknown) => typeof p === 'object' && p !== null && 'functionCall' in p); const fcParts = parts.filter((p: unknown) => typeof p === 'object' && p !== null && 'functionCall' in p);
if (fcParts.length === 0) return undefined; if (fcParts.length === 0) {return undefined;}
return fcParts.map((p: unknown) => (p as { functionCall: { name: string; args: object } }).functionCall); return fcParts.map((p: unknown) => (p as { functionCall: { name: string; args: object } }).functionCall);
}, },
}, },
+2 -2
View File
@@ -72,7 +72,7 @@ export class GitHubModelsClient implements ModelClient {
* callback is provided, invoke it to obtain a token (e.g. via OAuth device flow). * callback is provided, invoke it to obtain a token (e.g. via OAuth device flow).
*/ */
private async ensureToken(): Promise<void> { private async ensureToken(): Promise<void> {
if (this.tokenResolved) return; if (this.tokenResolved) {return;}
// Try resolving again (user might have logged in via /login since construction) // Try resolving again (user might have logged in via /login since construction)
const token = getGitHubToken(); const token = getGitHubToken();
@@ -85,7 +85,7 @@ export class GitHubModelsClient implements ModelClient {
if (this.onLoginRequired) { if (this.onLoginRequired) {
const newToken = await this.onLoginRequired(); const newToken = await this.onLoginRequired();
this.rebuildClient(newToken); this.rebuildClient(newToken);
return;
} }
// No token and no callback — the API call will fail with an auth error // No token and no callback — the API call will fail with an auth error
+6 -6
View File
@@ -115,7 +115,7 @@ export function normalizeMessagesForLlamaCpp(
} else if (block.type === 'tool_use') { } else if (block.type === 'tool_use') {
const name = block.name as string; const name = block.name as string;
const id = block.id as string; const id = block.id as string;
if (id) toolNameMap.set(id, name); if (id) {toolNameMap.set(id, name);}
let argsStr: string; let argsStr: string;
try { try {
argsStr = JSON.stringify(block.input); argsStr = JSON.stringify(block.input);
@@ -321,7 +321,7 @@ export class LlamaCppClient implements ModelClient {
while (true) { while (true) {
const { done, value } = await reader.read(); const { done, value } = await reader.read();
if (done) break; if (done) {break;}
buffer += decoder.decode(value, { stream: true }); buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n'); const lines = buffer.split('\n');
@@ -329,10 +329,10 @@ export class LlamaCppClient implements ModelClient {
for (const line of lines) { for (const line of lines) {
const trimmed = line.trim(); const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith('data: ')) continue; if (!trimmed || !trimmed.startsWith('data: ')) {continue;}
const data = trimmed.slice(6); const data = trimmed.slice(6);
if (data === '[DONE]') continue; if (data === '[DONE]') {continue;}
try { try {
const chunk = JSON.parse(data) as LlamaCppStreamChunk; const chunk = JSON.parse(data) as LlamaCppStreamChunk;
@@ -352,8 +352,8 @@ export class LlamaCppClient implements ModelClient {
}); });
} }
const acc = toolCallAccumulators.get(tc.index)!; const acc = toolCallAccumulators.get(tc.index)!;
if (tc.function?.name) acc.name = tc.function.name; if (tc.function?.name) {acc.name = tc.function.name;}
if (tc.function?.arguments) acc.arguments += tc.function.arguments; if (tc.function?.arguments) {acc.arguments += tc.function.arguments;}
} }
} }
+2 -2
View File
@@ -55,7 +55,7 @@ export function normalizeMessagesForOllama(
} else if (block.type === 'tool_use') { } else if (block.type === 'tool_use') {
const name = block.name as string; const name = block.name as string;
const id = block.id as string; const id = block.id as string;
if (id) toolNameMap.set(id, name); if (id) {toolNameMap.set(id, name);}
toolCalls.push({ toolCalls.push({
function: { function: {
name, name,
@@ -135,7 +135,7 @@ export class OllamaClient implements ModelClient {
* true (optimistic let the server decide). * true (optimistic let the server decide).
*/ */
private async checkToolSupport(): Promise<boolean> { private async checkToolSupport(): Promise<boolean> {
if (this._supportsTools !== null) return this._supportsTools; if (this._supportsTools !== null) {return this._supportsTools;}
try { try {
const info = await this.client.show({ model: this.model }); const info = await this.client.show({ model: this.model });
const caps: string[] = (info as any).capabilities ?? []; const caps: string[] = (info as any).capabilities ?? [];
+1 -1
View File
@@ -178,7 +178,7 @@ export function normalizeMessagesForLocal(
for (const msg of messages) { for (const msg of messages) {
const text = getMessageTextWithTools(msg); const text = getMessageTextWithTools(msg);
if (!text) continue; // drop empty messages if (!text) {continue;} // drop empty messages
const last = result.length > 0 ? result[result.length - 1] : undefined; const last = result.length > 0 ? result[result.length - 1] : undefined;
if (last && last.role === msg.role) { if (last && last.role === msg.role) {
+2 -2
View File
@@ -134,7 +134,7 @@ describe('withRetry', () => {
let callCount = 0; let callCount = 0;
const fn = vi.fn().mockImplementation(() => { const fn = vi.fn().mockImplementation(() => {
callCount++; callCount++;
if (callCount < 3) return Promise.reject(new Error('fail')); if (callCount < 3) {return Promise.reject(new Error('fail'));}
return Promise.resolve('ok'); return Promise.resolve('ok');
}); });
@@ -166,7 +166,7 @@ describe('withRetry', () => {
const fn = vi.fn().mockImplementation(() => { const fn = vi.fn().mockImplementation(() => {
timestamps.push(Date.now()); timestamps.push(Date.now());
if (timestamps.length < 3) return Promise.reject(new Error('fail')); if (timestamps.length < 3) {return Promise.reject(new Error('fail'));}
return Promise.resolve('ok'); return Promise.resolve('ok');
}); });
+1 -1
View File
@@ -72,7 +72,7 @@ describe('ModelRouter', () => {
const response = await router.chat( const response = await router.chat(
{ messages: [{ role: 'user', content: 'Hi' }] }, { messages: [{ role: 'user', content: 'Hi' }] },
'fast' 'fast',
); );
expect(response.content).toBe('Response from fast'); expect(response.content).toBe('Response from fast');
+8 -8
View File
@@ -41,9 +41,9 @@ export class ModelRouter implements ModelClient {
} }
this.clients.set('default', config.default); this.clients.set('default', config.default);
if (config.fast) this.clients.set('fast', config.fast); if (config.fast) {this.clients.set('fast', config.fast);}
if (config.complex) this.clients.set('complex', config.complex); if (config.complex) {this.clients.set('complex', config.complex);}
if (config.local) this.clients.set('local', config.local); if (config.local) {this.clients.set('local', config.local);}
if (config.labels) { if (config.labels) {
for (const tier of ['fast', 'default', 'complex', 'local'] as ModelTier[]) { for (const tier of ['fast', 'default', 'complex', 'local'] as ModelTier[]) {
@@ -145,7 +145,7 @@ export class ModelRouter implements ModelClient {
yield event; yield event;
} }
if (!hasError) return; if (!hasError) {return;}
} else { } else {
primaryError = 'Primary client does not support streaming'; primaryError = 'Primary client does not support streaming';
} }
@@ -154,7 +154,7 @@ export class ModelRouter implements ModelClient {
const tierFallbackList = this.tierFallbacks.get(useTier) ?? []; const tierFallbackList = this.tierFallbacks.get(useTier) ?? [];
for (let i = 0; i < tierFallbackList.length; i++) { for (let i = 0; i < tierFallbackList.length; i++) {
const fallbackClient = tierFallbackList[i]; const fallbackClient = tierFallbackList[i];
if (!fallbackClient.chatStream) continue; if (!fallbackClient.chatStream) {continue;}
const reason = `Primary model failed (${primaryError}), using tier fallback #${i + 1}`; const reason = `Primary model failed (${primaryError}), using tier fallback #${i + 1}`;
logger.debug(reason); logger.debug(reason);
@@ -170,13 +170,13 @@ export class ModelRouter implements ModelClient {
yield event; yield event;
} }
if (!hasError) return; if (!hasError) {return;}
} }
// Then try global fallback chain // Then try global fallback chain
for (let i = 0; i < this.fallbackChain.length; i++) { for (let i = 0; i < this.fallbackChain.length; i++) {
const fallbackClient = this.fallbackChain[i]; const fallbackClient = this.fallbackChain[i];
if (!fallbackClient.chatStream) continue; if (!fallbackClient.chatStream) {continue;}
const reason = `Primary model failed (${primaryError}), using global fallback #${i + 1}`; const reason = `Primary model failed (${primaryError}), using global fallback #${i + 1}`;
logger.debug(reason); logger.debug(reason);
@@ -192,7 +192,7 @@ export class ModelRouter implements ModelClient {
yield event; yield event;
} }
if (!hasError) return; if (!hasError) {return;}
} }
yield { type: 'error', error: new Error('All streaming providers failed') }; yield { type: 'error', error: new Error('All streaming providers failed') };
+3 -3
View File
@@ -81,7 +81,7 @@ export class DockerSandbox {
/** Force-remove the container. */ /** Force-remove the container. */
async destroy(): Promise<void> { async destroy(): Promise<void> {
if (!this._containerId) return; if (!this._containerId) {return;}
try { try {
await this.dockerCmd(['rm', '-f', this._containerId]); await this.dockerCmd(['rm', '-f', this._containerId]);
@@ -98,8 +98,8 @@ export class DockerSandbox {
execFile('docker', ['version', '--format', '{{.Server.Version}}'], { execFile('docker', ['version', '--format', '{{.Server.Version}}'], {
timeout: 5000, timeout: 5000,
}, (error, stdout) => { }, (error, stdout) => {
if (error) reject(error); if (error) {reject(error);}
else resolve(stdout); else {resolve(stdout);}
}); });
}); });
return true; return true;
+2 -2
View File
@@ -16,7 +16,7 @@ export class SandboxManager {
/** Get or create a sandbox for a session. */ /** Get or create a sandbox for a session. */
async getOrCreate(sessionId: string): Promise<DockerSandbox> { async getOrCreate(sessionId: string): Promise<DockerSandbox> {
let sandbox = this.sandboxes.get(sessionId); let sandbox = this.sandboxes.get(sessionId);
if (sandbox) return sandbox; if (sandbox) {return sandbox;}
sandbox = new DockerSandbox({ sandbox = new DockerSandbox({
sessionId, sessionId,
@@ -36,7 +36,7 @@ export class SandboxManager {
/** Destroy a specific session's sandbox. */ /** Destroy a specific session's sandbox. */
async destroy(sessionId: string): Promise<void> { async destroy(sessionId: string): Promise<void> {
const sandbox = this.sandboxes.get(sessionId); const sandbox = this.sandboxes.get(sessionId);
if (!sandbox) return; if (!sandbox) {return;}
await sandbox.destroy(); await sandbox.destroy();
this.sandboxes.delete(sessionId); this.sandboxes.delete(sessionId);
+2 -2
View File
@@ -13,7 +13,7 @@ export class ManagedSession implements Session {
constructor( constructor(
public readonly id: string, public readonly id: string,
private store: SessionStore, private store: SessionStore,
private history: Message[] = [] private history: Message[] = [],
) {} ) {}
addMessage(message: Message): Message { addMessage(message: Message): Message {
@@ -76,7 +76,7 @@ export class SessionManager {
fromFrontend: string, fromFrontend: string,
fromUserId: string, fromUserId: string,
toFrontend: string, toFrontend: string,
toUserId: string toUserId: string,
): void { ): void {
const fromSession = this.getSession(fromFrontend, fromUserId); const fromSession = this.getSession(fromFrontend, fromUserId);
const toSession = this.getSession(toFrontend, toUserId); const toSession = this.getSession(toFrontend, toUserId);
+8 -8
View File
@@ -4,9 +4,9 @@ import type { PairingStore, ApprovedSender } from '../channels/pairing.js';
/** Parse a duration string like '30d', '7d', '12h' to milliseconds. Returns null if invalid or '0'. */ /** Parse a duration string like '30d', '7d', '12h' to milliseconds. Returns null if invalid or '0'. */
export function parseDuration(s: string): number | null { export function parseDuration(s: string): number | null {
if (s === '0' || s === 'false') return null; if (s === '0' || s === 'false') {return null;}
const match = s.match(/^(\d+)(h|d)$/); const match = s.match(/^(\d+)(h|d)$/);
if (!match) return null; if (!match) {return null;}
const [, n, unit] = match; const [, n, unit] = match;
return unit === 'h' ? Number(n) * 3600_000 : Number(n) * 86_400_000; return unit === 'h' ? Number(n) * 3600_000 : Number(n) * 86_400_000;
} }
@@ -41,14 +41,14 @@ export class SessionStore {
addMessage(sessionId: string, message: Message): void { addMessage(sessionId: string, message: Message): void {
const stmt = this.db.prepare( const stmt = this.db.prepare(
'INSERT INTO messages (session_id, role, content) VALUES (?, ?, ?)' 'INSERT INTO messages (session_id, role, content) VALUES (?, ?, ?)',
); );
stmt.run(sessionId, message.role, message.content); stmt.run(sessionId, message.role, message.content);
} }
getMessages(sessionId: string): Message[] { getMessages(sessionId: string): Message[] {
const stmt = this.db.prepare( const stmt = this.db.prepare(
'SELECT role, content FROM messages WHERE session_id = ? ORDER BY id ASC' 'SELECT role, content FROM messages WHERE session_id = ? ORDER BY id ASC',
); );
const rows = stmt.all(sessionId) as Array<{ role: string; content: string }>; const rows = stmt.all(sessionId) as Array<{ role: string; content: string }>;
return rows.map(row => ({ return rows.map(row => ({
@@ -68,7 +68,7 @@ export class SessionStore {
this.db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId); this.db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId);
// Re-insert in order // Re-insert in order
const insert = this.db.prepare( const insert = this.db.prepare(
'INSERT INTO messages (session_id, role, content) VALUES (?, ?, ?)' 'INSERT INTO messages (session_id, role, content) VALUES (?, ?, ?)',
); );
for (const msg of messages) { for (const msg of messages) {
insert.run(sessionId, msg.role, msg.content); insert.run(sessionId, msg.role, msg.content);
@@ -96,7 +96,7 @@ export class SessionStore {
HAVING MAX(created_at) < ? HAVING MAX(created_at) < ?
`).all(beforeTimestamp) as Array<{ session_id: string }>; `).all(beforeTimestamp) as Array<{ session_id: string }>;
if (stale.length === 0) return []; if (stale.length === 0) {return [];}
const deleteStmt = this.db.prepare('DELETE FROM messages WHERE session_id = ?'); const deleteStmt = this.db.prepare('DELETE FROM messages WHERE session_id = ?');
const transaction = this.db.transaction(() => { const transaction = this.db.transaction(() => {
@@ -113,7 +113,7 @@ export class SessionStore {
return { return {
loadApproved: (): ApprovedSender[] => { loadApproved: (): ApprovedSender[] => {
const rows = this.db.prepare( const rows = this.db.prepare(
'SELECT channel, sender_id, approved_at, code_used FROM pairing_approved' 'SELECT channel, sender_id, approved_at, code_used FROM pairing_approved',
).all() as Array<{ channel: string; sender_id: string; approved_at: number; code_used: string }>; ).all() as Array<{ channel: string; sender_id: string; approved_at: number; code_used: string }>;
return rows.map(r => ({ return rows.map(r => ({
channel: r.channel, channel: r.channel,
@@ -130,7 +130,7 @@ export class SessionStore {
}, },
removeApproved: (channel: string, senderId: string): void => { removeApproved: (channel: string, senderId: string): void => {
this.db.prepare( this.db.prepare(
'DELETE FROM pairing_approved WHERE channel = ? AND sender_id = ?' 'DELETE FROM pairing_approved WHERE channel = ? AND sender_id = ?',
).run(channel, senderId); ).run(channel, senderId);
}, },
}; };
+1 -1
View File
@@ -96,7 +96,7 @@ export class SkillInstaller {
const entries = readdirSync(this.managedDir, { withFileTypes: true }); const entries = readdirSync(this.managedDir, { withFileTypes: true });
return entries return entries
.filter(entry => { .filter(entry => {
if (!entry.isDirectory()) return false; if (!entry.isDirectory()) {return false;}
const skillMd = resolve(this.managedDir, entry.name, 'SKILL.md'); const skillMd = resolve(this.managedDir, entry.name, 'SKILL.md');
return existsSync(skillMd); return existsSync(skillMd);
}) })
+1 -1
View File
@@ -13,7 +13,7 @@ export class SkillRegistry {
register(skill: Skill): void { register(skill: Skill): void {
this.skills.set(skill.manifest.name, skill); this.skills.set(skill.manifest.name, skill);
console.log( console.log(
`Skill '${skill.manifest.name}' registered (${skill.manifest.tier}, ${skill.available ? 'available' : 'unavailable'})` `Skill '${skill.manifest.name}' registered (${skill.manifest.tier}, ${skill.available ? 'available' : 'unavailable'})`,
); );
} }
+3 -3
View File
@@ -27,9 +27,9 @@ export function createAgentsListTool(registry: AgentConfigRegistry): Tool {
const lines = configs.map((c) => { const lines = configs.map((c) => {
const parts = [`- **${c.name}**`]; const parts = [`- **${c.name}**`];
if (c.modelTier) parts.push(`tier=${c.modelTier}`); if (c.modelTier) {parts.push(`tier=${c.modelTier}`);}
if (c.toolProfile) parts.push(`profile=${c.toolProfile}`); if (c.toolProfile) {parts.push(`profile=${c.toolProfile}`);}
if (c.sandbox) parts.push('sandboxed'); if (c.sandbox) {parts.push('sandboxed');}
if (c.systemPrompt) { if (c.systemPrompt) {
const preview = c.systemPrompt.slice(0, 80).replace(/\n/g, ' '); const preview = c.systemPrompt.slice(0, 80).replace(/\n/g, ' ');
parts.push(`prompt="${preview}${c.systemPrompt.length > 80 ? '...' : ''}"`); parts.push(`prompt="${preview}${c.systemPrompt.length > 80 ? '...' : ''}"`);
+2 -2
View File
@@ -31,7 +31,7 @@ function findChrome(): string {
]; ];
for (const candidate of candidates) { for (const candidate of candidates) {
if (existsSync(candidate)) return candidate; if (existsSync(candidate)) {return candidate;}
} }
throw new Error('Chrome/Chromium not found. Set browser.executable_path in config or install Chrome.'); throw new Error('Chrome/Chromium not found. Set browser.executable_path in config or install Chrome.');
@@ -120,7 +120,7 @@ export class BrowserManager {
async shutdown(): Promise<void> { async shutdown(): Promise<void> {
for (const [, page] of this.pages) { for (const [, page] of this.pages) {
try { try {
if (!page.isClosed()) await page.close(); if (!page.isClosed()) {await page.close();}
} catch { /* ignore */ } } catch { /* ignore */ }
} }
this.pages.clear(); this.pages.clear();
+1 -1
View File
@@ -23,7 +23,7 @@ export function createCronTools(scheduler: CronScheduler): Tool[] {
const lines = jobNames.map((name) => { const lines = jobNames.map((name) => {
const job = scheduler.getJob(name); const job = scheduler.getJob(name);
if (!job) return `- ${name}`; if (!job) {return `- ${name}`;}
return `- **${name}** — schedule: \`${job.schedule}\`, enabled: ${job.enabled}, output: ${job.output.channel}/${job.output.peer}\n message: "${job.message.length > 80 ? job.message.slice(0, 80) + '...' : job.message}"`; return `- **${name}** — schedule: \`${job.schedule}\`, enabled: ${job.enabled}, output: ${job.output.channel}/${job.output.peer}\n message: "${job.message.length > 80 ? job.message.slice(0, 80) + '...' : job.message}"`;
}); });
return { return {
+2 -2
View File
@@ -62,8 +62,8 @@ function setupValidAuth() {
mockExistsSync.mockReturnValue(true); mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockImplementation((path: unknown) => { mockReadFileSync.mockImplementation((path: unknown) => {
const p = String(path); const p = String(path);
if (p.includes('creds')) return JSON.stringify(fakeCredentials); if (p.includes('creds')) {return JSON.stringify(fakeCredentials);}
if (p.includes('token')) return JSON.stringify(fakeToken); if (p.includes('token')) {return JSON.stringify(fakeToken);}
return ''; return '';
}); });
} }
+3 -3
View File
@@ -101,9 +101,9 @@ function formatEvents(events: EventSummary[]): string {
return events return events
.map(e => { .map(e => {
const parts = [`[${e.id}] ${e.summary}`, ` Time: ${e.start}${e.end}`]; const parts = [`[${e.id}] ${e.summary}`, ` Time: ${e.start}${e.end}`];
if (e.location) parts.push(` Location: ${e.location}`); if (e.location) {parts.push(` Location: ${e.location}`);}
if (e.attendees.length > 0) parts.push(` Attendees: ${e.attendees.join(', ')}`); if (e.attendees.length > 0) {parts.push(` Attendees: ${e.attendees.join(', ')}`);}
if (e.htmlLink) parts.push(` Link: ${e.htmlLink}`); if (e.htmlLink) {parts.push(` Link: ${e.htmlLink}`);}
return parts.join('\n'); return parts.join('\n');
}) })
.join('\n\n'); .join('\n\n');
+2 -2
View File
@@ -67,8 +67,8 @@ function setupValidAuth() {
mockExistsSync.mockReturnValue(true); mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockImplementation((path: unknown) => { mockReadFileSync.mockImplementation((path: unknown) => {
const p = String(path); const p = String(path);
if (p.includes('creds')) return JSON.stringify(fakeCredentials); if (p.includes('creds')) {return JSON.stringify(fakeCredentials);}
if (p.includes('token')) return JSON.stringify(fakeToken); if (p.includes('token')) {return JSON.stringify(fakeToken);}
return ''; return '';
}); });
} }
+3 -3
View File
@@ -67,8 +67,8 @@ function formatDocs(docs: DocSummary[]): string {
.map(d => { .map(d => {
const parts = [`[${d.id}] ${d.name}`]; const parts = [`[${d.id}] ${d.name}`];
parts.push(` Modified: ${d.modifiedTime}`); parts.push(` Modified: ${d.modifiedTime}`);
if (d.owners.length > 0) parts.push(` Owners: ${d.owners.join(', ')}`); if (d.owners.length > 0) {parts.push(` Owners: ${d.owners.join(', ')}`);}
if (d.webViewLink) parts.push(` Link: ${d.webViewLink}`); if (d.webViewLink) {parts.push(` Link: ${d.webViewLink}`);}
return parts.join('\n'); return parts.join('\n');
}) })
.join('\n\n'); .join('\n\n');
@@ -76,7 +76,7 @@ function formatDocs(docs: DocSummary[]): string {
/** Extract plain text from a Google Docs document body. */ /** Extract plain text from a Google Docs document body. */
function extractPlainText(body: import('googleapis').docs_v1.Schema$Body): string { function extractPlainText(body: import('googleapis').docs_v1.Schema$Body): string {
if (!body.content) return ''; if (!body.content) {return '';}
const parts: string[] = []; const parts: string[] = [];
for (const structural of body.content) { for (const structural of body.content) {
+2 -2
View File
@@ -65,8 +65,8 @@ function setupValidAuth() {
mockExistsSync.mockReturnValue(true); mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockImplementation((path: unknown) => { mockReadFileSync.mockImplementation((path: unknown) => {
const p = String(path); const p = String(path);
if (p.includes('creds')) return JSON.stringify(fakeCredentials); if (p.includes('creds')) {return JSON.stringify(fakeCredentials);}
if (p.includes('token')) return JSON.stringify(fakeToken); if (p.includes('token')) {return JSON.stringify(fakeToken);}
return ''; return '';
}); });
} }
+7 -7
View File
@@ -75,11 +75,11 @@ function friendlyMimeType(mimeType: string): string {
/** Format file size in human-readable form. */ /** Format file size in human-readable form. */
function formatSize(bytes: string | undefined): string { function formatSize(bytes: string | undefined): string {
if (!bytes) return ''; if (!bytes) {return '';}
const n = parseInt(bytes, 10); const n = parseInt(bytes, 10);
if (isNaN(n)) return ''; if (isNaN(n)) {return '';}
if (n < 1024) return `${n} B`; if (n < 1024) {return `${n} B`;}
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; if (n < 1024 * 1024) {return `${(n / 1024).toFixed(1)} KB`;}
return `${(n / (1024 * 1024)).toFixed(1)} MB`; return `${(n / (1024 * 1024)).toFixed(1)} MB`;
} }
@@ -95,9 +95,9 @@ function formatFiles(files: FileSummary[]): string {
parts.push(` Type: ${friendlyMimeType(f.mimeType)}`); parts.push(` Type: ${friendlyMimeType(f.mimeType)}`);
parts.push(` Modified: ${f.modifiedTime}`); parts.push(` Modified: ${f.modifiedTime}`);
const size = formatSize(f.size); const size = formatSize(f.size);
if (size) parts.push(` Size: ${size}`); if (size) {parts.push(` Size: ${size}`);}
if (f.owners.length > 0) parts.push(` Owners: ${f.owners.join(', ')}`); if (f.owners.length > 0) {parts.push(` Owners: ${f.owners.join(', ')}`);}
if (f.webViewLink) parts.push(` Link: ${f.webViewLink}`); if (f.webViewLink) {parts.push(` Link: ${f.webViewLink}`);}
return parts.join('\n'); return parts.join('\n');
}) })
.join('\n\n'); .join('\n\n');
+2 -2
View File
@@ -69,8 +69,8 @@ function setupValidAuth() {
mockExistsSync.mockReturnValue(true); mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockImplementation((path: unknown) => { mockReadFileSync.mockImplementation((path: unknown) => {
const p = String(path); const p = String(path);
if (p.includes('creds')) return JSON.stringify(fakeCredentials); if (p.includes('creds')) {return JSON.stringify(fakeCredentials);}
if (p.includes('token')) return JSON.stringify(fakeToken); if (p.includes('token')) {return JSON.stringify(fakeToken);}
return ''; return '';
}); });
} }
+5 -5
View File
@@ -120,7 +120,7 @@ function extractTextBody(payload: {
// Recurse into nested multipart // Recurse into nested multipart
if (part.parts) { if (part.parts) {
const nested = extractTextBody(part as typeof payload); const nested = extractTextBody(part as typeof payload);
if (nested) return nested; if (nested) {return nested;}
} }
} }
if (htmlFallback) { if (htmlFallback) {
@@ -184,9 +184,9 @@ export function createGmailTools(config: NonNullable<GmailConfig>): Tool[] {
const emails: EmailSummary[] = []; const emails: EmailSummary[] = [];
for (const msg of messages) { for (const msg of messages) {
if (!msg.id) continue; if (!msg.id) {continue;}
const details = await fetchMessageDetails(gmail, msg.id); const details = await fetchMessageDetails(gmail, msg.id);
if (details) emails.push(details); if (details) {emails.push(details);}
} }
return { return {
@@ -239,9 +239,9 @@ export function createGmailTools(config: NonNullable<GmailConfig>): Tool[] {
const emails: EmailSummary[] = []; const emails: EmailSummary[] = [];
for (const msg of messages) { for (const msg of messages) {
if (!msg.id) continue; if (!msg.id) {continue;}
const details = await fetchMessageDetails(gmail, msg.id); const details = await fetchMessageDetails(gmail, msg.id);
if (details) emails.push(details); if (details) {emails.push(details);}
} }
return { return {
+2 -2
View File
@@ -65,8 +65,8 @@ function setupValidAuth() {
mockExistsSync.mockReturnValue(true); mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockImplementation((path: unknown) => { mockReadFileSync.mockImplementation((path: unknown) => {
const p = String(path); const p = String(path);
if (p.includes('creds')) return JSON.stringify(fakeCredentials); if (p.includes('creds')) {return JSON.stringify(fakeCredentials);}
if (p.includes('token')) return JSON.stringify(fakeToken); if (p.includes('token')) {return JSON.stringify(fakeToken);}
return ''; return '';
}); });
} }
+2 -2
View File
@@ -86,8 +86,8 @@ function formatTasks(tasks: TaskSummary[]): string {
.map(t => { .map(t => {
const checkbox = t.status === 'completed' ? '[x]' : '[ ]'; const checkbox = t.status === 'completed' ? '[x]' : '[ ]';
const parts = [`${checkbox} ${t.title}`]; const parts = [`${checkbox} ${t.title}`];
if (t.due) parts.push(` Due: ${t.due}`); if (t.due) {parts.push(` Due: ${t.due}`);}
if (t.notes) parts.push(` Notes: ${t.notes}`); if (t.notes) {parts.push(` Notes: ${t.notes}`);}
parts.push(` ID: ${t.id}`); parts.push(` ID: ${t.id}`);
return parts.join('\n'); return parts.join('\n');
}) })
+42 -42
View File
@@ -7,7 +7,7 @@ describe('image.analyze tool', () => {
beforeEach(() => { beforeEach(() => {
mockClient = { mockClient = {
chat: vi.fn() chat: vi.fn(),
}; };
}); });
@@ -25,7 +25,7 @@ describe('image.analyze tool', () => {
mockClient.chat = vi.fn().mockResolvedValueOnce({ mockClient.chat = vi.fn().mockResolvedValueOnce({
content: 'This is a beautiful sunset over the ocean.', content: 'This is a beautiful sunset over the ocean.',
stopReason: 'end_turn', stopReason: 'end_turn',
usage: { inputTokens: 100, outputTokens: 50 } usage: { inputTokens: 100, outputTokens: 50 },
}); });
const tool = createImageAnalyzeTool(mockClient); const tool = createImageAnalyzeTool(mockClient);
@@ -46,15 +46,15 @@ describe('image.analyze tool', () => {
source: expect.objectContaining({ source: expect.objectContaining({
type: 'url', type: 'url',
media_type: 'image/jpeg', media_type: 'image/jpeg',
url: 'https://example.com/image.jpg' url: 'https://example.com/image.jpg',
}) }),
} },
]) ]),
}) }),
]), ]),
system: expect.stringContaining('vision assistant'), system: expect.stringContaining('vision assistant'),
maxTokens: 1024 maxTokens: 1024,
}) }),
); );
}); });
@@ -63,13 +63,13 @@ describe('image.analyze tool', () => {
mockClient.chat = vi.fn().mockResolvedValueOnce({ mockClient.chat = vi.fn().mockResolvedValueOnce({
content: 'This is a sample image.', content: 'This is a sample image.',
stopReason: 'end_turn', stopReason: 'end_turn',
usage: { inputTokens: 100, outputTokens: 20 } usage: { inputTokens: 100, outputTokens: 20 },
}); });
const tool = createImageAnalyzeTool(mockClient); const tool = createImageAnalyzeTool(mockClient);
const result = await tool.execute({ const result = await tool.execute({
data: base64Data, data: base64Data,
media_type: 'image/png' media_type: 'image/png',
}); });
expect(result.success).toBe(true); expect(result.success).toBe(true);
@@ -87,15 +87,15 @@ describe('image.analyze tool', () => {
source: expect.objectContaining({ source: expect.objectContaining({
type: 'base64', type: 'base64',
media_type: 'image/png', media_type: 'image/png',
data: base64Data data: base64Data,
}) }),
} },
]) ]),
}) }),
]), ]),
system: expect.stringContaining('vision assistant'), system: expect.stringContaining('vision assistant'),
maxTokens: 1024 maxTokens: 1024,
}) }),
); );
}); });
@@ -103,13 +103,13 @@ describe('image.analyze tool', () => {
mockClient.chat = vi.fn().mockResolvedValueOnce({ mockClient.chat = vi.fn().mockResolvedValueOnce({
content: 'The image shows a cat sitting on a mat.', content: 'The image shows a cat sitting on a mat.',
stopReason: 'end_turn', stopReason: 'end_turn',
usage: { inputTokens: 100, outputTokens: 30 } usage: { inputTokens: 100, outputTokens: 30 },
}); });
const tool = createImageAnalyzeTool(mockClient); const tool = createImageAnalyzeTool(mockClient);
const result = await tool.execute({ const result = await tool.execute({
url: 'https://example.com/cat.jpg', url: 'https://example.com/cat.jpg',
prompt: 'What is in this image?' prompt: 'What is in this image?',
}); });
expect(result.success).toBe(true); expect(result.success).toBe(true);
@@ -120,11 +120,11 @@ describe('image.analyze tool', () => {
expect.objectContaining({ expect.objectContaining({
content: expect.arrayContaining([ content: expect.arrayContaining([
{ type: 'text', text: 'What is in this image?' }, { type: 'text', text: 'What is in this image?' },
expect.any(Object) expect.any(Object),
]) ]),
}) }),
]) ]),
}) }),
); );
}); });
@@ -132,7 +132,7 @@ describe('image.analyze tool', () => {
mockClient.chat = vi.fn().mockResolvedValueOnce({ mockClient.chat = vi.fn().mockResolvedValueOnce({
content: 'This is the default prompt response.', content: 'This is the default prompt response.',
stopReason: 'end_turn', stopReason: 'end_turn',
usage: { inputTokens: 100, outputTokens: 10 } usage: { inputTokens: 100, outputTokens: 10 },
}); });
const tool = createImageAnalyzeTool(mockClient); const tool = createImageAnalyzeTool(mockClient);
@@ -144,11 +144,11 @@ describe('image.analyze tool', () => {
expect.objectContaining({ expect.objectContaining({
content: expect.arrayContaining([ content: expect.arrayContaining([
{ type: 'text', text: 'Describe this image in detail.' }, { type: 'text', text: 'Describe this image in detail.' },
expect.any(Object) expect.any(Object),
]) ]),
}) }),
]) ]),
}) }),
); );
}); });
@@ -165,7 +165,7 @@ describe('image.analyze tool', () => {
const tool = createImageAnalyzeTool(mockClient); const tool = createImageAnalyzeTool(mockClient);
const result = await tool.execute({ const result = await tool.execute({
url: 'https://example.com/image.jpg', url: 'https://example.com/image.jpg',
data: 'base64data' data: 'base64data',
}); });
expect(result.success).toBe(false); expect(result.success).toBe(false);
@@ -177,7 +177,7 @@ describe('image.analyze tool', () => {
const tool = createImageAnalyzeTool(mockClient); const tool = createImageAnalyzeTool(mockClient);
const result = await tool.execute({ const result = await tool.execute({
data: 'base64data', data: 'base64data',
prompt: 'Test' prompt: 'Test',
}); });
expect(result.success).toBe(false); expect(result.success).toBe(false);
@@ -189,7 +189,7 @@ describe('image.analyze tool', () => {
const tool = createImageAnalyzeTool(mockClient); const tool = createImageAnalyzeTool(mockClient);
const result = await tool.execute({ const result = await tool.execute({
data: 'base64data', data: 'base64data',
media_type: 'image/tiff' media_type: 'image/tiff',
}); });
expect(result.success).toBe(false); expect(result.success).toBe(false);
@@ -204,13 +204,13 @@ describe('image.analyze tool', () => {
mockClient.chat = vi.fn().mockResolvedValueOnce({ mockClient.chat = vi.fn().mockResolvedValueOnce({
content: 'Success', content: 'Success',
stopReason: 'end_turn', stopReason: 'end_turn',
usage: { inputTokens: 10, outputTokens: 10 } usage: { inputTokens: 10, outputTokens: 10 },
}); });
const tool = createImageAnalyzeTool(mockClient); const tool = createImageAnalyzeTool(mockClient);
const result = await tool.execute({ const result = await tool.execute({
data: 'base64data', data: 'base64data',
media_type: mediaType media_type: mediaType,
}); });
expect(result.success).toBe(true); expect(result.success).toBe(true);
@@ -245,12 +245,12 @@ describe('image.analyze tool', () => {
const mockRequest = { const mockRequest = {
messages: [] as any, messages: [] as any,
system: '', system: '',
maxTokens: 1024 maxTokens: 1024,
}; };
const mockResponse = { const mockResponse = {
content: 'Analysis complete.', content: 'Analysis complete.',
stopReason: 'end_turn', stopReason: 'end_turn',
usage: { inputTokens: 100, outputTokens: 10 } usage: { inputTokens: 100, outputTokens: 10 },
}; };
mockClient.chat = vi.fn().mockResolvedValue(mockResponse).mockImplementationOnce(async (r) => { mockClient.chat = vi.fn().mockResolvedValue(mockResponse).mockImplementationOnce(async (r) => {
@@ -260,7 +260,7 @@ describe('image.analyze tool', () => {
const tool = createImageAnalyzeTool(mockClient); const tool = createImageAnalyzeTool(mockClient);
await tool.execute({ await tool.execute({
url: 'https://example.com/image.jpg', url: 'https://example.com/image.jpg',
prompt: 'Analyze the colors.' prompt: 'Analyze the colors.',
}); });
const callArgs = (mockClient.chat as any).mock.calls[0][0]; const callArgs = (mockClient.chat as any).mock.calls[0][0];
@@ -272,12 +272,12 @@ describe('image.analyze tool', () => {
const mockRequest = { const mockRequest = {
messages: [] as any, messages: [] as any,
system: '', system: '',
maxTokens: 1024 maxTokens: 1024,
}; };
const mockResponse = { const mockResponse = {
content: 'Short response', content: 'Short response',
stopReason: 'end_turn', stopReason: 'end_turn',
usage: { inputTokens: 10, outputTokens: 10 } usage: { inputTokens: 10, outputTokens: 10 },
}; };
mockClient.chat = vi.fn().mockResolvedValueOnce(mockResponse); mockClient.chat = vi.fn().mockResolvedValueOnce(mockResponse);
@@ -294,7 +294,7 @@ describe('image.analyze tool', () => {
mockClient.chat.mockResolvedValueOnce({ mockClient.chat.mockResolvedValueOnce({
content: expectedContent, content: expectedContent,
stopReason: 'end_turn', stopReason: 'end_turn',
usage: { inputTokens: 100, outputTokens: 100 } usage: { inputTokens: 100, outputTokens: 100 },
}); });
const tool = createImageAnalyzeTool(mockClient); const tool = createImageAnalyzeTool(mockClient);
+24 -24
View File
@@ -21,24 +21,24 @@ export function createImageAnalyzeTool(modelClient: ModelClient): Tool {
properties: { properties: {
url: { url: {
type: 'string', type: 'string',
description: 'URL of the image to analyze' description: 'URL of the image to analyze',
}, },
data: { data: {
type: 'string', type: 'string',
description: 'Base64-encoded image data (alternative to url)' description: 'Base64-encoded image data (alternative to url)',
}, },
media_type: { media_type: {
type: 'string', type: 'string',
description: description:
'MIME type of the image (required when using data). One of: image/jpeg, image/png, image/gif, image/webp' 'MIME type of the image (required when using data). One of: image/jpeg, image/png, image/gif, image/webp',
}, },
prompt: { prompt: {
type: 'string', type: 'string',
description: description:
'What to analyze or describe about the image. Default: "Describe this image in detail."' 'What to analyze or describe about the image. Default: "Describe this image in detail."',
} },
}, },
required: [] required: [],
}, },
execute: async (rawArgs: unknown): Promise<ToolResult> => { execute: async (rawArgs: unknown): Promise<ToolResult> => {
try { try {
@@ -48,7 +48,7 @@ export function createImageAnalyzeTool(modelClient: ModelClient): Tool {
return { return {
success: false, success: false,
output: '', output: '',
error: 'Either "url" or "data" must be provided' error: 'Either "url" or "data" must be provided',
}; };
} }
@@ -56,7 +56,7 @@ export function createImageAnalyzeTool(modelClient: ModelClient): Tool {
return { return {
success: false, success: false,
output: '', output: '',
error: 'Cannot provide both "url" and "data" - choose one' error: 'Cannot provide both "url" and "data" - choose one',
}; };
} }
@@ -64,7 +64,7 @@ export function createImageAnalyzeTool(modelClient: ModelClient): Tool {
return { return {
success: false, success: false,
output: '', output: '',
error: 'media_type is required when providing data' error: 'media_type is required when providing data',
}; };
} }
@@ -72,7 +72,7 @@ export function createImageAnalyzeTool(modelClient: ModelClient): Tool {
return { return {
success: false, success: false,
output: '', output: '',
error: `Invalid media_type: ${args.media_type}. Must be one of: ${VALID_MEDIA_TYPES.join(', ')}` error: `Invalid media_type: ${args.media_type}. Must be one of: ${VALID_MEDIA_TYPES.join(', ')}`,
}; };
} }
@@ -80,42 +80,42 @@ export function createImageAnalyzeTool(modelClient: ModelClient): Tool {
const imageSource = args.url const imageSource = args.url
? { ? {
type: 'url' as const, type: 'url' as const,
media_type: args.media_type || 'image/jpeg', media_type: args.media_type || 'image/jpeg',
url: args.url url: args.url,
} }
: { : {
type: 'base64' as const, type: 'base64' as const,
media_type: args.media_type!, media_type: args.media_type!,
data: args.data data: args.data,
}; };
const message = { const message = {
role: 'user' as const, role: 'user' as const,
content: [ content: [
{ type: 'text' as const, text: prompt }, { type: 'text' as const, text: prompt },
{ type: 'image' as const, source: imageSource } { type: 'image' as const, source: imageSource },
] ],
}; };
const response = await modelClient.chat({ const response = await modelClient.chat({
messages: [message], messages: [message],
system: system:
'You are a vision assistant. Analyze the provided image according to the user\'s request. Provide detailed, helpful descriptions.', 'You are a vision assistant. Analyze the provided image according to the user\'s request. Provide detailed, helpful descriptions.',
maxTokens: 1024 maxTokens: 1024,
}); });
return { return {
success: true, success: true,
output: response.content output: response.content,
}; };
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
output: '', output: '',
error: error instanceof Error ? error.message : String(error) error: error instanceof Error ? error.message : String(error),
}; };
} }
} },
}; };
} }
+2 -2
View File
@@ -43,7 +43,7 @@ export function createMemorySearchTool(store: MemoryStore, hybridSearch?: Hybrid
const formatted = results.map((result) => { const formatted = results.map((result) => {
const sourceLabel = result.source === 'both' ? 'keyword+vector' const sourceLabel = result.source === 'both' ? 'keyword+vector'
: result.source === 'vector' ? 'vector' : result.source === 'vector' ? 'vector'
: 'keyword'; : 'keyword';
return `[${result.namespace}:${result.line}] (${sourceLabel}, score: ${result.score.toFixed(3)}) ${result.content}\n context: ${result.context}`; return `[${result.namespace}:${result.line}] (${sourceLabel}, score: ${result.score.toFixed(3)}) ${result.content}\n context: ${result.context}`;
}).join('\n\n'); }).join('\n\n');
@@ -66,7 +66,7 @@ export function createMemorySearchTool(store: MemoryStore, hybridSearch?: Hybrid
// Format each result as a readable block with namespace, line number, and context // Format each result as a readable block with namespace, line number, and context
const formatted = results.map((result) => const formatted = results.map((result) =>
`[${result.namespace}:${result.line}] ${result.content}\n context: ${result.context}` `[${result.namespace}:${result.line}] ${result.content}\n context: ${result.context}`,
).join('\n\n'); ).join('\n\n');
return { return {
+1 -1
View File
@@ -21,7 +21,7 @@ export function createProcessListTool(manager: ProcessManager): Tool {
? `${Math.round((Date.now() - p.startedAt) / 1000)}s` ? `${Math.round((Date.now() - p.startedAt) / 1000)}s`
: 'N/A'; : 'N/A';
let line = `${p.id} PID=${p.pid} status=${p.status} uptime=${uptime} cmd="${p.command}"`; let line = `${p.id} PID=${p.pid} status=${p.status} uptime=${uptime} cmd="${p.command}"`;
if (p.exitCode !== undefined) line += ` exit=${p.exitCode}`; if (p.exitCode !== undefined) {line += ` exit=${p.exitCode}`;}
return line; return line;
}); });
+1 -1
View File
@@ -204,7 +204,7 @@ describe('Process tools', () => {
let manager: ProcessManager; let manager: ProcessManager;
afterEach(async () => { afterEach(async () => {
if (manager) await manager.shutdown(); if (manager) {await manager.shutdown();}
}); });
it('process.start tool creates and returns process info', async () => { it('process.start tool creates and returns process info', async () => {
+6 -6
View File
@@ -39,7 +39,7 @@ export class RingBuffer {
/** Read all buffered data as a UTF-8 string. */ /** Read all buffered data as a UTF-8 string. */
read(): string { read(): string {
if (this.length === 0) return ''; if (this.length === 0) {return '';}
if (this.length < this.capacity) { if (this.length < this.capacity) {
// Buffer not full — data is contiguous, starting at (writePos - length) // Buffer not full — data is contiguous, starting at (writePos - length)
@@ -202,15 +202,15 @@ export class ProcessManager {
/** Get recent output from a process's ring buffer. */ /** Get recent output from a process's ring buffer. */
getOutput(id: string): string { getOutput(id: string): string {
const proc = this.processes.get(id); const proc = this.processes.get(id);
if (!proc) throw new Error(`Process ${id} not found`); if (!proc) {throw new Error(`Process ${id} not found`);}
return proc.outputBuffer.read(); return proc.outputBuffer.read();
} }
/** Kill a process by sending a signal. Returns true if signal was sent. */ /** Kill a process by sending a signal. Returns true if signal was sent. */
kill(id: string, signal: NodeJS.Signals = 'SIGTERM'): boolean { kill(id: string, signal: NodeJS.Signals = 'SIGTERM'): boolean {
const proc = this.processes.get(id); const proc = this.processes.get(id);
if (!proc) throw new Error(`Process ${id} not found`); if (!proc) {throw new Error(`Process ${id} not found`);}
if (proc.status !== 'running') return false; if (proc.status !== 'running') {return false;}
const child = this.childProcesses.get(id); const child = this.childProcesses.get(id);
if (child) { if (child) {
@@ -229,7 +229,7 @@ export class ProcessManager {
runningCount(): number { runningCount(): number {
let count = 0; let count = 0;
for (const proc of this.processes.values()) { for (const proc of this.processes.values()) {
if (proc.status === 'running') count++; if (proc.status === 'running') {count++;}
} }
return count; return count;
} }
@@ -254,7 +254,7 @@ export class ProcessManager {
} }
const running = this.list().filter(p => p.status === 'running'); const running = this.list().filter(p => p.status === 'running');
if (running.length === 0) return; if (running.length === 0) {return;}
console.log(`[ProcessManager] Killing ${running.length} running process(es)...`); console.log(`[ProcessManager] Killing ${running.length} running process(es)...`);
+3 -3
View File
@@ -32,9 +32,9 @@ export function createProcessStatusTool(manager: ProcessManager): Tool {
info += `PID: ${proc.pid}\n`; info += `PID: ${proc.pid}\n`;
info += `Status: ${proc.status}\n`; info += `Status: ${proc.status}\n`;
info += `Uptime: ${uptime}\n`; info += `Uptime: ${uptime}\n`;
if (proc.exitCode !== undefined) info += `Exit code: ${proc.exitCode}\n`; if (proc.exitCode !== undefined) {info += `Exit code: ${proc.exitCode}\n`;}
if (proc.errorMessage) info += `Error: ${proc.errorMessage}\n`; if (proc.errorMessage) {info += `Error: ${proc.errorMessage}\n`;}
if (proc.cwd) info += `CWD: ${proc.cwd}\n`; if (proc.cwd) {info += `CWD: ${proc.cwd}\n`;}
return { success: true, output: info.trimEnd() }; return { success: true, output: info.trimEnd() };
}, },
+1 -1
View File
@@ -61,7 +61,7 @@ export class ToolExecutor {
const result = await Promise.race([ const result = await Promise.race([
tool.execute(args), tool.execute(args),
new Promise<ToolResult>((_, reject) => new Promise<ToolResult>((_, reject) =>
setTimeout(() => reject(new Error(`Tool '${toolName}' timed out after ${this.defaultTimeoutMs}ms`)), this.defaultTimeoutMs) setTimeout(() => reject(new Error(`Tool '${toolName}' timed out after ${this.defaultTimeoutMs}ms`)), this.defaultTimeoutMs),
), ),
]); ]);
+1 -1
View File
@@ -81,7 +81,7 @@ export class ToolRegistry {
/** Return tools filtered by the policy for a given context. */ /** Return tools filtered by the policy for a given context. */
filteredList(context?: ToolPolicyContext): Tool[] { filteredList(context?: ToolPolicyContext): Tool[] {
if (!this._policy) return this.list(); if (!this._policy) {return this.list();}
return this._policy.filterTools(this.list(), context); return this._policy.filterTools(this.list(), context);
} }
+2 -2
View File
@@ -21,7 +21,7 @@ const NAMED_ENTITIES: Record<string, string> = {
function decodeEntity(entity: string): string { function decodeEntity(entity: string): string {
// Named entity // Named entity
const named = NAMED_ENTITIES[entity.toLowerCase()]; const named = NAMED_ENTITIES[entity.toLowerCase()];
if (named) return named; if (named) {return named;}
// Decimal numeric entity: &#NNN; // Decimal numeric entity: &#NNN;
const decMatch = entity.match(/^&#(\d+);$/); const decMatch = entity.match(/^&#(\d+);$/);
@@ -52,7 +52,7 @@ function decodeEntity(entity: string): string {
* @returns Clean plain text * @returns Clean plain text
*/ */
export function sanitizeHtml(text: string): string { export function sanitizeHtml(text: string): string {
if (!text) return ''; if (!text) {return '';}
let result = text; let result = text;