openmausbot 0.1.63 → 0.1.66
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-BftlaHAF.css +1 -0
- package/dist/assets/index-DuR3H-Hf.js +278 -0
- package/dist/assets/{index-C-SoIGdn.js → index-kW5L1QeO.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +121 -0
- package/dist-server/container-mcp.js +4 -1
- package/dist-server/index.js +17996 -15876
- package/dist-server/openmausbot.js +752 -245
- package/dist-server/pair-cli.js +752 -245
- package/dist-server/prepare-cloudflared.js +10 -2
- package/dist-server/proxy-paths.js +1 -0
- package/dist-server/server/attachments.js +140 -16
- package/dist-server/server/browser-engine-release.js +10 -8
- package/dist-server/server/browser-engine.js +226 -26
- package/dist-server/server/browser-live.js +567 -0
- package/dist-server/server/browser-proxy.js +129 -0
- package/dist-server/server/browser-runtime.js +370 -0
- package/dist-server/server/claude-accounts.js +103 -0
- package/dist-server/server/config.js +31 -20
- package/dist-server/server/custom-domain.js +196 -0
- package/dist-server/server/drivers/claude.js +77 -19
- package/dist-server/server/drivers/codex-device-auth.js +234 -0
- package/dist-server/server/drivers/codex.js +10 -0
- package/dist-server/server/harness/bus.js +9 -4
- package/dist-server/server/harness/registry.js +15 -0
- package/dist-server/server/index.js +446 -91
- package/dist-server/server/provider-auth-sessions.js +118 -0
- package/dist-server/server/proxy-paths.js +1 -0
- package/dist-server/server/sse-fanout.js +53 -0
- package/dist-server/src/lib/composer-attachments.js +23 -6
- package/dist-server/vps-container-mcp.js +4 -1
- package/package.json +1 -1
- package/dist/assets/index-BuntoOSX.js +0 -264
- package/dist/assets/index-oPMv6a_k.css +0 -1
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const failure = (message, status) => Object.assign(new Error(message), { status });
|
|
2
|
+
export class ProviderAuthSessions {
|
|
3
|
+
flows = new Map();
|
|
4
|
+
async start(instance, owner) {
|
|
5
|
+
if (!instance.startAuthentication)
|
|
6
|
+
throw failure("Account setup is unavailable for this provider.", 404);
|
|
7
|
+
const existing = this.flows.get(instance.instanceId);
|
|
8
|
+
if (existing && (existing.busy || (existing.owner !== owner && existing.expiresAt > Date.now()))) {
|
|
9
|
+
throw failure("A sign-in is already in progress. Finish or cancel it in the browser that started it, or wait for it to expire.", 409);
|
|
10
|
+
}
|
|
11
|
+
// Reopening Settings may resume the same owner's flow. Keep its ownership
|
|
12
|
+
// if the driver's retry fails, rather than orphaning a still-running CLI.
|
|
13
|
+
const resuming = existing && existing.owner === owner && existing.expiresAt > Date.now() && !existing.revoked;
|
|
14
|
+
const flow = resuming ? existing : { instance, owner, flowId: null, expiresAt: Date.now() + 15 * 60_000, busy: false, starting: false, revoked: false };
|
|
15
|
+
flow.busy = true;
|
|
16
|
+
flow.starting = true;
|
|
17
|
+
this.flows.set(instance.instanceId, flow);
|
|
18
|
+
try {
|
|
19
|
+
const result = await instance.startAuthentication();
|
|
20
|
+
if (flow.revoked || this.flows.get(instance.instanceId) !== flow) {
|
|
21
|
+
await instance.cancelAuthentication?.();
|
|
22
|
+
throw failure("Sign-in cancelled because the session or provider changed.", 409);
|
|
23
|
+
}
|
|
24
|
+
flow.flowId = result.flowId;
|
|
25
|
+
const expiry = Date.parse(result.expiresAt ?? "");
|
|
26
|
+
if (Number.isFinite(expiry))
|
|
27
|
+
flow.expiresAt = expiry;
|
|
28
|
+
if (result.phase === "succeeded")
|
|
29
|
+
this.flows.delete(instance.instanceId);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if ((!resuming || flow.revoked) && this.flows.get(instance.instanceId) === flow)
|
|
34
|
+
this.flows.delete(instance.instanceId);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
flow.busy = false;
|
|
39
|
+
flow.starting = false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
owned(instanceId, owner, flowId) {
|
|
43
|
+
const flow = this.flows.get(instanceId);
|
|
44
|
+
if (!flowId || !flow || flow.revoked || flow.owner !== owner || flow.flowId !== flowId) {
|
|
45
|
+
throw failure("This sign-in is no longer available in this browser. Start a new sign-in.", 404);
|
|
46
|
+
}
|
|
47
|
+
if (flow.busy)
|
|
48
|
+
throw failure("The sign-in is still being updated. Try again shortly.", 409);
|
|
49
|
+
return flow;
|
|
50
|
+
}
|
|
51
|
+
async status(instanceId, owner, flowId) {
|
|
52
|
+
const flow = this.owned(instanceId, owner, flowId);
|
|
53
|
+
if (!flow.instance.getAuthentication)
|
|
54
|
+
throw failure("Sign-in status is unavailable for this provider.", 404);
|
|
55
|
+
const status = await flow.instance.getAuthentication(flowId);
|
|
56
|
+
if (flow.revoked)
|
|
57
|
+
throw failure("This sign-in session ended.", 404);
|
|
58
|
+
// Keep the terminal result available to its owner, but release the slot so
|
|
59
|
+
// another admin need not wait fifteen minutes after success or cancellation.
|
|
60
|
+
if (status.phase !== "waiting")
|
|
61
|
+
flow.expiresAt = 0;
|
|
62
|
+
return status;
|
|
63
|
+
}
|
|
64
|
+
async complete(instanceId, owner, flowId, callbackUrl) {
|
|
65
|
+
const flow = this.owned(instanceId, owner, flowId);
|
|
66
|
+
if (!flow.instance.completeAuthentication)
|
|
67
|
+
throw failure("This provider does not use a callback URL.", 400);
|
|
68
|
+
flow.busy = true;
|
|
69
|
+
try {
|
|
70
|
+
await flow.instance.completeAuthentication(flowId, callbackUrl);
|
|
71
|
+
if (this.flows.get(instanceId) === flow)
|
|
72
|
+
this.flows.delete(instanceId);
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
flow.busy = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async cancel(instanceId, owner, flowId) {
|
|
79
|
+
const flow = this.owned(instanceId, owner, flowId);
|
|
80
|
+
flow.busy = true;
|
|
81
|
+
try {
|
|
82
|
+
await flow.instance.cancelAuthentication?.();
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
flow.busy = false;
|
|
86
|
+
flow.expiresAt = 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
revokeOwner(owner) {
|
|
90
|
+
for (const [id, flow] of this.flows) {
|
|
91
|
+
if (flow.owner !== owner)
|
|
92
|
+
continue;
|
|
93
|
+
flow.revoked = true;
|
|
94
|
+
// A pending start owns its eventual cancellation. Keep the reservation
|
|
95
|
+
// until it finishes so another browser cannot race into this auth home.
|
|
96
|
+
if (flow.starting)
|
|
97
|
+
continue;
|
|
98
|
+
flow.busy = true;
|
|
99
|
+
void Promise.resolve().then(() => flow.instance.cancelAuthentication?.()).catch(() => { }).finally(() => {
|
|
100
|
+
if (this.flows.get(id) === flow)
|
|
101
|
+
this.flows.delete(id);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
clear() {
|
|
106
|
+
for (const flow of this.flows.values())
|
|
107
|
+
flow.revoked = true;
|
|
108
|
+
// Called when disposing the provider fleet, which owns child teardown.
|
|
109
|
+
this.flows.clear();
|
|
110
|
+
}
|
|
111
|
+
clearInstance(instanceId) {
|
|
112
|
+
const flow = this.flows.get(instanceId);
|
|
113
|
+
if (flow)
|
|
114
|
+
flow.revoked = true;
|
|
115
|
+
this.flows.delete(instanceId);
|
|
116
|
+
// The registry's per-instance disposal owns child teardown.
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -31,6 +31,7 @@ export function resolveProxy(relative) {
|
|
|
31
31
|
* the smoke test can assert each one actually exists in a packaged layout —
|
|
32
32
|
* the check that would have caught the 0.1.24 breakage. */
|
|
33
33
|
export const SPAWNED_PROXIES = {
|
|
34
|
+
browser: resolveProxy("browser-proxy"),
|
|
34
35
|
computer: resolveProxy("computer-proxy"),
|
|
35
36
|
permission: resolveProxy("permission-proxy"),
|
|
36
37
|
containerMcp: resolveProxy("container-mcp"),
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Per-client delivery decision for the SSE broadcast fan-out in index.ts.
|
|
2
|
+
// Pulled into its own file — not just its own function — because index.ts
|
|
3
|
+
// starts the real HTTP server as an import-time side effect (see the bottom
|
|
4
|
+
// of index.ts: `server.listen(...)` runs unconditionally on import), so it
|
|
5
|
+
// can never be imported from a plain unit test. This one function has no
|
|
6
|
+
// dependency on that module and can be driven directly with a fake response.
|
|
7
|
+
/** Past this many buffered bytes, the client isn't draining on any useful
|
|
8
|
+
* timeframe: cut it loose rather than let Node's per-connection write buffer
|
|
9
|
+
* grow without bound. Its own EventSource reconnects and replays via
|
|
10
|
+
* Last-Event-ID (server/index.ts's existing replayBuffer/cursorSeq). */
|
|
11
|
+
// ponytail: fixed byte bound, not adaptive. Retune from real writableLength
|
|
12
|
+
// telemetry (see the audit's validation step) if it trips too eager or late.
|
|
13
|
+
export const SSE_MAX_BUFFERED_BYTES = 4 * 1024 * 1024;
|
|
14
|
+
/** One client's fate for one frame.
|
|
15
|
+
*
|
|
16
|
+
* Screen captures are replaceable: once a client is backpressured, skip them
|
|
17
|
+
* until it drains, since the next capture supersedes whatever was dropped.
|
|
18
|
+
* Durable events (messages, bot/group state, config, ...) are never
|
|
19
|
+
* silently dropped here — they're written regardless of the backpressure
|
|
20
|
+
* flag. The only way a durable event goes missing is the bound below, which
|
|
21
|
+
* disconnects the client outright so its next reconnect replays what it
|
|
22
|
+
* missed instead of silently losing it. */
|
|
23
|
+
export function deliverSseFrame(client, kind, frame) {
|
|
24
|
+
if (client.res.writableLength > SSE_MAX_BUFFERED_BYTES) {
|
|
25
|
+
try {
|
|
26
|
+
client.res.end();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* already gone */
|
|
30
|
+
}
|
|
31
|
+
return "disconnected";
|
|
32
|
+
}
|
|
33
|
+
if (client.backpressured && kind === "screen")
|
|
34
|
+
return "dropped";
|
|
35
|
+
try {
|
|
36
|
+
if (!client.res.write(frame) && !client.backpressured) {
|
|
37
|
+
// write() returns false as soon as buffered bytes pass the socket's
|
|
38
|
+
// 16 KB highWaterMark, which a single screen frame clears on its own —
|
|
39
|
+
// so this says "hasn't drained yet", not "is a slow client". Without
|
|
40
|
+
// the drain listener the flag latches on for the life of the
|
|
41
|
+
// connection and screen streaming never resumes. One listener per
|
|
42
|
+
// transition into backpressure, never one per frame.
|
|
43
|
+
client.backpressured = true;
|
|
44
|
+
client.res.once("drain", () => {
|
|
45
|
+
client.backpressured = false;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return "sent";
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return "disconnected";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -88,6 +88,20 @@ const DOCUMENT_MIMES = {
|
|
|
88
88
|
odp: "application/vnd.oasis.opendocument.presentation",
|
|
89
89
|
};
|
|
90
90
|
const ACCEPTED_DOCUMENT_MIMES = new Set(Object.values(DOCUMENT_MIMES));
|
|
91
|
+
const AUDIO_MIMES_BY_EXTENSION = {
|
|
92
|
+
opus: "audio/opus",
|
|
93
|
+
ogg: "audio/ogg",
|
|
94
|
+
mp3: "audio/mpeg",
|
|
95
|
+
m4a: "audio/mp4",
|
|
96
|
+
aac: "audio/aac",
|
|
97
|
+
wav: "audio/wav",
|
|
98
|
+
flac: "audio/flac",
|
|
99
|
+
webm: "audio/webm",
|
|
100
|
+
};
|
|
101
|
+
const ACCEPTED_AUDIO_MIMES = new Set([
|
|
102
|
+
...Object.values(AUDIO_MIMES_BY_EXTENSION),
|
|
103
|
+
"audio/x-m4a", "audio/x-wav", "audio/wave", "audio/x-flac",
|
|
104
|
+
]);
|
|
91
105
|
export function documentMime(file) {
|
|
92
106
|
const declared = file.type.split(";", 1)[0].trim().toLowerCase();
|
|
93
107
|
if (ACCEPTED_DOCUMENT_MIMES.has(declared))
|
|
@@ -257,11 +271,14 @@ export async function imageAttachmentFromFile(file, optimistic) {
|
|
|
257
271
|
...(optimistic?.previewUrl ? { previewUrl: optimistic.previewUrl } : {}),
|
|
258
272
|
};
|
|
259
273
|
}
|
|
260
|
-
/** Copy a supported document into the private attachment store. The prompt
|
|
274
|
+
/** Copy a supported document or audio file into the private attachment store. The prompt
|
|
261
275
|
* then carries the same durable path for the local agent and paired phones,
|
|
262
276
|
* instead of exposing an arbitrary Finder path to the companion route. */
|
|
263
277
|
export async function fileAttachmentFromFile(file) {
|
|
264
|
-
const
|
|
278
|
+
const declared = file.type.split(";", 1)[0].trim().toLowerCase();
|
|
279
|
+
const extension = file.name.split(".").at(-1)?.toLowerCase() ?? "";
|
|
280
|
+
const mime = documentMime(file) ??
|
|
281
|
+
(ACCEPTED_AUDIO_MIMES.has(declared) ? declared : AUDIO_MIMES_BY_EXTENSION[extension]);
|
|
265
282
|
if (!mime)
|
|
266
283
|
return null;
|
|
267
284
|
if (file.size > FILE_MAX_BYTES) {
|
|
@@ -494,8 +511,8 @@ function transcriptBlockStarting(line) {
|
|
|
494
511
|
}
|
|
495
512
|
const TRANSCRIPT_ATTACHMENT_TAG = /^<attached-(image|file)[\t ]+path="([^"\r\n]*)"(?:[\t ]+name="([^"\r\n]*)")?[\t ]*\/>[\t ]*$/;
|
|
496
513
|
/** Split a stored user message into its display text and attachments for
|
|
497
|
-
* transcript rendering.
|
|
498
|
-
export function splitTranscriptAttachments(text) {
|
|
514
|
+
* transcript rendering. Markdown exports preserve whitespace; bubbles trim it. */
|
|
515
|
+
export function splitTranscriptAttachments(text, trimDisplay = true) {
|
|
499
516
|
const images = [];
|
|
500
517
|
const files = [];
|
|
501
518
|
let display = "";
|
|
@@ -555,7 +572,7 @@ export function splitTranscriptAttachments(text) {
|
|
|
555
572
|
display += text.slice(cursor, wholeLineEnd);
|
|
556
573
|
cursor = wholeLineEnd;
|
|
557
574
|
}
|
|
558
|
-
return { display: display.trim(), images, files };
|
|
575
|
+
return { display: trimDisplay ? display.trim() : display, images, files };
|
|
559
576
|
}
|
|
560
577
|
/** Kept for callers outside the desktop bundle that used the old helper. */
|
|
561
578
|
export function splitAttachedImages(text) {
|
|
@@ -666,7 +683,7 @@ export async function intakeFiles(_files, _opts) {
|
|
|
666
683
|
const rejectedNames = results.flatMap((result) => result.rejectedNames);
|
|
667
684
|
const uploadErrors = results.flatMap((result) => result.uploadError ? [result.uploadError] : []);
|
|
668
685
|
const pathless = rejectedNames.length
|
|
669
|
-
?
|
|
686
|
+
? `Unable to attach ${rejectedNames.join(", ")}. Choose a supported image, document, or audio file.`
|
|
670
687
|
: null;
|
|
671
688
|
const failed = uploadErrors.length ? uploadErrors.join("; ") : null;
|
|
672
689
|
return {
|
|
@@ -15086,6 +15086,8 @@ var defaultModelSelectionSchema = external_exports.object({
|
|
|
15086
15086
|
effort: external_exports.enum(EFFORT_LEVELS).optional()
|
|
15087
15087
|
});
|
|
15088
15088
|
var appConfigSchema = external_exports.object({
|
|
15089
|
+
/** Verified by the dedicated domain endpoint, never a generic config patch. */
|
|
15090
|
+
customDomain: external_exports.string().optional(),
|
|
15089
15091
|
defaultModelSelection: defaultModelSelectionSchema.optional(),
|
|
15090
15092
|
/** CLI-only launch preferences. Never enable remote access implicitly. */
|
|
15091
15093
|
cliStartup: external_exports.object({
|
|
@@ -15129,7 +15131,7 @@ var appConfigSchema = external_exports.object({
|
|
|
15129
15131
|
var storedAppConfigSchema = appConfigSchema.extend({
|
|
15130
15132
|
browserProfiles: storedBrowserProfilesSchema.optional()
|
|
15131
15133
|
});
|
|
15132
|
-
var appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true });
|
|
15134
|
+
var appConfigPatchSchema = appConfigSchema.omit({ instances: true, mcpServers: true, cliStartup: true, customDomain: true });
|
|
15133
15135
|
var jsonObjectSchema = external_exports.record(external_exports.string(), external_exports.json());
|
|
15134
15136
|
var DATA_DIR = process.env.OMB_DATA_DIR ?? join2(homedir2(), ".openmausbot");
|
|
15135
15137
|
var LEGACY_DATA_DIR = join2(homedir2(), ".opengrokbot");
|
|
@@ -15146,6 +15148,7 @@ function resolveProxy(relative) {
|
|
|
15146
15148
|
return existsSync2(source) ? source : join3(SERVER_ROOT, `${relative}.js`);
|
|
15147
15149
|
}
|
|
15148
15150
|
var SPAWNED_PROXIES = {
|
|
15151
|
+
browser: resolveProxy("browser-proxy"),
|
|
15149
15152
|
computer: resolveProxy("computer-proxy"),
|
|
15150
15153
|
permission: resolveProxy("permission-proxy"),
|
|
15151
15154
|
containerMcp: resolveProxy("container-mcp"),
|