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:
+1
-1
@@ -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();
|
||||
|
||||
@@ -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()}`,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -31,7 +31,7 @@ export async function startFullscreenTui(config: FullscreenTuiConfig): Promise<v
|
||||
model: config.model,
|
||||
agent: config.agent,
|
||||
onExit: config.onExit,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
await waitUntilExit();
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
|
||||
@@ -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>';
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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') };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
})
|
||||
|
||||
@@ -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'})`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ? '...' : ''}"`);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 '';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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)...`);
|
||||
|
||||
|
||||
@@ -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() };
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user