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 {
// 1. Environment variable
const envToken = process.env.GITHUB_TOKEN;
if (envToken) return envToken;
if (envToken) {return envToken;}
// 2. Stored OAuth token
return loadStoredToken();
+2 -2
View File
@@ -31,7 +31,7 @@ export class CronScheduler implements ChannelAdapter {
this._status = 'connected';
for (const job of this.jobConfigs) {
if (!job.enabled) continue;
if (!job.enabled) {continue;}
const cronInstance = new Cron(job.schedule, {
timezone: job.timezone,
@@ -81,7 +81,7 @@ export class CronScheduler implements ChannelAdapter {
/** Manually trigger a job (also called by cron on schedule). */
triggerJob(jobName: string): void {
const job = this.jobs.get(jobName);
if (!job) return;
if (!job) {return;}
const msg: InboundMessage = {
id: `cron-${jobName}-${Date.now()}`,
+2 -2
View File
@@ -181,8 +181,8 @@ describe('GmailWatcher', () => {
// credentials file exists but token file does not
mockExistsSync.mockImplementation((path: unknown) => {
const p = String(path);
if (p.includes('credentials')) return true;
if (p.includes('token')) return false;
if (p.includes('credentials')) {return true;}
if (p.includes('token')) {return false;}
return true;
});
+7 -7
View File
@@ -212,7 +212,7 @@ export class GmailWatcher implements ChannelAdapter {
* Calls gmail.users.watch() and schedules renewal before expiry.
*/
private async setupWatch(): Promise<void> {
if (!this.oauth2Client) return;
if (!this.oauth2Client) {return;}
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.
*/
private async pollForNewMessages(): Promise<void> {
if (!this.oauth2Client) return;
if (!this.oauth2Client) {return;}
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.
*/
private async processHistoryChanges(startHistoryId: string): Promise<void> {
if (!this.oauth2Client) return;
if (!this.oauth2Client) {return;}
const gmail = google.gmail({ version: 'v1', auth: this.oauth2Client });
@@ -287,17 +287,17 @@ export class GmailWatcher implements ChannelAdapter {
const addedMessages = record.messagesAdded ?? [];
for (const added of addedMessages) {
const messageId = added.message?.id;
if (!messageId || processedIds.has(messageId)) continue;
if (!messageId || processedIds.has(messageId)) {continue;}
processedIds.add(messageId);
const email = await this.getMessageDetails(messageId);
if (!email) continue;
if (!email) {continue;}
// Skip messages before history_start if configured
if (this.config.history_start) {
const emailDate = new Date(email.date);
const startDate = new Date(this.config.history_start);
if (emailDate < startDate) continue;
if (emailDate < startDate) {continue;}
}
const text = this.renderTemplate(email);
@@ -348,7 +348,7 @@ export class GmailWatcher implements ChannelAdapter {
* Fetch full message details by ID and extract relevant headers.
*/
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 });
+2 -2
View File
@@ -73,7 +73,7 @@ export class HeartbeatMonitor {
/** Start the heartbeat monitor. Does nothing if disabled. */
start(): void {
if (!this.deps.config.enabled) return;
if (!this.deps.config.enabled) {return;}
const intervalMs = parseInterval(this.deps.config.interval);
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> {
const notifyConfig = this.deps.config.notify;
if (!notifyConfig) return;
if (!notifyConfig) {return;}
const adapter = this.deps.channelLookup.get(notifyConfig.channel);
if (!adapter) {
+1 -1
View File
@@ -36,7 +36,7 @@ function mockResponse(): ServerResponse & { statusCode_: number; body_: string;
headers_: {},
writeHead(code: number, headers?: Record<string, string>) {
res.statusCode_ = code;
if (headers) res.headers_ = headers;
if (headers) {res.headers_ = headers;}
return res;
},
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 sig = signature.startsWith('sha256=') ? signature.slice(7) : signature;
if (expected.length !== sig.length) return false;
if (expected.length !== sig.length) {return false;}
try {
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];
if (value === undefined || value === null) return '';
if (value === undefined || value === null) {return '';}
return typeof value === 'string' ? value : JSON.stringify(value);
});
+1 -1
View File
@@ -243,7 +243,7 @@ describe('AgentOrchestrator', () => {
});
expect(consoleSpy).toHaveBeenCalledWith(
'[Flynn:delegate] tier=fast tokens=50+25'
'[Flynn:delegate] tier=fast tokens=50+25',
);
consoleSpy.mockRestore();
+3 -3
View File
@@ -364,10 +364,10 @@ export class AgentOrchestrator {
* Called before each `process()` call when compaction is configured.
*/
private async compactIfNeeded(): Promise<void> {
if (!this._compactionConfig) return;
if (!this._compactionConfig) {return;}
const messages = this.getHistory();
if (messages.length === 0) return;
if (messages.length === 0) {return;}
const model = this._modelName ?? 'unknown';
const needs = shouldCompact({
@@ -377,7 +377,7 @@ export class AgentOrchestrator {
thresholdPct: this._compactionConfig.thresholdPct,
});
if (!needs) return;
if (!needs) {return;}
await this.compact();
}
+2 -2
View File
@@ -14,7 +14,7 @@ function createMockClient() {
_handlers: handlers,
user: null as { id: string; tag: string } | null,
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);
}),
login: vi.fn(async (_token: string) => {
@@ -23,7 +23,7 @@ function createMockClient() {
// Trigger ready event asynchronously
setTimeout(() => {
const readyHandlers = handlers.get('ready') ?? [];
for (const h of readyHandlers) h();
for (const h of readyHandlers) {h();}
}, 0);
}),
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. */
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);
if (!channel || !('send' in channel)) {
@@ -163,10 +163,10 @@ export class DiscordAdapter implements ChannelAdapter {
/** Internal: process an inbound Discord message. */
private handleMessage(message: DiscordMessage): void {
if (!this.messageHandler) return;
if (!this.messageHandler) {return;}
// Ignore bot messages
if (message.author.bot) return;
if (message.author.bot) {return;}
const isDM = !message.guild;
+1 -1
View File
@@ -83,7 +83,7 @@ export class PairingManager {
const normalizedCode = code.trim().toUpperCase();
const pending = this.pendingCodes.get(normalizedCode);
if (!pending) return false;
if (!pending) {return false;}
if (Date.now() > pending.expiresAt) {
this.pendingCodes.delete(normalizedCode);
return false;
+1 -1
View File
@@ -32,7 +32,7 @@ export class ChannelRegistry {
/** Unregister an adapter by name. Calls disconnect() if connected. */
async unregister(name: string): Promise<void> {
const adapter = this.adapters.get(name);
if (!adapter) return;
if (!adapter) {return;}
if (adapter.status === 'connected' || adapter.status === 'connecting') {
await adapter.disconnect();
+1 -1
View File
@@ -50,7 +50,7 @@ const baseConfig: SlackAdapterConfig = {
/** Helper: simulate a Slack message event through the captured handler. */
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 });
}
+11 -11
View File
@@ -128,15 +128,15 @@ export class SlackAdapter implements ChannelAdapter {
/** Send an outbound message, automatically chunking if it exceeds 4000 chars. */
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"
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 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;
@@ -171,7 +171,7 @@ export class SlackAdapter implements ChannelAdapter {
threadTs: string,
attachment: OutboundAttachment,
): Promise<void> {
if (!this.app) return;
if (!this.app) {return;}
try {
if (attachment.data) {
@@ -200,7 +200,7 @@ export class SlackAdapter implements ChannelAdapter {
/** Resolve a Slack user ID to a display name, with caching. */
private async resolveUserName(userId: string): Promise<string> {
const cached = this.userNameCache.get(userId);
if (cached) return cached;
if (cached) {return cached;}
try {
const result = await this.app!.client.users.info({ user: userId });
@@ -219,16 +219,16 @@ export class SlackAdapter implements ChannelAdapter {
private async extractMediaAttachments(
files?: SlackMessageEvent['files'],
): Promise<Attachment[]> {
if (!files || files.length === 0) return [];
if (!files || files.length === 0) {return [];}
const attachments: Attachment[] = [];
for (const file of 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;
if (!downloadUrl) continue;
if (!downloadUrl) {continue;}
try {
const response = await fetch(downloadUrl, {
@@ -264,13 +264,13 @@ export class SlackAdapter implements ChannelAdapter {
/** Internal: process an inbound Slack message event. */
private async handleMessage(message: SlackMessageEvent): Promise<void> {
if (!this.messageHandler) return;
if (!this.messageHandler) {return;}
// 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;
if (!channelId) return;
if (!channelId) {return;}
// Check allowed channel IDs
if (
+15 -15
View File
@@ -53,13 +53,13 @@ export class TelegramAdapter implements ChannelAdapter {
private async downloadFileToBase64(fileId: string): Promise<string | null> {
try {
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 url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
const response = await fetch(url);
if (!response.ok) return null;
if (!response.ok) {return null;}
const buffer = Buffer.from(await response.arrayBuffer());
return buffer.toString('base64');
@@ -82,7 +82,7 @@ export class TelegramAdapter implements ChannelAdapter {
// ── Auth middleware — reject messages from unknown chats (with pairing fallback) ──
this.bot.use(async (ctx, next) => {
const chatId = ctx.chat?.id;
if (chatId === undefined) return;
if (chatId === undefined) {return;}
// Allowlist check
if (isAllowedChat(chatId, this.config.allowedChatIds)) {
@@ -166,7 +166,7 @@ export class TelegramAdapter implements ChannelAdapter {
// ── Text message handler ──
this.bot.on('message:text', async (ctx) => {
if (!this.messageHandler) return;
if (!this.messageHandler) {return;}
// Group chat mention gating
const isGroup = ctx.chat.type === 'group' || ctx.chat.type === 'supergroup';
@@ -212,10 +212,10 @@ export class TelegramAdapter implements ChannelAdapter {
// ── Photo message handler ──
this.bot.on('message:photo', async (ctx) => {
if (!this.messageHandler) return;
if (!this.messageHandler) {return;}
const photo = ctx.message.photo;
if (!photo || photo.length === 0) return;
if (!photo || photo.length === 0) {return;}
const largestPhoto = photo[photo.length - 1];
@@ -250,13 +250,13 @@ export class TelegramAdapter implements ChannelAdapter {
// ── Image document handler ──
this.bot.on('message:document', async (ctx) => {
if (!this.messageHandler) return;
if (!this.messageHandler) {return;}
const document = ctx.message.document;
if (!document) return;
if (!document) {return;}
const mimeType = document.mime_type ?? '';
if (!mimeType.startsWith('image/')) return;
if (!mimeType.startsWith('image/')) {return;}
await ctx.replyWithChatAction('typing');
@@ -290,10 +290,10 @@ export class TelegramAdapter implements ChannelAdapter {
// ── Voice message handler ──
this.bot.on('message:voice', async (ctx) => {
if (!this.messageHandler) return;
if (!this.messageHandler) {return;}
const voice = ctx.message.voice;
if (!voice) return;
if (!voice) {return;}
await ctx.replyWithChatAction('typing');
@@ -327,10 +327,10 @@ export class TelegramAdapter implements ChannelAdapter {
// ── Audio message handler ──
this.bot.on('message:audio', async (ctx) => {
if (!this.messageHandler) return;
if (!this.messageHandler) {return;}
const audio = ctx.message.audio;
if (!audio) return;
if (!audio) {return;}
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. */
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 text = message.text;
@@ -413,7 +413,7 @@ export class TelegramAdapter implements ChannelAdapter {
/** Send a single outbound attachment via the Telegram API. */
private async sendAttachment(chatId: number, attachment: OutboundAttachment): Promise<void> {
if (!this.bot) return;
if (!this.bot) {return;}
try {
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. */
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;
@@ -169,7 +169,7 @@ export class WhatsAppAdapter implements ChannelAdapter {
/** Send a single outbound attachment via WhatsApp using MessageMedia. */
private async sendAttachment(peerId: string, attachment: OutboundAttachment): Promise<void> {
if (!this.client) return;
if (!this.client) {return;}
try {
if (attachment.data) {
@@ -194,10 +194,10 @@ export class WhatsAppAdapter implements ChannelAdapter {
/** Internal: process an inbound WhatsApp message. */
private async handleMessage(message: WhatsAppMessage): Promise<void> {
if (!this.messageHandler) return;
if (!this.messageHandler) {return;}
// Ignore messages from the bot itself
if (message.fromMe) return;
if (message.fromMe) {return;}
const from = message.from;
@@ -223,7 +223,7 @@ export class WhatsAppAdapter implements ChannelAdapter {
? message.body?.includes(`@${this.botId.replace(/@c\.us$/, '')}`) ||
(message as any).mentionedIds?.some((id: string) => id === this.botId)
: false;
if (!mentionsBot) return;
if (!mentionsBot) {return;}
}
}
+9 -9
View File
@@ -74,18 +74,18 @@ compdef _flynn flynn
export function generateFishCompletion(): string {
const lines = [
`# Flynn fish completion — generated by 'flynn completion fish'`,
`# Disable file completions by default`,
`complete -c flynn -f`,
``,
`# Subcommands`,
'# Flynn fish completion — generated by \'flynn completion fish\'',
'# Disable file completions by default',
'complete -c flynn -f',
'',
'# 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' -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 help -d \'Show help\'');
lines.push('complete -c flynn -n \'__fish_use_subcommand\' -l version -d \'Show version\'');
lines.push('');
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('');
return lines.join('\n');
@@ -141,7 +141,7 @@ export function registerCompletionCommand(program: Command): void {
writeFileSync(installPath, script, 'utf-8');
console.log(`Completion script installed to: ${installPath}`);
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 {
process.stdout.write(script);
+3 -3
View File
@@ -152,9 +152,9 @@ const checkModelConnectivity: Check = async (ctx) => {
// Build a summary of the model stack
const parts = [`default: ${model.provider}/${model.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.local) parts.push(`local: ${models.local.provider}/${models.local.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.local) {parts.push(`local: ${models.local.provider}/${models.local.model}`);}
parts.push(`fallback: [${models.fallback_chain.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'),
];
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.';
}
+1 -1
View File
@@ -11,7 +11,7 @@ describe('sessions command', () => {
afterEach(() => {
store?.close();
if (existsSync(dbPath)) unlinkSync(dbPath);
if (existsSync(dbPath)) {unlinkSync(dbPath);}
});
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
const wantGoogle = await p.confirm('Configure Google services (Gmail, Calendar, Docs, Drive, Tasks)?', false);
if (!wantGoogle) return;
if (!wantGoogle) {return;}
p.println();
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> {
const automation = config.automation as Record<string, any> | undefined;
if (!automation) return;
if (!automation) {return;}
const pending: { name: string; authCmd: string }[] = [];
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();
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') {
const moreChoice = await p.choose('Channel:', MORE_CHANNEL_OPTIONS);
const setup = CHANNEL_SETUP[moreChoice];
if (setup) await setup(p, builder);
if (setup) {await setup(p, builder);}
} else {
const setup = CHANNEL_SETUP[choice];
if (setup) await setup(p, builder);
if (setup) {await setup(p, builder);}
}
p.println();
+6 -6
View File
@@ -39,9 +39,9 @@ export class ConfigBuilder {
setProvider(tier: 'default' | 'fast' | 'complex' | 'local', cfg: ProviderConfig): void {
const models = (this.config.models ?? {}) as Record<string, unknown>;
const entry: Record<string, unknown> = { provider: cfg.provider, model: cfg.model };
if (cfg.api_key) entry.api_key = cfg.api_key;
if (cfg.auth_token) entry.auth_token = cfg.auth_token;
if (cfg.endpoint) entry.endpoint = cfg.endpoint;
if (cfg.api_key) {entry.api_key = cfg.api_key;}
if (cfg.auth_token) {entry.auth_token = cfg.auth_token;}
if (cfg.endpoint) {entry.endpoint = cfg.endpoint;}
models[tier] = entry;
this.config.models = models;
}
@@ -91,8 +91,8 @@ export class ConfigBuilder {
setMemoryEmbedding(cfg: EmbeddingConfig): void {
const memory = (this.config.memory ?? {}) as Record<string, unknown>;
const embedding: Record<string, unknown> = { enabled: true, provider: cfg.provider };
if (cfg.api_key) embedding.api_key = cfg.api_key;
if (cfg.endpoint) embedding.endpoint = cfg.endpoint;
if (cfg.api_key) {embedding.api_key = cfg.api_key;}
if (cfg.endpoint) {embedding.endpoint = cfg.endpoint;}
memory.embedding = embedding;
this.config.memory = memory;
}
@@ -151,7 +151,7 @@ export class ConfigBuilder {
setCronEnabled(): void {
const automation = (this.config.automation ?? {}) as Record<string, unknown>;
if (!automation.cron) automation.cron = [];
if (!automation.cron) {automation.cron = [];}
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(' information based on meaning, not just keywords.');
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(' 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 ?? {};
for (const tier of ['default', 'fast', 'complex', 'local']) {
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;
}
+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 idx = parseInt(answer, 10);
if (idx === 0 || isNaN(idx)) break;
if (idx === 0 || isNaN(idx)) {break;}
if (idx >= 1 && idx <= MENU_OPTIONS.length) {
const section = MENU_OPTIONS[idx - 1].value;
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 answer = await rl.question(`${question} ${hint} `);
const trimmed = answer.trim().toLowerCase();
if (trimmed === '') return defaultYes;
if (trimmed === '') {return defaultYes;}
return trimmed === 'y' || trimmed === 'yes';
},
@@ -34,9 +34,9 @@ export function createPrompter(rl: ReadlineInterface): Prompter {
for (let i = 0; i < options.length; i++) {
this.println(` ${i + 1}. ${options[i].label}`);
}
const answer = await rl.question(`> `);
const answer = await rl.question('> ');
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;
},
+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;
}> {
const help = PROVIDER_HELP[def.provider];
if (help) p.println(` ${help}`);
if (help) {p.println(` ${help}`);}
const config: Record<string, string> = { provider: def.provider };
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.needsApiKey) {config.api_key = await p.password(def.apiKeyLabel ?? 'API key');}
if (def.needsEndpoint) {config.endpoint = await p.ask('Host', def.defaultEndpoint);}
config.model = await p.ask('Model', def.defaultModel);
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'}`);
const channels: string[] = [];
if (config.server?.port) channels.push('webchat');
if (config.telegram) channels.push('telegram');
if (config.discord) channels.push('discord');
if (config.slack) channels.push('slack');
if (config.whatsapp) channels.push('whatsapp');
if (config.server?.port) {channels.push('webchat');}
if (config.telegram) {channels.push('telegram');}
if (config.discord) {channels.push('discord');}
if (config.slack) {channels.push('slack');}
if (config.whatsapp) {channels.push('whatsapp');}
lines.push(` Channels: ${channels.join(', ') || 'none'}`);
const embedding = config.memory?.embedding;
@@ -22,27 +22,27 @@ export function renderSummary(config: Record<string, any>): string {
const auto = config.automation ?? {};
const autoFeatures: string[] = [];
if (auto.cron?.length > 0) autoFeatures.push(`${auto.cron.length} cron jobs`);
if (auto.webhooks?.length > 0) autoFeatures.push('webhooks');
if (auto.gmail?.enabled) autoFeatures.push('gmail');
if (auto.gcal?.enabled) autoFeatures.push('gcal');
if (auto.gdocs?.enabled) autoFeatures.push('gdocs');
if (auto.gdrive?.enabled) autoFeatures.push('gdrive');
if (auto.gtasks?.enabled) autoFeatures.push('gtasks');
if (auto.heartbeat?.enabled) autoFeatures.push('heartbeat');
if (auto.cron?.length > 0) {autoFeatures.push(`${auto.cron.length} cron jobs`);}
if (auto.webhooks?.length > 0) {autoFeatures.push('webhooks');}
if (auto.gmail?.enabled) {autoFeatures.push('gmail');}
if (auto.gcal?.enabled) {autoFeatures.push('gcal');}
if (auto.gdocs?.enabled) {autoFeatures.push('gdocs');}
if (auto.gdrive?.enabled) {autoFeatures.push('gdrive');}
if (auto.gtasks?.enabled) {autoFeatures.push('gtasks');}
if (auto.heartbeat?.enabled) {autoFeatures.push('heartbeat');}
lines.push(` Automation: ${autoFeatures.join(', ') || 'none'}`);
const secFeatures: string[] = [];
secFeatures.push(`tools:${config.tools?.profile ?? 'full'}`);
if (config.sandbox?.enabled) secFeatures.push('sandbox');
if (config.pairing?.enabled) secFeatures.push('pairing');
if (config.sandbox?.enabled) {secFeatures.push('sandbox');}
if (config.pairing?.enabled) {secFeatures.push('pairing');}
lines.push(` Security: ${secFeatures.join(', ')}`);
const gw: string[] = [];
gw.push(`port ${config.server?.port ?? 18800}`);
if (config.server?.token) gw.push('auth');
if (config.server?.lock) gw.push('locked');
if (config.server?.tailscale?.serve) gw.push('tailscale');
if (config.server?.token) {gw.push('auth');}
if (config.server?.lock) {gw.push('locked');}
if (config.server?.tailscale?.serve) {gw.push('tailscale');}
lines.push(` Gateway: ${gw.join(', ')}`);
return lines.join('\n');
+3 -3
View File
@@ -21,7 +21,7 @@ export function getDataDir(): string {
*/
export function resolveOverlayPath(basePath: string): string | undefined {
const env = process.env.FLYNN_ENV;
if (!env) return undefined;
if (!env) {return undefined;}
const configDir = dirname(basePath);
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'];
function redact(obj: unknown): unknown {
if (obj === null || obj === undefined) return obj;
if (Array.isArray(obj)) return obj.map(redact);
if (obj === null || obj === undefined) {return obj;}
if (Array.isArray(obj)) {return obj.map(redact);}
if (typeof obj === 'object') {
const result: 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. */
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>);
if (entries.length === 0) return '';
if (entries.length === 0) {return '';}
const parts = entries.map(([key, value]) => {
if (typeof value === 'string') {
+1 -1
View File
@@ -14,7 +14,7 @@ export class Lifecycle {
}
async shutdown(): Promise<void> {
if (this.shuttingDown) return;
if (this.shuttingDown) {return;}
this.shuttingDown = true;
this._isRunning = false;
+1 -1
View File
@@ -57,7 +57,7 @@ export async function initMemory(deps: MemoryDeps): Promise<MemoryResult> {
}
const hash = contentHash(content);
if (vectorStore.hasContentHash(ns, hash)) continue;
if (vectorStore.hasContentHash(ns, hash)) {continue;}
const chunks = chunkText(content, ns, {
chunkSize: config.memory.embedding.chunk_size,
+5 -5
View File
@@ -12,7 +12,7 @@ function requireApiKey(cfg: ModelConfig, envVar: string): string {
if (!key) {
throw new Error(
`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;
@@ -118,7 +118,7 @@ export function anthropicToGitHubModel(anthropicModel: string): string | undefin
'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
// 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.
*/
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);
if (!githubModel) return undefined;
if (!githubModel) {return undefined;}
return new GitHubModelsClient({
model: githubModel,
@@ -202,7 +202,7 @@ export function createModelRouter(config: Config): ModelRouter {
const autoFallbackTiers: string[] = [];
for (const { tier, cfg } of tierConfigs) {
if (!cfg) continue;
if (!cfg) {continue;}
const fallbackList: ModelClient[] = [];
+1 -1
View File
@@ -92,7 +92,7 @@ export function loadSystemPrompt(config: Config, skillRegistry: SkillRegistry):
// ── Pairing Manager ─────────────────────────────────────────────
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 codeTtlMs = ttlMatch
+1 -1
View File
@@ -49,7 +49,7 @@ export function createTelegramBot(config: TelegramBotConfig): Bot {
});
await ctx.editMessageText(
ctx.callbackQuery.message?.text + `\n\n${parsed.approved ? '✅ Approved' : '❌ Denied'}`,
{ parse_mode: 'Markdown' }
{ parse_mode: 'Markdown' },
);
} else {
await ctx.answerCallbackQuery({ text: 'Confirmation expired or not found' });
+13 -13
View File
@@ -16,7 +16,7 @@ export type Command =
export function parseCommand(input: string): Command | null {
const trimmed = input.trim();
if (!trimmed) return null;
if (!trimmed) {return null;}
// Quit
if (trimmed === '/quit' || trimmed === '/exit') {
@@ -65,12 +65,12 @@ export function parseCommand(input: string): Command | null {
if (trimmed.startsWith('/model ')) {
const args = trimmed.slice('/model '.length).trim();
const parts = args.split(/\s+/);
// /model <tier> <provider/model> - change tier's provider/model
if (parts.length === 2 && parts[1].includes('/')) {
return { type: 'model', name: parts[0], providerModel: parts[1] };
}
// /model <name> - single word (backward compatibility)
const name = parts[0];
return { type: 'model', name };
@@ -205,12 +205,12 @@ export const MODEL_TOOLTIPS: Record<string, string> = {
export function getCommandCompletions(partial: string): string[] {
const trimmed = partial.trim();
// Complete /model <tier> <provider/model>
if (trimmed.startsWith('/model ')) {
const args = trimmed.slice('/model '.length).trim();
const parts = args.split(/\s+/);
if (parts.length === 1) {
// Single word - suggest model aliases
const modelPartial = parts[0].toLowerCase();
@@ -225,23 +225,23 @@ export function getCommandCompletions(partial: string): string[] {
.map(provider => `/model ${parts[0]} ${provider}`);
}
}
// Complete slash commands
if (trimmed.startsWith('/')) {
return SLASH_COMMANDS.filter(cmd => cmd.startsWith(trimmed.toLowerCase()));
}
return [];
}
export function getCommandTooltip(partial: string): string | null {
const trimmed = partial.trim().toLowerCase();
// Tooltip for /model arguments
if (trimmed.startsWith('/model ')) {
const args = trimmed.slice('/model '.length).trim();
const parts = args.split(/\s+/);
if (parts.length === 1) {
// Single word - model tier or provider
const modelArg = parts[0].toLowerCase();
@@ -261,15 +261,15 @@ export function getCommandTooltip(partial: string): string | null {
if (matches.length === 1) {
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)';
}
}
// Exact match tooltip
if (COMMAND_TOOLTIPS[trimmed]) {
return COMMAND_TOOLTIPS[trimmed];
}
// Partial match - show tooltip if only one command matches
if (trimmed.startsWith('/')) {
const matches = SLASH_COMMANDS.filter(cmd => cmd.startsWith(trimmed));
@@ -277,7 +277,7 @@ export function getCommandTooltip(partial: string): string | null {
return COMMAND_TOOLTIPS[matches[0]];
}
}
return null;
}
+6 -6
View File
@@ -21,9 +21,9 @@ function formatToolName(name: string): string {
/** Format tool args as a compact, readable summary. */
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>);
if (entries.length === 0) return '';
if (entries.length === 0) {return '';}
const parts = entries.map(([key, value]) => {
if (typeof value === 'string') {
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)
// with one that updates React state to show tool activity in the streaming area.
useEffect(() => {
if (!agent) return;
if (!agent) {return;}
const handleToolEvent = (event: ToolUseEvent) => {
if (event.type === 'start') {
@@ -137,7 +137,7 @@ export function App({
const handleSubmit = useCallback(async (value: string) => {
const command = parseCommand(value);
if (!command) return;
if (!command) {return;}
setInput('');
@@ -212,7 +212,7 @@ export function App({
return;
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);
setMessages(prev => [...prev, xferWithTs]);
return;
@@ -222,7 +222,7 @@ export function App({
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)
const userMessage: Message = { role: 'user', content: command.content };
+6 -6
View File
@@ -19,15 +19,15 @@ export const InputBar = memo(function InputBar({
placeholder = 'Type a message...',
}: InputBarProps): React.ReactElement {
const completions = useMemo(() => {
if (!value.startsWith('/')) return [];
if (!value.startsWith('/')) {return [];}
return getCommandCompletions(value);
}, [value]);
const tooltip = useMemo(() => {
if (!value.startsWith('/')) return null;
if (!value.startsWith('/')) {return null;}
return getCommandTooltip(value);
}, [value]);
const showTooltip = value.startsWith('/') && (tooltip || completions.length > 1);
return (
@@ -36,14 +36,14 @@ export const InputBar = memo(function InputBar({
{showTooltip && (
<Box paddingX={2} height={1} justifyContent="center">
<Text color="gray">
{tooltip
{tooltip
? `${tooltip}`
: `${completions.length} commands (Tab)`
}
</Text>
</Box>
)}
{/* Input bar */}
<Box borderStyle="single" borderColor="blue" paddingX={1}>
<Text color="blue">{'> '}</Text>
+12 -12
View File
@@ -19,26 +19,26 @@ function formatTimestamp(timestamp: number): string {
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if (seconds < 60) return 'just now';
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
if (days < 7) return `${days}d ago`;
if (seconds < 60) {return 'just now';}
if (minutes < 60) {return `${minutes}m ago`;}
if (hours < 24) {return `${hours}h ago`;}
if (days < 7) {return `${days}d ago`;}
return new Date(timestamp).toLocaleDateString([], { month: 'short', day: 'numeric' });
}
// Individual message component
const MessageItem = memo(function MessageItem({
message,
index
}: {
message: Message;
const MessageItem = memo(function MessageItem({
message,
index,
}: {
message: Message;
index: number;
}): React.ReactElement {
const isUser = message.role === 'user';
const accentColor = isUser ? 'blue' : '#ff8c00';
const timestampText = message.timestamp ? formatTimestamp(message.timestamp) : '';
return (
<Box
key={index}
@@ -59,7 +59,7 @@ const MessageItem = memo(function MessageItem({
</Text>
<Text color="gray">| {timestampText}</Text>
</Box>
{/* Content */}
<Text wrap="wrap">
{message.role === 'assistant'
+1 -1
View File
@@ -31,7 +31,7 @@ export async function startFullscreenTui(config: FullscreenTuiConfig): Promise<v
model: config.model,
agent: config.agent,
onExit: config.onExit,
})
}),
);
await waitUntilExit();
+7 -7
View File
@@ -91,10 +91,10 @@ const terminalRenderer: RendererObject = {
table({ header, rows, align }: Tokens.Table): string {
// Render cell contents
const headerTexts = header.map((cell: Tokens.TableCell) =>
this.parser.parseInline(cell.tokens)
this.parser.parseInline(cell.tokens),
);
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)
@@ -108,8 +108,8 @@ const terminalRenderer: RendererObject = {
const pad = (text: string, width: number, alignment: string | null) => {
const visible = stripAnsi(text).length;
const diff = width - visible;
if (diff <= 0) return text;
if (alignment === 'right') return ' '.repeat(diff) + text;
if (diff <= 0) {return text;}
if (alignment === 'right') {return ' '.repeat(diff) + text;}
if (alignment === 'center') {
const left = Math.floor(diff / 2);
return ' '.repeat(left) + text + ' '.repeat(diff - left);
@@ -119,7 +119,7 @@ const terminalRenderer: RendererObject = {
// Build header row
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(' │ ');
// Build separator
@@ -128,8 +128,8 @@ const terminalRenderer: RendererObject = {
// Build data rows
const dataRows = rowTexts.map((row: string[]) =>
' ' + row.map((cell: string, i: number) =>
pad(cell, colWidths[i], align[i])
).join(' │ ')
pad(cell, colWidths[i], align[i]),
).join(' │ '),
);
return [headerRow, separator, ...dataRows].join('\n') + '\n';
+5 -5
View File
@@ -63,21 +63,21 @@ export class MinimalTui {
const completions = getCommandCompletions(line);
const tooltip = getCommandTooltip(line);
let hint = '';
if (completions.length === 1 && completions[0] !== line) {
// Show the remaining part of the completion as a hint
hint = completions[0].slice(line.length);
}
// Add tooltip if available
if (tooltip) {
hint += ` ${colors.gray}${tooltip}${colors.reset}`;
} else if (completions.length > 1) {
hint += ` ${colors.gray}[${completions.length} options, Tab to complete]${colors.reset}`;
}
if (hint && hint !== this.currentHint) {
this.clearHint();
this.currentHint = hint;
@@ -467,7 +467,7 @@ export class MinimalTui {
fullContent += event.content;
}
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) {
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 {
if (!config.tailscaleIdentity) return undefined;
if (!config.tailscaleIdentity) {return undefined;}
const header = req.headers['tailscale-user-login'];
if (typeof header === 'string' && header.length > 0) {
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
const redact = (obj: Record<string, unknown> | undefined, ...keys: string[]) => {
if (!obj) return;
if (!obj) {return;}
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. */
const PATCHABLE_KEYS: Record<string, (config: Config, value: unknown) => boolean> = {
'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[];
return true;
},
'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[];
return true;
},
'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[];
return true;
},
'server.localhost': (config, value) => {
if (typeof value !== 'boolean') return false;
if (typeof value !== 'boolean') {return false;}
config.server.localhost = value;
return true;
},
+1 -1
View File
@@ -16,7 +16,7 @@ export function createSessionHandlers(deps: SessionHandlerDeps) {
id,
messageCount: deps.sessionManager.getSession(
id.split(':')[0],
id.split(':').slice(1).join(':')
id.split(':').slice(1).join(':'),
).getHistory().length,
}));
return makeResponse(request.id, { sessions });
+2 -2
View File
@@ -83,7 +83,7 @@ export class LaneQueue {
*/
cancel(laneId: string): void {
const lane = this.lanes.get(laneId);
if (!lane) return;
if (!lane) {return;}
const pending = lane.queue.splice(0);
for (const entry of pending) {
@@ -102,7 +102,7 @@ export class LaneQueue {
*/
private processNext(laneId: string): void {
const lane = this.lanes.get(laneId);
if (!lane) return;
if (!lane) {return;}
const entry = lane.queue.shift();
if (!entry) {
+2 -2
View File
@@ -109,7 +109,7 @@ export type OutboundMessage = GatewayResponse | GatewayError | GatewayEvent;
// ── Validation helpers ─────────────────────────────────────────
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>;
return (
typeof obj.id === 'number' &&
@@ -121,7 +121,7 @@ export function isValidRequest(msg: unknown): msg is GatewayRequest {
export function parseMessage(raw: string): GatewayRequest | null {
try {
const parsed = JSON.parse(raw);
if (isValidRequest(parsed)) return parsed;
if (isValidRequest(parsed)) {return parsed;}
return null;
} catch {
return null;
+2 -2
View File
@@ -327,7 +327,7 @@ export class GatewayServer {
if (uiDir) {
const served = await serveStatic(req, res, uiDir);
if (served) return;
if (served) {return;}
}
// 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
if (!request.params) request.params = {};
if (!request.params) {request.params = {};}
request.params.connectionId = connectionId;
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). */
switchSession(connectionId: string, sessionId: string): void {
const client = this.clients.get(connectionId);
if (!client) throw new Error(`Unknown connection: ${connectionId}`);
if (client.busy) throw new Error('Cannot switch session while agent is busy');
if (!client) {throw new Error(`Unknown connection: ${connectionId}`);}
if (client.busy) {throw new Error('Cannot switch session while agent is busy');}
const agent = this.getOrCreateAgent(sessionId);
client.sessionId = sessionId;
@@ -109,13 +109,13 @@ export class SessionBridge {
/** Mark a connection's agent as busy/idle. */
setBusy(connectionId: string, busy: boolean): void {
const client = this.clients.get(connectionId);
if (client) client.busy = busy;
if (client) {client.busy = busy;}
}
/** Set onToolUse callback for a connection's agent. */
setOnToolUse(connectionId: string, callback: ((event: ToolUseEvent) => void) | undefined): void {
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. */
@@ -159,7 +159,7 @@ export class SessionBridge {
const seen = new Set<string>();
for (const client of this.clients.values()) {
if (seen.has(client.sessionId)) continue;
if (seen.has(client.sessionId)) {continue;}
seen.add(client.sessionId);
const usage = client.agent.getUsage();
+2 -2
View File
@@ -65,12 +65,12 @@ export function initStatusIndicator() {
client.onStatusChange((status) => {
statusEl.textContent = status === 'connected' ? 'Connected' :
status === 'connecting' ? 'Connecting...' : 'Disconnected';
status === 'connecting' ? 'Connecting...' : 'Disconnected';
statusEl.className = `conn-status ${status}`;
});
// Set initial status
statusEl.textContent = client.status === 'connected' ? 'Connected' :
client.status === 'connecting' ? 'Connecting...' : 'Disconnected';
client.status === 'connecting' ? 'Connecting...' : 'Disconnected';
statusEl.className = `conn-status ${client.status}`;
}
+3 -3
View File
@@ -165,18 +165,18 @@ export class FlynnClient {
const handle = {
on(event, callback) {
if (!events.has(event)) events.set(event, []);
if (!events.has(event)) {events.set(event, []);}
events.get(event).push(callback);
return handle;
},
result: new Promise((resolve, reject) => {
// 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) => {
this._listeners.delete(id);
resolve(data);
});
if (!events.has('error')) events.set('error', []);
if (!events.has('error')) {events.set('error', []);}
events.get('error').push((data) => {
this._listeners.delete(id);
reject(new Error(data.message || 'Agent error'));
+22 -22
View File
@@ -133,7 +133,7 @@ function createMessageActions(role) {
copyBtn.innerHTML = COPY_ICON;
copyBtn.addEventListener('click', () => {
const msg = bar.closest('.message');
if (!msg) return;
if (!msg) {return;}
const text = getMessageText(msg);
navigator.clipboard.writeText(text).then(() => {
copyBtn.innerHTML = CHECK_ICON;
@@ -154,7 +154,7 @@ function createMessageActions(role) {
editBtn.innerHTML = EDIT_ICON;
editBtn.addEventListener('click', () => {
const msg = bar.closest('.message');
if (!msg) return;
if (!msg) {return;}
const text = getMessageText(msg);
const input = _elements.input;
if (input) {
@@ -177,7 +177,7 @@ function setSearchMode(active) {
_searchMode = active;
const btn = _elements.searchBtn;
const input = _elements.input;
if (!btn || !input) return;
if (!btn || !input) {return;}
if (active) {
btn.classList.add('active');
@@ -198,7 +198,7 @@ function getFilteredCommands(text) {
function showSlashPopup(filtered) {
const popup = _elements.slashPopup;
if (!popup) return;
if (!popup) {return;}
popup.innerHTML = '';
if (filtered.length === 0) {
@@ -225,13 +225,13 @@ function showSlashPopup(filtered) {
function hideSlashPopup() {
const popup = _elements.slashPopup;
if (popup) popup.classList.add('hidden');
if (popup) {popup.classList.add('hidden');}
_slashPopupIndex = -1;
}
function updatePopupSelection(filtered) {
const popup = _elements.slashPopup;
if (!popup) return;
if (!popup) {return;}
const items = popup.querySelectorAll('.slash-popup-item');
items.forEach((el, i) => {
el.classList.toggle('selected', i === _slashPopupIndex);
@@ -240,7 +240,7 @@ function updatePopupSelection(filtered) {
function selectSlashCommand(name) {
const input = _elements.input;
if (!input) return;
if (!input) {return;}
input.value = name;
hideSlashPopup();
input.focus();
@@ -248,14 +248,14 @@ function selectSlashCommand(name) {
function handleSlashPopupInput() {
const input = _elements.input;
if (!input) return;
if (!input) {return;}
const text = input.value;
// Show popup only when text starts with / and is at most a single word (the command itself)
if (text.startsWith('/') && !text.includes(' ')) {
const filtered = getFilteredCommands(text);
// Clamp selection index
if (_slashPopupIndex >= filtered.length) _slashPopupIndex = filtered.length - 1;
if (_slashPopupIndex >= filtered.length) {_slashPopupIndex = filtered.length - 1;}
showSlashPopup(filtered);
} else {
hideSlashPopup();
@@ -266,7 +266,7 @@ function handleSlashPopupInput() {
function parseSlashCommand(text) {
const trimmed = text.trim();
if (!trimmed.startsWith('/')) return null;
if (!trimmed.startsWith('/')) {return null;}
const parts = trimmed.split(/\s+/);
const cmd = parts[0].toLowerCase();
@@ -405,7 +405,7 @@ async function handleSlashCommand(cmd, client) {
async function loadSessions(client) {
const select = _elements.sessionSelect;
if (!select) return;
if (!select) {return;}
try {
const result = await client.call('sessions.list');
@@ -425,7 +425,7 @@ async function loadSessions(client) {
const opt = document.createElement('option');
opt.value = s.id;
opt.textContent = `${s.id} (${s.messageCount} msgs)`;
if (s.id === current) opt.selected = true;
if (s.id === current) {opt.selected = true;}
select.appendChild(opt);
}
}
@@ -439,7 +439,7 @@ async function loadSessions(client) {
async function loadHistory(client) {
const msgs = _elements.messages;
if (!msgs || !_currentSession) return;
if (!msgs || !_currentSession) {return;}
msgs.innerHTML = '';
@@ -464,21 +464,21 @@ async function loadHistory(client) {
async function sendMessage(client, overrideText) {
const input = _elements.input;
const rawText = overrideText ?? input?.value?.trim();
if (!rawText || _sending) return;
if (!rawText || _sending) {return;}
// Check for slash commands first
const cmd = parseSlashCommand(rawText);
if (cmd) {
if (!overrideText) input.value = '';
if (!overrideText) {input.value = '';}
hideSlashPopup();
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
}
_sending = true;
_elements.sendBtn.disabled = true;
if (!overrideText) input.value = '';
if (!overrideText) {input.value = '';}
// Apply search mode prefix
let messageText = rawText;
@@ -541,17 +541,17 @@ async function sendMessage(client, overrideText) {
placeholder.textContent = `Error: ${err.message}`;
} finally {
_sending = false;
if (_elements.sendBtn) _elements.sendBtn.disabled = false;
if (_elements.sendBtn) {_elements.sendBtn.disabled = false;}
scrollToBottom();
}
}
// ── 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 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 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 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 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>';
// ── Page Export ──────────────────────────────────────────────
+12 -12
View File
@@ -14,17 +14,17 @@ function formatUptime(seconds) {
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
const parts = [];
if (d > 0) parts.push(`${d}d`);
if (h > 0) parts.push(`${h}h`);
if (m > 0) parts.push(`${m}m`);
if (d > 0) {parts.push(`${d}d`);}
if (h > 0) {parts.push(`${h}h`);}
if (m > 0) {parts.push(`${m}m`);}
parts.push(`${s}s`);
return parts.join(' ');
}
function timeAgo(timestamp) {
const secs = Math.floor((Date.now() - timestamp) / 1000);
if (secs < 60) return `${secs}s ago`;
if (secs < 3600) return `${Math.floor(secs / 60)}m ago`;
if (secs < 60) {return `${secs}s ago`;}
if (secs < 3600) {return `${Math.floor(secs / 60)}m ago`;}
return `${Math.floor(secs / 3600)}h ago`;
}
@@ -76,7 +76,7 @@ function renderSkeleton(el) {
function updateCounters(metrics, health) {
const el = document.getElementById('ops-counters');
if (!el) return;
if (!el) {return;}
const sessions = health?.sessions ?? 0;
const errCount = metrics?.errors ?? 0;
@@ -94,13 +94,13 @@ function updateCounters(metrics, health) {
`<div class="stat-card">
<div class="stat-label">${c.label}</div>
<div class="stat-value ${c.cls}">${c.value}</div>
</div>`
</div>`,
).join('');
}
function updateModelTable(metrics) {
const el = document.getElementById('ops-model-table');
if (!el) return;
if (!el) {return;}
const mc = metrics?.modelCalls;
const calls = mc?.recentCalls ?? [];
@@ -155,7 +155,7 @@ function updateModelTable(metrics) {
function updateEvents(eventsData) {
const el = document.getElementById('ops-events');
if (!el) return;
if (!el) {return;}
const events = eventsData?.events ?? [];
@@ -180,7 +180,7 @@ function updateEvents(eventsData) {
function updateActiveRequests(requestsData) {
const el = document.getElementById('ops-requests');
if (!el) return;
if (!el) {return;}
const requests = requestsData?.requests ?? [];
@@ -219,7 +219,7 @@ function updateActiveRequests(requestsData) {
function updateChannels(channelsData) {
const el = document.getElementById('ops-channels');
if (!el) return;
if (!el) {return;}
const channels = channelsData?.channels ?? [];
@@ -232,7 +232,7 @@ function updateChannels(channelsData) {
`<div class="channel-card">
<span class="channel-dot ${ch.status}"></span>
<span class="channel-name">${escapeHtml(ch.name)}</span>
</div>`
</div>`,
).join('');
}
+3 -3
View File
@@ -14,11 +14,11 @@ let _client = null;
let _el = null;
async function loadSessionList() {
if (!_client || !_el) return;
if (!_client || !_el) {return;}
const listContainer = _el.querySelector('#sessions-list');
const detailContainer = _el.querySelector('#session-detail');
if (detailContainer) detailContainer.innerHTML = '';
if (detailContainer) {detailContainer.innerHTML = '';}
try {
const result = await _client.call('sessions.list');
@@ -78,7 +78,7 @@ async function loadSessionList() {
async function viewSession(sessionId) {
const detailContainer = _el.querySelector('#session-detail');
if (!detailContainer) return;
if (!detailContainer) {return;}
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;
async function loadSettings() {
if (!_client || !_el) return;
if (!_client || !_el) {return;}
let config, tools, channels;
@@ -154,7 +154,7 @@ async function saveHooks() {
// Clear status after 5s
setTimeout(() => {
if (status) status.textContent = '';
if (status) {status.textContent = '';}
}, 5000);
}
+5 -5
View File
@@ -13,14 +13,14 @@ function formatNumber(n) {
}
function formatCost(n) {
if (!n || n === 0) return '$0.00';
if (n < 0.01) return `$${n.toFixed(4)}`;
if (!n || n === 0) {return '$0.00';}
if (n < 0.01) {return `$${n.toFixed(4)}`;}
return `$${n.toFixed(2)}`;
}
function truncateId(id) {
if (!id) return '-';
if (id.length <= 24) return id;
if (!id) {return '-';}
if (id.length <= 24) {return id;}
return id.slice(0, 24) + '\u2026';
}
@@ -95,7 +95,7 @@ async function loadUsage(el, client) {
let delegationCell = '<span class="text-muted">-</span>';
if (delegationEntries.length > 0) {
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>');
}
+4 -4
View File
@@ -30,15 +30,15 @@ function shouldLog(level: LogLevel): boolean {
export const logger = {
debug(...args: unknown[]): void {
if (shouldLog('debug')) console.debug(...args);
if (shouldLog('debug')) {console.debug(...args);}
},
info(...args: unknown[]): void {
if (shouldLog('info')) console.log(...args);
if (shouldLog('info')) {console.log(...args);}
},
warn(...args: unknown[]): void {
if (shouldLog('warn')) console.warn(...args);
if (shouldLog('warn')) {console.warn(...args);}
},
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 {
const match = prefixedName.match(/^mcp:([^:]+):(.+)$/);
if (!match) return null;
if (!match) {return null;}
return { serverName: match[1], toolName: match[2] };
}
+3 -3
View File
@@ -86,7 +86,7 @@ export class McpManager {
*/
async stopServer(name: string): Promise<void> {
const client = this.clients.get(name);
if (!client) return;
if (!client) {return;}
// Unregister tools from the registry
const toolNames = this.registeredToolNames.get(name) ?? [];
@@ -133,7 +133,7 @@ export class McpManager {
*/
getServerState(name: string): McpServerState | undefined {
const client = this.clients.get(name);
if (!client) return undefined;
if (!client) {return undefined;}
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 name of toolNames) {
const tool = this.toolRegistry.get(name);
if (tool) tools.push(tool);
if (tool) {tools.push(tool);}
}
}
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);
if (magnitude === 0) return 0;
if (magnitude === 0) {return 0;}
return dotProduct / magnitude;
}
@@ -129,7 +129,7 @@ export class VectorStore {
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;
+4 -4
View File
@@ -89,10 +89,10 @@ export class BedrockClient implements ModelClient {
// Map stop reason
let stopReason: string = 'end_turn';
if (response.stopReason === 'max_tokens') stopReason = 'max_tokens';
else if (response.stopReason === 'tool_use') stopReason = 'tool_use';
else if (response.stopReason === 'end_turn') stopReason = 'end_turn';
else if (response.stopReason) stopReason = response.stopReason;
if (response.stopReason === 'max_tokens') {stopReason = 'max_tokens';}
else if (response.stopReason === 'tool_use') {stopReason = 'tool_use';}
else if (response.stopReason === 'end_turn') {stopReason = 'end_turn';}
else if (response.stopReason) {stopReason = response.stopReason;}
return {
content,
+2 -2
View File
@@ -27,12 +27,12 @@ function makeResponse(parts: unknown[], finishReason = 'STOP', usage = { promptT
usageMetadata: usage,
text: () => {
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('');
},
functionCalls: () => {
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);
},
},
+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).
*/
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)
const token = getGitHubToken();
@@ -85,7 +85,7 @@ export class GitHubModelsClient implements ModelClient {
if (this.onLoginRequired) {
const newToken = await this.onLoginRequired();
this.rebuildClient(newToken);
return;
}
// 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') {
const name = block.name as string;
const id = block.id as string;
if (id) toolNameMap.set(id, name);
if (id) {toolNameMap.set(id, name);}
let argsStr: string;
try {
argsStr = JSON.stringify(block.input);
@@ -321,7 +321,7 @@ export class LlamaCppClient implements ModelClient {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (done) {break;}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
@@ -329,10 +329,10 @@ export class LlamaCppClient implements ModelClient {
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith('data: ')) continue;
if (!trimmed || !trimmed.startsWith('data: ')) {continue;}
const data = trimmed.slice(6);
if (data === '[DONE]') continue;
if (data === '[DONE]') {continue;}
try {
const chunk = JSON.parse(data) as LlamaCppStreamChunk;
@@ -352,8 +352,8 @@ export class LlamaCppClient implements ModelClient {
});
}
const acc = toolCallAccumulators.get(tc.index)!;
if (tc.function?.name) acc.name = tc.function.name;
if (tc.function?.arguments) acc.arguments += tc.function.arguments;
if (tc.function?.name) {acc.name = tc.function.name;}
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') {
const name = block.name as string;
const id = block.id as string;
if (id) toolNameMap.set(id, name);
if (id) {toolNameMap.set(id, name);}
toolCalls.push({
function: {
name,
@@ -135,7 +135,7 @@ export class OllamaClient implements ModelClient {
* true (optimistic let the server decide).
*/
private async checkToolSupport(): Promise<boolean> {
if (this._supportsTools !== null) return this._supportsTools;
if (this._supportsTools !== null) {return this._supportsTools;}
try {
const info = await this.client.show({ model: this.model });
const caps: string[] = (info as any).capabilities ?? [];
+1 -1
View File
@@ -178,7 +178,7 @@ export function normalizeMessagesForLocal(
for (const msg of messages) {
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;
if (last && last.role === msg.role) {
+2 -2
View File
@@ -134,7 +134,7 @@ describe('withRetry', () => {
let callCount = 0;
const fn = vi.fn().mockImplementation(() => {
callCount++;
if (callCount < 3) return Promise.reject(new Error('fail'));
if (callCount < 3) {return Promise.reject(new Error('fail'));}
return Promise.resolve('ok');
});
@@ -166,7 +166,7 @@ describe('withRetry', () => {
const fn = vi.fn().mockImplementation(() => {
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');
});
+1 -1
View File
@@ -72,7 +72,7 @@ describe('ModelRouter', () => {
const response = await router.chat(
{ messages: [{ role: 'user', content: 'Hi' }] },
'fast'
'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);
if (config.fast) this.clients.set('fast', config.fast);
if (config.complex) this.clients.set('complex', config.complex);
if (config.local) this.clients.set('local', config.local);
if (config.fast) {this.clients.set('fast', config.fast);}
if (config.complex) {this.clients.set('complex', config.complex);}
if (config.local) {this.clients.set('local', config.local);}
if (config.labels) {
for (const tier of ['fast', 'default', 'complex', 'local'] as ModelTier[]) {
@@ -145,7 +145,7 @@ export class ModelRouter implements ModelClient {
yield event;
}
if (!hasError) return;
if (!hasError) {return;}
} else {
primaryError = 'Primary client does not support streaming';
}
@@ -154,7 +154,7 @@ export class ModelRouter implements ModelClient {
const tierFallbackList = this.tierFallbacks.get(useTier) ?? [];
for (let i = 0; i < tierFallbackList.length; i++) {
const fallbackClient = tierFallbackList[i];
if (!fallbackClient.chatStream) continue;
if (!fallbackClient.chatStream) {continue;}
const reason = `Primary model failed (${primaryError}), using tier fallback #${i + 1}`;
logger.debug(reason);
@@ -170,13 +170,13 @@ export class ModelRouter implements ModelClient {
yield event;
}
if (!hasError) return;
if (!hasError) {return;}
}
// Then try global fallback chain
for (let i = 0; i < this.fallbackChain.length; 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}`;
logger.debug(reason);
@@ -192,7 +192,7 @@ export class ModelRouter implements ModelClient {
yield event;
}
if (!hasError) return;
if (!hasError) {return;}
}
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. */
async destroy(): Promise<void> {
if (!this._containerId) return;
if (!this._containerId) {return;}
try {
await this.dockerCmd(['rm', '-f', this._containerId]);
@@ -98,8 +98,8 @@ export class DockerSandbox {
execFile('docker', ['version', '--format', '{{.Server.Version}}'], {
timeout: 5000,
}, (error, stdout) => {
if (error) reject(error);
else resolve(stdout);
if (error) {reject(error);}
else {resolve(stdout);}
});
});
return true;
+2 -2
View File
@@ -16,7 +16,7 @@ export class SandboxManager {
/** Get or create a sandbox for a session. */
async getOrCreate(sessionId: string): Promise<DockerSandbox> {
let sandbox = this.sandboxes.get(sessionId);
if (sandbox) return sandbox;
if (sandbox) {return sandbox;}
sandbox = new DockerSandbox({
sessionId,
@@ -36,7 +36,7 @@ export class SandboxManager {
/** Destroy a specific session's sandbox. */
async destroy(sessionId: string): Promise<void> {
const sandbox = this.sandboxes.get(sessionId);
if (!sandbox) return;
if (!sandbox) {return;}
await sandbox.destroy();
this.sandboxes.delete(sessionId);
+2 -2
View File
@@ -13,7 +13,7 @@ export class ManagedSession implements Session {
constructor(
public readonly id: string,
private store: SessionStore,
private history: Message[] = []
private history: Message[] = [],
) {}
addMessage(message: Message): Message {
@@ -76,7 +76,7 @@ export class SessionManager {
fromFrontend: string,
fromUserId: string,
toFrontend: string,
toUserId: string
toUserId: string,
): void {
const fromSession = this.getSession(fromFrontend, fromUserId);
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'. */
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)$/);
if (!match) return null;
if (!match) {return null;}
const [, n, unit] = match;
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 {
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);
}
getMessages(sessionId: string): Message[] {
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 }>;
return rows.map(row => ({
@@ -68,7 +68,7 @@ export class SessionStore {
this.db.prepare('DELETE FROM messages WHERE session_id = ?').run(sessionId);
// Re-insert in order
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) {
insert.run(sessionId, msg.role, msg.content);
@@ -96,7 +96,7 @@ export class SessionStore {
HAVING MAX(created_at) < ?
`).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 transaction = this.db.transaction(() => {
@@ -113,7 +113,7 @@ export class SessionStore {
return {
loadApproved: (): ApprovedSender[] => {
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 }>;
return rows.map(r => ({
channel: r.channel,
@@ -130,7 +130,7 @@ export class SessionStore {
},
removeApproved: (channel: string, senderId: string): void => {
this.db.prepare(
'DELETE FROM pairing_approved WHERE channel = ? AND sender_id = ?'
'DELETE FROM pairing_approved WHERE channel = ? AND sender_id = ?',
).run(channel, senderId);
},
};
+1 -1
View File
@@ -96,7 +96,7 @@ export class SkillInstaller {
const entries = readdirSync(this.managedDir, { withFileTypes: true });
return entries
.filter(entry => {
if (!entry.isDirectory()) return false;
if (!entry.isDirectory()) {return false;}
const skillMd = resolve(this.managedDir, entry.name, 'SKILL.md');
return existsSync(skillMd);
})
+1 -1
View File
@@ -13,7 +13,7 @@ export class SkillRegistry {
register(skill: Skill): void {
this.skills.set(skill.manifest.name, skill);
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 parts = [`- **${c.name}**`];
if (c.modelTier) parts.push(`tier=${c.modelTier}`);
if (c.toolProfile) parts.push(`profile=${c.toolProfile}`);
if (c.sandbox) parts.push('sandboxed');
if (c.modelTier) {parts.push(`tier=${c.modelTier}`);}
if (c.toolProfile) {parts.push(`profile=${c.toolProfile}`);}
if (c.sandbox) {parts.push('sandboxed');}
if (c.systemPrompt) {
const preview = c.systemPrompt.slice(0, 80).replace(/\n/g, ' ');
parts.push(`prompt="${preview}${c.systemPrompt.length > 80 ? '...' : ''}"`);
+2 -2
View File
@@ -31,7 +31,7 @@ function findChrome(): string {
];
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.');
@@ -120,7 +120,7 @@ export class BrowserManager {
async shutdown(): Promise<void> {
for (const [, page] of this.pages) {
try {
if (!page.isClosed()) await page.close();
if (!page.isClosed()) {await page.close();}
} catch { /* ignore */ }
}
this.pages.clear();
+1 -1
View File
@@ -23,7 +23,7 @@ export function createCronTools(scheduler: CronScheduler): Tool[] {
const lines = jobNames.map((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 {
+2 -2
View File
@@ -62,8 +62,8 @@ function setupValidAuth() {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockImplementation((path: unknown) => {
const p = String(path);
if (p.includes('creds')) return JSON.stringify(fakeCredentials);
if (p.includes('token')) return JSON.stringify(fakeToken);
if (p.includes('creds')) {return JSON.stringify(fakeCredentials);}
if (p.includes('token')) {return JSON.stringify(fakeToken);}
return '';
});
}
+3 -3
View File
@@ -101,9 +101,9 @@ function formatEvents(events: EventSummary[]): string {
return events
.map(e => {
const parts = [`[${e.id}] ${e.summary}`, ` Time: ${e.start}${e.end}`];
if (e.location) parts.push(` Location: ${e.location}`);
if (e.attendees.length > 0) parts.push(` Attendees: ${e.attendees.join(', ')}`);
if (e.htmlLink) parts.push(` Link: ${e.htmlLink}`);
if (e.location) {parts.push(` Location: ${e.location}`);}
if (e.attendees.length > 0) {parts.push(` Attendees: ${e.attendees.join(', ')}`);}
if (e.htmlLink) {parts.push(` Link: ${e.htmlLink}`);}
return parts.join('\n');
})
.join('\n\n');
+2 -2
View File
@@ -67,8 +67,8 @@ function setupValidAuth() {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockImplementation((path: unknown) => {
const p = String(path);
if (p.includes('creds')) return JSON.stringify(fakeCredentials);
if (p.includes('token')) return JSON.stringify(fakeToken);
if (p.includes('creds')) {return JSON.stringify(fakeCredentials);}
if (p.includes('token')) {return JSON.stringify(fakeToken);}
return '';
});
}
+3 -3
View File
@@ -67,8 +67,8 @@ function formatDocs(docs: DocSummary[]): string {
.map(d => {
const parts = [`[${d.id}] ${d.name}`];
parts.push(` Modified: ${d.modifiedTime}`);
if (d.owners.length > 0) parts.push(` Owners: ${d.owners.join(', ')}`);
if (d.webViewLink) parts.push(` Link: ${d.webViewLink}`);
if (d.owners.length > 0) {parts.push(` Owners: ${d.owners.join(', ')}`);}
if (d.webViewLink) {parts.push(` Link: ${d.webViewLink}`);}
return parts.join('\n');
})
.join('\n\n');
@@ -76,7 +76,7 @@ function formatDocs(docs: DocSummary[]): string {
/** Extract plain text from a Google Docs document body. */
function extractPlainText(body: import('googleapis').docs_v1.Schema$Body): string {
if (!body.content) return '';
if (!body.content) {return '';}
const parts: string[] = [];
for (const structural of body.content) {
+2 -2
View File
@@ -65,8 +65,8 @@ function setupValidAuth() {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockImplementation((path: unknown) => {
const p = String(path);
if (p.includes('creds')) return JSON.stringify(fakeCredentials);
if (p.includes('token')) return JSON.stringify(fakeToken);
if (p.includes('creds')) {return JSON.stringify(fakeCredentials);}
if (p.includes('token')) {return JSON.stringify(fakeToken);}
return '';
});
}
+7 -7
View File
@@ -75,11 +75,11 @@ function friendlyMimeType(mimeType: string): string {
/** Format file size in human-readable form. */
function formatSize(bytes: string | undefined): string {
if (!bytes) return '';
if (!bytes) {return '';}
const n = parseInt(bytes, 10);
if (isNaN(n)) return '';
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
if (isNaN(n)) {return '';}
if (n < 1024) {return `${n} B`;}
if (n < 1024 * 1024) {return `${(n / 1024).toFixed(1)} KB`;}
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
}
@@ -95,9 +95,9 @@ function formatFiles(files: FileSummary[]): string {
parts.push(` Type: ${friendlyMimeType(f.mimeType)}`);
parts.push(` Modified: ${f.modifiedTime}`);
const size = formatSize(f.size);
if (size) parts.push(` Size: ${size}`);
if (f.owners.length > 0) parts.push(` Owners: ${f.owners.join(', ')}`);
if (f.webViewLink) parts.push(` Link: ${f.webViewLink}`);
if (size) {parts.push(` Size: ${size}`);}
if (f.owners.length > 0) {parts.push(` Owners: ${f.owners.join(', ')}`);}
if (f.webViewLink) {parts.push(` Link: ${f.webViewLink}`);}
return parts.join('\n');
})
.join('\n\n');
+2 -2
View File
@@ -69,8 +69,8 @@ function setupValidAuth() {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockImplementation((path: unknown) => {
const p = String(path);
if (p.includes('creds')) return JSON.stringify(fakeCredentials);
if (p.includes('token')) return JSON.stringify(fakeToken);
if (p.includes('creds')) {return JSON.stringify(fakeCredentials);}
if (p.includes('token')) {return JSON.stringify(fakeToken);}
return '';
});
}
+5 -5
View File
@@ -120,7 +120,7 @@ function extractTextBody(payload: {
// Recurse into nested multipart
if (part.parts) {
const nested = extractTextBody(part as typeof payload);
if (nested) return nested;
if (nested) {return nested;}
}
}
if (htmlFallback) {
@@ -184,9 +184,9 @@ export function createGmailTools(config: NonNullable<GmailConfig>): Tool[] {
const emails: EmailSummary[] = [];
for (const msg of messages) {
if (!msg.id) continue;
if (!msg.id) {continue;}
const details = await fetchMessageDetails(gmail, msg.id);
if (details) emails.push(details);
if (details) {emails.push(details);}
}
return {
@@ -239,9 +239,9 @@ export function createGmailTools(config: NonNullable<GmailConfig>): Tool[] {
const emails: EmailSummary[] = [];
for (const msg of messages) {
if (!msg.id) continue;
if (!msg.id) {continue;}
const details = await fetchMessageDetails(gmail, msg.id);
if (details) emails.push(details);
if (details) {emails.push(details);}
}
return {
+2 -2
View File
@@ -65,8 +65,8 @@ function setupValidAuth() {
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockImplementation((path: unknown) => {
const p = String(path);
if (p.includes('creds')) return JSON.stringify(fakeCredentials);
if (p.includes('token')) return JSON.stringify(fakeToken);
if (p.includes('creds')) {return JSON.stringify(fakeCredentials);}
if (p.includes('token')) {return JSON.stringify(fakeToken);}
return '';
});
}
+2 -2
View File
@@ -86,8 +86,8 @@ function formatTasks(tasks: TaskSummary[]): string {
.map(t => {
const checkbox = t.status === 'completed' ? '[x]' : '[ ]';
const parts = [`${checkbox} ${t.title}`];
if (t.due) parts.push(` Due: ${t.due}`);
if (t.notes) parts.push(` Notes: ${t.notes}`);
if (t.due) {parts.push(` Due: ${t.due}`);}
if (t.notes) {parts.push(` Notes: ${t.notes}`);}
parts.push(` ID: ${t.id}`);
return parts.join('\n');
})
+42 -42
View File
@@ -7,7 +7,7 @@ describe('image.analyze tool', () => {
beforeEach(() => {
mockClient = {
chat: vi.fn()
chat: vi.fn(),
};
});
@@ -25,7 +25,7 @@ describe('image.analyze tool', () => {
mockClient.chat = vi.fn().mockResolvedValueOnce({
content: 'This is a beautiful sunset over the ocean.',
stopReason: 'end_turn',
usage: { inputTokens: 100, outputTokens: 50 }
usage: { inputTokens: 100, outputTokens: 50 },
});
const tool = createImageAnalyzeTool(mockClient);
@@ -46,15 +46,15 @@ describe('image.analyze tool', () => {
source: expect.objectContaining({
type: 'url',
media_type: 'image/jpeg',
url: 'https://example.com/image.jpg'
})
}
])
})
url: 'https://example.com/image.jpg',
}),
},
]),
}),
]),
system: expect.stringContaining('vision assistant'),
maxTokens: 1024
})
maxTokens: 1024,
}),
);
});
@@ -63,13 +63,13 @@ describe('image.analyze tool', () => {
mockClient.chat = vi.fn().mockResolvedValueOnce({
content: 'This is a sample image.',
stopReason: 'end_turn',
usage: { inputTokens: 100, outputTokens: 20 }
usage: { inputTokens: 100, outputTokens: 20 },
});
const tool = createImageAnalyzeTool(mockClient);
const result = await tool.execute({
data: base64Data,
media_type: 'image/png'
media_type: 'image/png',
});
expect(result.success).toBe(true);
@@ -87,15 +87,15 @@ describe('image.analyze tool', () => {
source: expect.objectContaining({
type: 'base64',
media_type: 'image/png',
data: base64Data
})
}
])
})
data: base64Data,
}),
},
]),
}),
]),
system: expect.stringContaining('vision assistant'),
maxTokens: 1024
})
maxTokens: 1024,
}),
);
});
@@ -103,13 +103,13 @@ describe('image.analyze tool', () => {
mockClient.chat = vi.fn().mockResolvedValueOnce({
content: 'The image shows a cat sitting on a mat.',
stopReason: 'end_turn',
usage: { inputTokens: 100, outputTokens: 30 }
usage: { inputTokens: 100, outputTokens: 30 },
});
const tool = createImageAnalyzeTool(mockClient);
const result = await tool.execute({
url: 'https://example.com/cat.jpg',
prompt: 'What is in this image?'
prompt: 'What is in this image?',
});
expect(result.success).toBe(true);
@@ -120,11 +120,11 @@ describe('image.analyze tool', () => {
expect.objectContaining({
content: expect.arrayContaining([
{ 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({
content: 'This is the default prompt response.',
stopReason: 'end_turn',
usage: { inputTokens: 100, outputTokens: 10 }
usage: { inputTokens: 100, outputTokens: 10 },
});
const tool = createImageAnalyzeTool(mockClient);
@@ -144,11 +144,11 @@ describe('image.analyze tool', () => {
expect.objectContaining({
content: expect.arrayContaining([
{ 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 result = await tool.execute({
url: 'https://example.com/image.jpg',
data: 'base64data'
data: 'base64data',
});
expect(result.success).toBe(false);
@@ -177,7 +177,7 @@ describe('image.analyze tool', () => {
const tool = createImageAnalyzeTool(mockClient);
const result = await tool.execute({
data: 'base64data',
prompt: 'Test'
prompt: 'Test',
});
expect(result.success).toBe(false);
@@ -189,7 +189,7 @@ describe('image.analyze tool', () => {
const tool = createImageAnalyzeTool(mockClient);
const result = await tool.execute({
data: 'base64data',
media_type: 'image/tiff'
media_type: 'image/tiff',
});
expect(result.success).toBe(false);
@@ -204,13 +204,13 @@ describe('image.analyze tool', () => {
mockClient.chat = vi.fn().mockResolvedValueOnce({
content: 'Success',
stopReason: 'end_turn',
usage: { inputTokens: 10, outputTokens: 10 }
usage: { inputTokens: 10, outputTokens: 10 },
});
const tool = createImageAnalyzeTool(mockClient);
const result = await tool.execute({
data: 'base64data',
media_type: mediaType
media_type: mediaType,
});
expect(result.success).toBe(true);
@@ -245,12 +245,12 @@ describe('image.analyze tool', () => {
const mockRequest = {
messages: [] as any,
system: '',
maxTokens: 1024
maxTokens: 1024,
};
const mockResponse = {
content: 'Analysis complete.',
stopReason: 'end_turn',
usage: { inputTokens: 100, outputTokens: 10 }
usage: { inputTokens: 100, outputTokens: 10 },
};
mockClient.chat = vi.fn().mockResolvedValue(mockResponse).mockImplementationOnce(async (r) => {
@@ -260,7 +260,7 @@ describe('image.analyze tool', () => {
const tool = createImageAnalyzeTool(mockClient);
await tool.execute({
url: 'https://example.com/image.jpg',
prompt: 'Analyze the colors.'
prompt: 'Analyze the colors.',
});
const callArgs = (mockClient.chat as any).mock.calls[0][0];
@@ -272,12 +272,12 @@ describe('image.analyze tool', () => {
const mockRequest = {
messages: [] as any,
system: '',
maxTokens: 1024
maxTokens: 1024,
};
const mockResponse = {
content: 'Short response',
stopReason: 'end_turn',
usage: { inputTokens: 10, outputTokens: 10 }
usage: { inputTokens: 10, outputTokens: 10 },
};
mockClient.chat = vi.fn().mockResolvedValueOnce(mockResponse);
@@ -294,7 +294,7 @@ describe('image.analyze tool', () => {
mockClient.chat.mockResolvedValueOnce({
content: expectedContent,
stopReason: 'end_turn',
usage: { inputTokens: 100, outputTokens: 100 }
usage: { inputTokens: 100, outputTokens: 100 },
});
const tool = createImageAnalyzeTool(mockClient);
+24 -24
View File
@@ -21,24 +21,24 @@ export function createImageAnalyzeTool(modelClient: ModelClient): Tool {
properties: {
url: {
type: 'string',
description: 'URL of the image to analyze'
description: 'URL of the image to analyze',
},
data: {
type: 'string',
description: 'Base64-encoded image data (alternative to url)'
description: 'Base64-encoded image data (alternative to url)',
},
media_type: {
type: 'string',
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: {
type: 'string',
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> => {
try {
@@ -48,7 +48,7 @@ export function createImageAnalyzeTool(modelClient: ModelClient): Tool {
return {
success: false,
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 {
success: false,
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 {
success: false,
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 {
success: false,
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
? {
type: 'url' as const,
media_type: args.media_type || 'image/jpeg',
url: args.url
}
type: 'url' as const,
media_type: args.media_type || 'image/jpeg',
url: args.url,
}
: {
type: 'base64' as const,
media_type: args.media_type!,
data: args.data
};
type: 'base64' as const,
media_type: args.media_type!,
data: args.data,
};
const message = {
role: 'user' as const,
content: [
{ type: 'text' as const, text: prompt },
{ type: 'image' as const, source: imageSource }
]
{ type: 'image' as const, source: imageSource },
],
};
const response = await modelClient.chat({
messages: [message],
system:
'You are a vision assistant. Analyze the provided image according to the user\'s request. Provide detailed, helpful descriptions.',
maxTokens: 1024
maxTokens: 1024,
});
return {
success: true,
output: response.content
output: response.content,
};
} catch (error) {
return {
success: false,
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 sourceLabel = result.source === 'both' ? 'keyword+vector'
: result.source === 'vector' ? 'vector'
: 'keyword';
: 'keyword';
return `[${result.namespace}:${result.line}] (${sourceLabel}, score: ${result.score.toFixed(3)}) ${result.content}\n context: ${result.context}`;
}).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
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');
return {
+1 -1
View File
@@ -21,7 +21,7 @@ export function createProcessListTool(manager: ProcessManager): Tool {
? `${Math.round((Date.now() - p.startedAt) / 1000)}s`
: 'N/A';
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;
});
+1 -1
View File
@@ -204,7 +204,7 @@ describe('Process tools', () => {
let manager: ProcessManager;
afterEach(async () => {
if (manager) await manager.shutdown();
if (manager) {await manager.shutdown();}
});
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(): string {
if (this.length === 0) return '';
if (this.length === 0) {return '';}
if (this.length < this.capacity) {
// 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. */
getOutput(id: string): string {
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();
}
/** Kill a process by sending a signal. Returns true if signal was sent. */
kill(id: string, signal: NodeJS.Signals = 'SIGTERM'): boolean {
const proc = this.processes.get(id);
if (!proc) throw new Error(`Process ${id} not found`);
if (proc.status !== 'running') return false;
if (!proc) {throw new Error(`Process ${id} not found`);}
if (proc.status !== 'running') {return false;}
const child = this.childProcesses.get(id);
if (child) {
@@ -229,7 +229,7 @@ export class ProcessManager {
runningCount(): number {
let count = 0;
for (const proc of this.processes.values()) {
if (proc.status === 'running') count++;
if (proc.status === 'running') {count++;}
}
return count;
}
@@ -254,7 +254,7 @@ export class ProcessManager {
}
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)...`);
+3 -3
View File
@@ -32,9 +32,9 @@ export function createProcessStatusTool(manager: ProcessManager): Tool {
info += `PID: ${proc.pid}\n`;
info += `Status: ${proc.status}\n`;
info += `Uptime: ${uptime}\n`;
if (proc.exitCode !== undefined) info += `Exit code: ${proc.exitCode}\n`;
if (proc.errorMessage) info += `Error: ${proc.errorMessage}\n`;
if (proc.cwd) info += `CWD: ${proc.cwd}\n`;
if (proc.exitCode !== undefined) {info += `Exit code: ${proc.exitCode}\n`;}
if (proc.errorMessage) {info += `Error: ${proc.errorMessage}\n`;}
if (proc.cwd) {info += `CWD: ${proc.cwd}\n`;}
return { success: true, output: info.trimEnd() };
},
+1 -1
View File
@@ -61,7 +61,7 @@ export class ToolExecutor {
const result = await Promise.race([
tool.execute(args),
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. */
filteredList(context?: ToolPolicyContext): Tool[] {
if (!this._policy) return this.list();
if (!this._policy) {return this.list();}
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 {
// Named entity
const named = NAMED_ENTITIES[entity.toLowerCase()];
if (named) return named;
if (named) {return named;}
// Decimal numeric entity: &#NNN;
const decMatch = entity.match(/^&#(\d+);$/);
@@ -52,7 +52,7 @@ function decodeEntity(entity: string): string {
* @returns Clean plain text
*/
export function sanitizeHtml(text: string): string {
if (!text) return '';
if (!text) {return '';}
let result = text;