@polderlabs/bizar-omp 0.7.9 → 0.8.0
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/README.md +35 -4
- package/dist/cli/agents.d.ts.map +1 -1
- package/dist/cli/agents.js +46 -23
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/doctor.d.ts +4 -4
- package/dist/cli/install.d.ts +126 -9
- package/dist/cli/install.d.ts.map +1 -1
- package/dist/cli/install.js +19 -4
- package/dist/cli/install.js.map +1 -1
- package/dist/cli/mobile-claim.d.ts +143 -0
- package/dist/cli/mobile-claim.d.ts.map +1 -0
- package/dist/cli/mobile-claim.js +399 -0
- package/dist/cli/mobile-claim.js.map +1 -0
- package/dist/cli/omb-commands.d.ts +1 -1
- package/dist/cli/omb-commands.d.ts.map +1 -1
- package/dist/cli/omb-commands.js +22 -1
- package/dist/cli/omb-commands.js.map +1 -1
- package/dist/cli/omb-mobile.d.ts +47 -0
- package/dist/cli/omb-mobile.d.ts.map +1 -0
- package/dist/cli/omb-mobile.js +307 -0
- package/dist/cli/omb-mobile.js.map +1 -0
- package/dist/cli/omb-sessions.d.ts +64 -0
- package/dist/cli/omb-sessions.d.ts.map +1 -0
- package/dist/cli/omb-sessions.js +231 -0
- package/dist/cli/omb-sessions.js.map +1 -0
- package/dist/cli/omb.js +20 -15
- package/dist/cli/omb.js.map +1 -1
- package/dist/cli/omp-mobile-dispatch.d.ts +10 -0
- package/dist/cli/omp-mobile-dispatch.d.ts.map +1 -0
- package/dist/cli/omp-mobile-dispatch.js +148 -0
- package/dist/cli/omp-mobile-dispatch.js.map +1 -0
- package/dist/omp/compatibility-receipt.d.ts +69 -0
- package/dist/omp/compatibility-receipt.d.ts.map +1 -0
- package/dist/omp/compatibility-receipt.js +60 -0
- package/dist/omp/compatibility-receipt.js.map +1 -0
- package/dist/omp/compatibility.d.ts +5 -5
- package/dist/omp/compatibility.d.ts.map +1 -1
- package/dist/omp/compatibility.js +2 -2
- package/dist/omp/compatibility.js.map +1 -1
- package/dist/omp/daemon-service.d.ts +4 -0
- package/dist/omp/daemon-service.d.ts.map +1 -1
- package/dist/omp/daemon-service.js +418 -8
- package/dist/omp/daemon-service.js.map +1 -1
- package/dist/omp/omb-sessions.d.ts +10 -0
- package/dist/omp/omb-sessions.d.ts.map +1 -1
- package/dist/omp/omb-sessions.js +8 -6
- package/dist/omp/omb-sessions.js.map +1 -1
- package/dist/omp/tmux.d.ts +6 -0
- package/dist/omp/tmux.d.ts.map +1 -1
- package/dist/omp/tmux.js +11 -0
- package/dist/omp/tmux.js.map +1 -1
- package/dist/scripts/emit-compatibility-receipt.d.ts +2 -0
- package/dist/scripts/emit-compatibility-receipt.d.ts.map +1 -0
- package/dist/scripts/emit-compatibility-receipt.js +33 -0
- package/dist/scripts/emit-compatibility-receipt.js.map +1 -0
- package/docs/07-bizarharness-integration.md +44 -0
- package/docs/compatibility/baseline.json +10 -8
- package/docs/compatibility/supported-surfaces.json +3 -1
- package/docs/releases/0.7.10.md +27 -0
- package/docs/releases/0.8.0.md +3 -0
- package/docs/releases/support-matrix.md +9 -1
- package/package.json +6 -4
- package/skills/omp-native-development/SKILL.md +1 -1
- package/skills/omp-native-development/references/accuracy-and-versioning.md +1 -1
- package/skills/omp-native-development/references/source-manifest.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compatibility.d.ts","sourceRoot":"","sources":["../../src/omp/compatibility.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa;aAAK,OAAO,EAAE,QAAQ;aAAE,MAAM,EAAE,0CAA0C;CAAW,CAAC;AAChH,eAAO,MAAM,sBAAsB,YAAI,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"compatibility.d.ts","sourceRoot":"","sources":["../../src/omp/compatibility.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa;aAAK,OAAO,EAAE,QAAQ;aAAE,MAAM,EAAE,0CAA0C;CAAW,CAAC;AAChH,eAAO,MAAM,sBAAsB,YAAI,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAU,CAAC;AACzK,eAAO,MAAM,aAAa,aAAa,CAAC;AAExC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAG9D;AAED,kEAAkE;AAClE,eAAO,MAAM,4BAA4B,QAGxC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAItE;AAED,MAAM,WAAW,gBAAgB;IAAG,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAAE;AAC3F,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,gBAAgB,CAUlE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKjE;AAED,MAAM,WAAW,YAAY;IAAG,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,sBAAsB,EAAE,OAAO,CAAC;IAAC,gBAAgB,EAAE,OAAO,CAAC;CAAE;AACvI,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,YAAY;;0BA/Cf,QAAQ;yBAAU,0CAA0C;;;;;;EA+CqM"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export const SUPPORTED_OMP = { version: "18.4.
|
|
2
|
-
export const QUALIFIED_OMP_VERSIONS = ["18.2.4", "18.2.5", "18.2.6", "18.2.7", "18.2.8", "18.2.11", "18.3.0", "18.3.2", "18.4.1", "18.4.2"];
|
|
1
|
+
export const SUPPORTED_OMP = { version: "18.4.8", commit: "717f97f4d22b3d65c4a4eef6a744255d46f4d1a6" };
|
|
2
|
+
export const QUALIFIED_OMP_VERSIONS = ["18.2.4", "18.2.5", "18.2.6", "18.2.7", "18.2.8", "18.2.11", "18.3.0", "18.3.2", "18.4.1", "18.4.2", "18.4.4", "18.4.8"];
|
|
3
3
|
export const SUPPORTED_BUN = ">=1.3.14";
|
|
4
4
|
export function isQualifiedOmpVersion(version) {
|
|
5
5
|
const normalized = version.trim().replace(/^omp\//, "");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compatibility.js","sourceRoot":"","sources":["../../src/omp/compatibility.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,aAAa,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,0CAA0C,EAAW,CAAC;AAChH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"compatibility.js","sourceRoot":"","sources":["../../src/omp/compatibility.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OmbSessionInfo } from "./omb-sessions.js";
|
|
1
2
|
export interface DaemonServiceOptions {
|
|
2
3
|
/** Environment that supplies the state dir and the tmux/omp binaries. */
|
|
3
4
|
env?: NodeJS.ProcessEnv;
|
|
@@ -18,6 +19,9 @@ export interface DaemonService {
|
|
|
18
19
|
}
|
|
19
20
|
export declare const DASHBOARD_MAX_BODY_BYTES: number;
|
|
20
21
|
export declare const DASHBOARD_BODY_TIMEOUT_MS = 10000;
|
|
22
|
+
/** A mobile delete is allowed only after one authoritative fresh scan proves
|
|
23
|
+
* the exact host-owned generation is fully stopped and no longer observable. */
|
|
24
|
+
export declare function isSafeMobileSessionRemoval(session: Pick<OmbSessionInfo, "authorityKnown" | "hosted" | "state" | "attached" | "uncertainLiveness" | "processObserved">): boolean;
|
|
21
25
|
/**
|
|
22
26
|
* Long-running half of the daemon: an authenticated HTTP boundary on
|
|
23
27
|
* 127.0.0.1 plus a scan loop that keeps the on-disk snapshot fresh. Spawned
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"daemon-service.d.ts","sourceRoot":"","sources":["../../src/omp/daemon-service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"daemon-service.d.ts","sourceRoot":"","sources":["../../src/omp/daemon-service.ts"],"names":[],"mappings":"AAQA,OAAO,EAAyF,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AA6B/I,MAAM,WAAW,oBAAoB;IACnC,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gDAAgD;IAChD,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAID,eAAO,MAAM,wBAAwB,QAAY,CAAC;AAClD,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAmBhD;gFACgF;AAChF,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,IAAI,CAAC,cAAc,EAAE,gBAAgB,GAAG,QAAQ,GAAG,OAAO,GAAG,UAAU,GAAG,mBAAmB,GAAG,iBAAiB,CAAC,GAAG,OAAO,CAE/K;AA0WD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,oBAAyB,GAAG,aAAa,CAs4BpF"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { deferred } from "./daemon.js";
|
|
4
5
|
import { randomBytes } from "node:crypto";
|
|
5
|
-
import { mkdirSync, statSync } from "node:fs";
|
|
6
|
-
import {
|
|
6
|
+
import { mkdirSync, realpathSync, statSync } from "node:fs";
|
|
7
|
+
import { readdir } from "node:fs/promises";
|
|
8
|
+
import { basename, isAbsolute, relative, resolve, sep } from "node:path";
|
|
7
9
|
import { listOmbSessions, ombSessionDisplayName, ombSessionJournalPath, readOmbSessionMessages } from "./omb-sessions.js";
|
|
8
10
|
import { ombSessionName } from "./tmux.js";
|
|
9
11
|
import { DAEMON_PROTOCOL_VERSION, DaemonError, daemonStateDir, writeSnapshot } from "./daemon.js";
|
|
@@ -11,8 +13,11 @@ import { captureOmbSessionConsole, createOmbSession, deleteOmbSession, forgetOmb
|
|
|
11
13
|
import { DASHBOARD_PATH, dashboardPageHtml } from "./dashboard-html.js";
|
|
12
14
|
import { OMB_APPROVAL_MODES, OMB_THINKING_LEVELS, loadOmbSettings, saveOmbSettings } from "./omb-settings.js";
|
|
13
15
|
import { startTray, stopTray } from "./tray.js";
|
|
16
|
+
import { acquireMobileSessionLease, listClaimsDetailed } from "../cli/mobile-claim.js";
|
|
17
|
+
import { expandDirectoryPath } from "../cli/directory-selector.js";
|
|
14
18
|
import { deleteCustomOmpProvider, listCustomOmpProviders, OMP_PROVIDER_APIS, saveCustomOmpProvider, } from "./omp-provider-config.js";
|
|
15
19
|
const SCAN_INTERVAL_MS = 5_000;
|
|
20
|
+
const MOBILE_HOST_ENTRY = fileURLToPath(new URL("../../mobile/packages/host/dist/cli/main.js", import.meta.url));
|
|
16
21
|
export const DASHBOARD_MAX_BODY_BYTES = 64 * 1024;
|
|
17
22
|
export const DASHBOARD_BODY_TIMEOUT_MS = 10_000;
|
|
18
23
|
/** One-time dashboard access codes live a short while and are never reused. */
|
|
@@ -25,6 +30,86 @@ class DashboardRequestError extends Error {
|
|
|
25
30
|
this.status = status;
|
|
26
31
|
}
|
|
27
32
|
}
|
|
33
|
+
const MOBILE_PROJECT_ID_TTL_MS = 10 * 60_000;
|
|
34
|
+
const MOBILE_PROJECT_LIMIT = 512;
|
|
35
|
+
const MOBILE_PROJECT_CHOICES_LIMIT = MOBILE_PROJECT_LIMIT * 4;
|
|
36
|
+
const MOBILE_PROJECT_DEPTH = 5;
|
|
37
|
+
const IGNORED_PROJECT_DIRECTORIES = new Set([".git", "node_modules", ".cache", ".turbo", ".next", ".venv", "venv"]);
|
|
38
|
+
/** A mobile delete is allowed only after one authoritative fresh scan proves
|
|
39
|
+
* the exact host-owned generation is fully stopped and no longer observable. */
|
|
40
|
+
export function isSafeMobileSessionRemoval(session) {
|
|
41
|
+
return session.authorityKnown && session.hosted && session.state === "stopped" && !session.attached && !session.uncertainLiveness && session.processObserved !== true;
|
|
42
|
+
}
|
|
43
|
+
/** Discover only directories the local OMB Hub already treats as project choices. */
|
|
44
|
+
async function discoverMobileProjectDirectories(cwd, projectsFolder, sessions) {
|
|
45
|
+
const candidates = new Map();
|
|
46
|
+
const add = (path, suggestedLabel) => {
|
|
47
|
+
try {
|
|
48
|
+
if (!isDirectory(path))
|
|
49
|
+
return;
|
|
50
|
+
const canonical = realpathSync(path);
|
|
51
|
+
if (!candidates.has(canonical))
|
|
52
|
+
candidates.set(canonical, suggestedLabel ?? "");
|
|
53
|
+
}
|
|
54
|
+
catch { /* A project removed after the hub scan is omitted. */ }
|
|
55
|
+
};
|
|
56
|
+
const current = (() => { try {
|
|
57
|
+
return realpathSync(cwd);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return resolve(cwd);
|
|
61
|
+
} })();
|
|
62
|
+
add(current);
|
|
63
|
+
const rootPath = projectsFolder.trim() ? expandDirectoryPath(projectsFolder, cwd) : "";
|
|
64
|
+
let canonicalRoot = "";
|
|
65
|
+
if (rootPath) {
|
|
66
|
+
add(rootPath, "Projects folder");
|
|
67
|
+
try {
|
|
68
|
+
canonicalRoot = realpathSync(rootPath);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
canonicalRoot = "";
|
|
72
|
+
}
|
|
73
|
+
if (canonicalRoot) {
|
|
74
|
+
const visit = async (directory, depth) => {
|
|
75
|
+
if (depth >= MOBILE_PROJECT_DEPTH || candidates.size >= MOBILE_PROJECT_LIMIT)
|
|
76
|
+
return;
|
|
77
|
+
let entries;
|
|
78
|
+
try {
|
|
79
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
85
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || IGNORED_PROJECT_DIRECTORIES.has(entry.name))
|
|
86
|
+
continue;
|
|
87
|
+
const path = resolve(directory, entry.name);
|
|
88
|
+
const label = relative(canonicalRoot, path).replaceAll("\\", "/");
|
|
89
|
+
add(path, label);
|
|
90
|
+
if (candidates.size >= MOBILE_PROJECT_LIMIT)
|
|
91
|
+
break;
|
|
92
|
+
await visit(path, depth + 1);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
await visit(canonicalRoot, 0);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const session of sessions) {
|
|
99
|
+
if (session.cwd !== "?")
|
|
100
|
+
add(session.cwd, session.project);
|
|
101
|
+
if (candidates.size >= MOBILE_PROJECT_LIMIT)
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
return [...candidates.entries()].slice(0, MOBILE_PROJECT_LIMIT).map(([path, suggested]) => {
|
|
105
|
+
const currentProject = path === current;
|
|
106
|
+
const fromRoot = canonicalRoot === "" ? "" : relative(canonicalRoot, path);
|
|
107
|
+
const insideRoot = canonicalRoot !== "" && (fromRoot === "" || (fromRoot !== ".." && !fromRoot.startsWith(`..${sep}`) && !isAbsolute(fromRoot)));
|
|
108
|
+
const relativeLabel = insideRoot && path !== canonicalRoot ? relative(canonicalRoot, path).replaceAll("\\", "/") : "";
|
|
109
|
+
const label = (currentProject ? `${basename(path)} (current)` : relativeLabel || suggested || basename(path)).replace(/[\u0000-\u001f\u007f]/g, "�").slice(0, 120);
|
|
110
|
+
return { cwd: path, label, current: currentProject };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
28
113
|
/**
|
|
29
114
|
* Session fields the browser needs. The scan records the pane command, the pid,
|
|
30
115
|
* and the native-session flag for the hub, but none of them reach the page: the
|
|
@@ -309,6 +394,18 @@ function dashboardSession(session) {
|
|
|
309
394
|
displayName: session.displayName ?? ombSessionDisplayName(session),
|
|
310
395
|
};
|
|
311
396
|
}
|
|
397
|
+
function mobileHubSettings(settings) {
|
|
398
|
+
return {
|
|
399
|
+
version: settings.version,
|
|
400
|
+
projectsFolderConfigured: settings.projectsFolder.trim() !== "",
|
|
401
|
+
agentsScope: settings.agentsScope,
|
|
402
|
+
sessionFilter: settings.sessionFilter,
|
|
403
|
+
autoAttachLatest: settings.autoAttachLatest,
|
|
404
|
+
mouseScrolling: settings.mouseScrolling,
|
|
405
|
+
showStoppedSessions: settings.showStoppedSessions,
|
|
406
|
+
trayEnabled: settings.trayEnabled,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
312
409
|
/**
|
|
313
410
|
* Long-running half of the daemon: an authenticated HTTP boundary on
|
|
314
411
|
* 127.0.0.1 plus a scan loop that keeps the on-disk snapshot fresh. Spawned
|
|
@@ -327,6 +424,11 @@ export function startDaemonService(options = {}) {
|
|
|
327
424
|
let scanInFlight;
|
|
328
425
|
let lastScanAt;
|
|
329
426
|
let lastScanError;
|
|
427
|
+
let mobileHost;
|
|
428
|
+
let mobileOutput = "";
|
|
429
|
+
let mobilePair;
|
|
430
|
+
let mobileExit;
|
|
431
|
+
const mobileProjectChoices = new Map();
|
|
330
432
|
const ready = deferred();
|
|
331
433
|
async function scan(force = false) {
|
|
332
434
|
if (scanInFlight) {
|
|
@@ -358,25 +460,46 @@ export function startDaemonService(options = {}) {
|
|
|
358
460
|
}
|
|
359
461
|
}
|
|
360
462
|
async function requireCurrentSession(name, body) {
|
|
361
|
-
if (!sessions.some(candidate => candidate.name === name))
|
|
362
|
-
throw new DashboardRequestError(404, "unknown session");
|
|
363
463
|
const generation = typeof body.generation === "string" ? body.generation : "";
|
|
364
464
|
if (!generation)
|
|
365
465
|
throw new DashboardRequestError(400, "generation is required");
|
|
366
466
|
let fresh;
|
|
367
467
|
try {
|
|
368
|
-
fresh = await listOmbSessions(process.cwd(), env, { includeStopped: true, requireMultiplexer: true });
|
|
468
|
+
fresh = await listOmbSessions(process.cwd(), env, { includeStopped: true, includeAllStopped: true, requireMultiplexer: true });
|
|
369
469
|
}
|
|
370
470
|
catch {
|
|
371
471
|
throw new DashboardRequestError(503, "session authority is unavailable");
|
|
372
472
|
}
|
|
373
|
-
const
|
|
374
|
-
if (
|
|
473
|
+
const matches = fresh.filter(candidate => candidate.name === name);
|
|
474
|
+
if (matches.length === 0)
|
|
475
|
+
throw new DashboardRequestError(404, "unknown session");
|
|
476
|
+
if (matches.length !== 1 || matches[0]?.generation !== generation)
|
|
375
477
|
throw new DashboardRequestError(409, "session changed externally");
|
|
478
|
+
const current = matches[0];
|
|
376
479
|
if (!current.authorityKnown)
|
|
377
480
|
throw new DashboardRequestError(503, "session authority is unavailable");
|
|
378
481
|
return current;
|
|
379
482
|
}
|
|
483
|
+
async function withUnclaimedSession(name, generation, action) {
|
|
484
|
+
let lease;
|
|
485
|
+
try {
|
|
486
|
+
lease = acquireMobileSessionLease(name, env);
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
throw new DashboardRequestError(409, "session ownership is changing");
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
const claims = listClaimsDetailed(env);
|
|
493
|
+
if (claims.unreadable.length > 0 || claims.claims.some(claim => claim.session === name)) {
|
|
494
|
+
throw new DashboardRequestError(409, "session is claimed for mobile ownership");
|
|
495
|
+
}
|
|
496
|
+
const current = await requireCurrentSession(name, { generation });
|
|
497
|
+
return await action(current);
|
|
498
|
+
}
|
|
499
|
+
finally {
|
|
500
|
+
lease.release();
|
|
501
|
+
}
|
|
502
|
+
}
|
|
380
503
|
// Dashboard access codes: the bearer token must never land in browser
|
|
381
504
|
// history, so `omb dashboard` mints a code, the page trades that code for the
|
|
382
505
|
// token once, and the token is kept in sessionStorage only.
|
|
@@ -519,6 +642,286 @@ export function startDaemonService(options = {}) {
|
|
|
519
642
|
res.end('{"error":"unauthorized"}\n');
|
|
520
643
|
return;
|
|
521
644
|
}
|
|
645
|
+
if (path === "/mobile/pair" && req.method === "POST") {
|
|
646
|
+
const reuseMobileHost = Boolean(mobileHost && !mobileExit && mobilePair);
|
|
647
|
+
if (!mobileHost || mobileHost.exitCode !== null || mobileHost.signalCode !== null) {
|
|
648
|
+
mobilePair = undefined;
|
|
649
|
+
mobileOutput = "";
|
|
650
|
+
mobileExit = undefined;
|
|
651
|
+
const entry = env.BIZAROMP_MOBILE_ENTRY || MOBILE_HOST_ENTRY;
|
|
652
|
+
mobileHost = spawn(process.execPath, [entry, "pair", "--json"], {
|
|
653
|
+
cwd: process.cwd(),
|
|
654
|
+
env: { ...env, BIZAR_OMB_DAEMON_PORT: String(service.port), BIZAR_OMB_DAEMON_TOKEN: token },
|
|
655
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
656
|
+
});
|
|
657
|
+
mobileHost.on("error", error => { mobileExit = { code: 1, signal: null }; log(`mobile host failed to start: ${error.message}`); });
|
|
658
|
+
mobileHost.stdout?.setEncoding("utf8");
|
|
659
|
+
mobileHost.stderr?.setEncoding("utf8");
|
|
660
|
+
mobileHost.stdout?.on("data", (chunk) => {
|
|
661
|
+
mobileOutput = (mobileOutput + String(chunk)).slice(-64 * 1024);
|
|
662
|
+
const start = Math.max(mobileOutput.lastIndexOf("{\n \"deepLink\""), mobileOutput.lastIndexOf("{\"deepLink\""));
|
|
663
|
+
if (start >= 0) {
|
|
664
|
+
try {
|
|
665
|
+
const prettyEnd = mobileOutput.indexOf("\n}", start);
|
|
666
|
+
const lineEnd = mobileOutput.indexOf("\n", start);
|
|
667
|
+
const end = prettyEnd >= 0 ? prettyEnd + 2 : lineEnd;
|
|
668
|
+
if (end >= 0)
|
|
669
|
+
mobilePair = JSON.parse(mobileOutput.slice(start, end));
|
|
670
|
+
}
|
|
671
|
+
catch { /* wait for complete JSON */ }
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
mobileHost.stderr?.on("data", (chunk) => { log(`mobile host: ${String(chunk).trim().slice(0, 1000)}`); });
|
|
675
|
+
mobileHost.on("exit", (code, signal) => { mobileExit = { code, signal }; });
|
|
676
|
+
}
|
|
677
|
+
else if (reuseMobileHost) {
|
|
678
|
+
// Every explicit `omb mobile pair` gets a fresh one-time token. The
|
|
679
|
+
// host renews only its pairing window over this private pipe; restarting
|
|
680
|
+
// it here would tear down authenticated sockets and re-provision a
|
|
681
|
+
// Quick Tunnel with a different public hostname.
|
|
682
|
+
mobilePair = undefined;
|
|
683
|
+
mobileOutput = "";
|
|
684
|
+
if (!mobileHost.stdin?.write("pair\n")) {
|
|
685
|
+
json(res, 503, { error: "mobile host could not renew its pairing window" });
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const deadline = Date.now() + 60_000;
|
|
690
|
+
while (!mobilePair && !mobileExit && Date.now() < deadline)
|
|
691
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
692
|
+
if (!mobilePair) {
|
|
693
|
+
json(res, 503, { error: mobileExit ? `mobile host exited (${mobileExit.code ?? mobileExit.signal})` : "mobile pairing startup timed out" });
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
json(res, 200, mobilePair);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (path === "/mobile/status" && req.method === "GET") {
|
|
700
|
+
json(res, 200, {
|
|
701
|
+
running: Boolean(mobileHost && !mobileExit),
|
|
702
|
+
pid: mobileHost?.pid ?? null,
|
|
703
|
+
hostId: typeof mobilePair?.hostId === "string" ? mobilePair.hostId : null,
|
|
704
|
+
endpoint: typeof mobilePair?.endpoint === "string" ? mobilePair.endpoint : null,
|
|
705
|
+
hostKeyFingerprint: typeof mobilePair?.hostKeyFingerprint === "string" ? mobilePair.hostKeyFingerprint : null,
|
|
706
|
+
pairingExpiresAt: typeof mobilePair?.expiresAt === "number" ? mobilePair.expiresAt : null,
|
|
707
|
+
});
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
if (path === "/mobile/projects" && req.method === "GET") {
|
|
711
|
+
let knownSessions;
|
|
712
|
+
try {
|
|
713
|
+
knownSessions = await listOmbSessions(process.cwd(), env, { includeStopped: true, includeAllStopped: true, requireMultiplexer: true });
|
|
714
|
+
}
|
|
715
|
+
catch {
|
|
716
|
+
json(res, 503, { authorityKnown: false, reason: "session authority is unavailable", projects: [] });
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const settings = loadOmbSettings(env);
|
|
720
|
+
const discovered = await discoverMobileProjectDirectories(process.cwd(), settings.projectsFolder, knownSessions);
|
|
721
|
+
const now = Date.now();
|
|
722
|
+
for (const [id, choice] of mobileProjectChoices)
|
|
723
|
+
if (choice.expiresAt <= now)
|
|
724
|
+
mobileProjectChoices.delete(id);
|
|
725
|
+
const projects = discovered.map(project => {
|
|
726
|
+
while (mobileProjectChoices.size >= MOBILE_PROJECT_CHOICES_LIMIT) {
|
|
727
|
+
const oldest = mobileProjectChoices.keys().next().value;
|
|
728
|
+
if (oldest === undefined)
|
|
729
|
+
break;
|
|
730
|
+
mobileProjectChoices.delete(oldest);
|
|
731
|
+
}
|
|
732
|
+
const id = randomBytes(24).toString("base64url");
|
|
733
|
+
mobileProjectChoices.set(id, { ...project, expiresAt: now + MOBILE_PROJECT_ID_TTL_MS });
|
|
734
|
+
return { id, label: project.label, current: project.current };
|
|
735
|
+
});
|
|
736
|
+
json(res, 200, { authorityKnown: true, projectsFolderConfigured: Boolean(settings.projectsFolder.trim()), projects });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (path === "/mobile/settings" && req.method === "GET") {
|
|
740
|
+
json(res, 200, mobileHubSettings(loadOmbSettings(env)));
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (path === "/mobile/settings" && req.method === "PATCH") {
|
|
744
|
+
const body = await readBody(req);
|
|
745
|
+
const version = body.version;
|
|
746
|
+
const allowedKeys = new Set(["version", "agentsScope", "sessionFilter", "autoAttachLatest", "mouseScrolling", "showStoppedSessions", "trayEnabled"]);
|
|
747
|
+
const keys = Object.keys(body);
|
|
748
|
+
if (keys.some(key => !allowedKeys.has(key)) || typeof version !== "number" || !Number.isSafeInteger(version) || version < 0 || keys.length < 2) {
|
|
749
|
+
json(res, 400, { error: "invalid OMB Hub settings update" });
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const previous = loadOmbSettings(env);
|
|
753
|
+
const next = { ...previous };
|
|
754
|
+
if (body.agentsScope !== undefined) {
|
|
755
|
+
if (body.agentsScope !== "cwd" && body.agentsScope !== "all") {
|
|
756
|
+
json(res, 400, { error: "agentsScope is invalid" });
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
next.agentsScope = body.agentsScope;
|
|
760
|
+
}
|
|
761
|
+
if (body.sessionFilter !== undefined) {
|
|
762
|
+
if (body.sessionFilter !== "active" && body.sessionFilter !== "stopped" && body.sessionFilter !== "all") {
|
|
763
|
+
json(res, 400, { error: "sessionFilter is invalid" });
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
next.sessionFilter = body.sessionFilter;
|
|
767
|
+
}
|
|
768
|
+
for (const key of ["autoAttachLatest", "mouseScrolling", "showStoppedSessions", "trayEnabled"]) {
|
|
769
|
+
if (body[key] !== undefined && typeof body[key] !== "boolean") {
|
|
770
|
+
json(res, 400, { error: `${key} must be a boolean` });
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (typeof body[key] === "boolean")
|
|
774
|
+
next[key] = body[key];
|
|
775
|
+
}
|
|
776
|
+
if (previous.version !== version) {
|
|
777
|
+
json(res, 409, { error: "settings changed externally", settings: mobileHubSettings(previous) });
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
let saved;
|
|
781
|
+
try {
|
|
782
|
+
saved = saveOmbSettings(next, env);
|
|
783
|
+
}
|
|
784
|
+
catch (error) {
|
|
785
|
+
if (error instanceof Error && error.message === "settings changed externally") {
|
|
786
|
+
json(res, 409, { error: "settings changed externally", settings: mobileHubSettings(loadOmbSettings(env)) });
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
throw error;
|
|
790
|
+
}
|
|
791
|
+
if (previous.mouseScrolling !== next.mouseScrolling) {
|
|
792
|
+
await Promise.all(sessions.filter(session => session.hosted !== false && session.state !== "stopped")
|
|
793
|
+
.map(session => setOmbSessionMouse(session.name, next.mouseScrolling, session.cwd === "?" ? process.cwd() : session.cwd, env)));
|
|
794
|
+
}
|
|
795
|
+
if (previous.trayEnabled !== next.trayEnabled) {
|
|
796
|
+
if (next.trayEnabled)
|
|
797
|
+
await startTray(env);
|
|
798
|
+
else
|
|
799
|
+
await stopTray(env);
|
|
800
|
+
}
|
|
801
|
+
await scan(true);
|
|
802
|
+
json(res, 200, { ok: true, settings: mobileHubSettings(saved) });
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if (path === "/mobile/sessions/create" && req.method === "POST") {
|
|
806
|
+
const body = await readBody(req);
|
|
807
|
+
const projectId = typeof body.projectId === "string" ? body.projectId : "";
|
|
808
|
+
const task = typeof body.task === "string" ? body.task.trim() : "";
|
|
809
|
+
const requestedId = typeof body.sessionId === "string" ? body.sessionId.trim() : "";
|
|
810
|
+
if (!/^[A-Za-z0-9_-]{32}$/.test(projectId) || ("cwd" in body) || task.length > 20_000 || (body.sessionId !== undefined && (typeof body.sessionId !== "string" || requestedId.length > 120 || !/^[A-Za-z0-9 _.-]*$/.test(requestedId)))) {
|
|
811
|
+
json(res, 400, { error: "invalid OMB session creation request" });
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const choice = mobileProjectChoices.get(projectId);
|
|
815
|
+
mobileProjectChoices.delete(projectId);
|
|
816
|
+
if (!choice || choice.expiresAt <= Date.now()) {
|
|
817
|
+
json(res, 409, { error: "project choice expired; refresh the project list" });
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
let currentCwd;
|
|
821
|
+
try {
|
|
822
|
+
currentCwd = realpathSync(choice.cwd);
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
json(res, 409, { error: "selected project is no longer available" });
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (currentCwd !== choice.cwd || !isDirectory(currentCwd)) {
|
|
829
|
+
json(res, 409, { error: "selected project changed; refresh the project list" });
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const sessionId = requestedId || `mobile-${Date.now().toString(36)}-${randomBytes(5).toString("hex")}`;
|
|
833
|
+
const sessionName = ombSessionName(currentCwd, sessionId, env.PI_PROFILE);
|
|
834
|
+
let fresh;
|
|
835
|
+
try {
|
|
836
|
+
fresh = await listOmbSessions(process.cwd(), env, { includeStopped: true, includeAllStopped: true, requireMultiplexer: true });
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
json(res, 503, { error: "session authority is unavailable" });
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (fresh.some(session => session.name === sessionName)) {
|
|
843
|
+
json(res, 409, { error: "a session with that name already exists" });
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const settings = loadOmbSettings(env);
|
|
847
|
+
const args = [];
|
|
848
|
+
if (settings.defaultModel)
|
|
849
|
+
args.push("--model", settings.defaultModel);
|
|
850
|
+
if (settings.defaultThinking)
|
|
851
|
+
args.push("--thinking", settings.defaultThinking);
|
|
852
|
+
if (settings.defaultApprovalMode)
|
|
853
|
+
args.push("--approval-mode", settings.defaultApprovalMode);
|
|
854
|
+
if (task)
|
|
855
|
+
args.push(task);
|
|
856
|
+
const created = await createOmbSession(currentCwd, args, env, sessionId);
|
|
857
|
+
if (created.code !== 0) {
|
|
858
|
+
json(res, 409, { error: "OMP could not start the OMB session" });
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
await scan(true);
|
|
862
|
+
json(res, 200, { ok: true });
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
if (path === "/mobile/stop" && req.method === "POST") {
|
|
866
|
+
if (mobileHost && !mobileExit)
|
|
867
|
+
mobileHost.kill("SIGTERM");
|
|
868
|
+
json(res, 200, { ok: true });
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if (path === "/mobile/sessions/rename" && req.method === "POST") {
|
|
872
|
+
const body = await readBody(req);
|
|
873
|
+
const name = typeof body.name === "string" ? body.name : "";
|
|
874
|
+
const generation = typeof body.generation === "string" ? body.generation : "";
|
|
875
|
+
const newId = typeof body.newId === "string" ? body.newId.trim() : "";
|
|
876
|
+
if (!/^[A-Za-z0-9-]{1,64}$/.test(name) || !/^[a-f0-9]{32}$/.test(generation) || !newId || newId.length > 120 || /[\\/\u0000-\u001f\u007f]/.test(newId)) {
|
|
877
|
+
json(res, 400, { error: "invalid OMB session identity or label" });
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const outcome = await withUnclaimedSession(name, generation, async (current) => {
|
|
881
|
+
if (!current.hosted || current.state === "unknown" || current.uncertainLiveness) {
|
|
882
|
+
throw new DashboardRequestError(409, "session cannot be renamed in its current state");
|
|
883
|
+
}
|
|
884
|
+
const projectCwd = current.cwd === "?" ? process.cwd() : current.cwd;
|
|
885
|
+
const nextName = ombSessionName(projectCwd, newId, env.PI_PROFILE);
|
|
886
|
+
const code = await renameOmbSession(name, nextName, projectCwd, env);
|
|
887
|
+
if (code !== 0)
|
|
888
|
+
throw new DashboardRequestError(409, "session could not be renamed");
|
|
889
|
+
return { ok: true };
|
|
890
|
+
});
|
|
891
|
+
await scan(true);
|
|
892
|
+
json(res, 200, outcome);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (path === "/mobile/sessions/delete" && req.method === "POST") {
|
|
896
|
+
const body = await readBody(req);
|
|
897
|
+
const name = typeof body.name === "string" ? body.name : "";
|
|
898
|
+
const generation = typeof body.generation === "string" ? body.generation : "";
|
|
899
|
+
if (!/^[A-Za-z0-9-]{1,64}$/.test(name) || !/^[a-f0-9]{32}$/.test(generation)) {
|
|
900
|
+
json(res, 400, { error: "invalid OMB session identity" });
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const outcome = await withUnclaimedSession(name, generation, async (current) => {
|
|
904
|
+
// Mobile removal is destructive. It is only safe for a fresh,
|
|
905
|
+
// host-owned row whose multiplexer and process observations prove that
|
|
906
|
+
// the exact generation is stopped and unattached. In particular,
|
|
907
|
+
// hosted rows must not be treated as removable merely because their
|
|
908
|
+
// liveness is not uncertain: an active hosted OMP session can still be
|
|
909
|
+
// the one writer for a native session.
|
|
910
|
+
if (!isSafeMobileSessionRemoval(current)) {
|
|
911
|
+
throw new DashboardRequestError(409, "session liveness is not safe to remove");
|
|
912
|
+
}
|
|
913
|
+
const projectCwd = current.cwd === "?" ? process.cwd() : current.cwd;
|
|
914
|
+
const code = current.hosted
|
|
915
|
+
? await deleteOmbSession(name, projectCwd, env)
|
|
916
|
+
: forgetOmbSession(name, env);
|
|
917
|
+
if (code !== 0)
|
|
918
|
+
throw new DashboardRequestError(409, "session removal could not be confirmed");
|
|
919
|
+
return { ok: true };
|
|
920
|
+
});
|
|
921
|
+
await scan(true);
|
|
922
|
+
json(res, 200, outcome);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
522
925
|
if (path === "/dash/code" && req.method === "POST") {
|
|
523
926
|
json(res, 200, { code: mintDashCode(), path: DASHBOARD_PATH, port: service.port, expiresInMs: DASH_CODE_TTL_MS });
|
|
524
927
|
return;
|
|
@@ -933,7 +1336,8 @@ export function startDaemonService(options = {}) {
|
|
|
933
1336
|
const name = decodeURIComponent(deleteMatch[1]);
|
|
934
1337
|
const body = await readBody(req);
|
|
935
1338
|
const current = await requireCurrentSession(name, body);
|
|
936
|
-
const
|
|
1339
|
+
const projectCwd = current.cwd === "?" ? process.cwd() : current.cwd;
|
|
1340
|
+
const code = current.hosted === false ? forgetOmbSession(name, env) : await deleteOmbSession(name, projectCwd, env);
|
|
937
1341
|
await scan(true);
|
|
938
1342
|
json(res, code === 0 ? 200 : 500, { code });
|
|
939
1343
|
return;
|
|
@@ -964,6 +1368,12 @@ export function startDaemonService(options = {}) {
|
|
|
964
1368
|
close: async () => {
|
|
965
1369
|
if (scanTimer)
|
|
966
1370
|
clearInterval(scanTimer);
|
|
1371
|
+
if (mobileHost && !mobileExit) {
|
|
1372
|
+
mobileHost.kill("SIGTERM");
|
|
1373
|
+
await Promise.race([new Promise(resolve => mobileHost?.once("exit", () => resolve())), new Promise(resolve => setTimeout(resolve, 5_000))]);
|
|
1374
|
+
if (!mobileExit)
|
|
1375
|
+
mobileHost.kill("SIGKILL");
|
|
1376
|
+
}
|
|
967
1377
|
// Run every SSE cleanup so poll/keepalive timers drop the response
|
|
968
1378
|
// reference immediately — otherwise the timers themselves keep the
|
|
969
1379
|
// socket (and therefore server.close()) alive past shutdown.
|