Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
42a5228
Let a message reach a running Claude turn (steer), and keep one proce…
aivsomkar Aug 18, 2026
11f7f4c
feat: let people turn usage analytics off
koeseo Aug 18, 2026
8fec1c4
chore: measure the palette against WCAG AA
koeseo Aug 18, 2026
22761aa
Fold the chat header chips to icon bubbles when the column is narrow
aivsomkar Aug 19, 2026
0bce8d7
merge: upstream main — the settings import block, both sides kept
koeseo Aug 19, 2026
7558ad7
fix: address review — comment stripping, floors for known pairs, count
koeseo Aug 19, 2026
cdd1833
fix: address review — hold the choice in memory, make the guard test …
koeseo Aug 19, 2026
5b3077f
Let Kimi run a local model without a Kimi login.
Aug 18, 2026
fc111ef
Patch existing Kimi aliases with protocol and context size.
Aug 18, 2026
e20c1ff
Parse Kimi config.toml with a string-aware scanner.
Aug 18, 2026
ee79549
Let Droid run a local model without a Factory login.
Aug 18, 2026
fadce23
Drop the unused tomlTableHasKey helper so typecheck passes.
Aug 18, 2026
8497c31
Don't crash the UI when older tasks have no costUsd.
Aug 18, 2026
974a348
Read Unsloth Studio's minted API token from the servers map.
Aug 18, 2026
4d194ec
Honor remaining review notes for Droid, Unsloth, usage, and Kimi TOML.
Aug 19, 2026
37aa31b
Harden the Kimi TOML scanner around comments and array tables.
Aug 20, 2026
848fea8
Allow Auto mode on this Mac's computer after a warning.
Aug 20, 2026
1a2cd0c
Decode TOML basic-string unicode escapes in Kimi headings.
Aug 20, 2026
61a8b48
Decode TOML basic-string unicode escapes in Kimi headings.
Aug 20, 2026
4a1cb62
Address CodeRabbit notes on the Mac local-computer Auto commit.
Aug 20, 2026
4f59e7c
Tighten CodeRabbit follow-ups: invalid TOML escapes, empty CUA arches…
Aug 20, 2026
69b7f67
feat: import a team as a project — one room, on a folder
koeseo Aug 20, 2026
a143c47
fix(build): extract Android Platform Tools on Windows (git-bash)
Ansygroup Aug 20, 2026
795ee65
docs(plan): plan fix for #211 zombie permission cards on turn teardown
Aug 18, 2026
cdd9004
fix(claude): kill the child process tree on every turn teardown
Aug 18, 2026
a5bcbcc
fix(claude): drop permission asks that arrive after broker close
Aug 18, 2026
98ccf9a
refactor(claude): extract systemEndedReply, shared by close() and the…
Aug 18, 2026
e687cad
test(claude): cover the question-kind branch of the late-ask reply
Aug 18, 2026
137bba6
fix(claude): integrate teardown with current main
lightcloud00 Aug 20, 2026
2b3aba8
feat: add pi coding agent as a native RPC engine
Aug 21, 2026
f3047e3
fix: harden pi engine process handling
Aug 21, 2026
3bfa27b
fix: replay history when a pi turn has no resume cursor
Aug 21, 2026
8af4b64
Give the Local VM panel a way to actually drive it (#321)
milind-soni Aug 21, 2026
a50a26d
Merge pull request #319 from lightcloud00/fix/claude-turn-teardown-cu…
milind-soni Aug 21, 2026
e53ff8d
Scrub provider and workspace credentials from the pi child env
milind-soni Aug 21, 2026
103d6b0
Name System32 bsdtar absolutely instead of juggling extractors
milind-soni Aug 21, 2026
25f43a9
Merge main into feat/team-import-project
milind-soni Aug 21, 2026
d76959e
feat(computer): open live desktop in app modal
milind-soni Aug 21, 2026
99cfc9f
Merge pull request #320 from cjpraia/feat/pi-native-engine
milind-soni Aug 21, 2026
95296b1
Merge pull request #317 from Ansygroup/fix/win-android-tools-extract
milind-soni Aug 21, 2026
ca4b9e7
Merge pull request #316 from koeseo/feat/team-import-project
milind-soni Aug 21, 2026
f06c414
Clear PostHog's persisted opt-out before the session's first capture
milind-soni Aug 21, 2026
dc56a62
Merge pull request #250 from koeseo/feat/analytics-opt-out
milind-soni Aug 21, 2026
30343a9
Merge pull request #322 from milind-soni/codex/in-app-vnc-control
milind-soni Aug 21, 2026
5b8511b
feat(local-vm): support isolated per-bot desktops
lightcloud00 Aug 21, 2026
02586f8
fix(local-vm): harden Windows Podman runtime checks
lightcloud00 Aug 21, 2026
54564f7
fix(codex): settle web search activity
08820048 Aug 21, 2026
89aa022
Merge branch 'main' into feat/compact-chat-header
milind-soni Aug 21, 2026
af42bae
Merge pull request #314 from maxkongerskov/feat/kimi-droid-local-no-l…
milind-soni Aug 21, 2026
49833b6
feat(profile): add paired-safe agent profiles and avatars
willsigmon Aug 21, 2026
f3201f6
Merge pull request #333 from 08820048/codex/fix-codex-web-search-status
milind-soni Aug 21, 2026
a4eee84
feat(profile): add persistent desktop profiles and avatar roster
willsigmon Aug 21, 2026
a170cd0
feat(routines): clarify scheduled tasks and exact notifications
willsigmon Aug 21, 2026
77a37a9
Merge pull request #248 from milind-soni/feat/compact-chat-header
milind-soni Aug 21, 2026
aee6065
Merge remote-tracking branch 'origin/main' into review/pr-315
milind-soni Aug 21, 2026
c1ec0a5
Merge pull request #332 from lightcloud00/feature/per-bot-local-vm
milind-soni Aug 21, 2026
341e7b1
fix(ios): follow active tasks and approval choices (#334)
milind-soni Aug 21, 2026
e6c8529
fix(codex): preserve full shell commands (#335)
milind-soni Aug 21, 2026
6601c5d
Merge remote-tracking branch 'origin/main' into review/pr-315
milind-soni Aug 21, 2026
0564fb6
Make the local-auto warning a server-side boundary, and scope Codex h…
milind-soni Aug 21, 2026
7e6ed78
Merge remote-tracking branch 'origin/main' into review/pr-315
milind-soni Aug 21, 2026
983b821
Merge pull request #315 from maxkongerskov/feat/mac-local-computer-auto
milind-soni Aug 21, 2026
9aa28bd
feat(composio): manage multiple connected accounts
willsigmon Aug 21, 2026
9ce214a
Merge pull request #249 from koeseo/chore/contrast-check
milind-soni Aug 21, 2026
9224343
Land review fixes for multi-account: churn guard, budgets, fallbacks
milind-soni Aug 21, 2026
084f594
Merge pull request #330 from willsigmon/codex/composio-multi-account
milind-soni Aug 21, 2026
3426fac
Merge main and harden Claude session steering
milind-soni Aug 21, 2026
f6cd588
Merge pull request #219 from milind-soni/feat/harness-3.2-steering
milind-soni Aug 21, 2026
e6a4630
Merge main into profiles; review fixes: paired test, existence check,…
milind-soni Aug 21, 2026
4f9fe8c
Merge profiles base; StrictMode-proof the patch queue; thread the con…
milind-soni Aug 21, 2026
840665e
Align the tts no-key test with the reworded Settings guidance
milind-soni Aug 21, 2026
db1d5ac
Merge branch 'review/pr-327' into review/pr-328
milind-soni Aug 21, 2026
e13ccd0
Merge pull request #327 from willsigmon/codex/agent-profile-contract-ios
milind-soni Aug 21, 2026
340d1f2
Route room notifications to the room; pin the done-dedup with real pa…
milind-soni Aug 21, 2026
b1e5326
Merge pull request #328 from willsigmon/codex/agent-profile-desktop
milind-soni Aug 21, 2026
279af59
Merge pull request #329 from willsigmon/codex/tasks-routines-notifica…
milind-soni Aug 21, 2026
8c95d11
docs: launch OpenMausBot documentation site (#336)
milind-soni Aug 21, 2026
89d25dd
feat(rooms): add rename action (#343)
08820048 Aug 21, 2026
727bff1
feat(models): project fleet catalog into OpenMausBot
lightcloud00 Aug 22, 2026
c937396
fix(skins): make controls visible on cards in the light skins (#366)
aivsomkar Aug 22, 2026
8d98ce3
Merge remote-tracking branch 'origin/main' into codex/openmaus-fleet-…
lightcloud00 Aug 22, 2026
277170c
fix(models): harden OpenMaus fleet catalog switching
lightcloud00 Aug 22, 2026
06bb384
test(models): exercise persistence rollback portably
lightcloud00 Aug 22, 2026
b818d95
feat(notifications): group desktop notifications by bot with bot avat…
willsigmon Aug 23, 2026
8cfbd3d
feat(desktop): enforce single-instance lock (#373)
willsigmon Aug 23, 2026
946b376
fix(grok): allow local models without subscription login (#371)
maxkongerskov Aug 23, 2026
b14dc8f
fix(codex): support MCP elicitation approvals (#378)
milind-soni Aug 23, 2026
3c1599e
fix(cursor): use the ACP session model ID (#379)
milind-soni Aug 23, 2026
05743ea
feat(desktop): export privacy-safe diagnostics (#377)
milind-soni Aug 23, 2026
03d32c7
fix(hermes): expose configured and advertised models (#380)
milind-soni Aug 23, 2026
6ff370e
feat(rooms): manage members after creation (#381)
milind-soni Aug 23, 2026
8717716
fix(drivers): flush assistant text before tool events (#382)
milind-soni Aug 23, 2026
74fa30b
feat(sidebar): file rooms under shared sections (#384)
milind-soni Aug 23, 2026
db52316
feat: scout a project folder into a suggested team (#339)
koeseo Aug 23, 2026
1dcb1a4
fix(rooms): explain when archived members cannot respond (#385)
milind-soni Aug 23, 2026
ef68ceb
feat(drivers): add a safe OpenAI-compatible provider (#386)
milind-soni Aug 23, 2026
0353c0e
fix(steer): queue busy 1:1 sends without corrupting the transcript (#…
milind-soni Aug 23, 2026
8ff4166
Merge current upstream main for OpenMaus #364 closeout
lightcloud00 Aug 23, 2026
4478e4f
Merge exact upstream PR 364 head into fork closeout
lightcloud00 Aug 23, 2026
429cf2f
fix: reconcile dual-VM bridge with current upstream
lightcloud00 Aug 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions electron/diagnostics.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// One-click bug-report bundle: app facts, a safe config summary and the
// server.log tail, formatted for pasting into a public issue. Pure string
// work lives here so redaction stays unit-testable without Electron; main.mjs
// owns the fs and dialog plumbing. Safety is layered: the collector never
// reads secret fields at all (only the server's booleans-only config status),
// and everything that does get in is scrubbed again here before it lands on
// disk — so a future collector mistake still cannot leak a credential.
//
// CREDENTIAL_ENV_NAMES mirrors WORKSPACE_CREDENTIAL_ENV (server/config.ts).
// Duplicated because the desktop shell cannot import TypeScript; a test
// asserts the two lists never drift apart.
export const CREDENTIAL_ENV_NAMES = [
"XAI_API_KEY",
"BOX_TOKEN",
"OPENCODE_API_KEY",
"OMB_TTS_KEY",
"OMB_OPENAI_IMAGE_KEY",
"COMPOSIO_API_KEY",
"OMB_COMPOSIO_BROKER_TOKEN",
];

// Credential-shaped tokens (server/redact.ts parity): unmistakable formats
// are masked wherever they appear, keyed or not.
const CREDENTIAL_TOKEN_FORMATS = [
/\bsk-[A-Za-z0-9_-]{16,}/g,
/\bxai-[A-Za-z0-9]{16,}/g,
/\bak_[A-Za-z0-9_-]{16,}/g,
/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g,
/\bgithub_pat_[A-Za-z0-9_]{20,}/g,
/\bxox[abposr]-[A-Za-z0-9-]{20,}/g,
/\bAKIA[0-9A-Z]{16}\b/g,
/\bAIza[0-9A-Za-z_-]{30,}/g,
/\bnpm_[A-Za-z0-9]{20,}/g,
];
const KEY_VALUE_PAIR =
/\b([A-Za-z0-9_.-]*(?:api[_-]?key|apikey|secret|token|password|passwd|authorization|auth[_-]?token|access[_-]?key|private[_-]?key)s?)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s"',;)\]}]+)/gi;
const AUTHORIZATION =
/\b(authorization)\s*[:=]\s*(?:"|')?([A-Za-z][A-Za-z0-9_-]*\s+[A-Za-z0-9._~+/=-]+)(?:"|')?/gi;
const BEARER = /(\bbearer\s+)([A-Za-z0-9._~+/=-]{8,})/gi;
const PEM_BLOCK =
/(-----BEGIN [A-Z ]*PRIVATE KEY-----)[\s\S]*?(-----END [A-Z ]*PRIVATE KEY-----)/g;

const mask = (value) => `«redacted ${value.length} chars»`;
const unquote = (value) => value.replace(/^["']|["']$/g, "");

// Shared value grammar; String.raw keeps \s and \] intact when the pattern
// is embedded into the dynamically built env-name regexes below.
const VALUE_PART = String.raw`("[^"]*"|'[^']*'|[^\s"',;)\]}]+)`;

export function redactSecretsInLine(line) {
let out = String(line ?? "");
const alreadyMasked = (value) => String(value).includes("«redacted");
for (const name of CREDENTIAL_ENV_NAMES) {
out = out.replace(
new RegExp(`\\b(${name})\\s*[:=]\\s*${VALUE_PART}`, "gi"),
(_match, key, value) => `${key}=${mask(unquote(value))}`,
);
}
out = out.replace(AUTHORIZATION, (_match, key, value) => `${key}=${mask(value)}`);
out = out.replace(BEARER, (_match, lead, token) => `${lead}${mask(token)}`);
out = out.replace(PEM_BLOCK, (_match, open, close) => `${open}«redacted private key»${close}`);
out = out.replace(KEY_VALUE_PAIR, (_match, key, value) =>
alreadyMasked(value) ? _match : `${key}=${mask(unquote(value))}`,
);
for (const format of CREDENTIAL_TOKEN_FORMATS) out = out.replace(format, (found) => mask(found));
return out;
}

/** Decode a bounded log buffer without exporting the partial first line that
* may begin before the read boundary. */
export function decodeLogTail(buffer, truncated = false) {
if (!Buffer.isBuffer(buffer)) return { tail: "", bytes: 0 };
let complete = buffer;
if (truncated) {
const newline = buffer.indexOf(0x0a);
complete = newline < 0 ? buffer.subarray(0, 0) : buffer.subarray(newline + 1);
}
return { tail: complete.toString("utf8"), bytes: complete.length };
}

const APP_INFO_KEYS = ["version", "platform", "arch", "electron", "node", "packaged", "uptimeSeconds"];

// A config summary entry is publishable only when it carries no credential:
// Only booleans and finite numbers pass. Strings can contain names, paths,
// account identifiers, or other personal data even when the field name is
// not credential-shaped, so they never reach the file. Everything else
// (objects beyond flattening, arrays, nulls) is dropped too.
const isFiniteNumber = (value) => Number.isFinite(value) && Object.prototype.toString.call(value) === "[object Number]";

function summaryAllows(value) {
if (Object.prototype.toString.call(value) === "[object Boolean]") return true;
return isFiniteNumber(value);
}

function flattenSummary(input, prefix = "", depth = 0, out = {}) {
if (!input || Object.prototype.toString.call(input) !== "[object Object]") return out;
for (const [key, value] of Object.entries(input)) {
const path = prefix ? `${prefix}.${key}` : key;
if (Array.isArray(value)) continue;
if (value && Object.prototype.toString.call(value) === "[object Object]" && depth < 3)
flattenSummary(value, path, depth + 1, out);
else out[path] = value;
}
return out;
}

export function buildDiagnosticsReport({
appInfo = {},
configSummary = {},
logTail,
now = new Date().toISOString(),
} = {}) {
const lines = [];
lines.push("OpenMausBot diagnostics");
lines.push(`Generated: ${now}`);
lines.push("");
lines.push("## App");
for (const key of APP_INFO_KEYS) {
if (appInfo[key] === undefined || appInfo[key] === null) continue;
lines.push(`${key}=${String(appInfo[key])}`);
}
lines.push("");
lines.push("## Configuration");
lines.push("# presence/count/mode only — credentials stay OS-encrypted and are never read");
const summary = flattenSummary(configSummary);
let shown = 0;
for (const key of Object.keys(summary).sort()) {
if (!summaryAllows(summary[key])) continue;
lines.push(`${key}=${summary[key]}`);
shown += 1;
}
if (!shown) lines.push("(no configuration summary available)");
lines.push("");
lines.push(logTail && logTail.trim() ? "## Server log tail — known credential patterns auto-masked" : "## Server log tail");
if (logTail && logTail.trim()) {
for (const line of redactSecretsInLine(logTail).split(/\r?\n/)) lines.push(line);
} else {
lines.push("(server log unavailable)");
}
lines.push("");
return lines.join("\n");
}

export function diagnosticsFileName(date = new Date()) {
const pad = (n) => String(n).padStart(2, "0");
return (
`openmausbot-diagnostics-${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}` +
`-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}.txt`
);
}
189 changes: 189 additions & 0 deletions electron/diagnostics.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { describe, expect, it } from "vitest";
import { createRequire } from "node:module";
import { readFileSync } from "node:fs";

const require = createRequire(import.meta.url);
const {
buildDiagnosticsReport,
decodeLogTail,
diagnosticsFileName,
redactSecretsInLine,
CREDENTIAL_ENV_NAMES,
} = require("./diagnostics.mjs");

// The desktop shell cannot import TypeScript, so its credential list is a
// hand copy of server/config.ts WORKSPACE_CREDENTIAL_ENV. This test is the
// drift alarm: a name added server-side without updating the copy here would
// otherwise ship an unredacted export path.
describe("credential env parity with server/config.ts", () => {
it("matches WORKSPACE_CREDENTIAL_ENV exactly", () => {
const config = readFileSync(new URL("../server/config.ts", import.meta.url), "utf8");
const match = config.match(/WORKSPACE_CREDENTIAL_ENV = \[([\s\S]*?)\] as const/);
expect(match).not.toBeNull();
const names = [...match[1].matchAll(/"([A-Z0-9_]+)"/g)].map((m) => m[1]);
expect(CREDENTIAL_ENV_NAMES).toEqual(names);
});
});

describe("buildDiagnosticsReport", () => {
const appInfo = {
version: "0.1.27",
platform: "darwin",
arch: "arm64",
electron: "43.4.0",
node: "24.0.0",
packaged: true,
uptimeSeconds: 42,
};

it("renders app facts and a sorted config summary", () => {
const report = buildDiagnosticsReport({
appInfo,
configSummary: {
xai: { configured: true },
box: { configured: false },
rooms: { turnTimeoutMinutes: 5 },
},
logTail: "",
});
expect(report).toContain("version=0.1.27");
expect(report).toContain("platform=darwin");
expect(report).toContain("arch=arm64");
expect(report).toContain("xai.configured=true");
expect(report).toContain("box.configured=false");
expect(report).toContain("rooms.turnTimeoutMinutes=5");
expect(report).toContain("(server log unavailable)");
});

it("drops strings, non-scalars and credential-shaped summary values", () => {
const report = buildDiagnosticsReport({
appInfo,
configSummary: {
xai: { key: "xai-real-secret" },
composio: { apiKey: "ak_live_abcdef123456789" },
vps: { sshAlias: "" },
profile: { name: "Ada" },
instances: [{ driver: "claudeAgent", environment: { TOKEN: "hunter2" } }],
note: "sk-ant-api03-abcdefghijklmnopqrstuvwxyz",
},
logTail: "",
});
expect(report).not.toContain("xai-real-secret");
expect(report).not.toContain("ak_live_abcdef123456789");
expect(report).not.toContain("hunter2");
expect(report).not.toContain("driver");
expect(report).not.toContain("environment");
expect(report).not.toContain("profile.name=");
expect(report).not.toContain("Ada");
expect(report).not.toContain("note=");
});

it("never includes an absolute log path in the report heading", () => {
const report = buildDiagnosticsReport({
appInfo,
configSummary: {},
logTail: "server ready",
logPath: "/Users/ada/Library/Logs/OpenMausBot/server.log",
});
expect(report).toContain("## Server log tail");
expect(report).not.toContain("/Users/ada");
});

it.each(CREDENTIAL_ENV_NAMES)("masks any value riding on %s in the log tail", (name) => {
const value = "s3cr3t-value-123456";
const line = `spawn env ${name}=${value} ready`;
const report = buildDiagnosticsReport({ appInfo, configSummary: {}, logTail: line });
expect(report).not.toContain(value);
expect(redactSecretsInLine(line)).toBe(`spawn env ${name}=«redacted ${value.length} chars» ready`);
});

it("masks generic key=value secrets and content-shaped tokens in the log tail", () => {
const report = buildDiagnosticsReport({
appInfo,
configSummary: {},
logTail: [
'config {"apiKey":"sk-proj-abcdefghijklmnop"}',
"Authorization: Bearer abcdefghijklmnop",
"password=hunter2000",
].join("\n"),
});
expect(report).not.toContain("sk-proj-abcdefghijklmnop");
expect(report).not.toContain("abcdefghijklmnop");
expect(report).not.toContain("hunter2000");
expect(report).toContain("«redacted");
});

it.each(["Bearer abcdefghijklmnop", "Basic dXNlcjpwYXNzd29yZA=="])(
"masks the full Authorization credential for %s",
(authorization) => {
const report = buildDiagnosticsReport({
appInfo,
configSummary: {},
logTail: `request Authorization: ${authorization}`,
});
expect(report).not.toContain(authorization);
expect(report).not.toContain(authorization.split(" ")[1]);
expect(report).toContain("Authorization=«redacted");
},
);

it("masks a multiline PEM private key as one value", () => {
const report = buildDiagnosticsReport({
appInfo,
configSummary: {},
logTail: [
"loading credential",
"-----BEGIN PRIVATE KEY-----",
"super-secret-line-one",
"super-secret-line-two",
"-----END PRIVATE KEY-----",
"ready",
].join("\n"),
});
expect(report).not.toContain("super-secret-line-one");
expect(report).not.toContain("super-secret-line-two");
expect(report).toContain("«redacted private key»");
});

it("leaves ordinary log lines untouched", () => {
const line = "[2026-08-22T20:00:00.000Z] [out] fork server/index.js port=8799 spawned pid=4242";
expect(redactSecretsInLine(line)).toBe(line);
});

it("handles an empty or missing log gracefully", () => {
for (const logTail of ["", null, undefined]) {
const report = buildDiagnosticsReport({ appInfo, configSummary: {}, logTail });
expect(report).toContain("(server log unavailable)");
expect(report.endsWith("\n")).toBe(true);
}
});

it("keeps long prose lines that merely mention a key by name", () => {
const line = "user asked whether the XAI_API_KEY variable needs to be set manually";
expect(redactSecretsInLine(line)).toBe(line);
});
});

describe("decodeLogTail", () => {
it("preserves the full buffer when the read starts at the beginning", () => {
expect(decodeLogTail(Buffer.from("first\nsecond"), false)).toEqual({ tail: "first\nsecond", bytes: 12 });
});

it("drops a credential assignment split by a bounded tail read", () => {
const decoded = decodeLogTail(Buffer.from("RET=split-secret\nserver ready\n"), true);
expect(decoded).toEqual({ tail: "server ready\n", bytes: 13 });
expect(decoded.tail).not.toContain("split-secret");
});

it("returns an empty tail when a truncated buffer has no complete line", () => {
expect(decodeLogTail(Buffer.from("partial-secret"), true)).toEqual({ tail: "", bytes: 0 });
});
});

describe("diagnosticsFileName", () => {
it("uses openmausbot-diagnostics-YYYYMMDD-HHmmss.txt", () => {
expect(diagnosticsFileName(new Date(2026, 7, 22, 16, 5, 9))).toBe(
"openmausbot-diagnostics-20260822-160509.txt",
);
});
});
Loading
Loading