openmausbot 0.1.84 → 0.1.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-Ba9G44HI.js +310 -0
- package/dist/assets/{index-DNa2umw-.js → index-CTNNoqSJ.js} +1 -1
- package/dist/assets/index-CkRNp7wX.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +1 -0
- package/dist-server/container-mcp.js +212 -7
- package/dist-server/drivers/agents-proxy.js +135 -12
- package/dist-server/enterprise/server/index.js +1 -0
- package/dist-server/hooks/omb-hook.js +74 -0
- package/dist-server/index.js +8051 -6252
- package/dist-server/local-computer-proxy.js +83 -1
- package/dist-server/local-computer.js +14 -2
- package/dist-server/openmausbot.js +759 -301
- package/dist-server/pair-cli.js +759 -301
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/agent-tool-policy.js +1 -0
- package/dist-server/server/bot-package.js +2 -0
- package/dist-server/server/box.js +23 -4
- package/dist-server/server/browser-engine.js +87 -10
- package/dist-server/server/browser-live.js +7 -5
- package/dist-server/server/browser-runtime.js +65 -10
- package/dist-server/server/checkpoints.js +62 -18
- package/dist-server/server/cli-prompts.js +3 -1
- package/dist-server/server/commands.js +27 -0
- package/dist-server/server/compaction-summary.js +78 -0
- package/dist-server/server/config.js +59 -2
- package/dist-server/server/context-budget.js +29 -0
- package/dist-server/server/context-rebuild.js +72 -0
- package/dist-server/server/digest.js +130 -0
- package/dist-server/server/drivers/acp/core.js +540 -229
- package/dist-server/server/drivers/agents-proxy.js +51 -12
- package/dist-server/server/drivers/agents-result.js +23 -0
- package/dist-server/server/drivers/claude.js +62 -10
- package/dist-server/server/drivers/codex.js +6 -0
- package/dist-server/server/drivers/openai-chat.js +66 -45
- package/dist-server/server/drivers/openai-compat.js +1 -0
- package/dist-server/server/hooks/omb-hook.js +103 -0
- package/dist-server/server/index.js +1127 -365
- package/dist-server/server/local-computer.js +21 -2
- package/dist-server/server/mcp-bridge.js +10 -2
- package/dist-server/server/mcp-tool-images.js +37 -0
- package/dist-server/server/mcp-tool-schema.js +108 -0
- package/dist-server/server/message-db.js +80 -21
- package/dist-server/server/message-file.js +3 -2
- package/dist-server/server/package-export.js +1 -0
- package/dist-server/server/peer-roster.js +4 -2
- package/dist-server/server/provider-icon.js +20 -0
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/request-auth.js +1 -0
- package/dist-server/server/room-handoffs.js +16 -2
- package/dist-server/server/routine-requests.js +39 -1
- package/dist-server/server/routines.js +43 -9
- package/dist-server/server/shared-computer-control.js +31 -4
- package/dist-server/server/steer-queue.js +6 -0
- package/dist-server/server/store.js +94 -14
- package/dist-server/server/system-prompt.js +4 -4
- package/dist-server/server/team-backup.js +9 -1
- package/dist-server/server/tool-results.js +72 -0
- package/dist-server/server/tts/grok.js +74 -0
- package/dist-server/server/tts/index.js +19 -1
- package/dist-server/server/webhooks.js +28 -0
- package/dist-server/shared/approval-mode.js +7 -1
- package/dist-server/shared/digest.js +1 -0
- package/dist-server/shared/markdown-windows-paths.js +36 -0
- package/dist-server/shared/provider-icon.js +110 -0
- package/dist-server/shared/team-backup.js +3 -0
- package/dist-server/vps-container-mcp.js +217 -12
- package/enterprise/server/index.js +1 -0
- package/package.json +1 -1
- package/dist/assets/index-BbU5REzd.js +0 -310
- package/dist/assets/index-CLGfYlx_.css +0 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Short-lived overflow for built-in agents tools, not another history store.
|
|
2
|
+
// Authorize the live turn before using this cache; ownership is BOTH the bot
|
|
3
|
+
// and conversation, so room speakers and sibling threads cannot share IDs.
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { redactSecretsInText } from "../shared/redact.js";
|
|
6
|
+
export const TOOL_RESULT_PREVIEW_CHARS = 16_000;
|
|
7
|
+
export const TOOL_RESULT_MAX_CHARS = 128 * 1024;
|
|
8
|
+
export const TOOL_RESULT_TTL_MS = 60 * 60_000;
|
|
9
|
+
const MAX_RESULTS = 128;
|
|
10
|
+
const MAX_RESULTS_PER_OWNER = 16;
|
|
11
|
+
const MAX_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
const MAX_OWNER_BYTES = 2 * 1024 * 1024;
|
|
13
|
+
export function toolResultPrefix(text, chars) {
|
|
14
|
+
const prefix = text.slice(0, chars);
|
|
15
|
+
const last = prefix.charCodeAt(prefix.length - 1);
|
|
16
|
+
return last >= 0xd800 && last <= 0xdbff ? prefix.slice(0, -1) : prefix;
|
|
17
|
+
}
|
|
18
|
+
/** Bounded in memory; lost on restart, expired after an hour, or evicted
|
|
19
|
+
* oldest-first under pressure. Reads do not extend retention. The transcript
|
|
20
|
+
* remains the durable record; this cache is only a way to page a large answer. */
|
|
21
|
+
export class ToolResults {
|
|
22
|
+
results = new Map();
|
|
23
|
+
now;
|
|
24
|
+
constructor(now = Date.now) { this.now = now; }
|
|
25
|
+
expire() {
|
|
26
|
+
const now = this.now();
|
|
27
|
+
for (const [id, result] of this.results) {
|
|
28
|
+
if (result.expiresAt <= now)
|
|
29
|
+
this.results.delete(id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
save(owner, text, truncated = false) {
|
|
33
|
+
this.expire();
|
|
34
|
+
// Redact before taking the prefix, including a secret crossing its edge.
|
|
35
|
+
const redacted = redactSecretsInText(text);
|
|
36
|
+
const bounded = toolResultPrefix(redacted, TOOL_RESULT_MAX_CHARS);
|
|
37
|
+
const result = { botId: owner.botId, threadId: owner.threadId, text: bounded, bytes: Buffer.byteLength(bounded),
|
|
38
|
+
expiresAt: this.now() + TOOL_RESULT_TTL_MS, truncated: truncated || bounded.length < redacted.length };
|
|
39
|
+
const id = `r-${randomUUID()}`;
|
|
40
|
+
this.results.set(id, result);
|
|
41
|
+
// At most 129 entries are inspected here. Enforce the owner's limit
|
|
42
|
+
// before the global one so one noisy thread does not evict its neighbours.
|
|
43
|
+
for (const ownOnly of [true, false]) {
|
|
44
|
+
const entries = [...this.results].filter(([, entry]) => !ownOnly ||
|
|
45
|
+
(entry.botId === owner.botId && entry.threadId === owner.threadId));
|
|
46
|
+
let bytes = entries.reduce((sum, [, entry]) => sum + entry.bytes, 0);
|
|
47
|
+
let count = entries.length;
|
|
48
|
+
for (const [oldId, entry] of entries) {
|
|
49
|
+
if (bytes <= (ownOnly ? MAX_OWNER_BYTES : MAX_BYTES) && count <= (ownOnly ? MAX_RESULTS_PER_OWNER : MAX_RESULTS))
|
|
50
|
+
break;
|
|
51
|
+
this.results.delete(oldId);
|
|
52
|
+
bytes -= entry.bytes;
|
|
53
|
+
count--;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { id, length: bounded.length, truncated: result.truncated, expiresAt: result.expiresAt };
|
|
57
|
+
}
|
|
58
|
+
read(owner, id, offset) {
|
|
59
|
+
this.expire();
|
|
60
|
+
const result = this.results.get(id);
|
|
61
|
+
if (!result || result.botId !== owner.botId || result.threadId !== owner.threadId)
|
|
62
|
+
return null;
|
|
63
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > result.text.length)
|
|
64
|
+
return null;
|
|
65
|
+
// A supplied offset may point to the low half of a surrogate pair.
|
|
66
|
+
const char = result.text.charCodeAt(offset);
|
|
67
|
+
const start = char >= 0xdc00 && char <= 0xdfff ? Math.max(0, offset - 1) : offset;
|
|
68
|
+
const text = toolResultPrefix(result.text.slice(start), TOOL_RESULT_PREVIEW_CHARS);
|
|
69
|
+
return { id, text, offset: start, nextOffset: start + text.length, length: result.text.length,
|
|
70
|
+
truncated: result.truncated, expiresAt: result.expiresAt };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// xAI TTS runs on the harness and reuses the existing workspace xAI key.
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const API = (process.env.OMB_XAI_TTS_API || "https://api.x.ai/v1").replace(/\/+$/, "");
|
|
4
|
+
const voicesSchema = z.object({
|
|
5
|
+
voices: z.array(z.object({
|
|
6
|
+
voice_id: z.string().min(1),
|
|
7
|
+
name: z.string().min(1),
|
|
8
|
+
description: z.string().optional(),
|
|
9
|
+
})),
|
|
10
|
+
});
|
|
11
|
+
function refusal(status) {
|
|
12
|
+
if (status === 401 || status === 403)
|
|
13
|
+
return "xAI rejected the key or its TTS permissions. Check the xAI key in Settings.";
|
|
14
|
+
if (status === 402)
|
|
15
|
+
return "The xAI account needs credits to generate speech.";
|
|
16
|
+
if (status === 429)
|
|
17
|
+
return "xAI is rate-limiting speech. Wait a moment and try again.";
|
|
18
|
+
if (status === 404)
|
|
19
|
+
return "xAI could not find that voice. Refresh the voice list and select a voice again.";
|
|
20
|
+
return `Grok voice failed (HTTP ${status}). Try again later.`;
|
|
21
|
+
}
|
|
22
|
+
async function request(path, init) {
|
|
23
|
+
let response;
|
|
24
|
+
try {
|
|
25
|
+
response = await fetch(`${API}${path}`, { ...init, redirect: "error" });
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
|
|
29
|
+
throw new Error("Grok voice timed out. Try again.");
|
|
30
|
+
}
|
|
31
|
+
throw new Error("Could not reach Grok voice. Check your connection.");
|
|
32
|
+
}
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
await response.body?.cancel();
|
|
35
|
+
// Remote error bodies can echo credentials or request text.
|
|
36
|
+
throw new Error(refusal(response.status));
|
|
37
|
+
}
|
|
38
|
+
return response;
|
|
39
|
+
}
|
|
40
|
+
export async function listVoices(key) {
|
|
41
|
+
const response = await request("/tts/voices", {
|
|
42
|
+
headers: { authorization: `Bearer ${key}` },
|
|
43
|
+
signal: AbortSignal.timeout(20_000),
|
|
44
|
+
});
|
|
45
|
+
const parsed = voicesSchema.safeParse(await response.json().catch(() => null));
|
|
46
|
+
if (!parsed.success)
|
|
47
|
+
throw new Error("Grok returned an invalid voice list. Try again later.");
|
|
48
|
+
return parsed.data.voices.map((voice) => ({
|
|
49
|
+
id: voice.voice_id, label: voice.name, description: voice.description,
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
export async function synthesize(text, voiceId, key) {
|
|
53
|
+
const response = await request("/tts", {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json", accept: "audio/mpeg" },
|
|
56
|
+
body: JSON.stringify({ text, voice_id: voiceId, language: "auto", output_format: { codec: "mp3" } }),
|
|
57
|
+
signal: AbortSignal.timeout(60_000),
|
|
58
|
+
});
|
|
59
|
+
const mime = (response.headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase();
|
|
60
|
+
if (mime !== "audio/mpeg" && mime !== "audio/mp3") {
|
|
61
|
+
await response.body?.cancel();
|
|
62
|
+
throw new Error("Grok returned an unexpected audio format. Try again later.");
|
|
63
|
+
}
|
|
64
|
+
let bytes;
|
|
65
|
+
try {
|
|
66
|
+
bytes = new Uint8Array(await response.arrayBuffer());
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new Error("Grok audio download was interrupted. Try again.");
|
|
70
|
+
}
|
|
71
|
+
if (!bytes.byteLength)
|
|
72
|
+
throw new Error("Grok returned empty audio. Try again.");
|
|
73
|
+
return { bytes, mime: "audio/mpeg" };
|
|
74
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as chatterbox from "./chatterbox.js";
|
|
2
2
|
import * as elevenlabs from "./elevenlabs.js";
|
|
3
|
+
import * as grok from "./grok.js";
|
|
3
4
|
import * as fish from "./fish.js";
|
|
4
5
|
import * as systemVoices from "./system-voices.js";
|
|
5
6
|
export class NoVoiceConfigured extends Error {
|
|
@@ -17,13 +18,15 @@ export class NoVoiceConfigured extends Error {
|
|
|
17
18
|
}
|
|
18
19
|
export function voiceProvider(cfg) {
|
|
19
20
|
const provider = cfg.tts?.provider;
|
|
20
|
-
return provider === "fish" || provider === "system" || provider === "chatterbox" ? provider : "elevenlabs";
|
|
21
|
+
return provider === "xai" || provider === "fish" || provider === "system" || provider === "chatterbox" ? provider : "elevenlabs";
|
|
21
22
|
}
|
|
22
23
|
/** The system provider needs no credential — it is only ever offered where
|
|
23
24
|
* the platform actually has it, so "configured" means "this engine can
|
|
24
25
|
* speak", not "a key is on file". */
|
|
25
26
|
export function providerConfigured(cfg) {
|
|
26
27
|
const provider = voiceProvider(cfg);
|
|
28
|
+
if (provider === "xai")
|
|
29
|
+
return Boolean(cfg.xai?.key);
|
|
27
30
|
if (provider === "system")
|
|
28
31
|
return systemVoices.systemVoicesAvailable();
|
|
29
32
|
if (provider === "chatterbox")
|
|
@@ -34,6 +37,8 @@ export function providerConfigured(cfg) {
|
|
|
34
37
|
}
|
|
35
38
|
export function voiceConfigured(cfg) {
|
|
36
39
|
const provider = voiceProvider(cfg);
|
|
40
|
+
if (provider === "xai")
|
|
41
|
+
return Boolean(cfg.xai?.key && cfg.tts?.voice);
|
|
37
42
|
if (provider === "system") {
|
|
38
43
|
return systemVoices.systemVoicesAvailable() && Boolean(cfg.tts?.voice);
|
|
39
44
|
}
|
|
@@ -47,6 +52,8 @@ export function voiceConfigured(cfg) {
|
|
|
47
52
|
* because the app-wide fallback has not been selected yet. */
|
|
48
53
|
export function voiceReady(cfg, voiceId) {
|
|
49
54
|
const provider = voiceProvider(cfg);
|
|
55
|
+
if (provider === "xai")
|
|
56
|
+
return Boolean(cfg.xai?.key && (voiceId || cfg.tts?.voice));
|
|
50
57
|
if (provider === "system") {
|
|
51
58
|
return systemVoices.systemVoicesAvailable() && Boolean(voiceId || cfg.tts?.voice);
|
|
52
59
|
}
|
|
@@ -75,6 +82,8 @@ export function verifyKey(provider, key) {
|
|
|
75
82
|
}
|
|
76
83
|
export async function listVoices(cfg, run) {
|
|
77
84
|
const provider = voiceProvider(cfg);
|
|
85
|
+
if (provider === "xai")
|
|
86
|
+
return cfg.xai?.key ? grok.listVoices(cfg.xai.key) : [];
|
|
78
87
|
if (provider === "system")
|
|
79
88
|
return systemVoices.listSystemVoices(run);
|
|
80
89
|
if (provider === "chatterbox") {
|
|
@@ -94,6 +103,15 @@ export async function listVoices(cfg, run) {
|
|
|
94
103
|
* to speak with, which the route turns into a 409 the client can explain. */
|
|
95
104
|
export function speak(cfg, text, voiceId, run) {
|
|
96
105
|
const provider = voiceProvider(cfg);
|
|
106
|
+
if (provider === "xai") {
|
|
107
|
+
const key = cfg.xai?.key;
|
|
108
|
+
if (!key)
|
|
109
|
+
throw new NoVoiceConfigured("key", "Add an xAI key in Settings to use Grok voice.");
|
|
110
|
+
const voice = voiceId || cfg.tts?.voice;
|
|
111
|
+
if (!voice)
|
|
112
|
+
throw new NoVoiceConfigured("voice");
|
|
113
|
+
return grok.synthesize(text, voice, key);
|
|
114
|
+
}
|
|
97
115
|
if (provider === "system") {
|
|
98
116
|
const voice = voiceId || cfg.tts?.voice;
|
|
99
117
|
// An injected runner is the cross-platform test seam for `/usr/bin/say`;
|
|
@@ -12,12 +12,14 @@ const RATE_WINDOW_MS = 60_000;
|
|
|
12
12
|
const RATE_LIMIT = 10;
|
|
13
13
|
const MAX_PENDING_RUNS = 3;
|
|
14
14
|
const runOnSchema = z.enum(["maus", "cloud"]);
|
|
15
|
+
const deliverySchema = z.enum(["run", "post"]);
|
|
15
16
|
const eventTypesSchema = z.array(z.string()).max(20).optional();
|
|
16
17
|
const triggerInputSchema = z.object({
|
|
17
18
|
name: z.string(),
|
|
18
19
|
prompt: z.string(),
|
|
19
20
|
botId: z.string(),
|
|
20
21
|
runOn: runOnSchema.optional(),
|
|
22
|
+
delivery: deliverySchema.optional(),
|
|
21
23
|
enabled: z.boolean().optional(),
|
|
22
24
|
verificationPending: z.boolean().optional(),
|
|
23
25
|
eventTypes: eventTypesSchema,
|
|
@@ -36,6 +38,7 @@ const storedWebhookSchema = z.object({
|
|
|
36
38
|
prompt: z.string(),
|
|
37
39
|
botId: z.string().min(1),
|
|
38
40
|
runOn: runOnSchema,
|
|
41
|
+
delivery: deliverySchema.optional(),
|
|
39
42
|
enabled: z.boolean(),
|
|
40
43
|
createdAt: z.number().finite().nonnegative(),
|
|
41
44
|
updatedAt: z.number().finite().nonnegative(),
|
|
@@ -120,6 +123,8 @@ function cleanInput(input) {
|
|
|
120
123
|
};
|
|
121
124
|
if (eventTypes.length)
|
|
122
125
|
clean.eventTypes = eventTypes;
|
|
126
|
+
if (input.delivery)
|
|
127
|
+
clean.delivery = input.delivery;
|
|
123
128
|
return clean;
|
|
124
129
|
}
|
|
125
130
|
function parseTriggerInput(value) {
|
|
@@ -256,6 +261,7 @@ export class WebhookManager {
|
|
|
256
261
|
prompt: patch.prompt ?? trigger.prompt,
|
|
257
262
|
botId: patch.botId ?? trigger.botId,
|
|
258
263
|
runOn: patch.runOn ?? trigger.runOn,
|
|
264
|
+
delivery: patch.delivery ?? trigger.delivery,
|
|
259
265
|
enabled: patch.enabled ?? trigger.enabled,
|
|
260
266
|
verificationPending: patch.verificationPending ?? trigger.verificationPending,
|
|
261
267
|
eventTypes: patch.eventTypes ?? trigger.eventTypes,
|
|
@@ -397,6 +403,28 @@ export class WebhookManager {
|
|
|
397
403
|
recent.push(now);
|
|
398
404
|
this.rate.set(trigger.endpointId, recent);
|
|
399
405
|
const deliveryId = requestedDeliveryId || randomUUID();
|
|
406
|
+
// delivery:"post": the payload text becomes the bot's own chat message — no task,
|
|
407
|
+
// no model turn. For notification-style webhooks (a scheduled brief, an alert)
|
|
408
|
+
// that should read like the bot said it. Dedup/rate/attempt bookkeeping is shared.
|
|
409
|
+
if (trigger.delivery === "post") {
|
|
410
|
+
// Never fall through to a task run: a stored post webhook on a server
|
|
411
|
+
// without a post sink is a configuration error, not a run request.
|
|
412
|
+
if (!this.options.post)
|
|
413
|
+
fail(503, "This server cannot post webhook messages to chat");
|
|
414
|
+
const raw = event.payload;
|
|
415
|
+
const text = raw && typeof raw === "object" && typeof raw.text === "string" && raw.text.trim() ? raw.text : serializePayload(event.payload);
|
|
416
|
+
this.options.post(trigger.botId, text.slice(0, 20000));
|
|
417
|
+
this.deliveries.push({ key: `${trigger.endpointId}:${deliveryId}`, runId: "post", at: now });
|
|
418
|
+
if (this.deliveries.length > MAX_DELIVERIES)
|
|
419
|
+
this.deliveries.splice(0, this.deliveries.length - MAX_DELIVERIES);
|
|
420
|
+
trigger.lastReceivedAt = now;
|
|
421
|
+
trigger.deliveryCount += 1;
|
|
422
|
+
trigger.updatedAt = now;
|
|
423
|
+
this.appendAttempt(trigger, event, { outcome: "accepted", statusCode: 202, deliveryId, reason: "Posted to chat (no task run)" });
|
|
424
|
+
this.save();
|
|
425
|
+
this.emit(trigger);
|
|
426
|
+
return { deliveryId, duplicate: false };
|
|
427
|
+
}
|
|
400
428
|
const run = this.options.enqueue({
|
|
401
429
|
webhookId: trigger.id,
|
|
402
430
|
webhookName: trigger.name,
|
|
@@ -15,7 +15,13 @@ export function supportsApprovalMode(driverKind, mode) {
|
|
|
15
15
|
return ["claudeAgent", "grokAgent", "antigravityAgent", "qwenAgent", "geminiAgent"].includes(driverKind ?? "");
|
|
16
16
|
if (mode !== "full")
|
|
17
17
|
return true;
|
|
18
|
-
|
|
18
|
+
// The chat-completions family has no provider-side reviewer, so Full is
|
|
19
|
+
// implemented in the harness: createOpenAIChatRuntime answers its own tool
|
|
20
|
+
// gate instead of opening a card. Without this a bot on one of these
|
|
21
|
+
// engines could never stop asking — not by its own level, and not through
|
|
22
|
+
// a Chief's delegated Full access either.
|
|
23
|
+
return ["codex", "claudeAgent", "antigravityAgent", "cursorAgent", "grokAgent", "opencodeGo", "qwenAgent", "geminiAgent",
|
|
24
|
+
"openai-compat", "grok", "minimax"].includes(driverKind ?? "");
|
|
19
25
|
}
|
|
20
26
|
/** A Full/Custom grant belongs to one provider's tool semantics. Other
|
|
21
27
|
* modes carry across only when the destination actually implements them.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Markdown reads a backslash before punctuation as an escape, so a bot's link
|
|
2
|
+
// to C:\Users\me\.openmausbot\report.md parsed as C:\Users\me.openmausbot\report.md:
|
|
3
|
+
// the separator before ".openmausbot" (the default data folder on Windows),
|
|
4
|
+
// "_drafts" or "-old" was dropped and the link named a file that isn't there.
|
|
5
|
+
// In a link, image or definition destination that starts with a drive letter,
|
|
6
|
+
// such a backslash stays a path separator. "\\" and the destination's own
|
|
7
|
+
// delimiters "\(", "\)", "\<" and "\>" still escape, so a destination whose
|
|
8
|
+
// backslashes were already escaped reads as before. The server authorizes a
|
|
9
|
+
// file by the target it parses and the client sends the target it rendered,
|
|
10
|
+
// so both parse with this extension.
|
|
11
|
+
const destinations = new WeakSet();
|
|
12
|
+
const DRIVE = /^[A-Za-z]:(?:[\\/]|$)/;
|
|
13
|
+
const STILL_ESCAPES = new Set(["\\", "(", ")", "<", ">"]);
|
|
14
|
+
function enterDestination() {
|
|
15
|
+
this.buffer();
|
|
16
|
+
destinations.add(this.stack[this.stack.length - 1]);
|
|
17
|
+
}
|
|
18
|
+
function exitCharacterEscapeValue(token) {
|
|
19
|
+
// The text node `characterEscape` opened, as mdast's own data handler pops it.
|
|
20
|
+
const tail = this.stack.pop();
|
|
21
|
+
const value = this.sliceSerialize(token);
|
|
22
|
+
const separator = destinations.has(this.stack[this.stack.length - 1])
|
|
23
|
+
&& DRIVE.test(tail.value)
|
|
24
|
+
&& !STILL_ESCAPES.has(value);
|
|
25
|
+
tail.value += separator ? `\\${value}` : value;
|
|
26
|
+
tail.position.end = { line: token.end.line, column: token.end.column, offset: token.end.offset };
|
|
27
|
+
}
|
|
28
|
+
export const windowsPathDestinations = {
|
|
29
|
+
enter: {
|
|
30
|
+
resourceDestinationString: enterDestination,
|
|
31
|
+
definitionDestinationString: enterDestination,
|
|
32
|
+
},
|
|
33
|
+
exit: {
|
|
34
|
+
characterEscapeValue: exitCharacterEscapeValue,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export const PROVIDER_ICON_PRESETS = [
|
|
2
|
+
"openai", "anthropic", "google", "azure", "aws", "xai", "deepseek",
|
|
3
|
+
"meta", "mistral", "qwen", "moonshot", "cohere", "openrouter",
|
|
4
|
+
];
|
|
5
|
+
export const PROVIDER_ICON_MAX_BYTES = 128 * 1024;
|
|
6
|
+
export const PROVIDER_ICON_MAX_DIMENSION = 1024;
|
|
7
|
+
export const PROVIDER_ICON_MEDIA_TYPES = ["image/png", "image/jpeg", "image/webp"];
|
|
8
|
+
export const PROVIDER_ICON_LABELS = {
|
|
9
|
+
openai: "OpenAI",
|
|
10
|
+
anthropic: "Anthropic",
|
|
11
|
+
google: "Google Gemini",
|
|
12
|
+
azure: "Microsoft Azure",
|
|
13
|
+
aws: "Amazon Bedrock (AWS)",
|
|
14
|
+
xai: "xAI",
|
|
15
|
+
deepseek: "DeepSeek",
|
|
16
|
+
meta: "Meta",
|
|
17
|
+
mistral: "Mistral AI",
|
|
18
|
+
qwen: "Qwen",
|
|
19
|
+
moonshot: "Moonshot AI",
|
|
20
|
+
cohere: "Cohere",
|
|
21
|
+
openrouter: "OpenRouter",
|
|
22
|
+
};
|
|
23
|
+
function decodedBase64Bytes(value) {
|
|
24
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
25
|
+
return Math.floor(value.length * 3 / 4) - padding;
|
|
26
|
+
}
|
|
27
|
+
function imageDimensions(bytes, mime) {
|
|
28
|
+
const u16be = (offset) => bytes[offset] * 256 + bytes[offset + 1];
|
|
29
|
+
const u16le = (offset) => bytes[offset] + bytes[offset + 1] * 256;
|
|
30
|
+
const u24le = (offset) => bytes[offset] + bytes[offset + 1] * 256 + bytes[offset + 2] * 65536;
|
|
31
|
+
const u32le = (offset) => bytes[offset] + bytes[offset + 1] * 0x100 + bytes[offset + 2] * 0x10000 + bytes[offset + 3] * 0x1000000;
|
|
32
|
+
const u32be = (offset) => bytes[offset] * 0x1000000 + bytes[offset + 1] * 0x10000 + bytes[offset + 2] * 0x100 + bytes[offset + 3];
|
|
33
|
+
if (mime === "image/png") {
|
|
34
|
+
if (bytes.length < 33 || bytes.slice(0, 8).join(",") !== "137,80,78,71,13,10,26,10" ||
|
|
35
|
+
String.fromCharCode(...bytes.slice(12, 16)) !== "IHDR" ||
|
|
36
|
+
bytes.slice(-8).join(",") !== "73,69,78,68,174,66,96,130")
|
|
37
|
+
return null;
|
|
38
|
+
return { width: u32be(16), height: u32be(20) };
|
|
39
|
+
}
|
|
40
|
+
if (mime === "image/jpeg") {
|
|
41
|
+
if (bytes.length < 12 || bytes[0] !== 0xff || bytes[1] !== 0xd8 || bytes.at(-2) !== 0xff || bytes.at(-1) !== 0xd9)
|
|
42
|
+
return null;
|
|
43
|
+
for (let offset = 2; offset + 8 < bytes.length;) {
|
|
44
|
+
if (bytes[offset] !== 0xff) {
|
|
45
|
+
offset += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const marker = bytes[offset + 1];
|
|
49
|
+
if ([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf].includes(marker)) {
|
|
50
|
+
return { height: u16be(offset + 5), width: u16be(offset + 7) };
|
|
51
|
+
}
|
|
52
|
+
if (marker === 0xd8 || marker === 0xd9 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
53
|
+
offset += 2;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const length = u16be(offset + 2);
|
|
57
|
+
if (length < 2)
|
|
58
|
+
return null;
|
|
59
|
+
offset += length + 2;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (bytes.length < 30 || String.fromCharCode(...bytes.slice(0, 4)) !== "RIFF" ||
|
|
64
|
+
String.fromCharCode(...bytes.slice(8, 12)) !== "WEBP" || u32le(4) + 8 !== bytes.length)
|
|
65
|
+
return null;
|
|
66
|
+
const format = String.fromCharCode(...bytes.slice(12, 16));
|
|
67
|
+
if (format === "VP8X")
|
|
68
|
+
return { width: u24le(24) + 1, height: u24le(27) + 1 };
|
|
69
|
+
if (format === "VP8L" && bytes[20] === 0x2f) {
|
|
70
|
+
const bits = bytes[21] | bytes[22] << 8 | bytes[23] << 16 | bytes[24] << 24;
|
|
71
|
+
return { width: (bits & 0x3fff) + 1, height: ((bits >>> 14) & 0x3fff) + 1 };
|
|
72
|
+
}
|
|
73
|
+
if (format === "VP8 " && bytes[23] === 0x9d && bytes[24] === 0x01 && bytes[25] === 0x2a) {
|
|
74
|
+
return { width: u16le(26) & 0x3fff, height: u16le(28) & 0x3fff };
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
/** Validate the complete persisted value. Custom images are local data URLs,
|
|
79
|
+
* never remote URLs, and use magic bytes so a forged MIME label is rejected. */
|
|
80
|
+
export function providerIconError(icon) {
|
|
81
|
+
if (icon.kind === "preset") {
|
|
82
|
+
return PROVIDER_ICON_PRESETS.includes(icon.preset)
|
|
83
|
+
? null : "Choose a supported provider icon.";
|
|
84
|
+
}
|
|
85
|
+
const match = /^data:(image\/(?:png|jpeg|webp));base64,([A-Za-z0-9+/]+={0,2})$/.exec(icon.dataUrl);
|
|
86
|
+
if (!match)
|
|
87
|
+
return "Upload a PNG, JPEG, or WebP image.";
|
|
88
|
+
const byteLength = decodedBase64Bytes(match[2]);
|
|
89
|
+
if (byteLength > PROVIDER_ICON_MAX_BYTES) {
|
|
90
|
+
return "Provider icons must be 128 KB or smaller.";
|
|
91
|
+
}
|
|
92
|
+
let decoded;
|
|
93
|
+
try {
|
|
94
|
+
decoded = atob(match[2]);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return "The uploaded image is not valid base64.";
|
|
98
|
+
}
|
|
99
|
+
if (decoded.length !== byteLength)
|
|
100
|
+
return "The uploaded image is not valid base64.";
|
|
101
|
+
const bytes = Uint8Array.from(decoded, (value) => value.charCodeAt(0));
|
|
102
|
+
const dimensions = imageDimensions(bytes, match[1]);
|
|
103
|
+
if (!dimensions)
|
|
104
|
+
return "The uploaded image is incomplete or does not match its file type.";
|
|
105
|
+
if (dimensions.width < 1 || dimensions.height < 1 || dimensions.width > PROVIDER_ICON_MAX_DIMENSION ||
|
|
106
|
+
dimensions.height > PROVIDER_ICON_MAX_DIMENSION || dimensions.width * dimensions.height > PROVIDER_ICON_MAX_DIMENSION ** 2) {
|
|
107
|
+
return `Provider icons must be at most ${PROVIDER_ICON_MAX_DIMENSION} × ${PROVIDER_ICON_MAX_DIMENSION} pixels.`;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
@@ -20,6 +20,9 @@ const task = z.object({
|
|
|
20
20
|
key,
|
|
21
21
|
title: z.string().max(2_000),
|
|
22
22
|
createdAt: timestamp,
|
|
23
|
+
// Travels only when the first message already named this task, so a
|
|
24
|
+
// restore cannot re-arm one generated title on a row that used it.
|
|
25
|
+
titleFromFirstMessage: z.literal(true).optional(),
|
|
23
26
|
openedBy: z.object({ botId: key, name, at: timestamp, kind: z.enum(["pair", "work"]).optional() }).optional(),
|
|
24
27
|
closedBy: z.object({ botId: key, name, at: timestamp }).optional(),
|
|
25
28
|
activeLeafId: key.nullable(),
|