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