chore: Enable "curly" rule to avoid single-statement if confusion/errors.

This commit is contained in:
cpojer
2026-01-31 16:19:20 +09:00
parent 009b16fab8
commit 5ceff756e1
1266 changed files with 27871 additions and 9393 deletions
+3 -1
View File
@@ -16,7 +16,9 @@ vi.mock("../agents/model-auth.js", () => ({
mode: "api-key",
})),
requireApiKey: (auth: { apiKey?: string; mode?: string }, provider: string) => {
if (auth?.apiKey) return auth.apiKey;
if (auth?.apiKey) {
return auth.apiKey;
}
throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth?.mode}).`);
},
}));
+24 -8
View File
@@ -120,7 +120,9 @@ function appendFileBlocks(body: string | undefined, blocks: string[]): string {
}
function resolveUtf16Charset(buffer?: Buffer): "utf-16le" | "utf-16be" | undefined {
if (!buffer || buffer.length < 2) return undefined;
if (!buffer || buffer.length < 2) {
return undefined;
}
const b0 = buffer[0];
const b1 = buffer[1];
if (b0 === 0xff && b1 === 0xfe) {
@@ -132,7 +134,9 @@ function resolveUtf16Charset(buffer?: Buffer): "utf-16le" | "utf-16be" | undefin
const sampleLen = Math.min(buffer.length, 2048);
let zeroCount = 0;
for (let i = 0; i < sampleLen; i += 1) {
if (buffer[i] === 0) zeroCount += 1;
if (buffer[i] === 0) {
zeroCount += 1;
}
}
if (zeroCount / sampleLen > 0.2) {
return "utf-16le";
@@ -141,7 +145,9 @@ function resolveUtf16Charset(buffer?: Buffer): "utf-16le" | "utf-16be" | undefin
}
function looksLikeUtf8Text(buffer?: Buffer): boolean {
if (!buffer || buffer.length === 0) return false;
if (!buffer || buffer.length === 0) {
return false;
}
const sampleLen = Math.min(buffer.length, 4096);
let printable = 0;
let other = 0;
@@ -158,12 +164,16 @@ function looksLikeUtf8Text(buffer?: Buffer): boolean {
}
}
const total = printable + other;
if (total === 0) return false;
if (total === 0) {
return false;
}
return printable / total > 0.85;
}
function decodeTextSample(buffer?: Buffer): string {
if (!buffer || buffer.length === 0) return "";
if (!buffer || buffer.length === 0) {
return "";
}
const sample = buffer.subarray(0, Math.min(buffer.length, 8192));
const utf16Charset = resolveUtf16Charset(sample);
if (utf16Charset === "utf-16be") {
@@ -181,7 +191,9 @@ function decodeTextSample(buffer?: Buffer): string {
}
function guessDelimitedMime(text: string): string | undefined {
if (!text) return undefined;
if (!text) {
return undefined;
}
const line = text.split(/\r?\n/)[0] ?? "";
const tabs = (line.match(/\t/g) ?? []).length;
const commas = (line.match(/,/g) ?? []).length;
@@ -195,7 +207,9 @@ function guessDelimitedMime(text: string): string | undefined {
}
function resolveTextMimeFromName(name?: string): string | undefined {
if (!name) return undefined;
if (!name) {
return undefined;
}
const ext = path.extname(name).toLowerCase();
return TEXT_EXT_MIME.get(ext);
}
@@ -363,7 +377,9 @@ export async function applyMediaUnderstanding(params: {
const outputs: MediaUnderstandingOutput[] = [];
const decisions: MediaUnderstandingDecision[] = [];
for (const entry of results) {
if (!entry) continue;
if (!entry) {
continue;
}
for (const output of entry.outputs) {
outputs.push(output);
}
+57 -19
View File
@@ -40,7 +40,9 @@ const DEFAULT_MAX_ATTACHMENTS = 1;
function normalizeAttachmentPath(raw?: string | null): string | undefined {
const value = raw?.trim();
if (!value) return undefined;
if (!value) {
return undefined;
}
if (value.startsWith("file://")) {
try {
return fileURLToPath(value);
@@ -58,7 +60,9 @@ export function normalizeAttachments(ctx: MsgContext): MediaAttachment[] {
const resolveMime = (count: number, index: number) => {
const typeHint = typesFromArray?.[index];
const trimmed = typeof typeHint === "string" ? typeHint.trim() : "";
if (trimmed) return trimmed;
if (trimmed) {
return trimmed;
}
return count === 1 ? ctx.MediaType : undefined;
};
@@ -89,7 +93,9 @@ export function normalizeAttachments(ctx: MsgContext): MediaAttachment[] {
const pathValue = ctx.MediaPath?.trim();
const url = ctx.MediaUrl?.trim();
if (!pathValue && !url) return [];
if (!pathValue && !url) {
return [];
}
return [
{
path: pathValue || undefined,
@@ -104,12 +110,20 @@ export function resolveAttachmentKind(
attachment: MediaAttachment,
): "image" | "audio" | "video" | "document" | "unknown" {
const kind = kindFromMime(attachment.mime);
if (kind === "image" || kind === "audio" || kind === "video") return kind;
if (kind === "image" || kind === "audio" || kind === "video") {
return kind;
}
const ext = getFileExtension(attachment.path ?? attachment.url);
if (!ext) return "unknown";
if ([".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v"].includes(ext)) return "video";
if (isAudioFileName(attachment.path ?? attachment.url)) return "audio";
if (!ext) {
return "unknown";
}
if ([".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v"].includes(ext)) {
return "video";
}
if (isAudioFileName(attachment.path ?? attachment.url)) {
return "audio";
}
if ([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tiff", ".tif"].includes(ext)) {
return "image";
}
@@ -129,14 +143,22 @@ export function isImageAttachment(attachment: MediaAttachment): boolean {
}
function isAbortError(err: unknown): boolean {
if (!err) return false;
if (err instanceof Error && err.name === "AbortError") return true;
if (!err) {
return false;
}
if (err instanceof Error && err.name === "AbortError") {
return true;
}
return false;
}
function resolveRequestUrl(input: RequestInfo | URL): string {
if (typeof input === "string") return input;
if (input instanceof URL) return input.toString();
if (typeof input === "string") {
return input;
}
if (input instanceof URL) {
return input.toString();
}
return input.url;
}
@@ -144,8 +166,12 @@ function orderAttachments(
attachments: MediaAttachment[],
prefer?: MediaUnderstandingAttachmentsConfig["prefer"],
): MediaAttachment[] {
if (!prefer || prefer === "first") return attachments;
if (prefer === "last") return [...attachments].toReversed();
if (!prefer || prefer === "first") {
return attachments;
}
if (prefer === "last") {
return [...attachments].toReversed();
}
if (prefer === "path") {
const withPath = attachments.filter((item) => item.path);
const withoutPath = attachments.filter((item) => !item.path);
@@ -166,11 +192,17 @@ export function selectAttachments(params: {
}): MediaAttachment[] {
const { capability, attachments, policy } = params;
const matches = attachments.filter((item) => {
if (capability === "image") return isImageAttachment(item);
if (capability === "audio") return isAudioAttachment(item);
if (capability === "image") {
return isImageAttachment(item);
}
if (capability === "audio") {
return isAudioAttachment(item);
}
return isVideoAttachment(item);
});
if (matches.length === 0) return [];
if (matches.length === 0) {
return [];
}
const ordered = orderAttachments(matches, policy?.prefer);
const mode = policy?.mode ?? "first";
@@ -367,13 +399,19 @@ export class MediaAttachmentCache {
private resolveLocalPath(attachment: MediaAttachment): string | undefined {
const rawPath = normalizeAttachmentPath(attachment.path);
if (!rawPath) return undefined;
if (!rawPath) {
return undefined;
}
return path.isAbsolute(rawPath) ? rawPath : path.resolve(rawPath);
}
private async ensureLocalStat(entry: AttachmentCacheEntry): Promise<number | undefined> {
if (!entry.resolvedPath) return undefined;
if (entry.statSize !== undefined) return entry.statSize;
if (!entry.resolvedPath) {
return undefined;
}
if (entry.statSize !== undefined) {
return entry.statSize;
}
try {
const stat = await fs.stat(entry.resolvedPath);
if (!stat.isFile()) {
+6 -2
View File
@@ -4,7 +4,9 @@ export async function runWithConcurrency<T>(
tasks: Array<() => Promise<T>>,
limit: number,
): Promise<T[]> {
if (tasks.length === 0) return [];
if (tasks.length === 0) {
return [];
}
const resolvedLimit = Math.max(1, Math.min(limit, tasks.length));
const results: T[] = Array.from({ length: tasks.length });
let next = 0;
@@ -13,7 +15,9 @@ export async function runWithConcurrency<T>(
while (true) {
const index = next;
next += 1;
if (index >= tasks.length) return;
if (index >= tasks.length) {
return;
}
try {
results[index] = await tasks[index]();
} catch (err) {
+9 -3
View File
@@ -5,8 +5,12 @@ const MEDIA_PLACEHOLDER_TOKEN_RE = /^<media:[^>]+>(\s*\([^)]*\))?\s*/i;
export function extractMediaUserText(body?: string): string | undefined {
const trimmed = body?.trim() ?? "";
if (!trimmed) return undefined;
if (MEDIA_PLACEHOLDER_RE.test(trimmed)) return undefined;
if (!trimmed) {
return undefined;
}
if (MEDIA_PLACEHOLDER_RE.test(trimmed)) {
return undefined;
}
const cleaned = trimmed.replace(MEDIA_PLACEHOLDER_TOKEN_RE, "").trim();
return cleaned || undefined;
}
@@ -87,6 +91,8 @@ export function formatMediaUnderstandingBody(params: {
}
export function formatAudioTranscripts(outputs: MediaUnderstandingOutput[]): string {
if (outputs.length === 1) return outputs[0].text;
if (outputs.length === 1) {
return outputs[0].text;
}
return outputs.map((output, index) => `Audio ${index + 1}:\n${output.text}`).join("\n\n");
}
@@ -3,8 +3,12 @@ import { describe, expect, it } from "vitest";
import { transcribeDeepgramAudio } from "./audio.js";
const resolveRequestUrl = (input: RequestInfo | URL) => {
if (typeof input === "string") return input;
if (input instanceof URL) return input.toString();
if (typeof input === "string") {
return input;
}
if (input instanceof URL) {
return input.toString();
}
return input.url;
};
@@ -28,10 +28,14 @@ export async function transcribeDeepgramAudio(
const url = new URL(`${baseUrl}/listen`);
url.searchParams.set("model", model);
if (params.language?.trim()) url.searchParams.set("language", params.language.trim());
if (params.language?.trim()) {
url.searchParams.set("language", params.language.trim());
}
if (params.query) {
for (const [key, value] of Object.entries(params.query)) {
if (value === undefined) continue;
if (value === undefined) {
continue;
}
url.searchParams.set(key, String(value));
}
}
@@ -8,7 +8,9 @@ const DEFAULT_GOOGLE_AUDIO_PROMPT = "Transcribe the audio.";
function resolveModel(model?: string): string {
const trimmed = model?.trim();
if (!trimmed) return DEFAULT_GOOGLE_AUDIO_MODEL;
if (!trimmed) {
return DEFAULT_GOOGLE_AUDIO_MODEL;
}
return normalizeGoogleModelId(trimmed);
}
@@ -3,8 +3,12 @@ import { describe, expect, it } from "vitest";
import { describeGeminiVideo } from "./video.js";
const resolveRequestUrl = (input: RequestInfo | URL) => {
if (typeof input === "string") return input;
if (input instanceof URL) return input.toString();
if (typeof input === "string") {
return input;
}
if (input instanceof URL) {
return input.toString();
}
return input.url;
};
@@ -8,7 +8,9 @@ const DEFAULT_GOOGLE_VIDEO_PROMPT = "Describe the video.";
function resolveModel(model?: string): string {
const trimmed = model?.trim();
if (!trimmed) return DEFAULT_GOOGLE_VIDEO_MODEL;
if (!trimmed) {
return DEFAULT_GOOGLE_VIDEO_MODEL;
}
return normalizeGoogleModelId(trimmed);
}
+3 -1
View File
@@ -18,7 +18,9 @@ const PROVIDERS: MediaUnderstandingProvider[] = [
export function normalizeMediaProviderId(id: string): string {
const normalized = normalizeProviderId(id);
if (normalized === "gemini") return "google";
if (normalized === "gemini") {
return "google";
}
return normalized;
}
@@ -3,8 +3,12 @@ import { describe, expect, it } from "vitest";
import { transcribeOpenAiCompatibleAudio } from "./audio.js";
const resolveRequestUrl = (input: RequestInfo | URL) => {
if (typeof input === "string") return input;
if (input instanceof URL) return input.toString();
if (typeof input === "string") {
return input;
}
if (input instanceof URL) {
return input.toString();
}
return input.url;
};
@@ -27,8 +27,12 @@ export async function transcribeOpenAiCompatibleAudio(
});
form.append("file", blob, fileName);
form.append("model", model);
if (params.language?.trim()) form.append("language", params.language.trim());
if (params.prompt?.trim()) form.append("prompt", params.prompt.trim());
if (params.language?.trim()) {
form.append("language", params.language.trim());
}
if (params.prompt?.trim()) {
form.append("prompt", params.prompt.trim());
}
const headers = new Headers(params.headers);
if (!headers.has("authorization")) {
+6 -2
View File
@@ -24,8 +24,12 @@ export async function readErrorResponse(res: Response): Promise<string | undefin
try {
const text = await res.text();
const collapsed = text.replace(/\s+/g, " ").trim();
if (!collapsed) return undefined;
if (collapsed.length <= MAX_ERROR_CHARS) return collapsed;
if (!collapsed) {
return undefined;
}
if (collapsed.length <= MAX_ERROR_CHARS) {
return collapsed;
}
return `${collapsed.slice(0, MAX_ERROR_CHARS)}`;
} catch {
return undefined;
+33 -11
View File
@@ -27,7 +27,9 @@ export function resolvePrompt(
maxChars?: number,
): string {
const base = prompt?.trim() || DEFAULT_PROMPT[capability];
if (!maxChars || capability === "audio") return base;
if (!maxChars || capability === "audio") {
return base;
}
return `${base} Respond in at most ${maxChars} characters.`;
}
@@ -40,7 +42,9 @@ export function resolveMaxChars(params: {
const { capability, entry, cfg } = params;
const configured =
entry.maxChars ?? params.config?.maxChars ?? cfg.tools?.media?.[capability]?.maxChars;
if (typeof configured === "number") return configured;
if (typeof configured === "number") {
return configured;
}
return DEFAULT_MAX_CHARS_BY_CAPABILITY[capability];
}
@@ -54,7 +58,9 @@ export function resolveMaxBytes(params: {
params.entry.maxBytes ??
params.config?.maxBytes ??
params.cfg.tools?.media?.[params.capability]?.maxBytes;
if (typeof configured === "number") return configured;
if (typeof configured === "number") {
return configured;
}
return DEFAULT_MAX_BYTES[params.capability];
}
@@ -82,9 +88,13 @@ function resolveEntryCapabilities(params: {
providerRegistry: Map<string, { capabilities?: MediaUnderstandingCapability[] }>;
}): MediaUnderstandingCapability[] | undefined {
const entryType = params.entry.type ?? (params.entry.command ? "cli" : "provider");
if (entryType === "cli") return undefined;
if (entryType === "cli") {
return undefined;
}
const providerId = normalizeMediaProviderId(params.entry.provider ?? "");
if (!providerId) return undefined;
if (!providerId) {
return undefined;
}
return params.providerRegistry.get(providerId)?.capabilities;
}
@@ -100,7 +110,9 @@ export function resolveModelEntries(params: {
...(config?.models ?? []).map((entry) => ({ entry, source: "capability" as const })),
...sharedModels.map((entry) => ({ entry, source: "shared" as const })),
];
if (entries.length === 0) return [];
if (entries.length === 0) {
return [];
}
return entries
.filter(({ entry, source }) => {
@@ -147,14 +159,24 @@ export function resolveEntriesWithActiveFallback(params: {
config: params.config,
providerRegistry: params.providerRegistry,
});
if (entries.length > 0) return entries;
if (params.config?.enabled !== true) return entries;
if (entries.length > 0) {
return entries;
}
if (params.config?.enabled !== true) {
return entries;
}
const activeProviderRaw = params.activeModel?.provider?.trim();
if (!activeProviderRaw) return entries;
if (!activeProviderRaw) {
return entries;
}
const activeProvider = normalizeMediaProviderId(activeProviderRaw);
if (!activeProvider) return entries;
if (!activeProvider) {
return entries;
}
const capabilities = params.providerRegistry.get(activeProvider)?.capabilities;
if (!capabilities || !capabilities.includes(params.capability)) return entries;
if (!capabilities || !capabilities.includes(params.capability)) {
return entries;
}
return [
{
type: "provider",
+231 -77
View File
@@ -89,10 +89,16 @@ const binaryCache = new Map<string, Promise<string | null>>();
const geminiProbeCache = new Map<string, Promise<boolean>>();
function expandHomeDir(value: string): string {
if (!value.startsWith("~")) return value;
if (!value.startsWith("~")) {
return value;
}
const home = os.homedir();
if (value === "~") return home;
if (value.startsWith("~/")) return path.join(home, value.slice(2));
if (value === "~") {
return home;
}
if (value.startsWith("~/")) {
return path.join(home, value.slice(2));
}
return value;
}
@@ -101,9 +107,13 @@ function hasPathSeparator(value: string): boolean {
}
function candidateBinaryNames(name: string): string[] {
if (process.platform !== "win32") return [name];
if (process.platform !== "win32") {
return [name];
}
const ext = path.extname(name);
if (ext) return [name];
if (ext) {
return [name];
}
const pathext = (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM")
.split(";")
.map((item) => item.trim())
@@ -116,8 +126,12 @@ function candidateBinaryNames(name: string): string[] {
async function isExecutable(filePath: string): Promise<boolean> {
try {
const stat = await fs.stat(filePath);
if (!stat.isFile()) return false;
if (process.platform === "win32") return true;
if (!stat.isFile()) {
return false;
}
if (process.platform === "win32") {
return true;
}
await fs.access(filePath, fsConstants.X_OK);
return true;
} catch {
@@ -127,25 +141,35 @@ async function isExecutable(filePath: string): Promise<boolean> {
async function findBinary(name: string): Promise<string | null> {
const cached = binaryCache.get(name);
if (cached) return cached;
if (cached) {
return cached;
}
const resolved = (async () => {
const direct = expandHomeDir(name.trim());
if (direct && hasPathSeparator(direct)) {
for (const candidate of candidateBinaryNames(direct)) {
if (await isExecutable(candidate)) return candidate;
if (await isExecutable(candidate)) {
return candidate;
}
}
}
const searchName = name.trim();
if (!searchName) return null;
if (!searchName) {
return null;
}
const pathEntries = (process.env.PATH ?? "").split(path.delimiter);
const candidates = candidateBinaryNames(searchName);
for (const entryRaw of pathEntries) {
const entry = expandHomeDir(entryRaw.trim().replace(/^"(.*)"$/, "$1"));
if (!entry) continue;
if (!entry) {
continue;
}
for (const candidate of candidates) {
const fullPath = path.join(entry, candidate);
if (await isExecutable(fullPath)) return fullPath;
if (await isExecutable(fullPath)) {
return fullPath;
}
}
}
@@ -160,7 +184,9 @@ async function hasBinary(name: string): Promise<boolean> {
}
async function fileExists(filePath?: string | null): Promise<boolean> {
if (!filePath) return false;
if (!filePath) {
return false;
}
try {
await fs.stat(filePath);
return true;
@@ -172,7 +198,9 @@ async function fileExists(filePath?: string | null): Promise<boolean> {
function extractLastJsonObject(raw: string): unknown {
const trimmed = raw.trim();
const start = trimmed.lastIndexOf("{");
if (start === -1) return null;
if (start === -1) {
return null;
}
const slice = trimmed.slice(start);
try {
return JSON.parse(slice);
@@ -183,9 +211,13 @@ function extractLastJsonObject(raw: string): unknown {
function extractGeminiResponse(raw: string): string | null {
const payload = extractLastJsonObject(raw);
if (!payload || typeof payload !== "object") return null;
if (!payload || typeof payload !== "object") {
return null;
}
const response = (payload as { response?: unknown }).response;
if (typeof response !== "string") return null;
if (typeof response !== "string") {
return null;
}
const trimmed = response.trim();
return trimmed || null;
}
@@ -193,9 +225,13 @@ function extractGeminiResponse(raw: string): string | null {
function extractSherpaOnnxText(raw: string): string | null {
const tryParse = (value: string): string | null => {
const trimmed = value.trim();
if (!trimmed) return null;
if (!trimmed) {
return null;
}
const head = trimmed[0];
if (head !== "{" && head !== '"') return null;
if (head !== "{" && head !== '"') {
return null;
}
try {
const parsed = JSON.parse(trimmed) as unknown;
if (typeof parsed === "string") {
@@ -212,7 +248,9 @@ function extractSherpaOnnxText(raw: string): string | null {
};
const direct = tryParse(raw);
if (direct) return direct;
if (direct) {
return direct;
}
const lines = raw
.split("\n")
@@ -220,16 +258,22 @@ function extractSherpaOnnxText(raw: string): string | null {
.filter(Boolean);
for (let i = lines.length - 1; i >= 0; i -= 1) {
const parsed = tryParse(lines[i] ?? "");
if (parsed) return parsed;
if (parsed) {
return parsed;
}
}
return null;
}
async function probeGeminiCli(): Promise<boolean> {
const cached = geminiProbeCache.get("gemini");
if (cached) return cached;
if (cached) {
return cached;
}
const resolved = (async () => {
if (!(await hasBinary("gemini"))) return false;
if (!(await hasBinary("gemini"))) {
return false;
}
try {
const { stdout } = await runExec("gemini", ["--output-format", "json", "ok"], {
timeoutMs: 8000,
@@ -244,11 +288,15 @@ async function probeGeminiCli(): Promise<boolean> {
}
async function resolveLocalWhisperCppEntry(): Promise<MediaUnderstandingModelConfig | null> {
if (!(await hasBinary("whisper-cli"))) return null;
if (!(await hasBinary("whisper-cli"))) {
return null;
}
const envModel = process.env.WHISPER_CPP_MODEL?.trim();
const defaultModel = "/opt/homebrew/share/whisper-cpp/for-tests-ggml-tiny.bin";
const modelPath = envModel && (await fileExists(envModel)) ? envModel : defaultModel;
if (!(await fileExists(modelPath))) return null;
if (!(await fileExists(modelPath))) {
return null;
}
return {
type: "cli",
command: "whisper-cli",
@@ -257,7 +305,9 @@ async function resolveLocalWhisperCppEntry(): Promise<MediaUnderstandingModelCon
}
async function resolveLocalWhisperEntry(): Promise<MediaUnderstandingModelConfig | null> {
if (!(await hasBinary("whisper"))) return null;
if (!(await hasBinary("whisper"))) {
return null;
}
return {
type: "cli",
command: "whisper",
@@ -276,17 +326,29 @@ async function resolveLocalWhisperEntry(): Promise<MediaUnderstandingModelConfig
}
async function resolveSherpaOnnxEntry(): Promise<MediaUnderstandingModelConfig | null> {
if (!(await hasBinary("sherpa-onnx-offline"))) return null;
if (!(await hasBinary("sherpa-onnx-offline"))) {
return null;
}
const modelDir = process.env.SHERPA_ONNX_MODEL_DIR?.trim();
if (!modelDir) return null;
if (!modelDir) {
return null;
}
const tokens = path.join(modelDir, "tokens.txt");
const encoder = path.join(modelDir, "encoder.onnx");
const decoder = path.join(modelDir, "decoder.onnx");
const joiner = path.join(modelDir, "joiner.onnx");
if (!(await fileExists(tokens))) return null;
if (!(await fileExists(encoder))) return null;
if (!(await fileExists(decoder))) return null;
if (!(await fileExists(joiner))) return null;
if (!(await fileExists(tokens))) {
return null;
}
if (!(await fileExists(encoder))) {
return null;
}
if (!(await fileExists(decoder))) {
return null;
}
if (!(await fileExists(joiner))) {
return null;
}
return {
type: "cli",
command: "sherpa-onnx-offline",
@@ -302,16 +364,22 @@ async function resolveSherpaOnnxEntry(): Promise<MediaUnderstandingModelConfig |
async function resolveLocalAudioEntry(): Promise<MediaUnderstandingModelConfig | null> {
const sherpa = await resolveSherpaOnnxEntry();
if (sherpa) return sherpa;
if (sherpa) {
return sherpa;
}
const whisperCpp = await resolveLocalWhisperCppEntry();
if (whisperCpp) return whisperCpp;
if (whisperCpp) {
return whisperCpp;
}
return await resolveLocalWhisperEntry();
}
async function resolveGeminiCliEntry(
_capability: MediaUnderstandingCapability,
): Promise<MediaUnderstandingModelConfig | null> {
if (!(await probeGeminiCli())) return null;
if (!(await probeGeminiCli())) {
return null;
}
return {
type: "cli",
command: "gemini",
@@ -341,10 +409,18 @@ async function resolveKeyEntry(params: {
model?: string,
): Promise<MediaUnderstandingModelConfig | null> => {
const provider = getMediaUnderstandingProvider(providerId, providerRegistry);
if (!provider) return null;
if (capability === "audio" && !provider.transcribeAudio) return null;
if (capability === "image" && !provider.describeImage) return null;
if (capability === "video" && !provider.describeVideo) return null;
if (!provider) {
return null;
}
if (capability === "audio" && !provider.transcribeAudio) {
return null;
}
if (capability === "image" && !provider.describeImage) {
return null;
}
if (capability === "video" && !provider.describeVideo) {
return null;
}
try {
await resolveApiKeyForProvider({ provider: providerId, cfg, agentDir });
return { type: "provider" as const, provider: providerId, model };
@@ -357,12 +433,16 @@ async function resolveKeyEntry(params: {
const activeProvider = params.activeModel?.provider?.trim();
if (activeProvider) {
const activeEntry = await checkProvider(activeProvider, params.activeModel?.model);
if (activeEntry) return activeEntry;
if (activeEntry) {
return activeEntry;
}
}
for (const providerId of AUTO_IMAGE_KEY_PROVIDERS) {
const model = DEFAULT_IMAGE_MODELS[providerId];
const entry = await checkProvider(providerId, model);
if (entry) return entry;
if (entry) {
return entry;
}
}
return null;
}
@@ -371,11 +451,15 @@ async function resolveKeyEntry(params: {
const activeProvider = params.activeModel?.provider?.trim();
if (activeProvider) {
const activeEntry = await checkProvider(activeProvider, params.activeModel?.model);
if (activeEntry) return activeEntry;
if (activeEntry) {
return activeEntry;
}
}
for (const providerId of AUTO_VIDEO_KEY_PROVIDERS) {
const entry = await checkProvider(providerId, undefined);
if (entry) return entry;
if (entry) {
return entry;
}
}
return null;
}
@@ -383,11 +467,15 @@ async function resolveKeyEntry(params: {
const activeProvider = params.activeModel?.provider?.trim();
if (activeProvider) {
const activeEntry = await checkProvider(activeProvider, params.activeModel?.model);
if (activeEntry) return activeEntry;
if (activeEntry) {
return activeEntry;
}
}
for (const providerId of AUTO_AUDIO_KEY_PROVIDERS) {
const entry = await checkProvider(providerId, undefined);
if (entry) return entry;
if (entry) {
return entry;
}
}
return null;
}
@@ -400,15 +488,23 @@ async function resolveAutoEntries(params: {
activeModel?: ActiveMediaModel;
}): Promise<MediaUnderstandingModelConfig[]> {
const activeEntry = await resolveActiveModelEntry(params);
if (activeEntry) return [activeEntry];
if (activeEntry) {
return [activeEntry];
}
if (params.capability === "audio") {
const localAudio = await resolveLocalAudioEntry();
if (localAudio) return [localAudio];
if (localAudio) {
return [localAudio];
}
}
const gemini = await resolveGeminiCliEntry(params.capability);
if (gemini) return [gemini];
if (gemini) {
return [gemini];
}
const keys = await resolveKeyEntry(params);
if (keys) return [keys];
if (keys) {
return [keys];
}
return [];
}
@@ -419,11 +515,17 @@ export async function resolveAutoImageModel(params: {
}): Promise<ActiveMediaModel | null> {
const providerRegistry = buildProviderRegistry();
const toActive = (entry: MediaUnderstandingModelConfig | null): ActiveMediaModel | null => {
if (!entry || entry.type === "cli") return null;
if (!entry || entry.type === "cli") {
return null;
}
const provider = entry.provider;
if (!provider) return null;
if (!provider) {
return null;
}
const model = entry.model ?? DEFAULT_IMAGE_MODELS[provider];
if (!model) return null;
if (!model) {
return null;
}
return { provider, model };
};
const activeEntry = await resolveActiveModelEntry({
@@ -434,7 +536,9 @@ export async function resolveAutoImageModel(params: {
activeModel: params.activeModel,
});
const resolvedActive = toActive(activeEntry);
if (resolvedActive) return resolvedActive;
if (resolvedActive) {
return resolvedActive;
}
const keyEntry = await resolveKeyEntry({
cfg: params.cfg,
agentDir: params.agentDir,
@@ -453,14 +557,26 @@ async function resolveActiveModelEntry(params: {
activeModel?: ActiveMediaModel;
}): Promise<MediaUnderstandingModelConfig | null> {
const activeProviderRaw = params.activeModel?.provider?.trim();
if (!activeProviderRaw) return null;
if (!activeProviderRaw) {
return null;
}
const providerId = normalizeMediaProviderId(activeProviderRaw);
if (!providerId) return null;
if (!providerId) {
return null;
}
const provider = getMediaUnderstandingProvider(providerId, params.providerRegistry);
if (!provider) return null;
if (params.capability === "audio" && !provider.transcribeAudio) return null;
if (params.capability === "image" && !provider.describeImage) return null;
if (params.capability === "video" && !provider.describeVideo) return null;
if (!provider) {
return null;
}
if (params.capability === "audio" && !provider.transcribeAudio) {
return null;
}
if (params.capability === "image" && !provider.describeImage) {
return null;
}
if (params.capability === "video" && !provider.describeVideo) {
return null;
}
try {
await resolveApiKeyForProvider({
provider: providerId,
@@ -479,7 +595,9 @@ async function resolveActiveModelEntry(params: {
function trimOutput(text: string, maxChars?: number): string {
const trimmed = text.trim();
if (!maxChars || trimmed.length <= maxChars) return trimmed;
if (!maxChars || trimmed.length <= maxChars) {
return trimmed;
}
return trimmed.slice(0, maxChars).trim();
}
@@ -491,7 +609,9 @@ function findArgValue(args: string[], keys: string[]): string | undefined {
for (let i = 0; i < args.length; i += 1) {
if (keys.includes(args[i] ?? "")) {
const value = args[i + 1];
if (value) return value;
if (value) {
return value;
}
}
}
return undefined;
@@ -504,17 +624,25 @@ function hasArg(args: string[], keys: string[]): boolean {
function resolveWhisperOutputPath(args: string[], mediaPath: string): string | null {
const outputDir = findArgValue(args, ["--output_dir", "-o"]);
const outputFormat = findArgValue(args, ["--output_format"]);
if (!outputDir || !outputFormat) return null;
if (!outputDir || !outputFormat) {
return null;
}
const formats = outputFormat.split(",").map((value) => value.trim());
if (!formats.includes("txt")) return null;
if (!formats.includes("txt")) {
return null;
}
const base = path.parse(mediaPath).name;
return path.join(outputDir, `${base}.txt`);
}
function resolveWhisperCppOutputPath(args: string[]): string | null {
if (!hasArg(args, ["-otxt", "--output-txt"])) return null;
if (!hasArg(args, ["-otxt", "--output-txt"])) {
return null;
}
const outputBase = findArgValue(args, ["-of", "--output-file"]);
if (!outputBase) return null;
if (!outputBase) {
return null;
}
return `${outputBase}.txt`;
}
@@ -534,18 +662,24 @@ async function resolveCliOutput(params: {
if (fileOutput && (await fileExists(fileOutput))) {
try {
const content = await fs.readFile(fileOutput, "utf8");
if (content.trim()) return content.trim();
if (content.trim()) {
return content.trim();
}
} catch {}
}
if (commandId === "gemini") {
const response = extractGeminiResponse(params.stdout);
if (response) return response;
if (response) {
return response;
}
}
if (commandId === "sherpa-onnx-offline") {
const response = extractSherpaOnnxText(params.stdout);
if (response) return response;
if (response) {
return response;
}
}
return params.stdout.trim();
@@ -556,10 +690,14 @@ type ProviderQuery = Record<string, string | number | boolean>;
function normalizeProviderQuery(
options?: Record<string, string | number | boolean>,
): ProviderQuery | undefined {
if (!options) return undefined;
if (!options) {
return undefined;
}
const query: ProviderQuery = {};
for (const [key, value] of Object.entries(options)) {
if (value === undefined) continue;
if (value === undefined) {
continue;
}
query[key] = value;
}
return Object.keys(query).length > 0 ? query : undefined;
@@ -570,11 +708,19 @@ function buildDeepgramCompatQuery(options?: {
punctuate?: boolean;
smartFormat?: boolean;
}): ProviderQuery | undefined {
if (!options) return undefined;
if (!options) {
return undefined;
}
const query: ProviderQuery = {};
if (typeof options.detectLanguage === "boolean") query.detect_language = options.detectLanguage;
if (typeof options.punctuate === "boolean") query.punctuate = options.punctuate;
if (typeof options.smartFormat === "boolean") query.smart_format = options.smartFormat;
if (typeof options.detectLanguage === "boolean") {
query.detect_language = options.detectLanguage;
}
if (typeof options.punctuate === "boolean") {
query.punctuate = options.punctuate;
}
if (typeof options.smartFormat === "boolean") {
query.smart_format = options.smartFormat;
}
return Object.keys(query).length > 0 ? query : undefined;
}
@@ -908,7 +1054,9 @@ async function runCliEntry(params: {
mediaPath,
});
const text = trimOutput(resolved, maxChars);
if (!text) return null;
if (!text) {
return null;
}
return {
kind: capability === "audio" ? "audio.transcription" : `${capability}.description`,
attachmentIndex: params.attachmentIndex,
@@ -964,8 +1112,12 @@ async function runAttachmentEntries(params: {
});
if (result) {
const decision = buildModelDecision({ entry, entryType, outcome: "success" });
if (result.provider) decision.provider = result.provider;
if (result.model) decision.model = result.model;
if (result.provider) {
decision.provider = result.provider;
}
if (result.model) {
decision.model = result.model;
}
attempts.push(decision);
return { output: result, attempts };
}
@@ -1129,7 +1281,9 @@ export async function runCapability(params: {
entries: resolvedEntries,
config,
});
if (output) outputs.push(output);
if (output) {
outputs.push(output);
}
attachmentDecisions.push({
attachmentIndex: attachment.index,
attempts,
+21 -7
View File
@@ -5,8 +5,12 @@ export type MediaUnderstandingScopeDecision = "allow" | "deny";
function normalizeDecision(value?: string | null): MediaUnderstandingScopeDecision | undefined {
const normalized = value?.trim().toLowerCase();
if (normalized === "allow") return "allow";
if (normalized === "deny") return "deny";
if (normalized === "allow") {
return "allow";
}
if (normalized === "deny") {
return "deny";
}
return undefined;
}
@@ -26,23 +30,33 @@ export function resolveMediaUnderstandingScope(params: {
chatType?: string;
}): MediaUnderstandingScopeDecision {
const scope = params.scope;
if (!scope) return "allow";
if (!scope) {
return "allow";
}
const channel = normalizeMatch(params.channel);
const chatType = normalizeMediaUnderstandingChatType(params.chatType);
const sessionKey = normalizeMatch(params.sessionKey) ?? "";
for (const rule of scope.rules ?? []) {
if (!rule) continue;
if (!rule) {
continue;
}
const action = normalizeDecision(rule.action) ?? "allow";
const match = rule.match ?? {};
const matchChannel = normalizeMatch(match.channel);
const matchChatType = normalizeMediaUnderstandingChatType(match.chatType);
const matchPrefix = normalizeMatch(match.keyPrefix);
if (matchChannel && matchChannel !== channel) continue;
if (matchChatType && matchChatType !== chatType) continue;
if (matchPrefix && !sessionKey.startsWith(matchPrefix)) continue;
if (matchChannel && matchChannel !== channel) {
continue;
}
if (matchChatType && matchChatType !== chatType) {
continue;
}
if (matchPrefix && !sessionKey.startsWith(matchPrefix)) {
continue;
}
return action;
}