openmausbot 0.1.77 → 0.1.79
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-DeK-z9ld.js → index-C2Je5jdF.js} +1 -1
- package/dist/assets/index-D_TFZLlo.js +305 -0
- package/dist/assets/index-Pbb6Ao0s.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +11 -5
- package/dist-server/drivers/agents-proxy.js +786 -29
- package/dist-server/enterprise/server/index.js +0 -1
- package/dist-server/index.js +17598 -7843
- package/dist-server/local-computer.js +0 -1
- package/dist-server/mcp-server.js +8 -1
- package/dist-server/openmausbot.js +9195 -759
- package/dist-server/pair-cli.js +9195 -759
- package/dist-server/proxy-paths.js +0 -1
- package/dist-server/server/auto-approve.js +16 -0
- package/dist-server/server/bot-overview.js +6 -2
- package/dist-server/server/bot-package.js +17 -3
- package/dist-server/server/box.js +98 -41
- package/dist-server/server/browser-engine.js +1 -1
- package/dist-server/server/browser-runtime.js +11 -3
- package/dist-server/server/browser-tool-shape.js +72 -0
- package/dist-server/server/chief-of-staff.js +2 -2
- package/dist-server/server/claude-accounts.js +1 -0
- package/dist-server/server/cli-setup.js +1 -1
- package/dist-server/server/composio.js +18 -0
- package/dist-server/server/config.js +16 -4
- package/dist-server/server/container-computer.js +0 -12
- package/dist-server/server/drivers/acp/core.js +4 -19
- package/dist-server/server/drivers/agents-proxy.js +74 -25
- package/dist-server/server/drivers/boxagent.js +48 -4
- package/dist-server/server/drivers/chat-mcp-tools.js +368 -0
- package/dist-server/server/drivers/chat-tool-approval.js +42 -0
- package/dist-server/server/drivers/claude.js +87 -32
- package/dist-server/server/drivers/codex.js +69 -31
- package/dist-server/server/drivers/grok.js +4 -0
- package/dist-server/server/drivers/minimax.js +11 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +92 -0
- package/dist-server/server/drivers/openai-chat.js +332 -88
- package/dist-server/server/drivers/openai-compat.js +4 -0
- package/dist-server/server/drivers/pi.js +1 -9
- package/dist-server/server/index.js +271 -136
- package/dist-server/server/package-export.js +16 -14
- package/dist-server/server/peer-approval.js +1 -1
- package/dist-server/server/profile-requests.js +26 -4
- package/dist-server/server/proxy-paths.js +0 -1
- package/dist-server/server/room-handoffs.js +85 -10
- package/dist-server/server/routine-requests.js +86 -3
- package/dist-server/server/routines.js +31 -13
- package/dist-server/server/setup-mode.js +11 -13
- package/dist-server/server/skill-learn.js +4 -4
- package/dist-server/server/skills.js +21 -0
- package/dist-server/server/store.js +90 -1
- package/dist-server/server/system-prompt.js +7 -5
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-setup-requests.js +83 -31
- package/dist-server/server/tts/chatterbox.js +83 -0
- package/dist-server/server/tts/index.js +45 -12
- package/dist-server/server/workspace.js +12 -2
- package/dist-server/shared/ask-question.js +28 -0
- package/dist-server/shared/computer-contention.js +17 -0
- package/dist-server/shared/cron-label.js +39 -0
- package/dist-server/shared/routine-schedule.js +89 -0
- package/dist-server/shared/team-backup.js +1 -1
- package/dist-server/vps-container-mcp.js +11 -5
- package/enterprise/server/index.js +0 -1
- package/package.json +1 -1
- package/dist/assets/index-XdHJ-9CF.css +0 -1
- package/dist/assets/index-lhqE1umn.js +0 -305
- package/dist-server/computer-proxy.js +0 -1196
- package/dist-server/server/computer-proxy.js +0 -1070
- package/dist-server/server/remote-computer.js +0 -170
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as chatterbox from "./chatterbox.js";
|
|
1
2
|
import * as elevenlabs from "./elevenlabs.js";
|
|
2
3
|
import * as systemVoices from "./system-voices.js";
|
|
3
4
|
export class NoVoiceConfigured extends Error {
|
|
@@ -5,52 +6,73 @@ export class NoVoiceConfigured extends Error {
|
|
|
5
6
|
// runs under `node --experimental-strip-types`, which is strip-ONLY, so a
|
|
6
7
|
// parameter property is rejected at load time even though it typechecks
|
|
7
8
|
reason;
|
|
8
|
-
constructor(reason) {
|
|
9
|
-
super(
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
constructor(reason, hint) {
|
|
10
|
+
super(hint ??
|
|
11
|
+
(reason === "key"
|
|
12
|
+
? "Add an ElevenLabs key in Settings on the computer to turn on voice."
|
|
13
|
+
: "Pick a voice in the agent profile."));
|
|
12
14
|
this.reason = reason;
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
export function voiceProvider(cfg) {
|
|
16
|
-
return cfg.tts?.provider === "system"
|
|
18
|
+
return cfg.tts?.provider === "system" || cfg.tts?.provider === "chatterbox" ? cfg.tts.provider : "elevenlabs";
|
|
17
19
|
}
|
|
18
20
|
/** The system provider needs no credential — it is only ever offered where
|
|
19
21
|
* the platform actually has it, so "configured" means "this engine can
|
|
20
22
|
* speak", not "a key is on file". */
|
|
21
23
|
export function providerConfigured(cfg) {
|
|
22
|
-
|
|
24
|
+
const provider = voiceProvider(cfg);
|
|
25
|
+
if (provider === "system")
|
|
26
|
+
return systemVoices.systemVoicesAvailable();
|
|
27
|
+
if (provider === "chatterbox")
|
|
28
|
+
return Boolean(cfg.tts?.baseUrl?.trim());
|
|
29
|
+
return Boolean(cfg.tts?.key);
|
|
23
30
|
}
|
|
24
31
|
export function voiceConfigured(cfg) {
|
|
25
|
-
|
|
32
|
+
const provider = voiceProvider(cfg);
|
|
33
|
+
if (provider === "system") {
|
|
26
34
|
return systemVoices.systemVoicesAvailable() && Boolean(cfg.tts?.voice);
|
|
27
35
|
}
|
|
36
|
+
if (provider === "chatterbox")
|
|
37
|
+
return Boolean(cfg.tts?.baseUrl?.trim() && cfg.tts?.voice);
|
|
28
38
|
return Boolean(cfg.tts?.key && cfg.tts?.voice);
|
|
29
39
|
}
|
|
30
40
|
/** A per-bot voice is a complete choice too; it should not be blocked just
|
|
31
41
|
* because the app-wide fallback has not been selected yet. */
|
|
32
42
|
export function voiceReady(cfg, voiceId) {
|
|
33
|
-
|
|
43
|
+
const provider = voiceProvider(cfg);
|
|
44
|
+
if (provider === "system") {
|
|
34
45
|
return systemVoices.systemVoicesAvailable() && Boolean(voiceId || cfg.tts?.voice);
|
|
35
46
|
}
|
|
47
|
+
if (provider === "chatterbox")
|
|
48
|
+
return Boolean(cfg.tts?.baseUrl?.trim() && (voiceId || cfg.tts?.voice));
|
|
36
49
|
return Boolean(cfg.tts?.key && (voiceId || cfg.tts?.voice));
|
|
37
50
|
}
|
|
38
51
|
/** What the settings panel needs. Never includes the key — same write-only
|
|
39
|
-
* rule as every other credential.
|
|
52
|
+
* rule as every other credential. baseUrl and model are Chatterbox
|
|
53
|
+
* settings, not credentials, so they come back in full. */
|
|
40
54
|
export function describeVoice(cfg) {
|
|
55
|
+
const provider = voiceProvider(cfg);
|
|
41
56
|
return {
|
|
42
57
|
configured: providerConfigured(cfg),
|
|
43
58
|
ready: voiceConfigured(cfg),
|
|
44
59
|
voice: cfg.tts?.voice ?? "",
|
|
45
|
-
provider
|
|
60
|
+
provider,
|
|
61
|
+
baseUrl: provider === "chatterbox" ? (cfg.tts?.baseUrl ?? "") : "",
|
|
62
|
+
model: provider === "chatterbox" ? (cfg.tts?.model ?? "") : "",
|
|
46
63
|
};
|
|
47
64
|
}
|
|
48
65
|
export function verifyKey(key) {
|
|
49
66
|
return elevenlabs.verifyKey(key);
|
|
50
67
|
}
|
|
51
68
|
export async function listVoices(cfg, run) {
|
|
52
|
-
|
|
69
|
+
const provider = voiceProvider(cfg);
|
|
70
|
+
if (provider === "system")
|
|
53
71
|
return systemVoices.listSystemVoices(run);
|
|
72
|
+
if (provider === "chatterbox") {
|
|
73
|
+
const baseUrl = cfg.tts?.baseUrl?.trim();
|
|
74
|
+
return baseUrl ? chatterbox.listChatterboxVoices(baseUrl) : [];
|
|
75
|
+
}
|
|
54
76
|
const key = cfg.tts?.key;
|
|
55
77
|
if (!key)
|
|
56
78
|
return [];
|
|
@@ -59,7 +81,8 @@ export async function listVoices(cfg, run) {
|
|
|
59
81
|
/** Synthesize one utterance. Throws NoVoiceConfigured when there is nothing
|
|
60
82
|
* to speak with, which the route turns into a 409 the client can explain. */
|
|
61
83
|
export function speak(cfg, text, voiceId, run) {
|
|
62
|
-
|
|
84
|
+
const provider = voiceProvider(cfg);
|
|
85
|
+
if (provider === "system") {
|
|
63
86
|
const voice = voiceId || cfg.tts?.voice;
|
|
64
87
|
// An injected runner is the cross-platform test seam for `/usr/bin/say`;
|
|
65
88
|
// production calls omit it and remain strictly Darwin-gated.
|
|
@@ -69,6 +92,16 @@ export function speak(cfg, text, voiceId, run) {
|
|
|
69
92
|
throw new NoVoiceConfigured("voice");
|
|
70
93
|
return systemVoices.synthesizeSystem(text, voice, run);
|
|
71
94
|
}
|
|
95
|
+
if (provider === "chatterbox") {
|
|
96
|
+
const baseUrl = cfg.tts?.baseUrl?.trim();
|
|
97
|
+
if (!baseUrl) {
|
|
98
|
+
throw new NoVoiceConfigured("key", "Add the address of your Chatterbox server in Settings on the computer to turn on voice.");
|
|
99
|
+
}
|
|
100
|
+
const voice = voiceId || cfg.tts?.voice;
|
|
101
|
+
if (!voice)
|
|
102
|
+
throw new NoVoiceConfigured("voice");
|
|
103
|
+
return chatterbox.synthesizeChatterbox(text, voice, baseUrl, cfg.tts?.model);
|
|
104
|
+
}
|
|
72
105
|
const key = cfg.tts?.key;
|
|
73
106
|
if (!key)
|
|
74
107
|
throw new NoVoiceConfigured("key");
|
|
@@ -16,6 +16,10 @@ import { redactSecretsInText } from "./redact.js";
|
|
|
16
16
|
import { DATA_DIR } from "./config.js";
|
|
17
17
|
export const WORKSPACES_DIR = join(DATA_DIR, "workspaces");
|
|
18
18
|
export const TASK_WORKSPACES_DIR = join(DATA_DIR, "task-workspaces");
|
|
19
|
+
/** MCP support does not imply native filesystem tools or a local working directory. */
|
|
20
|
+
export function supportsWorkspaceFiles(driverKind) {
|
|
21
|
+
return !["grok", "openai-compat", "minimax", "boxAgent"].includes(driverKind);
|
|
22
|
+
}
|
|
19
23
|
/** Default task files are private to the thread, outside the bot's shared
|
|
20
24
|
* memory folder. This is directory organization, not a shell sandbox. */
|
|
21
25
|
export function ensureTaskWorkspace(botId, threadId) {
|
|
@@ -519,9 +523,15 @@ export function memorySystemPrompt(botId, opts = {}) {
|
|
|
519
523
|
const memory = loadMemory(botId);
|
|
520
524
|
const memoryFile = join(workspaceDir(botId), "MEMORY.md");
|
|
521
525
|
const topicDir = join(workspaceDir(botId), "memory");
|
|
526
|
+
if (opts.fileTools === false && !opts.managedWrites) {
|
|
527
|
+
if (!memory)
|
|
528
|
+
return "";
|
|
529
|
+
return ` Your saved memory is supplied as context; this turn has no memory editing tools.\n\nYour memory (MEMORY.md):\n${memory.text}${memory.truncated ? " [Only the initial memory excerpt is visible.]" : ""}`;
|
|
530
|
+
}
|
|
522
531
|
const writeGuidance = opts.managedWrites
|
|
523
532
|
? " This memory is shared across your independent threads. Use memory_update for every change to MEMORY.md, never direct file tools or whole-file overwrites." +
|
|
524
|
-
" Append new facts, or replace/remove an exact unique old_text passage. If it conflicts,
|
|
533
|
+
" Append new facts, or replace/remove an exact unique old_text passage. If it conflicts, " +
|
|
534
|
+
(opts.fileTools === false ? "use session_search to find the current passage" : "read the current file") + " and retry only your intended change."
|
|
525
535
|
: " When you learn something worth keeping, update it with your file tools; remove notes that turn out to be wrong.";
|
|
526
536
|
const guidance = ` Your private long-term memory file is ${JSON.stringify(memoryFile)}.` +
|
|
527
537
|
" It stays separate from a custom project working folder." +
|
|
@@ -533,7 +543,7 @@ export function memorySystemPrompt(botId, opts = {}) {
|
|
|
533
543
|
return guidance;
|
|
534
544
|
const truncatedNote = memory.truncated
|
|
535
545
|
? ` [MEMORY.md is ${memory.lines} lines and ${memory.bytes} bytes; only the first ${MEMORY_MAX_LINES} lines / ${MEMORY_MAX_BYTES} bytes are shown above and the rest is not visible to you. ${opts.managedWrites
|
|
536
|
-
? "Consolidate it now with memory_update: replace or remove older entries, or move detail to a memory/<topic>.md file."
|
|
546
|
+
? "Consolidate it now with memory_update: replace or remove older entries" + (opts.fileTools === false ? "." : ", or move detail to a memory/<topic>.md file.")
|
|
537
547
|
: "Trim it with your file tools: merge or remove older entries, or move detail to a memory/<topic>.md file."}]`
|
|
538
548
|
: "";
|
|
539
549
|
return `${guidance}\n\nYour memory (MEMORY.md):\n${memory.text}${truncatedNote}`;
|
|
@@ -100,6 +100,34 @@ export function askQuestionSummary(questions) {
|
|
|
100
100
|
const rest = questions.length - 1;
|
|
101
101
|
return rest > 0 ? `${first} (+${rest} more question${rest > 1 ? "s" : ""})` : first;
|
|
102
102
|
}
|
|
103
|
+
/** How many one-tap answers an `ask_user` card offers; the tool advertises
|
|
104
|
+
* "2-5 suggested answers". */
|
|
105
|
+
export const MAX_CHOICES = 5;
|
|
106
|
+
/**
|
|
107
|
+
* Flat labels from a bot-authored `ask_user` `choices` array.
|
|
108
|
+
*
|
|
109
|
+
* The tool schema says strings, but a model that has also seen Claude's
|
|
110
|
+
* AskUserQuestion will hand over `{ label, description }` rows instead
|
|
111
|
+
* (MiniMax M3 does). The label is the choice either way. Anything else is
|
|
112
|
+
* dropped: a non-string choice that reached the card was drawn as a React
|
|
113
|
+
* child, which React refuses, taking the whole chat page down with it.
|
|
114
|
+
*/
|
|
115
|
+
export function parseChoices(input, limit = MAX_CHOICES) {
|
|
116
|
+
if (!Array.isArray(input))
|
|
117
|
+
return undefined;
|
|
118
|
+
const choices = [];
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
for (const raw of input) {
|
|
121
|
+
const label = typeof raw === "string" ? text(raw, MAX_LABEL) : isRecord(raw) ? text(raw.label, MAX_LABEL) : undefined;
|
|
122
|
+
if (!label || seen.has(label))
|
|
123
|
+
continue;
|
|
124
|
+
seen.add(label);
|
|
125
|
+
choices.push(label);
|
|
126
|
+
if (choices.length === limit)
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
return choices.length ? choices : undefined;
|
|
130
|
+
}
|
|
103
131
|
/** Flat labels for clients that only know how to render a list of choices
|
|
104
132
|
* (the phone companions, and any older desktop build). Only a single
|
|
105
133
|
* question can be answered that way without losing which one was answered. */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** One wording for "a turn already owns this bot's cloud computer", shared
|
|
2
|
+
* because two sides have to agree on it: the server sends it with 409 when
|
|
3
|
+
* provision or sleep arrives mid-turn, and the Computer panel recognises it
|
|
4
|
+
* to show the neutral busy state instead of a fault.
|
|
5
|
+
*
|
|
6
|
+
* Kept here rather than spelled out at each site: the panel used to match
|
|
7
|
+
* the server's prose with its own regex, so a copy-edit on the server would
|
|
8
|
+
* have silently turned the busy state back into "Couldn't reach the
|
|
9
|
+
* computer" — the exact bug the panel fix exists to prevent, and one no
|
|
10
|
+
* test would have caught. */
|
|
11
|
+
export const CLOUD_COMPUTER_BUSY_ERROR = "this bot's cloud computer is being used by an active turn — interrupt it first";
|
|
12
|
+
/** True when an error message carries that refusal, however it was wrapped.
|
|
13
|
+
* A substring test, not equality: the message reaches the client as the
|
|
14
|
+
* server wrote it today, but a future wrapper must not break recognition. */
|
|
15
|
+
export function isCloudComputerBusyMessage(message) {
|
|
16
|
+
return message.includes(CLOUD_COMPUTER_BUSY_ERROR);
|
|
17
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const WEEKDAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
2
|
+
const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
3
|
+
/** Describe familiar presets; retain the exact expression for everything else.
|
|
4
|
+
* This is presentation only, never a second parser or source of scheduling truth. */
|
|
5
|
+
export function cronScheduleLabel(schedule) {
|
|
6
|
+
const [minute, hour, day, month, weekday] = schedule.expression.trim().split(/\s+/);
|
|
7
|
+
let label = `Cron ${schedule.expression}`;
|
|
8
|
+
if (/^\d+$/.test(minute ?? "") && /^\d+$/.test(hour ?? "")) {
|
|
9
|
+
const time = `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}`;
|
|
10
|
+
const monthDay = /^\d+$/.test(day ?? "") ? Number(day) : null;
|
|
11
|
+
if (weekday === "*" && month === "*") {
|
|
12
|
+
if (day === "*")
|
|
13
|
+
label = `Every day at ${time}`;
|
|
14
|
+
else if (day === "L")
|
|
15
|
+
label = `Monthly on the last day at ${time}`;
|
|
16
|
+
else if (monthDay !== null)
|
|
17
|
+
label = `Monthly on day ${monthDay} at ${time}`;
|
|
18
|
+
}
|
|
19
|
+
else if (weekday === "*" && /^\d+$/.test(month ?? "") && monthDay !== null) {
|
|
20
|
+
const name = MONTHS[Number(month) - 1];
|
|
21
|
+
if (name)
|
|
22
|
+
label = `Yearly on ${name} ${monthDay} at ${time}`;
|
|
23
|
+
}
|
|
24
|
+
else if (day === "*" && month === "*") {
|
|
25
|
+
if (weekday === "1-5" || weekday?.toUpperCase() === "MON-FRI")
|
|
26
|
+
label = `Every weekday at ${time}`;
|
|
27
|
+
else if (/^[0-7]$/.test(weekday ?? ""))
|
|
28
|
+
label = `Weekly on ${WEEKDAYS[Number(weekday) % 7]} at ${time}`;
|
|
29
|
+
else {
|
|
30
|
+
const nth = /^([0-7])#([1-5]|L)$/i.exec(weekday ?? "");
|
|
31
|
+
if (nth) {
|
|
32
|
+
const order = nth[2].toUpperCase() === "L" ? "last" : ["", "first", "second", "third", "fourth", "fifth"][Number(nth[2])];
|
|
33
|
+
label = `Monthly on the ${order} ${WEEKDAYS[Number(nth[1]) % 7]} at ${time}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return `${label} · ${schedule.timeZone}`;
|
|
39
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Cron } from "croner";
|
|
2
|
+
const MAX_EXPRESSION_LENGTH = 256;
|
|
3
|
+
const MAX_PREVIEW_RUNS = 1000;
|
|
4
|
+
function cronCalculator(value) {
|
|
5
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6
|
+
throw new Error("Choose a valid cron schedule");
|
|
7
|
+
const input = value;
|
|
8
|
+
if (input.type !== "cron" || typeof input.expression !== "string")
|
|
9
|
+
throw new Error("Choose a valid cron schedule");
|
|
10
|
+
if (Object.keys(input).some(key => !["type", "expression", "timeZone"].includes(key))) {
|
|
11
|
+
throw new Error("Cron schedules accept only type, expression and timeZone; start/end dates and other restrictions are not supported");
|
|
12
|
+
}
|
|
13
|
+
const expression = input.expression.trim().replace(/\s+/g, " ");
|
|
14
|
+
if (expression.length > MAX_EXPRESSION_LENGTH || expression.split(" ").length !== 5 || expression.includes("@")) {
|
|
15
|
+
throw new Error("Cron must have five fields: minute hour day-of-month month weekday (no seconds, year, or macros)");
|
|
16
|
+
}
|
|
17
|
+
const timeZone = typeof input.timeZone === "string" ? input.timeZone.trim() : "";
|
|
18
|
+
// Intl also accepts numeric offsets in newer runtimes. Require a named zone
|
|
19
|
+
// so wall-clock schedules follow its daylight-saving rules instead.
|
|
20
|
+
if (timeZone.length > 128 || !(timeZone === "UTC" || /^[A-Za-z][A-Za-z0-9_+-]*(?:\/[A-Za-z0-9_+-]+)+$/.test(timeZone))) {
|
|
21
|
+
throw new Error("Choose an IANA timezone such as America/New_York, Asia/Kolkata, or UTC");
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
new Intl.DateTimeFormat("en-US", { timeZone }).format(0);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error("Choose a valid IANA timezone such as America/New_York, Asia/Kolkata, or UTC");
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
// Date calculator only: no callback, name, or timer. RoutineManager owns
|
|
31
|
+
// dispatch, persistence, catch-up and cancellation. Croner bounds its
|
|
32
|
+
// five-field search at year 3000, including impossible calendar dates.
|
|
33
|
+
const cron = new Cron(expression, { timezone: timeZone, mode: "5-part", paused: true });
|
|
34
|
+
return { schedule: { type: "cron", expression, timeZone }, cron };
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new Error(`Invalid cron expression: ${error.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function calculationDate(after) {
|
|
41
|
+
const date = new Date(after);
|
|
42
|
+
if (!Number.isFinite(after) || !Number.isFinite(date.getTime()))
|
|
43
|
+
throw new Error("Choose a valid cron preview date");
|
|
44
|
+
return date;
|
|
45
|
+
}
|
|
46
|
+
function futureRuns(cron, after, count) {
|
|
47
|
+
const from = calculationDate(after);
|
|
48
|
+
// Croner resolves an ambiguous wall-clock time to its first occurrence.
|
|
49
|
+
// If the cursor is already in the second occurrence, its first candidate
|
|
50
|
+
// can therefore be in the past. Ask the same calculator for more candidates
|
|
51
|
+
// and discard those, rather than moving the scheduler cursor backwards.
|
|
52
|
+
// Minute precision bounds even a full-day historical timezone fold at 1440.
|
|
53
|
+
const maximum = count + 1440;
|
|
54
|
+
let requested = count;
|
|
55
|
+
while (true) {
|
|
56
|
+
const candidates = cron.nextRuns(requested, from);
|
|
57
|
+
const runs = [];
|
|
58
|
+
for (const date of candidates) {
|
|
59
|
+
const at = date.getTime();
|
|
60
|
+
if (!Number.isFinite(at))
|
|
61
|
+
throw new Error("The cron schedule produced an invalid date");
|
|
62
|
+
if (at > (runs.at(-1) ?? after))
|
|
63
|
+
runs.push(at);
|
|
64
|
+
if (runs.length === count)
|
|
65
|
+
return runs;
|
|
66
|
+
}
|
|
67
|
+
if (candidates.length < requested || requested === maximum)
|
|
68
|
+
return runs;
|
|
69
|
+
// Sparse schedules normally need just one more candidate. Grow only when
|
|
70
|
+
// needed, so a daily preview never scans years to skip one repeated hour.
|
|
71
|
+
requested = Math.min(maximum, requested * 2);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Normalize once at the trust boundary, rejecting impossible schedules. */
|
|
75
|
+
export function normalizeCronSchedule(value, after = Date.now()) {
|
|
76
|
+
const { schedule, cron } = cronCalculator(value);
|
|
77
|
+
if (!futureRuns(cron, after, 1).length)
|
|
78
|
+
throw new Error("This cron expression has no future runs. Choose dates that exist.");
|
|
79
|
+
return schedule;
|
|
80
|
+
}
|
|
81
|
+
/** Strictly-future instants, shared by the scheduler, cards and calendar.
|
|
82
|
+
* Croner's DST policy is intentional: missing clock times advance through
|
|
83
|
+
* the gap, and repeated clock times use their first occurrence only. */
|
|
84
|
+
export function nextCronRuns(schedule, after, count) {
|
|
85
|
+
if (!Number.isInteger(count) || count < 1 || count > MAX_PREVIEW_RUNS)
|
|
86
|
+
throw new Error("Choose between 1 and 1000 cron preview runs");
|
|
87
|
+
const { cron } = cronCalculator(schedule);
|
|
88
|
+
return futureRuns(cron, after, count);
|
|
89
|
+
}
|
|
@@ -20,7 +20,7 @@ const task = z.object({
|
|
|
20
20
|
key,
|
|
21
21
|
title: z.string().max(2_000),
|
|
22
22
|
createdAt: timestamp,
|
|
23
|
-
openedBy: z.object({ botId: key, name, at: timestamp }).optional(),
|
|
23
|
+
openedBy: z.object({ botId: key, name, at: timestamp, kind: z.enum(["pair", "work"]).optional() }).optional(),
|
|
24
24
|
closedBy: z.object({ botId: key, name, at: timestamp }).optional(),
|
|
25
25
|
activeLeafId: key.nullable(),
|
|
26
26
|
messages: z.array(message).max(100_000),
|
|
@@ -15187,10 +15187,17 @@ var appConfigSchema = external_exports.object({
|
|
|
15187
15187
|
vps: vpsConfigSchema.optional(),
|
|
15188
15188
|
/** Optional OpenCode key; persisted write-only and passed only to its child. */
|
|
15189
15189
|
opencodeGo: external_exports.object({ apiKey: optionalText }).optional(),
|
|
15190
|
-
/** Voice
|
|
15191
|
-
* engine: "elevenlabs" (default; needs a key)
|
|
15192
|
-
* built-in voices, no key)
|
|
15193
|
-
|
|
15190
|
+
/** Voice settings and the selected voice id. `provider` picks the
|
|
15191
|
+
* engine: "elevenlabs" (default; needs a key), "system" (the Mac's
|
|
15192
|
+
* built-in voices, no key), or "chatterbox" (a local OpenAI-compatible
|
|
15193
|
+
* Chatterbox server; `baseUrl` and `model` are settings, not secrets). */
|
|
15194
|
+
tts: external_exports.object({
|
|
15195
|
+
key: optionalText,
|
|
15196
|
+
voice: optionalText,
|
|
15197
|
+
provider: external_exports.enum(["elevenlabs", "system", "chatterbox"]).optional(),
|
|
15198
|
+
baseUrl: external_exports.string().trim().max(2048).refine((value) => !value || /^https?:\/\//i.test(value), "the Chatterbox server address must start with http:// or https://").optional(),
|
|
15199
|
+
model: optionalText
|
|
15200
|
+
}).optional(),
|
|
15194
15201
|
/** Avatar provider credentials stay separate; choosing a router never reuses a cloud key. */
|
|
15195
15202
|
imageGen: external_exports.object({
|
|
15196
15203
|
provider: external_exports.enum(["openai", "xai", "custom"]).optional(),
|
|
@@ -15249,7 +15256,6 @@ function resolveProxy(relative) {
|
|
|
15249
15256
|
}
|
|
15250
15257
|
var SPAWNED_PROXIES = {
|
|
15251
15258
|
browser: resolveProxy("browser-proxy"),
|
|
15252
|
-
computer: resolveProxy("computer-proxy"),
|
|
15253
15259
|
localComputer: resolveProxy("local-computer-proxy"),
|
|
15254
15260
|
permission: resolveProxy("permission-proxy"),
|
|
15255
15261
|
containerMcp: resolveProxy("container-mcp"),
|
|
@@ -14593,7 +14593,6 @@ function resolveProxy(relative) {
|
|
|
14593
14593
|
}
|
|
14594
14594
|
var SPAWNED_PROXIES = {
|
|
14595
14595
|
browser: resolveProxy("browser-proxy"),
|
|
14596
|
-
computer: resolveProxy("computer-proxy"),
|
|
14597
14596
|
localComputer: resolveProxy("local-computer-proxy"),
|
|
14598
14597
|
permission: resolveProxy("permission-proxy"),
|
|
14599
14598
|
containerMcp: resolveProxy("container-mcp"),
|