feat(FN-1800): merge fusion/fn-1800

This commit is contained in:
gsxdsm
2026-04-15 04:45:56 -07:00
parent 0726254664
commit a7d966feb0
15 changed files with 175 additions and 146 deletions

View File

@@ -41,8 +41,6 @@ const promptCatalogReadyPromise = initPromptCatalog();
// Dynamic import for @fusion/engine to avoid resolution issues in test environment
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AgentResult = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let createKbAgent: any;
// Initialize the import (this runs in actual server, mocked in tests)

View File

@@ -16,8 +16,6 @@ import { resolvePrompt } from "@fusion/core";
// Dynamic import for @fusion/engine to avoid resolution issues in test environment
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AgentResult = any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let createKbAgent: any;
// Initialize the import (this runs in actual server, mocked in tests)

View File

@@ -390,8 +390,8 @@ export class AiSessionStore extends EventEmitter<AiSessionStoreEvents> {
`UPDATE ai_sessions SET status = 'awaiting_input', updatedAt = ?
WHERE status = 'generating' AND currentQuestion IS NOT NULL`,
)
.run(now);
recovered += Number((withQuestion as any).changes ?? 0);
.run(now) as { changes?: number };
recovered += Number(withQuestion.changes ?? 0);
// Sessions that were generating with no question — unrecoverable
const withoutQuestion = this.db
@@ -399,8 +399,8 @@ export class AiSessionStore extends EventEmitter<AiSessionStoreEvents> {
`UPDATE ai_sessions SET status = 'error', error = 'Session interrupted — please restart', updatedAt = ?
WHERE status = 'generating' AND currentQuestion IS NULL`,
)
.run(now);
recovered += Number((withoutQuestion as any).changes ?? 0);
.run(now) as { changes?: number };
recovered += Number(withoutQuestion.changes ?? 0);
if (recovered > 0) {
console.log(`[ai-session-store] Recovered ${recovered} stale sessions after restart`);

View File

@@ -19,8 +19,8 @@ import type {
} from "@fusion/core";
import { summarizeTitle } from "@fusion/core";
import { EventEmitter } from "node:events";
import type { Response } from "express";
import { SessionEventBuffer, writeSSEEvent, safeWriteSSE, formatSSEEvent } from "./sse-buffer.js";
import { SessionEventBuffer } from "./sse-buffer.js";
// Dynamic import for @fusion/engine to avoid resolution issues in test environment
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -297,13 +297,13 @@ export class ChatManager {
}
// Persist user message
let userMessageId: string;
let _userMessageId: string;
try {
const userMessage = this.chatStore.addMessage(sessionId, {
role: "user",
content,
});
userMessageId = userMessage.id;
_userMessageId = userMessage.id;
} catch (err) {
chatStreamManager.broadcast(sessionId, {
type: "error",
@@ -332,7 +332,7 @@ export class ChatManager {
if (title) {
this.chatStore.updateSession(sessionId, { title });
}
} catch (err) {
} catch {
// Fallback on any error
const fallback = content.trim().slice(0, 60).trim();
if (fallback) {

View File

@@ -1,8 +1,6 @@
import { join, resolve, relative, dirname, basename } from "node:path";
import { readdir, readFile as fsReadFile, writeFile as fsWriteFile, stat, copyFile as fsCopyFile, rename as fsRename, rm as fsRm, mkdir } from "node:fs/promises";
import { existsSync, createReadStream, statSync } from "node:fs";
import { pipeline } from "node:stream/promises";
import { createGzip } from "node:zlib";
import { existsSync } from "node:fs";
import type { TaskStore } from "@fusion/core";
/**
@@ -67,30 +65,6 @@ export class FileServiceError extends Error {
}
}
/**
* Text file extensions set.
*/
const TEXT_EXTENSIONS = new Set([
".txt", ".md", ".markdown",
".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs",
".json", ".jsonc",
".css", ".scss", ".sass", ".less",
".html", ".htm", ".xml", ".svg",
".yaml", ".yml",
".toml",
".ini", ".cfg", ".conf", ".config",
".sh", ".bash", ".zsh", ".fish",
".py", ".rb", ".php", ".pl", ".perl",
".java", ".kt", ".scala", ".groovy",
".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".hh",
".cs", ".fs", ".fsx",
".go", ".rs", ".swift",
".sql",
".dockerfile", ".env", ".envrc", ".nvmrc",
".gitignore", ".gitattributes", ".editorconfig",
".lock", ".log",
]);
export type WorkspaceId = "project" | string;
/**
@@ -107,8 +81,9 @@ async function getTaskBasePath(store: TaskStore, taskId: string): Promise<string
// Fall back to task directory
const rootDir = store.getRootDir();
return resolve(join(rootDir, ".fusion", "tasks", taskId));
} catch (err: any) {
if (err.code === "ENOENT" || err.message?.includes("not found")) {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT" || (error.message && error.message.includes("not found"))) {
throw new FileServiceError(`Task ${taskId} not found`, "ENOTASK");
}
throw err;
@@ -181,8 +156,9 @@ async function listFilesForBasePath(basePath: string, subPath?: string): Promise
let stats;
try {
stats = await stat(targetPath);
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError(`Directory not found: ${subPath || "."}`, "ENOENT");
}
throw err;
@@ -222,11 +198,12 @@ async function listFilesForBasePath(basePath: string, subPath?: string): Promise
path: relativeBase || ".",
entries: fileNodes,
};
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError(`Directory not found: ${subPath || "."}`, "ENOENT");
}
if (err.code === "EACCES" || err.code === "EPERM") {
if (error.code === "EACCES" || error.code === "EPERM") {
throw new FileServiceError(`Permission denied: ${subPath || "."}`, "EACCES");
}
throw err;
@@ -243,8 +220,9 @@ async function readFileForBasePath(basePath: string, filePath: string): Promise<
let stats;
try {
stats = await stat(resolvedPath);
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError(`File not found: ${filePath}`, "ENOENT");
}
throw err;
@@ -266,11 +244,12 @@ async function readFileForBasePath(basePath: string, filePath: string): Promise<
mtime: stats.mtime.toISOString(),
size: stats.size,
};
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError(`File not found: ${filePath}`, "ENOENT");
}
if (err.code === "EACCES" || err.code === "EPERM") {
if (error.code === "EACCES" || error.code === "EPERM") {
throw new FileServiceError(`Permission denied: ${filePath}`, "EACCES");
}
throw err;
@@ -294,8 +273,9 @@ async function writeFileForBasePath(basePath: string, filePath: string, content:
if (stats.isDirectory()) {
throw new FileServiceError(`Cannot write to directory: ${filePath}`, "EISDIR");
}
} catch (err: any) {
if (err.code !== "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code !== "ENOENT") {
throw err;
}
}
@@ -306,8 +286,9 @@ async function writeFileForBasePath(basePath: string, filePath: string, content:
if (!parentStats.isDirectory()) {
throw new FileServiceError(`Parent is not a directory: ${filePath}`, "ENOENT");
}
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError(`Parent directory does not exist: ${filePath}`, "ENOENT");
}
throw err;
@@ -322,11 +303,12 @@ async function writeFileForBasePath(basePath: string, filePath: string, content:
mtime: stats.mtime.toISOString(),
size: stats.size,
};
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError(`Parent directory does not exist: ${filePath}`, "ENOENT");
}
if (err.code === "EACCES" || err.code === "EPERM") {
if (error.code === "EACCES" || error.code === "EPERM") {
throw new FileServiceError(`Permission denied: ${filePath}`, "EACCES");
}
throw err;
@@ -527,8 +509,9 @@ export async function copyWorkspaceFile(
let sourceStats;
try {
sourceStats = await stat(resolvedSource);
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError(`Source not found: ${sourcePath}`, "ENOENT");
}
throw err;
@@ -538,8 +521,9 @@ export async function copyWorkspaceFile(
try {
await stat(resolvedDest);
throw new FileServiceError(`Destination already exists: ${destinationPath}`, "EEXIST");
} catch (err: any) {
if (err.code !== "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code !== "ENOENT") {
if (err instanceof FileServiceError) throw err;
}
// ENOENT is expected - destination should not exist
@@ -552,8 +536,9 @@ export async function copyWorkspaceFile(
if (!parentStats.isDirectory()) {
throw new FileServiceError("Destination parent is not a directory", "ENOTDIR");
}
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError("Destination parent directory does not exist", "ENOENT");
}
throw err;
@@ -566,9 +551,10 @@ export async function copyWorkspaceFile(
await copyDirectoryRecursive(resolvedSource, resolvedDest);
}
return { success: true, message: `Copied "${sourcePath}" to "${destinationPath}"` };
} catch (err: any) {
if (err.code === "EACCES" || err.code === "EPERM") {
throw new FileServiceError(`Permission denied: ${err.message}`, "EACCES");
} catch (err: unknown) {
const error = err as Error & { code?: string; message?: string };
if (error.code === "EACCES" || error.code === "EPERM") {
throw new FileServiceError(`Permission denied: ${error.message}`, "EACCES");
}
throw err;
}
@@ -603,8 +589,9 @@ export async function moveWorkspaceFile(
// Verify source exists
try {
await stat(resolvedSource);
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError(`Source not found: ${sourcePath}`, "ENOENT");
}
throw err;
@@ -614,8 +601,9 @@ export async function moveWorkspaceFile(
try {
await stat(resolvedDest);
throw new FileServiceError(`Destination already exists: ${destinationPath}`, "EEXIST");
} catch (err: any) {
if (err.code !== "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code !== "ENOENT") {
if (err instanceof FileServiceError) throw err;
}
}
@@ -627,8 +615,9 @@ export async function moveWorkspaceFile(
if (!parentStats.isDirectory()) {
throw new FileServiceError("Destination parent is not a directory", "ENOTDIR");
}
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError("Destination parent directory does not exist", "ENOENT");
}
throw err;
@@ -637,11 +626,12 @@ export async function moveWorkspaceFile(
try {
await fsRename(resolvedSource, resolvedDest);
return { success: true, message: `Moved "${sourcePath}" to "${destinationPath}"` };
} catch (err: any) {
if (err.code === "EACCES" || err.code === "EPERM") {
throw new FileServiceError(`Permission denied: ${err.message}`, "EACCES");
} catch (err: unknown) {
const error = err as Error & { code?: string; message?: string };
if (error.code === "EACCES" || error.code === "EPERM") {
throw new FileServiceError(`Permission denied: ${error.message}`, "EACCES");
}
if (err.code === "EXDEV") {
if (error.code === "EXDEV") {
// Cross-device move: copy then delete
await copyWorkspaceFile(store, workspace, sourcePath, destinationPath);
await deleteWorkspaceFile(store, workspace, sourcePath);
@@ -683,8 +673,9 @@ export async function deleteWorkspaceFile(
let stats;
try {
stats = await stat(resolvedPath);
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError(`Not found: ${filePath}`, "ENOENT");
}
throw err;
@@ -697,8 +688,9 @@ export async function deleteWorkspaceFile(
await fsRm(resolvedPath);
}
return { success: true, message: `Deleted "${filePath}"` };
} catch (err: any) {
if (err.code === "EACCES" || err.code === "EPERM") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "EACCES" || error.code === "EPERM") {
throw new FileServiceError(`Permission denied: ${filePath}`, "EACCES");
}
throw err;
@@ -746,8 +738,9 @@ export async function renameWorkspaceFile(
// Verify source exists
try {
await stat(resolvedPath);
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError(`Not found: ${filePath}`, "ENOENT");
}
throw err;
@@ -770,8 +763,9 @@ export async function renameWorkspaceFile(
try {
await stat(destPath);
throw new FileServiceError(`A file or directory named "${newName}" already exists`, "EEXIST");
} catch (err: any) {
if (err.code !== "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code !== "ENOENT") {
if (err instanceof FileServiceError) throw err;
}
}
@@ -779,8 +773,9 @@ export async function renameWorkspaceFile(
try {
await fsRename(resolvedPath, destPath);
return { success: true, message: `Renamed to "${newName}"` };
} catch (err: any) {
if (err.code === "EACCES" || err.code === "EPERM") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "EACCES" || error.code === "EPERM") {
throw new FileServiceError(`Permission denied: ${filePath}`, "EACCES");
}
throw err;
@@ -818,8 +813,9 @@ export async function getWorkspaceFileForDownload(
let stats;
try {
stats = await stat(resolvedPath);
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError(`File not found: ${filePath}`, "ENOENT");
}
throw err;
@@ -870,8 +866,9 @@ export async function getWorkspaceFolderForZip(
let stats;
try {
stats = await stat(resolvedPath);
} catch (err: any) {
if (err.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
throw new FileServiceError(`Directory not found: ${dirPath}`, "ENOENT");
}
throw err;

View File

@@ -29,7 +29,7 @@ export interface GitHubPollingServiceOptions {
rateLimiter?: GitHubRateLimiter;
}
export interface GitHubPollingServiceEvents {}
export type GitHubPollingServiceEvents = Record<string, never>;
interface RepoBatchConsumer {
taskId: string;
@@ -315,8 +315,9 @@ export class GitHubPollingService extends EventEmitter<GitHubPollingServiceEvent
let task;
try {
task = await this.store.getTask(taskId);
} catch (err: any) {
if (err?.code === "ENOENT") {
} catch (err: unknown) {
const error = err as Error & { code?: string };
if (error.code === "ENOENT") {
this.unwatchTask(taskId);
}
return;

View File

@@ -1,7 +1,6 @@
import { createHmac, timingSafeEqual } from "node:crypto";
import { readFileSync } from "node:fs";
import type { IssueInfo, PrInfo } from "@fusion/core";
import { GitHubClient } from "./github.js";
// Module-level cache for the GitHub App private key
// undefined = not yet read, null = read failed, string = cached key

View File

@@ -21,7 +21,6 @@ import { EventEmitter } from "node:events";
import type { AiSessionStore, AiSessionRow } from "./ai-session-store.js";
import { SessionEventBuffer, type SessionBufferedEvent } from "./sse-buffer.js";
import {
parseMissionAgentResponse,
extractJsonCandidate,
repairJson,
} from "./mission-interview.js";
@@ -48,7 +47,7 @@ function parseTargetInterviewResponseImpl(text: string): TargetInterviewResponse
let parsed: unknown;
try {
parsed = JSON.parse(candidate);
} catch (parseErr) {
} catch (_parseErr) {
try {
const repaired = repairJson(candidate);
parsed = JSON.parse(repaired);

View File

@@ -19,7 +19,6 @@ import { Router, type Request, type Response } from "express";
import { existsSync, statSync } from "node:fs";
import { join, isAbsolute, dirname, basename } from "node:path";
import type {
PluginInstallation,
PluginLoader,
PluginStore,
PluginContext,

View File

@@ -11,10 +11,10 @@ import { createSSE } from "./sse.js";
import { rateLimit, RATE_LIMITS } from "./rate-limit.js";
import { ApiError, sendErrorResponse } from "./api-error.js";
import { getOrCreateProjectStore, evictAllProjectStores, setOnProjectFirstCreated } from "./project-store-resolver.js";
import { getTerminalService, type TerminalSession, STALE_SESSION_THRESHOLD_MS } from "./terminal-service.js";
import { getTerminalService, STALE_SESSION_THRESHOLD_MS } from "./terminal-service.js";
import { WebSocketServer, type WebSocket } from "ws";
import { terminalSessionManager } from "./terminal.js";
import { parseBadgeUrl } from "./github.js";
import { WebSocketManager, type BadgeSnapshot } from "./websocket.js";
import type { BadgePubSub } from "./badge-pubsub.js";
import { createBadgePubSub, type BadgePubSubMessage } from "./badge-pubsub.js";
@@ -361,8 +361,8 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
createSSE(scopedStore, scopedStore.getMissionStore(), aiSessionStore, scopedStore.getPluginStore(), {
projectId,
})(req, res);
} catch (err: any) {
sendErrorResponse(res, 500, err.message ?? "Failed to open project event stream");
} catch (err: unknown) {
sendErrorResponse(res, 500, err instanceof Error ? err.message : "Failed to open project event stream");
}
});
@@ -397,7 +397,7 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
let scopedStore: TaskStore;
try {
scopedStore = await resolveProjectScopedStore(projectId);
} catch (err) {
} catch {
res.write(`event: error\ndata: ${JSON.stringify({ message: "Failed to resolve project store" })}\n\n`);
res.end();
return;

View File

@@ -104,7 +104,7 @@ export interface PluginLifecyclePayload {
function mapSourceEventToTransition(
sourceEvent: string,
plugin: PluginInstallation,
previousState?: PluginState,
_previousState?: PluginState,
): PluginLifecycleTransition {
switch (sourceEvent) {
case "plugin:registered":
@@ -198,7 +198,7 @@ export function createSSE(
};
// --- Event handler definitions ---
/* eslint-disable @typescript-eslint/no-explicit-any -- EventEmitter handlers receive untyped event data */
const onCreated = (task: any) => {
send(`event: task:created\ndata: ${JSON.stringify(stripTaskListHeavyFields(task))}\n\n`);
};
@@ -287,6 +287,8 @@ export function createSSE(
send(`event: ai_session:deleted\ndata: ${JSON.stringify(data)}\n\n`);
};
/* eslint-enable @typescript-eslint/no-explicit-any */
// --- Unified plugin lifecycle handler ---
// Instead of emitting individual plugin events, we normalize all plugin
// lifecycle changes into a single `plugin:lifecycle` SSE event with

View File

@@ -50,7 +50,16 @@ export type SubtaskStreamCallback = (event: SubtaskStreamEvent, eventId?: number
const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
const sessions = new Map<string, SubtaskSession & { updatedAt: Date; agent?: any; thinkingOutput: string }>();
/** Minimal interface for the agent object created by createKbAgent */
interface SubtaskAgent {
session: {
dispose?: () => void;
prompt: (input: string) => Promise<unknown>;
state: { messages: Array<{ role: string; content?: string | Array<{ type: string; text: string }> }> };
};
}
const sessions = new Map<string, SubtaskSession & { updatedAt: Date; agent?: SubtaskAgent; thinkingOutput: string }>();
// ── AI Session Persistence ────────────────────────────────────────────────
@@ -69,7 +78,7 @@ export function setAiSessionStore(store: AiSessionStore): void {
_aiSessionStore.on("ai_session:deleted", _aiSessionDeletedListener);
}
type SubtaskInternalSession = SubtaskSession & { updatedAt: Date; agent?: any; thinkingOutput: string; projectId?: string };
type SubtaskInternalSession = SubtaskSession & { updatedAt: Date; agent?: SubtaskAgent; thinkingOutput: string; projectId?: string };
function safeParseJson<T>(
text: string | null,

View File

@@ -77,7 +77,7 @@ async function loadPtyModule(): Promise<typeof import("node-pty")> {
if (existsSync(nativePath)) {
try {
const nativeModule: { exports?: unknown } = { exports: {} };
// @ts-ignore - process.dlopen is Node internal
// process.dlopen is a Node internal API
process.dlopen(nativeModule, nativePath);
console.log("[terminal] Pre-loaded native module via dlopen");
} catch (dlopenErr) {

View File

@@ -216,6 +216,7 @@ function httpsRequest(
/**
* Decode JWT payload without verification
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- JWT payloads are untyped
function decodeJwtPayload(token: string): any {
try {
const parts = token.split(".");
@@ -248,7 +249,9 @@ async function readAuthKeyFromFile(authPath: string, provider: string): Promise<
if (entry && (entry.type === "api_key" || entry.type === "key") && entry.key) {
return entry.key;
}
} catch {}
} catch {
// No auth file or invalid format - fall through to return null
}
return null;
}
@@ -259,19 +262,25 @@ async function readAuthKeyFromFile(authPath: string, provider: string): Promise<
async function readConfiguredApiKey(provider: string, authStorage?: AuthStorageLike): Promise<string | null> {
try {
authStorage?.reload();
} catch {}
} catch {
// Reload may fail if no storage - ignore
}
try {
const apiKey = await authStorage?.getApiKey?.(provider);
if (apiKey) return apiKey;
} catch {}
} catch {
// getApiKey may not be implemented - ignore
}
try {
const entry = authStorage?.get?.(provider);
if (entry && (entry.type === "api_key" || entry.type === "key") && entry.key) {
return entry.key;
}
} catch {}
} catch {
// get() may not be implemented - ignore
}
for (const authPath of getAuthFileCandidates()) {
const apiKey = await readAuthKeyFromFile(authPath, provider);
@@ -287,6 +296,7 @@ async function readConfiguredApiKey(provider: string, authStorage?: AuthStorageL
* Read Claude credentials from macOS keychain.
* Returns the parsed credentials object or null if not found/error.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped keychain data
async function readClaudeKeychainCredentials(): Promise<any | null> {
try {
const { stdout } = await execFileAsync(
@@ -401,27 +411,36 @@ export function _resetSleepFn(): void {
export function _stripClaudeAnsi(text: string): string {
let clean = text
// Cursor forward (CSI n C): replace with n spaces
// eslint-disable-next-line no-control-regex -- terminal ANSI escape sequence
.replace(/\x1B\[(\d+)C/g, (_m, n) => " ".repeat(parseInt(n, 10)))
// Cursor movement (up/down/back/position)
// eslint-disable-next-line no-control-regex -- terminal ANSI escape sequence
.replace(/\x1B\[\d*[ABD]/g, "")
// eslint-disable-next-line no-control-regex -- terminal ANSI escape sequence
.replace(/\x1B\[\d+;\d+[Hf]/g, "\n")
// Remaining CSI sequences (colors, modes, etc.)
// eslint-disable-next-line no-control-regex -- terminal ANSI escape sequence
.replace(/\x1B\[[0-9;?]*[A-Za-z@]/g, "")
// OSC sequences
// eslint-disable-next-line no-control-regex -- terminal ANSI escape sequence
.replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)?/g, "")
// Other ESC sequences
// eslint-disable-next-line no-control-regex -- terminal ANSI escape sequence
.replace(/\x1B[A-Za-z]/g, "")
// Carriage returns
.replace(/\r\n/g, "\n")
.replace(/\r/g, "\n");
// Handle backspaces
/* eslint-disable no-control-regex -- backspace control character */
while (clean.includes("\x08")) {
clean = clean.replace(/[^\x08]\x08/, "");
clean = clean.replace(/^\x08+/, "");
}
/* eslint-enable no-control-regex */
// Strip remaining non-printable control characters (except newline)
// eslint-disable-next-line no-control-regex -- control character cleanup
clean = clean.replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "");
return clean;
}
@@ -585,7 +604,7 @@ async function fetchClaudeUsageViaCli(): Promise<ProviderUsage> {
? ["/c", "claude", "--add-dir", cwd]
: ["-c", `claude --add-dir "${cwd}"`];
const ptyOptions: any = {
const ptyOptions: Record<string, unknown> = {
name: "xterm-256color",
cols: 120,
rows: 30,
@@ -606,7 +625,9 @@ async function fetchClaudeUsageViaCli(): Promise<ProviderUsage> {
const timeout = setTimeout(() => {
if (settled) return;
settled = true;
try { ptyProcess.kill(); } catch {}
try { ptyProcess.kill(); } catch {
// Kill may fail if process already exited - ignore
}
// Return whatever we have if it contains usage data
const clean = _stripClaudeAnsi(buf);
if (clean.includes("Current session") || clean.includes("% left") || clean.includes("% used")) {
@@ -631,7 +652,9 @@ async function fetchClaudeUsageViaCli(): Promise<ProviderUsage> {
) {
settled = true;
clearTimeout(timeout);
try { ptyProcess.kill(); } catch {}
try { ptyProcess.kill(); } catch {
// Kill may fail if process already exited - ignore
}
reject(new Error("Claude CLI auth error"));
return;
}
@@ -682,7 +705,9 @@ async function fetchClaudeUsageViaCli(): Promise<ProviderUsage> {
if (!settled) {
settled = true;
clearTimeout(timeout);
try { ptyProcess.kill(); } catch {}
try { ptyProcess.kill(); } catch {
// Kill may fail if process already exited - ignore
}
resolve(buf);
}
}, 2000);
@@ -777,9 +802,9 @@ async function fetchClaudeUsageViaCli(): Promise<ProviderUsage> {
usage.status = "error";
usage.error = "Could not parse usage from CLI output";
}
} catch (e: any) {
} catch (e: unknown) {
usage.status = "error";
usage.error = e.message || "CLI fallback failed";
usage.error = e instanceof Error ? e.message : "CLI fallback failed";
}
return usage;
@@ -807,12 +832,15 @@ async function fetchClaudeUsage(): Promise<ProviderUsage> {
path.join(process.env.HOME || "~", ".config", "claude", ".credentials.json"),
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped credentials JSON
let creds: any = null;
for (const p of credPaths) {
try {
creds = JSON.parse(await readFile(p, "utf-8"));
break;
} catch {}
} catch {
// File doesn't exist or invalid JSON - continue to next path
}
}
// Fallback to macOS keychain if file credentials not found
@@ -941,6 +969,7 @@ async function fetchClaudeUsage(): Promise<ProviderUsage> {
* Get a window data object from the API response, checking multiple possible keys
* for backward compatibility (API may use `session` instead of `five_hour`).
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped API response
const getWindowData = (primaryKey: string, fallbackKeys: string[] = []): any => {
if (data[primaryKey] && typeof data[primaryKey] === "object") {
return data[primaryKey];
@@ -999,9 +1028,9 @@ async function fetchClaudeUsage(): Promise<ProviderUsage> {
if (sevenDay) usage.windows.push(sevenDay);
if (sonnet) usage.windows.push(sonnet);
if (opus) usage.windows.push(opus);
} catch (e: any) {
} catch (e: unknown) {
usage.status = "error";
usage.error = e.message || "Failed to fetch Claude usage";
usage.error = e instanceof Error ? e.message : "Failed to fetch Claude usage";
}
return usage;
@@ -1021,6 +1050,7 @@ async function fetchCodexUsage(): Promise<ProviderUsage> {
const codexHome = process.env.CODEX_HOME || path.join(process.env.HOME || "~", ".codex");
const authPath = path.join(codexHome, "auth.json");
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped auth JSON
let auth: any = null;
try {
auth = JSON.parse(await readFile(authPath, "utf-8"));
@@ -1074,6 +1104,7 @@ async function fetchCodexUsage(): Promise<ProviderUsage> {
if (data.email) usage.email = data.email;
if (data.plan_type) usage.plan = data.plan_type.charAt(0).toUpperCase() + data.plan_type.slice(1);
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped API response
const parseWindow = (win: any, label: string): UsageWindow | null => {
if (!win || typeof win !== "object") return null;
const pctUsed: number = win.used_percent ?? 0;
@@ -1112,9 +1143,9 @@ async function fetchCodexUsage(): Promise<ProviderUsage> {
if (primary) usage.windows.push(primary);
if (secondary) usage.windows.push(secondary);
}
} catch (e: any) {
} catch (e: unknown) {
usage.status = "error";
usage.error = e.message || "Failed to fetch";
usage.error = e instanceof Error ? e.message : "Failed to fetch";
}
return usage;
@@ -1132,6 +1163,7 @@ async function fetchGeminiUsage(): Promise<ProviderUsage> {
// Load Gemini OAuth credentials
const oauthPath = path.join(process.env.HOME || "~", ".gemini", "oauth_creds.json");
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped OAuth JSON
let oauthCreds: any = null;
try {
oauthCreds = JSON.parse(await readFile(oauthPath, "utf-8"));
@@ -1161,7 +1193,9 @@ async function fetchGeminiUsage(): Promise<ProviderUsage> {
usage.error = `Unsupported auth type: ${authType} (need oauth-personal)`;
return usage;
}
} catch {}
} catch {
// Settings file doesn't exist or invalid JSON - continue
}
try {
const res = await httpsRequest(
@@ -1192,6 +1226,7 @@ async function fetchGeminiUsage(): Promise<ProviderUsage> {
usage.status = "ok";
// Parse buckets array
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped API response
const buckets: any[] = data.buckets || [];
if (Array.isArray(buckets) && buckets.length > 0) {
// Group by model family, pick lowest remainingFraction per family
@@ -1250,9 +1285,9 @@ async function fetchGeminiUsage(): Promise<ProviderUsage> {
});
}
}
} catch (e: any) {
} catch (e: unknown) {
usage.status = "error";
usage.error = e.message || "Failed to fetch";
usage.error = e instanceof Error ? e.message : "Failed to fetch";
}
return usage;
@@ -1300,6 +1335,7 @@ async function fetchMinimaxUsage(authStorage?: AuthStorageLike): Promise<Provide
usage.status = "ok";
// Parse model_remains array — group by model family
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped API response
const modelRemains: any[] = data?.model_remains || [];
if (Array.isArray(modelRemains) && modelRemains.length > 0) {
for (const model of modelRemains) {
@@ -1344,9 +1380,9 @@ async function fetchMinimaxUsage(authStorage?: AuthStorageLike): Promise<Provide
}
}
}
} catch (e: any) {
} catch (e: unknown) {
usage.status = "error";
usage.error = e.message || "Failed to fetch";
usage.error = e instanceof Error ? e.message : "Failed to fetch";
}
return usage;
@@ -1400,10 +1436,11 @@ async function fetchZaiUsage(authStorage?: AuthStorageLike): Promise<ProviderUsa
usage.status = "ok";
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped API response
const limits: any[] = data?.data?.limits || [];
// Find TOKENS_LIMIT (5-hour rolling window)
const tokensLimit = limits.find((l: any) => l.type === "TOKENS_LIMIT");
const tokensLimit = limits.find((l) => l.type === "TOKENS_LIMIT");
if (tokensLimit) {
const percentage: number = tokensLimit.percentage ?? 0;
// The percentage field represents percentage USED
@@ -1439,11 +1476,12 @@ async function fetchZaiUsage(authStorage?: AuthStorageLike): Promise<ProviderUsa
}
// Find TIME_LIMIT (MCP monthly search quota)
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped API response
const timeLimit = limits.find((l: any) => l.type === "TIME_LIMIT");
if (timeLimit) {
const total: number = timeLimit.usage ?? 0;
const used: number = timeLimit.currentValue ?? 0;
const remaining: number = timeLimit.remaining ?? Math.max(0, total - used);
const _remaining: number = timeLimit.remaining ?? Math.max(0, total - used);
const percentage: number = timeLimit.percentage ?? 0;
let resetText: string | null = null;
@@ -1472,9 +1510,9 @@ async function fetchZaiUsage(authStorage?: AuthStorageLike): Promise<ProviderUsa
if (data?.data?.level) {
usage.plan = data.data.level.charAt(0).toUpperCase() + data.data.level.slice(1);
}
} catch (e: any) {
} catch (e: unknown) {
usage.status = "error";
usage.error = e.message || "Failed to fetch";
usage.error = e instanceof Error ? e.message : "Failed to fetch";
}
return usage;
@@ -1521,13 +1559,13 @@ export function withTimeout(
clearTimeout(timer);
resolve(result);
})
.catch((err: any) => {
.catch((err: unknown) => {
clearTimeout(timer);
resolve({
name: providerName,
icon: "⏱️",
status: "error",
error: err.message || "Failed",
error: err instanceof Error ? err.message : "Failed",
windows: [],
});
});

View File

@@ -386,17 +386,6 @@ function extractPartsFromChannel(channel: string): { taskId: string | null; proj
return { projectId: null, taskId: null };
}
/**
* Extract taskId from any badge channel key (without knowing the projectId).
* @deprecated Use extractPartsFromChannel instead
*/
function extractTaskIdFromChannel(channel: string): string | null {
// Channel format: badge:{projectId}:{taskId}
// We need to find the taskId after the second colon
const match = channel.match(/^badge:[^:]+:(.+)$/);
return match ? match[1] : null;
}
function parseClientMessage(raw: WebSocket.RawData):
| { ok: true; value: BadgeClientMessage }
| { ok: false; error: string } {