omp-mobile 0.4.4 → 0.6.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/NOTICE +3 -0
- package/README.md +5 -3
- package/package.json +3 -2
- package/src/attention.mjs +6 -1
- package/src/omp-extension.mjs +88 -34
- package/src/relay-control.mjs +104 -0
- package/src/relay.mjs +368 -0
- package/src/rpc-session.mjs +4 -1
- package/src/server.mjs +174 -18
- package/src/state.mjs +36 -0
- package/src/transcripts.mjs +1 -1
- package/vendor/LICENSE.dsh-remote +21 -0
- package/vendor/README.md +9 -0
- package/vendor/dsh-remote-host.mjs +20109 -0
package/src/server.mjs
CHANGED
|
@@ -21,6 +21,8 @@ import { AttentionTracker, serveEvents } from "./attention.mjs";
|
|
|
21
21
|
import { listOmpSessions, oneLine, promptTitle, sessionOwners } from "./omp-sessions.mjs";
|
|
22
22
|
import { askOwnerToLeave, forksOf, isOmpCommand, processInfo, sessionBusy, takeoverFarewell, tellTerminal } from "./takeover.mjs";
|
|
23
23
|
import { TranscriptStore, deltaBody, deltaQuery } from "./transcripts.mjs";
|
|
24
|
+
import { RELAY_HEADER, RelayHost } from "./relay.mjs";
|
|
25
|
+
import { DEFAULT_RELAY, runRelayCli } from "./relay-control.mjs";
|
|
24
26
|
|
|
25
27
|
const { version: VERSION } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
26
28
|
const PROTOCOL = 1;
|
|
@@ -28,6 +30,7 @@ const DEFAULT_PORT = 7390;
|
|
|
28
30
|
const DEFAULT_TOKEN_FILE = join(homedir(), ".omp-mobile", "token");
|
|
29
31
|
const DEFAULT_REGISTRY_FILE = join(homedir(), ".omp-mobile", "sessions.json");
|
|
30
32
|
const DEFAULT_STATE_FILE = join(homedir(), ".omp-mobile", "daemon.json");
|
|
33
|
+
const DEFAULT_RELAY_DIR = join(homedir(), ".omp-mobile", "relay");
|
|
31
34
|
const DEFAULT_OMP_HOME = join(homedir(), ".omp");
|
|
32
35
|
const MAX_BODY = 256 * 1024;
|
|
33
36
|
/** Terminal sessions listed next to the daemon's own, newest first. */
|
|
@@ -116,7 +119,8 @@ const OPEN_FAILED_CLOSE_CODE = 4000;
|
|
|
116
119
|
const BLOB_NAME = /^[0-9a-f]{64}$/;
|
|
117
120
|
|
|
118
121
|
/** @typedef {{pid: number, busy: boolean, terminal: boolean}} SessionOwner */
|
|
119
|
-
/** @typedef {{id: string,
|
|
122
|
+
/** @typedef {{id: string, method: string, title: string, message?: string, options?: string[]}} PendingRequest */
|
|
123
|
+
/** @typedef {{id: string, title: string, cwd: string, status: string, model?: string, createdAt: number, updatedAt: number, sessionFile?: string, origin: "daemon" | "omp", openElsewhere?: boolean, owner?: SessionOwner, waiting?: number, request?: PendingRequest}} SessionSummary */
|
|
120
124
|
|
|
121
125
|
/** An error with the HTTP status it answers, and any detail its JSON answer carries. */
|
|
122
126
|
function httpError(status, message, detail = {}) {
|
|
@@ -129,6 +133,29 @@ function errorBody(error) {
|
|
|
129
133
|
return { error: error instanceof Error ? error.message : String(error), ...detail };
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The part of an open extension request a session list shows: enough to answer
|
|
138
|
+
* a tool approval or a short choice from the home, nothing for a text prompt.
|
|
139
|
+
* @param {any} request
|
|
140
|
+
* @returns {PendingRequest | undefined}
|
|
141
|
+
*/
|
|
142
|
+
function pendingOf(request) {
|
|
143
|
+
if (request === null || typeof request !== "object" || typeof request.id !== "string") return undefined;
|
|
144
|
+
const pending = { id: request.id, method: String(request.method), title: typeof request.title === "string" ? request.title : "" };
|
|
145
|
+
if (typeof request.message === "string") pending.message = request.message;
|
|
146
|
+
if (Array.isArray(request.options)) pending.options = request.options.filter((option) => typeof option === "string");
|
|
147
|
+
return pending;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The answer fields a phone sends, over the session socket or `POST /sessions/:id/answer`. */
|
|
151
|
+
function answerOf(message) {
|
|
152
|
+
const answer = {};
|
|
153
|
+
if (typeof message.value === "string") answer.value = message.value;
|
|
154
|
+
if (typeof message.confirmed === "boolean") answer.confirmed = message.confirmed;
|
|
155
|
+
if (message.cancelled === true) answer.cancelled = true;
|
|
156
|
+
return answer;
|
|
157
|
+
}
|
|
158
|
+
|
|
132
159
|
function parseArgs(argv) {
|
|
133
160
|
const options = {
|
|
134
161
|
host: "127.0.0.1",
|
|
@@ -146,6 +173,8 @@ function parseArgs(argv) {
|
|
|
146
173
|
tailscale: "tailscale",
|
|
147
174
|
whois: true,
|
|
148
175
|
qr: true,
|
|
176
|
+
relay: "",
|
|
177
|
+
relayDir: DEFAULT_RELAY_DIR,
|
|
149
178
|
};
|
|
150
179
|
for (let i = 0; i < argv.length; i += 1) {
|
|
151
180
|
const flag = argv[i];
|
|
@@ -208,6 +237,19 @@ function parseArgs(argv) {
|
|
|
208
237
|
case "--no-qr":
|
|
209
238
|
options.qr = false;
|
|
210
239
|
break;
|
|
240
|
+
case "--relay":
|
|
241
|
+
// The server is optional: `--relay` alone means the public ds-harness-remote relay.
|
|
242
|
+
if (value !== undefined && !value.startsWith("--")) {
|
|
243
|
+
options.relay = value;
|
|
244
|
+
i += 1;
|
|
245
|
+
} else {
|
|
246
|
+
options.relay = DEFAULT_RELAY;
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
case "--relay-dir":
|
|
250
|
+
options.relayDir = resolve(value);
|
|
251
|
+
i += 1;
|
|
252
|
+
break;
|
|
211
253
|
case "--help":
|
|
212
254
|
case "-h":
|
|
213
255
|
printHelp();
|
|
@@ -245,6 +287,14 @@ Admission: on a non-loopback --host every request and WebSocket upgrade must nam
|
|
|
245
287
|
this node in Host (421 otherwise) and come from a tailnet device of this node's
|
|
246
288
|
user (403 otherwise), before the token is checked. Loopback hosts skip admission.
|
|
247
289
|
--no-qr Do not print the pairing QR code at startup
|
|
290
|
+
--relay [url] Also serve paired phones through a ds-harness-remote relay (default ${DEFAULT_RELAY}),
|
|
291
|
+
end-to-end encrypted; pair with \`omp-mobile relay pair\` or /mobile relay
|
|
292
|
+
--relay-dir <dir> Relay identity and device tokens (default ~/.omp-mobile/relay)
|
|
293
|
+
|
|
294
|
+
Relay commands (talk to the running daemon; same --state-file):
|
|
295
|
+
omp-mobile relay pair Show a pairing QR code, then confirm the phone's 8-digit code
|
|
296
|
+
omp-mobile relay status Relay server, connection and paired phones
|
|
297
|
+
omp-mobile relay revoke <phone device id>
|
|
248
298
|
`);
|
|
249
299
|
}
|
|
250
300
|
|
|
@@ -605,6 +655,8 @@ class SessionEntry {
|
|
|
605
655
|
origin: "daemon",
|
|
606
656
|
// OMP questions and tool approvals still waiting for an answer: the session needs the user.
|
|
607
657
|
waiting: this.openRequests().length,
|
|
658
|
+
// The oldest of them, so a list can answer it without opening the session.
|
|
659
|
+
request: pendingOf(this.openRequests()[0]),
|
|
608
660
|
};
|
|
609
661
|
}
|
|
610
662
|
|
|
@@ -657,6 +709,8 @@ class SessionEntry {
|
|
|
657
709
|
if (STATE_EVENTS.has(event.type)) void this.refreshState();
|
|
658
710
|
// A `todo` tool call rewrites the list before any reminder fires.
|
|
659
711
|
if (event.type === "tool_execution_end" && event.toolName === "todo") void this.refreshState();
|
|
712
|
+
// A run starting is what makes a session "running" in the list; OMP's state read would lag behind.
|
|
713
|
+
if (event.type === "agent_start" && this.state !== null && typeof this.state === "object") this.state = { ...this.state, isStreaming: true };
|
|
660
714
|
if (event.type === "turn_end" || event.type === "agent_end") void this.refreshState();
|
|
661
715
|
this.broadcast({ type: "event", event });
|
|
662
716
|
});
|
|
@@ -746,6 +800,29 @@ class Daemon {
|
|
|
746
800
|
// the phone's WebSocket client (OkHttp) asks for permessage-deflate on its own.
|
|
747
801
|
this.wsServer = new WebSocketServer({ noServer: true, perMessageDeflate: { threshold: 1_024 } });
|
|
748
802
|
this.server.on("upgrade", (req, socket, head) => void this.admitUpgrade(req, socket, head));
|
|
803
|
+
/**
|
|
804
|
+
* Phones paired through a relay reach the daemon through this loopback-only
|
|
805
|
+
* listener (`relay.mjs` adds the token): the same API, without the tailnet admission.
|
|
806
|
+
*/
|
|
807
|
+
this.tunnelServer = undefined;
|
|
808
|
+
/** @type {RelayHost | undefined} */
|
|
809
|
+
this.relay = undefined;
|
|
810
|
+
if (options.relay) {
|
|
811
|
+
this.tunnelServer = createServer((req, res) => void this.handleHttp(req, res));
|
|
812
|
+
this.tunnelServer.on("upgrade", (req, socket, head) => {
|
|
813
|
+
socket.on("error", () => socket.destroy());
|
|
814
|
+
this.handleUpgrade(req, socket, head);
|
|
815
|
+
});
|
|
816
|
+
this.relay = new RelayHost({
|
|
817
|
+
serverUrl: options.relay,
|
|
818
|
+
dir: options.relayDir,
|
|
819
|
+
name: `omp-mobile on ${hostname().replace(/\.local$/, "")}`,
|
|
820
|
+
version: VERSION,
|
|
821
|
+
port: () => this.tunnelServer.address().port,
|
|
822
|
+
token: this.token,
|
|
823
|
+
log: (line) => this.log(line),
|
|
824
|
+
});
|
|
825
|
+
}
|
|
749
826
|
}
|
|
750
827
|
|
|
751
828
|
baseUrl() {
|
|
@@ -828,9 +905,14 @@ class Daemon {
|
|
|
828
905
|
liveSessions: this.sessions.size,
|
|
829
906
|
eventStreams: this.streams.size,
|
|
830
907
|
lastPhone: this.lastPhone,
|
|
908
|
+
relay: this.relay === undefined ? null : await this.relay.status(),
|
|
831
909
|
});
|
|
832
910
|
return;
|
|
833
911
|
}
|
|
912
|
+
if (url.pathname === "/relay" || url.pathname.startsWith("/relay/")) {
|
|
913
|
+
await this.handleRelay(req, res, url);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
834
916
|
if (url.pathname === "/events" && req.method === "GET") {
|
|
835
917
|
const end = serveEvents(this.attention, req, res);
|
|
836
918
|
this.streams.add(end);
|
|
@@ -868,6 +950,18 @@ class Daemon {
|
|
|
868
950
|
sendJson(res, 200, this.transcript(decodeURIComponent(transcript[1]), deltaQuery(url.searchParams)));
|
|
869
951
|
return;
|
|
870
952
|
}
|
|
953
|
+
const answer = /^\/sessions\/([^/]+)\/answer$/.exec(url.pathname);
|
|
954
|
+
if (answer !== null && req.method === "POST") {
|
|
955
|
+
const body = await readBody(req);
|
|
956
|
+
const live = this.sessions.get(decodeURIComponent(answer[1]));
|
|
957
|
+
if (live === undefined) throw httpError(404, "session is not running");
|
|
958
|
+
if (typeof body.id !== "string" || !live.openRequests().some((request) => request.id === body.id)) {
|
|
959
|
+
throw httpError(409, "that question was already answered");
|
|
960
|
+
}
|
|
961
|
+
live.rpc.respondExtension(body.id, answerOf(body));
|
|
962
|
+
sendJson(res, 200, { ok: true });
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
871
965
|
const match = /^\/sessions\/([^/]+)$/.exec(url.pathname);
|
|
872
966
|
if (match !== null) {
|
|
873
967
|
const id = decodeURIComponent(match[1]);
|
|
@@ -1264,11 +1358,7 @@ class Daemon {
|
|
|
1264
1358
|
return;
|
|
1265
1359
|
case "approval": {
|
|
1266
1360
|
if (typeof message.id !== "string") throw new Error("missing approval id");
|
|
1267
|
-
|
|
1268
|
-
if (typeof message.value === "string") answer.value = message.value;
|
|
1269
|
-
if (typeof message.confirmed === "boolean") answer.confirmed = message.confirmed;
|
|
1270
|
-
if (message.cancelled === true) answer.cancelled = true;
|
|
1271
|
-
entry.rpc.respondExtension(message.id, answer);
|
|
1361
|
+
entry.rpc.respondExtension(message.id, answerOf(message));
|
|
1272
1362
|
ws.send(JSON.stringify({ type: "accepted", command: "approval" }));
|
|
1273
1363
|
return;
|
|
1274
1364
|
}
|
|
@@ -1328,17 +1418,72 @@ class Daemon {
|
|
|
1328
1418
|
*/
|
|
1329
1419
|
noteClient(req) {
|
|
1330
1420
|
if (req.headers["x-omp-mobile-probe"] !== undefined) return;
|
|
1331
|
-
|
|
1421
|
+
// Through the relay every request comes from loopback: name the path instead.
|
|
1422
|
+
const address = req.headers[RELAY_HEADER] !== undefined ? "relay" : String(req.socket?.remoteAddress ?? "").replace(/^::ffff:/i, "");
|
|
1332
1423
|
this.lastPhone = { address, at: Date.now() };
|
|
1333
1424
|
}
|
|
1334
1425
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1426
|
+
/**
|
|
1427
|
+
* Relay pairing and phones, for `omp-mobile relay` and `/mobile relay` on this
|
|
1428
|
+
* computer; never through the relay itself (a paired phone cannot pair or revoke).
|
|
1429
|
+
*/
|
|
1430
|
+
async handleRelay(req, res, url) {
|
|
1431
|
+
if (req.headers[RELAY_HEADER] !== undefined) {
|
|
1432
|
+
sendJson(res, 403, { error: "relay control is local only" });
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
if (this.relay === undefined) {
|
|
1436
|
+
sendJson(res, 409, { error: "the daemon runs without --relay" });
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
if (url.pathname === "/relay" && req.method === "GET") {
|
|
1441
|
+
sendJson(res, 200, { ...(await this.relay.status()), phones: await this.relay.phones() });
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
if (url.pathname === "/relay/pair" && req.method === "POST") {
|
|
1445
|
+
sendJson(res, 200, await this.relay.pairStart());
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (url.pathname === "/relay/pair" && req.method === "GET") {
|
|
1449
|
+
sendJson(res, 200, { pairing: (await this.relay.pairStatus()) ?? null });
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
if ((url.pathname === "/relay/pair/confirm" || url.pathname === "/relay/pair/cancel") && req.method === "POST") {
|
|
1453
|
+
const body = await readBody(req);
|
|
1454
|
+
if (typeof body.sessionId !== "string") {
|
|
1455
|
+
sendJson(res, 400, { error: "sessionId is required" });
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const confirm = url.pathname.endsWith("/confirm");
|
|
1459
|
+
if (confirm && (typeof body.code !== "string" || !/^\d{8}$/.test(body.code))) {
|
|
1460
|
+
sendJson(res, 400, { error: "the code is 8 digits" });
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
sendJson(res, 200, confirm ? await this.relay.pairConfirm(body.sessionId, body.code) : await this.relay.pairCancel(body.sessionId));
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
const phone = /^\/relay\/phones\/([^/]+)$/.exec(url.pathname);
|
|
1467
|
+
if (phone !== null && req.method === "DELETE") {
|
|
1468
|
+
await this.relay.revoke(decodeURIComponent(phone[1]));
|
|
1469
|
+
sendJson(res, 200, { ok: true });
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
sendJson(res, 404, { error: "not found" });
|
|
1473
|
+
} catch (error) {
|
|
1474
|
+
// The relay's own refusals carry a code (PAIRING_EXPIRED, PAIRING_ATTEMPT_USED, ...): pass it on.
|
|
1475
|
+
const code = typeof error?.code === "string" ? error.code : undefined;
|
|
1476
|
+
sendJson(res, 502, { error: error instanceof Error ? error.message : String(error), ...(code === undefined ? {} : { code }) });
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
async listen() {
|
|
1481
|
+
await new Promise((resolveListen) => this.server.listen(this.options.port, this.options.host, () => resolveListen()));
|
|
1482
|
+
if (this.relay !== undefined) {
|
|
1483
|
+
await new Promise((resolveListen) => this.tunnelServer.listen(0, "127.0.0.1", () => resolveListen()));
|
|
1484
|
+
await this.relay.start();
|
|
1485
|
+
}
|
|
1486
|
+
this.startTimers();
|
|
1342
1487
|
}
|
|
1343
1488
|
|
|
1344
1489
|
/** Keepalive pings and the idle-session reaper, once the daemon listens; old stashed images go. */
|
|
@@ -1395,7 +1540,9 @@ class Daemon {
|
|
|
1395
1540
|
const outcomes = await Promise.all(closing);
|
|
1396
1541
|
const stuck = outcomes.filter((outcome) => outcome === "running").length;
|
|
1397
1542
|
if (stuck > 0) console.error(`omp-mobile: ${stuck} omp process(es) ignored SIGINT and SIGTERM and are still running`);
|
|
1543
|
+
await this.relay?.close();
|
|
1398
1544
|
this.wsServer.close();
|
|
1545
|
+
this.tunnelServer?.close();
|
|
1399
1546
|
await new Promise((resolveClose) => this.server.close(() => resolveClose()));
|
|
1400
1547
|
}
|
|
1401
1548
|
}
|
|
@@ -1473,6 +1620,10 @@ function clearState(path) {
|
|
|
1473
1620
|
}
|
|
1474
1621
|
|
|
1475
1622
|
async function main() {
|
|
1623
|
+
if (process.argv[2] === "relay") {
|
|
1624
|
+
await runRelayCli(process.argv.slice(3));
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1476
1627
|
const options = parseArgs(process.argv.slice(2));
|
|
1477
1628
|
if (options.host === "tailscale") options.host = await tailscaleHost(options);
|
|
1478
1629
|
const token = loadToken(options);
|
|
@@ -1481,21 +1632,26 @@ async function main() {
|
|
|
1481
1632
|
await daemon.listen();
|
|
1482
1633
|
const url = daemon.baseUrl();
|
|
1483
1634
|
const pair = `omp-mobile://pair?url=${encodeURIComponent(url)}&token=${encodeURIComponent(token)}`;
|
|
1484
|
-
writeState(options.stateFile, { pid: process.pid, version: VERSION, url, pair, startedAt: daemon.startedAt });
|
|
1635
|
+
writeState(options.stateFile, { pid: process.pid, version: VERSION, url, pair, relay: options.relay || null, startedAt: daemon.startedAt });
|
|
1485
1636
|
console.log(`omp-mobile daemon ${VERSION}`);
|
|
1486
1637
|
console.log(` listening ${url}`);
|
|
1487
1638
|
console.log(` cwd ${options.cwd}`);
|
|
1488
1639
|
for (const line of identity.notes) (line.includes("WARNING") ? console.warn : console.log)(line);
|
|
1489
1640
|
console.log(` token ${token}`);
|
|
1490
1641
|
console.log(` pair ${pair}`);
|
|
1491
|
-
if (
|
|
1642
|
+
if (daemon.relay !== undefined) {
|
|
1643
|
+
const relay = await daemon.relay.status();
|
|
1644
|
+
console.log(` relay ${relay.server} · ${relay.paired ? "paired" : "no phone paired yet"} · pair a phone: omp-mobile relay pair`);
|
|
1645
|
+
}
|
|
1646
|
+
const tailnet = isTailnetAddress(options.host);
|
|
1647
|
+
if (options.qr && (tailnet || daemon.relay === undefined)) {
|
|
1492
1648
|
console.log("");
|
|
1493
1649
|
console.log(" Scan with OMP on your phone (Machines > Scan to pair):");
|
|
1494
1650
|
console.log(renderQr(pair));
|
|
1495
1651
|
// The app accepts only tailnet addresses from a code; say so before the user tries.
|
|
1496
|
-
if (!
|
|
1652
|
+
if (!tailnet) {
|
|
1497
1653
|
console.log(` note: ${options.host} is not a Tailscale address, so the app will refuse this code.`);
|
|
1498
|
-
console.log(" Start with --host <this computer's Tailscale IP
|
|
1654
|
+
console.log(" Start with --host <this computer's Tailscale IP>, or --relay, to pair a phone.");
|
|
1499
1655
|
}
|
|
1500
1656
|
}
|
|
1501
1657
|
const shutdown = async () => {
|
package/src/state.mjs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** The running daemon's state file, as `/mobile` and `omp-mobile relay` read it. */
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
export const STATE_FILE = join(homedir(), ".omp-mobile", "daemon.json");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The running daemon's record, if its process is alive.
|
|
10
|
+
* @returns {{pid: number, version: string, url: string, pair: string, relay?: string | null, startedAt: number} | undefined}
|
|
11
|
+
*/
|
|
12
|
+
export function readState(path = STATE_FILE) {
|
|
13
|
+
let state;
|
|
14
|
+
try {
|
|
15
|
+
state = JSON.parse(readFileSync(path, "utf8"));
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
if (typeof state?.pid !== "number" || typeof state.url !== "string" || typeof state.pair !== "string") return undefined;
|
|
20
|
+
return alive(state.pid) ? state : undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function alive(pid) {
|
|
24
|
+
try {
|
|
25
|
+
process.kill(pid, 0);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
// EPERM: it exists but belongs to someone else.
|
|
29
|
+
return error?.code === "EPERM";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The token inside a pairing link. */
|
|
34
|
+
export function tokenOf(pair) {
|
|
35
|
+
return new URL(pair.replace(/^omp-mobile:\/\//, "http://x/")).searchParams.get("token") ?? "";
|
|
36
|
+
}
|
package/src/transcripts.mjs
CHANGED
|
@@ -136,7 +136,7 @@ export class TranscriptStore {
|
|
|
136
136
|
base: Number.isSafeInteger(base) && base > 0 ? base : 0,
|
|
137
137
|
messages,
|
|
138
138
|
state: state !== null && typeof state === "object" ? { ...state, isStreaming: false } : state,
|
|
139
|
-
session: { ...session, status: "idle", waiting: 0 },
|
|
139
|
+
session: { ...session, status: "idle", waiting: 0, request: undefined },
|
|
140
140
|
};
|
|
141
141
|
try {
|
|
142
142
|
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DSH Remote contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/vendor/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
`dsh-remote-host.mjs` is the Host side of ds-harness-remote's relay (device pairing, the
|
|
2
|
+
relay control connection and its Noise IK channel), bundled from
|
|
3
|
+
[liguobao/ds-harness-remote](https://github.com/liguobao/ds-harness-remote) at commit
|
|
4
|
+
ea1c0925f8be825a5f5afac32ad2d0b2c699f7f7 with `bun build --target=node --format=esm` over an
|
|
5
|
+
entry that re-exports `HostPairing`, `HostServerConnection`, `ConnectionController`,
|
|
6
|
+
`IdentityStore`, `HostServerApi`, `ServerCredentialStore` and `SafeLogger` from
|
|
7
|
+
`packages/plugin/src`, and `createEvent`, `createRpcError`, `createRpcResponse` and
|
|
8
|
+
`MAX_SECURE_MESSAGE_BYTES` from `@dsh-remote/protocol`. Not edited here; see
|
|
9
|
+
`LICENSE.dsh-remote` (MIT).
|