Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff581a1ec4 | ||
|
|
eb0c48631f | ||
|
|
f362a76e38 | ||
|
|
4289a2b255 | ||
|
|
8ad99cbc0f |
No files matched your search
+1
-1
@@ -72,12 +72,12 @@ Status: beta.
|
||||
- **BREAKING:** Gateway auth mode "none" is removed; gateway now requires token/password (Tailscale Serve identity still allowed).
|
||||
|
||||
### Fixes
|
||||
- Hooks: make session-memory message count configurable and filter messages before slicing. (#2732) Thanks @rnijhara.
|
||||
- Discord: restore username directory lookup in target resolution. (#3131) Thanks @bonald.
|
||||
- Agents: align MiniMax base URL test expectation with default provider config. (#3131) Thanks @bonald.
|
||||
- Agents: prevent retries on oversized image errors and surface size limits. (#2871) Thanks @Suksham-sharma.
|
||||
- Agents: inherit provider baseUrl/api for inline models. (#2740) Thanks @lploc94.
|
||||
- Memory Search: keep auto provider model defaults and only include remote when configured. (#2576) Thanks @papago2355.
|
||||
- TTS: read OPENAI_TTS_BASE_URL at runtime instead of module load to honor config.env. (#3341) Thanks @hclsys.
|
||||
- macOS: auto-scroll to bottom when sending a new message while scrolled up. (#2471) Thanks @kennyklee.
|
||||
- Web UI: auto-expand the chat compose textarea while typing (with sensible max height). (#2950) Thanks @shivamraut101.
|
||||
- Gateway: prevent crashes on transient network errors (fetch failures, timeouts, DNS). Added fatal error detection to only exit on truly critical errors. Fixes #2895, #2879, #2873. (#2980) Thanks @elliotsecops.
|
||||
|
||||
@@ -71,7 +71,6 @@ const LOBSTER_ASCII = [
|
||||
"██░███░██░▀▀▀░██░▀▀░███░████░▀▀░██░▀▀▀░███░████",
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
|
||||
" 🦞 FRESH DAILY 🦞 ",
|
||||
" ",
|
||||
];
|
||||
|
||||
export function formatCliBannerArt(options: BannerOptions = {}): string {
|
||||
|
||||
@@ -168,11 +168,6 @@ const entries: SubCliEntry[] = [
|
||||
name: "pairing",
|
||||
description: "Pairing helpers",
|
||||
register: async (program) => {
|
||||
// Initialize plugins before registering pairing CLI.
|
||||
// The pairing CLI calls listPairingChannels() at registration time,
|
||||
// which requires the plugin registry to be populated with channel plugins.
|
||||
const { registerPluginCliCommands } = await import("../../plugins/cli.js");
|
||||
registerPluginCliCommands(program, await loadConfig());
|
||||
const mod = await import("../pairing-cli.js");
|
||||
mod.registerPairingCli(program);
|
||||
},
|
||||
|
||||
@@ -69,8 +69,7 @@ export function printWizardHeader(runtime: RuntimeEnv) {
|
||||
"██░█░█░██░███░██░██████░████░▄▄▀██░███░███░████",
|
||||
"██░███░██░▀▀▀░██░▀▀░███░████░▀▀░██░▀▀▀░███░████",
|
||||
"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀",
|
||||
" 🦞 FRESH DAILY 🦞 ",
|
||||
" ",
|
||||
" 🦞 FRESH DAILY 🦞 ",
|
||||
].join("\n");
|
||||
runtime.log(header);
|
||||
}
|
||||
|
||||
+1
-45
@@ -1,7 +1,7 @@
|
||||
import JSZip from "jszip";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { detectMime, extensionForMime, imageMimeFromFormat } from "./mime.js";
|
||||
import { detectMime, imageMimeFromFormat } from "./mime.js";
|
||||
|
||||
async function makeOoxmlZip(opts: { mainMime: string; partPath: string }): Promise<Buffer> {
|
||||
const zip = new JSZip();
|
||||
@@ -53,47 +53,3 @@ describe("mime detection", () => {
|
||||
expect(mime).toBe("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extensionForMime", () => {
|
||||
it("maps image MIME types to extensions", () => {
|
||||
expect(extensionForMime("image/jpeg")).toBe(".jpg");
|
||||
expect(extensionForMime("image/png")).toBe(".png");
|
||||
expect(extensionForMime("image/webp")).toBe(".webp");
|
||||
expect(extensionForMime("image/gif")).toBe(".gif");
|
||||
expect(extensionForMime("image/heic")).toBe(".heic");
|
||||
});
|
||||
|
||||
it("maps audio MIME types to extensions", () => {
|
||||
expect(extensionForMime("audio/mpeg")).toBe(".mp3");
|
||||
expect(extensionForMime("audio/ogg")).toBe(".ogg");
|
||||
expect(extensionForMime("audio/x-m4a")).toBe(".m4a");
|
||||
expect(extensionForMime("audio/mp4")).toBe(".m4a");
|
||||
});
|
||||
|
||||
it("maps video MIME types to extensions", () => {
|
||||
expect(extensionForMime("video/mp4")).toBe(".mp4");
|
||||
expect(extensionForMime("video/quicktime")).toBe(".mov");
|
||||
});
|
||||
|
||||
it("maps document MIME types to extensions", () => {
|
||||
expect(extensionForMime("application/pdf")).toBe(".pdf");
|
||||
expect(extensionForMime("text/plain")).toBe(".txt");
|
||||
expect(extensionForMime("text/markdown")).toBe(".md");
|
||||
});
|
||||
|
||||
it("handles case insensitivity", () => {
|
||||
expect(extensionForMime("IMAGE/JPEG")).toBe(".jpg");
|
||||
expect(extensionForMime("Audio/X-M4A")).toBe(".m4a");
|
||||
expect(extensionForMime("Video/QuickTime")).toBe(".mov");
|
||||
});
|
||||
|
||||
it("returns undefined for unknown MIME types", () => {
|
||||
expect(extensionForMime("video/unknown")).toBeUndefined();
|
||||
expect(extensionForMime("application/x-custom")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for null or undefined input", () => {
|
||||
expect(extensionForMime(null)).toBeUndefined();
|
||||
expect(extensionForMime(undefined)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -13,10 +13,7 @@ const EXT_BY_MIME: Record<string, string> = {
|
||||
"image/gif": ".gif",
|
||||
"audio/ogg": ".ogg",
|
||||
"audio/mpeg": ".mp3",
|
||||
"audio/x-m4a": ".m4a",
|
||||
"audio/mp4": ".m4a",
|
||||
"video/mp4": ".mp4",
|
||||
"video/quicktime": ".mov",
|
||||
"application/pdf": ".pdf",
|
||||
"application/json": ".json",
|
||||
"application/zip": ".zip",
|
||||
|
||||
+7
-15
@@ -757,19 +757,11 @@ export const OPENAI_TTS_MODELS = ["gpt-4o-mini-tts", "tts-1", "tts-1-hd"] as con
|
||||
* Custom OpenAI-compatible TTS endpoint.
|
||||
* When set, model/voice validation is relaxed to allow non-OpenAI models.
|
||||
* Example: OPENAI_TTS_BASE_URL=http://localhost:8880/v1
|
||||
*
|
||||
* Note: Read at runtime (not module load) to support config.env loading.
|
||||
*/
|
||||
function getOpenAITtsBaseUrl(): string {
|
||||
return (process.env.OPENAI_TTS_BASE_URL?.trim() || "https://api.openai.com/v1").replace(
|
||||
/\/+$/,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
function isCustomOpenAIEndpoint(): boolean {
|
||||
return getOpenAITtsBaseUrl() !== "https://api.openai.com/v1";
|
||||
}
|
||||
const OPENAI_TTS_BASE_URL = (
|
||||
process.env.OPENAI_TTS_BASE_URL?.trim() || "https://api.openai.com/v1"
|
||||
).replace(/\/+$/, "");
|
||||
const isCustomOpenAIEndpoint = OPENAI_TTS_BASE_URL !== "https://api.openai.com/v1";
|
||||
export const OPENAI_TTS_VOICES = [
|
||||
"alloy",
|
||||
"ash",
|
||||
@@ -786,13 +778,13 @@ type OpenAiTtsVoice = (typeof OPENAI_TTS_VOICES)[number];
|
||||
|
||||
function isValidOpenAIModel(model: string): boolean {
|
||||
// Allow any model when using custom endpoint (e.g., Kokoro, LocalAI)
|
||||
if (isCustomOpenAIEndpoint()) return true;
|
||||
if (isCustomOpenAIEndpoint) return true;
|
||||
return OPENAI_TTS_MODELS.includes(model as (typeof OPENAI_TTS_MODELS)[number]);
|
||||
}
|
||||
|
||||
function isValidOpenAIVoice(voice: string): voice is OpenAiTtsVoice {
|
||||
// Allow any voice when using custom endpoint (e.g., Kokoro Chinese voices)
|
||||
if (isCustomOpenAIEndpoint()) return true;
|
||||
if (isCustomOpenAIEndpoint) return true;
|
||||
return OPENAI_TTS_VOICES.includes(voice as OpenAiTtsVoice);
|
||||
}
|
||||
|
||||
@@ -1019,7 +1011,7 @@ async function openaiTTS(params: {
|
||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${getOpenAITtsBaseUrl()}/audio/speech`, {
|
||||
const response = await fetch(`${OPENAI_TTS_BASE_URL}/audio/speech`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
|
||||
@@ -42,7 +42,6 @@ import { renderNodes } from "./views/nodes";
|
||||
import { renderOverview } from "./views/overview";
|
||||
import { renderSessions } from "./views/sessions";
|
||||
import { renderExecApprovalPrompt } from "./views/exec-approval";
|
||||
import { renderGatewayUrlConfirmation } from "./views/gateway-url-confirmation";
|
||||
import {
|
||||
approveDevicePairing,
|
||||
loadDevices,
|
||||
@@ -579,7 +578,6 @@ export function renderApp(state: AppViewState) {
|
||||
: nothing}
|
||||
</main>
|
||||
${renderExecApprovalPrompt(state)}
|
||||
${renderGatewayUrlConfirmation(state)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -33,7 +33,6 @@ type SettingsHost = {
|
||||
basePath: string;
|
||||
themeMedia: MediaQueryList | null;
|
||||
themeMediaHandler: ((event: MediaQueryListEvent) => void) | null;
|
||||
pendingGatewayUrl?: string | null;
|
||||
};
|
||||
|
||||
export function applySettings(host: SettingsHost, next: UiSettings) {
|
||||
@@ -99,7 +98,7 @@ export function applySettingsFromUrl(host: SettingsHost) {
|
||||
if (gatewayUrlRaw != null) {
|
||||
const gatewayUrl = gatewayUrlRaw.trim();
|
||||
if (gatewayUrl && gatewayUrl !== host.settings.gatewayUrl) {
|
||||
host.pendingGatewayUrl = gatewayUrl;
|
||||
applySettings(host, { ...host.settings, gatewayUrl });
|
||||
}
|
||||
params.delete("gatewayUrl");
|
||||
shouldCleanUrl = true;
|
||||
|
||||
@@ -73,7 +73,6 @@ export type AppViewState = {
|
||||
execApprovalQueue: ExecApprovalRequest[];
|
||||
execApprovalBusy: boolean;
|
||||
execApprovalError: string | null;
|
||||
pendingGatewayUrl: string | null;
|
||||
configLoading: boolean;
|
||||
configRaw: string;
|
||||
configRawOriginal: string;
|
||||
@@ -166,8 +165,6 @@ export type AppViewState = {
|
||||
handleNostrProfileImport: () => Promise<void>;
|
||||
handleNostrProfileToggleAdvanced: () => void;
|
||||
handleExecApprovalDecision: (decision: "allow-once" | "allow-always" | "deny") => Promise<void>;
|
||||
handleGatewayUrlConfirm: () => void;
|
||||
handleGatewayUrlCancel: () => void;
|
||||
handleConfigLoad: () => Promise<void>;
|
||||
handleConfigSave: () => Promise<void>;
|
||||
handleConfigApply: () => Promise<void>;
|
||||
|
||||
@@ -152,7 +152,6 @@ export class MoltbotApp extends LitElement {
|
||||
@state() execApprovalQueue: ExecApprovalRequest[] = [];
|
||||
@state() execApprovalBusy = false;
|
||||
@state() execApprovalError: string | null = null;
|
||||
@state() pendingGatewayUrl: string | null = null;
|
||||
|
||||
@state() configLoading = false;
|
||||
@state() configRaw = "{\n}\n";
|
||||
@@ -449,21 +448,6 @@ export class MoltbotApp extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
handleGatewayUrlConfirm() {
|
||||
const nextGatewayUrl = this.pendingGatewayUrl;
|
||||
if (!nextGatewayUrl) return;
|
||||
this.pendingGatewayUrl = null;
|
||||
applySettingsInternal(
|
||||
this as unknown as Parameters<typeof applySettingsInternal>[0],
|
||||
{ ...this.settings, gatewayUrl: nextGatewayUrl },
|
||||
);
|
||||
this.connect();
|
||||
}
|
||||
|
||||
handleGatewayUrlCancel() {
|
||||
this.pendingGatewayUrl = null;
|
||||
}
|
||||
|
||||
// Sidebar handlers for tool output viewing
|
||||
handleOpenSidebar(content: string) {
|
||||
if (this.sidebarCloseTimer != null) {
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { html, nothing } from "lit";
|
||||
|
||||
import type { AppViewState } from "../app-view-state";
|
||||
|
||||
export function renderGatewayUrlConfirmation(state: AppViewState) {
|
||||
const { pendingGatewayUrl } = state;
|
||||
if (!pendingGatewayUrl) return nothing;
|
||||
|
||||
return html`
|
||||
<div class="exec-approval-overlay" role="dialog" aria-modal="true" aria-live="polite">
|
||||
<div class="exec-approval-card">
|
||||
<div class="exec-approval-header">
|
||||
<div>
|
||||
<div class="exec-approval-title">Change Gateway URL</div>
|
||||
<div class="exec-approval-sub">This will reconnect to a different gateway server</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="exec-approval-command mono">${pendingGatewayUrl}</div>
|
||||
<div class="callout danger" style="margin-top: 12px;">
|
||||
Only confirm if you trust this URL. Malicious URLs can compromise your system.
|
||||
</div>
|
||||
<div class="exec-approval-actions">
|
||||
<button
|
||||
class="btn primary"
|
||||
@click=${() => state.handleGatewayUrlConfirm()}
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
@click=${() => state.handleGatewayUrlCancel()}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
Reference in new issue
Block a user