@longleash/cli 0.1.0-rc.1 → 0.1.0-rc.10
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 +6 -3
- package/bin/longleash.mjs +1940 -53
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/runtime/app/dist/assets/app-CJy31Egy.js +27 -0
- package/runtime/app/dist/assets/styles-CamwzwbV.css +1 -0
- package/runtime/app/dist/assets/{styles-C-1gzhnT.js → styles-YZYGkrmF.js} +3 -3
- package/runtime/app/dist/assets/welcome-BOXvwUDw.js +40 -0
- package/runtime/app/dist/build.json +1 -1
- package/runtime/app/dist/index.html +3 -3
- package/runtime/app/dist/sw.js +6 -1
- package/runtime/app/dist/welcome.html +3 -3
- package/runtime/daemon/bin/longleashd.mjs +511 -92
- package/runtime/app/dist/assets/app-CxamvvoS.js +0 -27
- package/runtime/app/dist/assets/styles-CkAMG7sK.css +0 -1
- package/runtime/app/dist/assets/welcome-CG_rEa--.js +0 -27
|
@@ -7,8 +7,8 @@ var __export = (target, all) => {
|
|
|
7
7
|
|
|
8
8
|
// ../daemon/bin/longleashd.ts
|
|
9
9
|
import { existsSync as existsSync7 } from "node:fs";
|
|
10
|
-
import { homedir as
|
|
11
|
-
import { dirname as dirname4, join as
|
|
10
|
+
import { homedir as homedir5 } from "node:os";
|
|
11
|
+
import { dirname as dirname4, join as join10, resolve as resolve6 } from "node:path";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
|
|
14
14
|
// ../daemon/src/daemon.ts
|
|
@@ -44,8 +44,8 @@ async function pauseCurrentSessionOwner(managed, external, session, actor, reaso
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// ../daemon/src/daemon.ts
|
|
47
|
-
import { homedir as
|
|
48
|
-
import { delimiter, join as
|
|
47
|
+
import { homedir as homedir4 } from "node:os";
|
|
48
|
+
import { delimiter, join as join7, resolve as resolve5 } from "node:path";
|
|
49
49
|
|
|
50
50
|
// ../daemon/src/eventlog.ts
|
|
51
51
|
import Database from "better-sqlite3";
|
|
@@ -5432,7 +5432,10 @@ var SessionOrigin = external_exports.enum(["phone", "daemon", "terminal", "vscod
|
|
|
5432
5432
|
var WorkspaceMode = external_exports.enum(["auto", "shared", "isolated"]);
|
|
5433
5433
|
var AgentEffort = external_exports.enum(["low", "medium", "high", "xhigh", "max"]);
|
|
5434
5434
|
var ThinkingMode = external_exports.enum(["adaptive", "disabled", "fixed"]);
|
|
5435
|
+
var SessionMode = external_exports.enum(["manual", "auto", "plan"]);
|
|
5435
5436
|
var SessionSettings = external_exports.object({
|
|
5437
|
+
/** Omitted means LongLeash's conservative manual mode. */
|
|
5438
|
+
mode: SessionMode.optional(),
|
|
5436
5439
|
/** Omitted means the provider's current default; model ids never enter a shell command. */
|
|
5437
5440
|
model: external_exports.string().trim().min(1).max(120).optional(),
|
|
5438
5441
|
effort: AgentEffort.optional(),
|
|
@@ -5493,6 +5496,8 @@ var sessionStartedPayload = external_exports.object({
|
|
|
5493
5496
|
origin: SessionOrigin.optional(),
|
|
5494
5497
|
/** Who owns the process now; unlike origin, this changes after a safe handoff. */
|
|
5495
5498
|
controller: external_exports.enum(["longleash", "external"]).optional(),
|
|
5499
|
+
/** Observation-only means the native surface is visible but has not exposed process control. */
|
|
5500
|
+
control: external_exports.enum(["full", "observe"]).optional(),
|
|
5496
5501
|
/**
|
|
5497
5502
|
* The agent's own conversation id. Carried to the phone so a person can pick the
|
|
5498
5503
|
* SAME conversation back up at their keyboard — `claude --resume <id>` in the
|
|
@@ -5526,6 +5531,8 @@ var sessionStatusPayload = external_exports.object({
|
|
|
5526
5531
|
/** LongLeash's own gate for this session: whether it should page the phone at all. */
|
|
5527
5532
|
gate: SessionGate.optional(),
|
|
5528
5533
|
controller: external_exports.enum(["longleash", "external"]).optional(),
|
|
5534
|
+
/** Observation-only sessions are never presented as remotely stoppable or writable. */
|
|
5535
|
+
control: external_exports.enum(["full", "observe"]).optional(),
|
|
5529
5536
|
/** Current overrides. An empty object explicitly means provider defaults. */
|
|
5530
5537
|
settings: SessionSettings.optional(),
|
|
5531
5538
|
/** A second writer was observed in this checkout; text accompanies color for accessibility. */
|
|
@@ -5614,7 +5621,9 @@ var subscribeMessage = external_exports.object({
|
|
|
5614
5621
|
...clientBase,
|
|
5615
5622
|
type: external_exports.literal("subscribe"),
|
|
5616
5623
|
sessionId: external_exports.string().min(1),
|
|
5617
|
-
fromCursor: external_exports.number().int().nonnegative()
|
|
5624
|
+
fromCursor: external_exports.number().int().nonnegative(),
|
|
5625
|
+
/** Optional replay barrier. New clients use the echoed id to paint catch-up only once. */
|
|
5626
|
+
syncId: external_exports.string().min(1).max(120).optional()
|
|
5618
5627
|
}).passthrough();
|
|
5619
5628
|
var decisionMessage = external_exports.object({
|
|
5620
5629
|
...clientBase,
|
|
@@ -5859,6 +5868,7 @@ var EventLog = class {
|
|
|
5859
5868
|
insertStmt;
|
|
5860
5869
|
maxSeqStmt;
|
|
5861
5870
|
minSeqStmt;
|
|
5871
|
+
latestTsStmt;
|
|
5862
5872
|
selectFromStmt;
|
|
5863
5873
|
pruneStmt;
|
|
5864
5874
|
constructor(path, opts = {}) {
|
|
@@ -5886,6 +5896,9 @@ var EventLog = class {
|
|
|
5886
5896
|
this.minSeqStmt = this.rawDb.prepare(
|
|
5887
5897
|
"SELECT COALESCE(MIN(seq), 0) AS seq FROM events WHERE session_id = ?"
|
|
5888
5898
|
);
|
|
5899
|
+
this.latestTsStmt = this.rawDb.prepare(
|
|
5900
|
+
"SELECT COALESCE(MAX(ts), 0) AS ts FROM events WHERE session_id = ?"
|
|
5901
|
+
);
|
|
5889
5902
|
this.selectFromStmt = this.rawDb.prepare(
|
|
5890
5903
|
"SELECT * FROM events WHERE session_id = ? AND seq > ? ORDER BY seq ASC"
|
|
5891
5904
|
);
|
|
@@ -5940,6 +5953,9 @@ var EventLog = class {
|
|
|
5940
5953
|
latestSeq(sessionId) {
|
|
5941
5954
|
return this.maxSeqStmt.get(sessionId).seq;
|
|
5942
5955
|
}
|
|
5956
|
+
latestTimestamp(sessionId) {
|
|
5957
|
+
return this.latestTsStmt.get(sessionId).ts;
|
|
5958
|
+
}
|
|
5943
5959
|
pruneBefore(sessionId, uptoExclusive) {
|
|
5944
5960
|
this.pruneStmt.run(sessionId, uptoExclusive);
|
|
5945
5961
|
}
|
|
@@ -6668,13 +6684,13 @@ function workspaceFromRow(row) {
|
|
|
6668
6684
|
}
|
|
6669
6685
|
var newId = (prefix) => `${prefix}_${randomBytes2(9).toString("base64url")}`;
|
|
6670
6686
|
function settlesWithin(promise, timeoutMs) {
|
|
6671
|
-
return new Promise((
|
|
6687
|
+
return new Promise((resolve7) => {
|
|
6672
6688
|
let settled = false;
|
|
6673
6689
|
const finish = (value) => {
|
|
6674
6690
|
if (settled) return;
|
|
6675
6691
|
settled = true;
|
|
6676
6692
|
clearTimeout(timer);
|
|
6677
|
-
|
|
6693
|
+
resolve7(value);
|
|
6678
6694
|
};
|
|
6679
6695
|
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
6680
6696
|
timer.unref?.();
|
|
@@ -7609,7 +7625,7 @@ var SessionManager = class {
|
|
|
7609
7625
|
this.approvals.rawDb.prepare("DELETE FROM session_deliveries WHERE delivery_id = ?").run(deliveryId);
|
|
7610
7626
|
}
|
|
7611
7627
|
releasePending(session, message) {
|
|
7612
|
-
for (const [approvalId,
|
|
7628
|
+
for (const [approvalId, resolve7] of session.waiting) {
|
|
7613
7629
|
const decided = this.approvals.decide(approvalId, "denied", "system:session-ended", message);
|
|
7614
7630
|
if (decided) {
|
|
7615
7631
|
this.emit(session.sessionId, {
|
|
@@ -7622,7 +7638,7 @@ var SessionManager = class {
|
|
|
7622
7638
|
}
|
|
7623
7639
|
});
|
|
7624
7640
|
}
|
|
7625
|
-
|
|
7641
|
+
resolve7({ behavior: "deny", message });
|
|
7626
7642
|
}
|
|
7627
7643
|
session.waiting.clear();
|
|
7628
7644
|
}
|
|
@@ -7643,9 +7659,9 @@ var SessionManager = class {
|
|
|
7643
7659
|
waitForApproval(sessionId) {
|
|
7644
7660
|
const alreadyPending = this.approvals.listPending().some((a) => a.sessionId === sessionId);
|
|
7645
7661
|
if (alreadyPending) return Promise.resolve();
|
|
7646
|
-
return new Promise((
|
|
7662
|
+
return new Promise((resolve7) => {
|
|
7647
7663
|
const waiters = this.approvalWaiters.get(sessionId) ?? [];
|
|
7648
|
-
waiters.push(
|
|
7664
|
+
waiters.push(resolve7);
|
|
7649
7665
|
this.approvalWaiters.set(sessionId, waiters);
|
|
7650
7666
|
});
|
|
7651
7667
|
}
|
|
@@ -7694,8 +7710,8 @@ var SessionManager = class {
|
|
|
7694
7710
|
outsideRoot
|
|
7695
7711
|
}
|
|
7696
7712
|
});
|
|
7697
|
-
const decision = new Promise((
|
|
7698
|
-
waiting.set(approvalId,
|
|
7713
|
+
const decision = new Promise((resolve7) => {
|
|
7714
|
+
waiting.set(approvalId, resolve7);
|
|
7699
7715
|
});
|
|
7700
7716
|
for (const waiter of this.approvalWaiters.get(sessionId) ?? []) waiter();
|
|
7701
7717
|
this.approvalWaiters.delete(sessionId);
|
|
@@ -7943,7 +7959,8 @@ var ExternalSessions = class {
|
|
|
7943
7959
|
offset: existsSync(row.transcriptPath) ? statSync(row.transcriptPath).size : 0,
|
|
7944
7960
|
remainder: "",
|
|
7945
7961
|
timer: null,
|
|
7946
|
-
suspended: false
|
|
7962
|
+
suspended: false,
|
|
7963
|
+
observedOnly: false
|
|
7947
7964
|
};
|
|
7948
7965
|
this.sessions.set(row.agentSessionId, session);
|
|
7949
7966
|
this.claimWorkspace(session);
|
|
@@ -7964,6 +7981,26 @@ var ExternalSessions = class {
|
|
|
7964
7981
|
}
|
|
7965
7982
|
this.claimWorkspace(session);
|
|
7966
7983
|
}
|
|
7984
|
+
/** Adopt a Codex transcript observed after startup, including sessions older than the hooks. */
|
|
7985
|
+
observeCodexSession(info) {
|
|
7986
|
+
const session = this.ensure(info.sessionId, info.cwd, info.transcriptPath, "codex", info.surface, true);
|
|
7987
|
+
if (info.title !== void 0 && info.title !== session.title) {
|
|
7988
|
+
session.named = true;
|
|
7989
|
+
session.title = info.title;
|
|
7990
|
+
this.persist(info.sessionId, session);
|
|
7991
|
+
}
|
|
7992
|
+
this.emit(session.sessionId, {
|
|
7993
|
+
type: "session.status",
|
|
7994
|
+
payload: {
|
|
7995
|
+
status: session.status,
|
|
7996
|
+
live: true,
|
|
7997
|
+
controller: "external",
|
|
7998
|
+
control: "observe",
|
|
7999
|
+
...info.title === void 0 ? {} : { title: info.title }
|
|
8000
|
+
}
|
|
8001
|
+
});
|
|
8002
|
+
this.claimWorkspace(session);
|
|
8003
|
+
}
|
|
7967
8004
|
/**
|
|
7968
8005
|
* Stop a terminal session from the phone — for real. The recorded pid is only
|
|
7969
8006
|
* trusted after re-verifying it still belongs to a claude process, because PIDs
|
|
@@ -7973,6 +8010,7 @@ var ExternalSessions = class {
|
|
|
7973
8010
|
const entry = [...this.sessions.entries()].find(([, s]) => s.sessionId === externalSessionId);
|
|
7974
8011
|
if (!entry) return false;
|
|
7975
8012
|
const [claudeSessionId, session] = entry;
|
|
8013
|
+
if (session.observedOnly) return false;
|
|
7976
8014
|
if (session.pid === null || !this.isClaudeProcess(session.pid)) {
|
|
7977
8015
|
this.emit(session.sessionId, {
|
|
7978
8016
|
type: "stream.delta",
|
|
@@ -8055,7 +8093,7 @@ var ExternalSessions = class {
|
|
|
8055
8093
|
const deadline = Date.now() + timeoutMs;
|
|
8056
8094
|
while (Date.now() < deadline) {
|
|
8057
8095
|
if (!this.isClaudeProcess(pid)) return true;
|
|
8058
|
-
await new Promise((
|
|
8096
|
+
await new Promise((resolve7) => setTimeout(resolve7, 50));
|
|
8059
8097
|
}
|
|
8060
8098
|
return !this.isClaudeProcess(pid);
|
|
8061
8099
|
}
|
|
@@ -8063,7 +8101,7 @@ var ExternalSessions = class {
|
|
|
8063
8101
|
* A tool wants to run in the terminal. Returns what the hook should tell
|
|
8064
8102
|
* Claude Code — and "ask" always means "behave as if we were never here".
|
|
8065
8103
|
*/
|
|
8066
|
-
preToolUse(claudeSessionId, cwd, transcriptPath, toolName, toolInput,
|
|
8104
|
+
preToolUse(claudeSessionId, cwd, transcriptPath, toolName, toolInput, permissionMode2, opts = {}) {
|
|
8067
8105
|
const { dedupeKey, agent = "claude", surface = "terminal", abandoned, enforceWorkspace } = opts;
|
|
8068
8106
|
const session = this.ensure(claudeSessionId, cwd, transcriptPath, agent, surface);
|
|
8069
8107
|
if (enforceWorkspace === true && session.workspaceConflict !== void 0) {
|
|
@@ -8080,11 +8118,11 @@ var ExternalSessions = class {
|
|
|
8080
8118
|
if (session.gate === "auto") {
|
|
8081
8119
|
return Promise.resolve({ decision: "allow", reason: "Auto-approved: you muted this session." });
|
|
8082
8120
|
}
|
|
8083
|
-
if (
|
|
8084
|
-
session.permissionMode =
|
|
8121
|
+
if (permissionMode2 !== void 0 && permissionMode2 !== session.permissionMode) {
|
|
8122
|
+
session.permissionMode = permissionMode2;
|
|
8085
8123
|
this.emit(session.sessionId, {
|
|
8086
8124
|
type: "session.status",
|
|
8087
|
-
payload: { status: session.status, live: true, permissionMode }
|
|
8125
|
+
payload: { status: session.status, live: true, permissionMode: permissionMode2 }
|
|
8088
8126
|
});
|
|
8089
8127
|
}
|
|
8090
8128
|
const who = this.audience();
|
|
@@ -8119,7 +8157,7 @@ var ExternalSessions = class {
|
|
|
8119
8157
|
...questions === null ? {} : { questions }
|
|
8120
8158
|
}
|
|
8121
8159
|
});
|
|
8122
|
-
const pending = new Promise((
|
|
8160
|
+
const pending = new Promise((resolve7) => {
|
|
8123
8161
|
const timer = setTimeout(() => {
|
|
8124
8162
|
this.waiting.delete(approvalId);
|
|
8125
8163
|
this.approvals.decide(
|
|
@@ -8137,11 +8175,11 @@ var ExternalSessions = class {
|
|
|
8137
8175
|
reply: "Answered in the terminal instead."
|
|
8138
8176
|
}
|
|
8139
8177
|
});
|
|
8140
|
-
|
|
8178
|
+
resolve7({ decision: "ask", reason: "No answer from the phone; deciding in the terminal." });
|
|
8141
8179
|
}, holdMs);
|
|
8142
8180
|
timer.unref?.();
|
|
8143
8181
|
this.waiting.set(approvalId, {
|
|
8144
|
-
resolve:
|
|
8182
|
+
resolve: resolve7,
|
|
8145
8183
|
timer,
|
|
8146
8184
|
sessionId: session.sessionId,
|
|
8147
8185
|
...questions === null ? {} : { questions }
|
|
@@ -8258,7 +8296,7 @@ var ExternalSessions = class {
|
|
|
8258
8296
|
/** Mute or unmute a session from the phone. Returns false for a session it does not know. */
|
|
8259
8297
|
setGate(externalSessionId, gate) {
|
|
8260
8298
|
const session = [...this.sessions.values()].find((s) => s.sessionId === externalSessionId);
|
|
8261
|
-
if (!session) return false;
|
|
8299
|
+
if (!session || session.observedOnly) return false;
|
|
8262
8300
|
session.gate = gate;
|
|
8263
8301
|
this.emit(session.sessionId, {
|
|
8264
8302
|
type: "session.status",
|
|
@@ -8311,11 +8349,13 @@ var ExternalSessions = class {
|
|
|
8311
8349
|
resumeId: claudeSessionId,
|
|
8312
8350
|
gate: session.gate,
|
|
8313
8351
|
controller: "external",
|
|
8352
|
+
control: session.observedOnly ? "observe" : "full",
|
|
8314
8353
|
...session.workspaceConflict === void 0 ? {} : { workspaceConflict: session.workspaceConflict }
|
|
8315
8354
|
}));
|
|
8316
8355
|
}
|
|
8317
8356
|
/** Write this session down so a restart can take it back. */
|
|
8318
8357
|
persist(agentSessionId, session) {
|
|
8358
|
+
if (session.observedOnly) return;
|
|
8319
8359
|
this.registry?.remember({
|
|
8320
8360
|
agentSessionId,
|
|
8321
8361
|
sessionId: session.sessionId,
|
|
@@ -8367,9 +8407,19 @@ var ExternalSessions = class {
|
|
|
8367
8407
|
this.sessions.clear();
|
|
8368
8408
|
}
|
|
8369
8409
|
/* ------------------------------------------------------------------ internals */
|
|
8370
|
-
ensure(claudeSessionId, cwd, transcriptPath, agent = "claude", surface = "terminal") {
|
|
8410
|
+
ensure(claudeSessionId, cwd, transcriptPath, agent = "claude", surface = "terminal", tailOnly = false) {
|
|
8371
8411
|
const existing = this.sessions.get(claudeSessionId);
|
|
8372
|
-
if (existing)
|
|
8412
|
+
if (existing) {
|
|
8413
|
+
if (!tailOnly && existing.observedOnly) {
|
|
8414
|
+
existing.observedOnly = false;
|
|
8415
|
+
this.persist(claudeSessionId, existing);
|
|
8416
|
+
this.emit(existing.sessionId, {
|
|
8417
|
+
type: "session.status",
|
|
8418
|
+
payload: { status: existing.status, live: true, controller: "external", control: "full" }
|
|
8419
|
+
});
|
|
8420
|
+
}
|
|
8421
|
+
return existing;
|
|
8422
|
+
}
|
|
8373
8423
|
const sessionId = `ext_${claudeSessionId}`;
|
|
8374
8424
|
const replay = this.eventLog.replay(sessionId, 0);
|
|
8375
8425
|
const hasHistory = replay.gap === false ? replay.events.length > 0 : true;
|
|
@@ -8386,10 +8436,11 @@ var ExternalSessions = class {
|
|
|
8386
8436
|
named: false,
|
|
8387
8437
|
permissionMode: null,
|
|
8388
8438
|
gate: "ask",
|
|
8389
|
-
offset: hasHistory && existsSync(transcriptPath) ? statSync(transcriptPath).size : 0,
|
|
8439
|
+
offset: (tailOnly || hasHistory) && existsSync(transcriptPath) ? statSync(transcriptPath).size : 0,
|
|
8390
8440
|
remainder: "",
|
|
8391
8441
|
timer: null,
|
|
8392
|
-
suspended: false
|
|
8442
|
+
suspended: false,
|
|
8443
|
+
observedOnly: tailOnly
|
|
8393
8444
|
};
|
|
8394
8445
|
this.sessions.set(claudeSessionId, session);
|
|
8395
8446
|
this.persist(claudeSessionId, session);
|
|
@@ -8402,13 +8453,19 @@ var ExternalSessions = class {
|
|
|
8402
8453
|
title: session.title,
|
|
8403
8454
|
origin: surface,
|
|
8404
8455
|
controller: "external",
|
|
8456
|
+
control: tailOnly ? "observe" : "full",
|
|
8405
8457
|
resumeId: claudeSessionId
|
|
8406
8458
|
}
|
|
8407
8459
|
});
|
|
8408
8460
|
}
|
|
8409
8461
|
this.emit(sessionId, {
|
|
8410
8462
|
type: "session.status",
|
|
8411
|
-
payload: {
|
|
8463
|
+
payload: {
|
|
8464
|
+
status: "running",
|
|
8465
|
+
live: true,
|
|
8466
|
+
controller: "external",
|
|
8467
|
+
control: tailOnly ? "observe" : "full"
|
|
8468
|
+
}
|
|
8412
8469
|
});
|
|
8413
8470
|
session.timer = setInterval(() => this.drain(session), this.pollMs);
|
|
8414
8471
|
session.timer.unref?.();
|
|
@@ -8636,6 +8693,9 @@ var RelayLink = class {
|
|
|
8636
8693
|
reconnectTimer = null;
|
|
8637
8694
|
keepaliveTimer = null;
|
|
8638
8695
|
keepaliveMs;
|
|
8696
|
+
/** WebCrypto promises may resolve out of order; preserve the server's event order explicitly. */
|
|
8697
|
+
outbound = Promise.resolve();
|
|
8698
|
+
inbound = Promise.resolve();
|
|
8639
8699
|
constructor(opts) {
|
|
8640
8700
|
this.opts = opts;
|
|
8641
8701
|
this.log = opts.log ?? (() => {
|
|
@@ -8672,10 +8732,12 @@ var RelayLink = class {
|
|
|
8672
8732
|
const socket = this.socket;
|
|
8673
8733
|
const identity = this.identity;
|
|
8674
8734
|
if (this.currentStatus !== "connected" || !socket || !identity) return;
|
|
8675
|
-
|
|
8676
|
-
|
|
8735
|
+
this.outbound = this.outbound.then(async () => {
|
|
8736
|
+
const payload = await seal(identity, plaintext);
|
|
8737
|
+
if (this.socket === socket && socket.readyState === WebSocket.OPEN) {
|
|
8677
8738
|
socket.send(JSON.stringify({ v: 1, type: "frame", payload }));
|
|
8678
8739
|
}
|
|
8740
|
+
}).catch(() => {
|
|
8679
8741
|
});
|
|
8680
8742
|
}
|
|
8681
8743
|
async connect() {
|
|
@@ -8714,9 +8776,12 @@ var RelayLink = class {
|
|
|
8714
8776
|
return;
|
|
8715
8777
|
}
|
|
8716
8778
|
if (message.type === "frame" && typeof message.payload === "string") {
|
|
8717
|
-
|
|
8779
|
+
const payload = message.payload;
|
|
8780
|
+
this.inbound = this.inbound.then(async () => {
|
|
8781
|
+
const plaintext = await open(identity, payload);
|
|
8718
8782
|
if (plaintext !== null) this.opts.onMessage(plaintext);
|
|
8719
8783
|
else this.log("dropped a relay frame that failed to open");
|
|
8784
|
+
}).catch(() => {
|
|
8720
8785
|
});
|
|
8721
8786
|
return;
|
|
8722
8787
|
}
|
|
@@ -9958,6 +10023,7 @@ var LongLeashServer = class {
|
|
|
9958
10023
|
external = null;
|
|
9959
10024
|
delegations = null;
|
|
9960
10025
|
hookSecret = null;
|
|
10026
|
+
localPairing = null;
|
|
9961
10027
|
staticRoot;
|
|
9962
10028
|
/** Captured at process start so updating files on disk cannot impersonate a daemon restart. */
|
|
9963
10029
|
expectedBuild;
|
|
@@ -10085,6 +10151,15 @@ var LongLeashServer = class {
|
|
|
10085
10151
|
this.log(`device ${deviceId} revoked \u2014 its sessions are closed and its relay room is shut`);
|
|
10086
10152
|
return { revoked: true, deviceId };
|
|
10087
10153
|
});
|
|
10154
|
+
this.app.post("/local/pairing", async (request, reply) => {
|
|
10155
|
+
if (!localOnly(request)) return reply.code(401).send({ reason: "unauthorized" });
|
|
10156
|
+
if (this.localPairing === null) return reply.code(503).send({ reason: "pairing-unavailable" });
|
|
10157
|
+
try {
|
|
10158
|
+
return { url: this.localPairing() };
|
|
10159
|
+
} catch {
|
|
10160
|
+
return reply.code(503).send({ reason: "pairing-failed" });
|
|
10161
|
+
}
|
|
10162
|
+
});
|
|
10088
10163
|
this.app.get("/pair", async (_request, reply) => reply.code(405).send({ reason: "use-post" }));
|
|
10089
10164
|
this.app.post("/pair", async (request, reply) => {
|
|
10090
10165
|
const query2 = request.query;
|
|
@@ -10190,15 +10265,15 @@ var LongLeashServer = class {
|
|
|
10190
10265
|
*/
|
|
10191
10266
|
async rebind(host) {
|
|
10192
10267
|
const wanted = this.boundPort;
|
|
10193
|
-
await new Promise((
|
|
10194
|
-
const tryListen = (port) => new Promise((
|
|
10268
|
+
await new Promise((resolve7) => this.app.server.close(() => resolve7()));
|
|
10269
|
+
const tryListen = (port) => new Promise((resolve7, reject) => {
|
|
10195
10270
|
const onError = (err) => {
|
|
10196
10271
|
this.app.server.removeListener("listening", onListening);
|
|
10197
10272
|
reject(err);
|
|
10198
10273
|
};
|
|
10199
10274
|
const onListening = () => {
|
|
10200
10275
|
this.app.server.removeListener("error", onError);
|
|
10201
|
-
|
|
10276
|
+
resolve7();
|
|
10202
10277
|
};
|
|
10203
10278
|
this.app.server.once("error", onError);
|
|
10204
10279
|
this.app.server.once("listening", onListening);
|
|
@@ -10223,6 +10298,10 @@ var LongLeashServer = class {
|
|
|
10223
10298
|
this.external = external;
|
|
10224
10299
|
this.hookSecret = hookSecret;
|
|
10225
10300
|
}
|
|
10301
|
+
/** A laptop-local CLI may request a transient QR without persisting its secret in service logs. */
|
|
10302
|
+
attachLocalPairing(create) {
|
|
10303
|
+
this.localPairing = create;
|
|
10304
|
+
}
|
|
10226
10305
|
/** Persist an event, then fan it out to every subscriber of that session. */
|
|
10227
10306
|
publish(sessionId, input) {
|
|
10228
10307
|
const event2 = this.eventLog.append(sessionId, input);
|
|
@@ -10340,7 +10419,10 @@ var LongLeashServer = class {
|
|
|
10340
10419
|
roots: this.sessions?.listAllowedRoots() ?? [],
|
|
10341
10420
|
// Everything the daemon knows about, so a reloaded phone rebuilds instead of showing
|
|
10342
10421
|
// nothing — including sessions the human started in a terminal.
|
|
10343
|
-
sessions: [...this.sessions?.listSessions() ?? [], ...this.external?.listSessions() ?? []]
|
|
10422
|
+
sessions: [...this.sessions?.listSessions() ?? [], ...this.external?.listSessions() ?? []].map((session) => ({
|
|
10423
|
+
...session,
|
|
10424
|
+
lastActivityAt: this.eventLog.latestTimestamp(session.sessionId) || session.startedAt
|
|
10425
|
+
})).sort((left, right) => left.startedAt - right.startedAt || left.sessionId.localeCompare(right.sessionId)),
|
|
10344
10426
|
capabilities: {
|
|
10345
10427
|
startSession: this.sessions !== null,
|
|
10346
10428
|
stopSession: this.sessions !== null,
|
|
@@ -10418,6 +10500,15 @@ var LongLeashServer = class {
|
|
|
10418
10500
|
return;
|
|
10419
10501
|
}
|
|
10420
10502
|
for (const event2 of replay.events) this.sendTo(connection, event2);
|
|
10503
|
+
if (message.syncId !== void 0) {
|
|
10504
|
+
this.sendTo(connection, {
|
|
10505
|
+
v: PROTOCOL_VERSION,
|
|
10506
|
+
type: "sync.complete",
|
|
10507
|
+
sessionId: message.sessionId,
|
|
10508
|
+
syncId: message.syncId,
|
|
10509
|
+
cursor: this.eventLog.latestSeq(message.sessionId)
|
|
10510
|
+
});
|
|
10511
|
+
}
|
|
10421
10512
|
return;
|
|
10422
10513
|
}
|
|
10423
10514
|
if (message.type === "pushSubscribe") {
|
|
@@ -10721,6 +10812,12 @@ var LongLeashServer = class {
|
|
|
10721
10812
|
try {
|
|
10722
10813
|
const liveExternal = this.external?.hasLiveSession(message.sessionId) === true;
|
|
10723
10814
|
const externalSession = liveExternal ? this.external?.listSessions().find((item) => item.sessionId === message.sessionId) : void 0;
|
|
10815
|
+
if (externalSession?.control === "observe") {
|
|
10816
|
+
throw new SessionError(
|
|
10817
|
+
"session-busy",
|
|
10818
|
+
"This Codex conversation is visible through its active VS Code transcript, but its process has not exposed a control hook yet. Continue or stop it in VS Code; LongLeash changed nothing."
|
|
10819
|
+
);
|
|
10820
|
+
}
|
|
10724
10821
|
if (externalSession?.agent === "codex" && message.settings.thinking !== void 0) {
|
|
10725
10822
|
throw new SessionError(
|
|
10726
10823
|
"unsupported-setting",
|
|
@@ -11044,6 +11141,11 @@ function normalizeRelayUrl(raw) {
|
|
|
11044
11141
|
|
|
11045
11142
|
// ../daemon/src/adapters/claude.ts
|
|
11046
11143
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
11144
|
+
function permissionMode(mode) {
|
|
11145
|
+
if (mode === "auto") return "auto";
|
|
11146
|
+
if (mode === "plan") return "plan";
|
|
11147
|
+
return "default";
|
|
11148
|
+
}
|
|
11047
11149
|
function summarizeToolInput(input) {
|
|
11048
11150
|
if (input && typeof input === "object") {
|
|
11049
11151
|
const record = input;
|
|
@@ -11077,8 +11179,8 @@ function createClaudeAgentFactory(options = {}) {
|
|
|
11077
11179
|
};
|
|
11078
11180
|
}
|
|
11079
11181
|
if (closed) return;
|
|
11080
|
-
await new Promise((
|
|
11081
|
-
wake =
|
|
11182
|
+
await new Promise((resolve7) => {
|
|
11183
|
+
wake = resolve7;
|
|
11082
11184
|
});
|
|
11083
11185
|
}
|
|
11084
11186
|
}
|
|
@@ -11101,6 +11203,7 @@ function createClaudeAgentFactory(options = {}) {
|
|
|
11101
11203
|
...request.settings?.model === void 0 && options.model === void 0 ? {} : { model: request.settings?.model ?? options.model },
|
|
11102
11204
|
...request.settings?.effort === void 0 ? {} : { effort: request.settings.effort },
|
|
11103
11205
|
...request.settings?.thinking === void 0 ? {} : request.settings.thinking.mode === "fixed" ? { thinking: { type: "enabled", budgetTokens: request.settings.thinking.budgetTokens } } : request.settings.thinking.mode === "adaptive" ? { thinking: { type: "adaptive" } } : { thinking: { type: "disabled" } },
|
|
11206
|
+
permissionMode: permissionMode(request.settings?.mode),
|
|
11104
11207
|
...options.allowedTools === void 0 ? {} : { allowedTools: options.allowedTools },
|
|
11105
11208
|
...options.isolateFromUserSettings ? { settingSources: [] } : {},
|
|
11106
11209
|
// Reopening a closed conversation: Claude replays its own transcript, so the agent
|
|
@@ -11143,6 +11246,7 @@ function createClaudeAgentFactory(options = {}) {
|
|
|
11143
11246
|
});
|
|
11144
11247
|
let activeSettings = { ...request.settings ?? {} };
|
|
11145
11248
|
const applySettings = async (settings) => {
|
|
11249
|
+
await run.setPermissionMode(permissionMode(settings.mode));
|
|
11146
11250
|
await run.setModel(settings.model);
|
|
11147
11251
|
await run.applyFlagSettings({ effortLevel: settings.effort ?? null });
|
|
11148
11252
|
if (settings.thinking === void 0) {
|
|
@@ -11301,6 +11405,32 @@ var CodexRun = class {
|
|
|
11301
11405
|
log;
|
|
11302
11406
|
/** Per-turn overrides; updating this affects the next turn without replacing the thread. */
|
|
11303
11407
|
settings;
|
|
11408
|
+
approvalPolicy() {
|
|
11409
|
+
if (this.settings.mode === "auto") return "on-request";
|
|
11410
|
+
if (this.settings.mode === "manual" || this.settings.mode === "plan") return "untrusted";
|
|
11411
|
+
return this.opts.approvalPolicy ?? "untrusted";
|
|
11412
|
+
}
|
|
11413
|
+
sandbox() {
|
|
11414
|
+
if (this.settings.mode === "plan") return "read-only";
|
|
11415
|
+
if (this.settings.mode === "manual" || this.settings.mode === "auto") return "workspace-write";
|
|
11416
|
+
return this.opts.sandbox ?? "workspace-write";
|
|
11417
|
+
}
|
|
11418
|
+
sandboxPolicy() {
|
|
11419
|
+
const sandbox = this.sandbox();
|
|
11420
|
+
if (sandbox === "read-only") return { type: "readOnly", networkAccess: false };
|
|
11421
|
+
if (sandbox === "danger-full-access") return { type: "dangerFullAccess" };
|
|
11422
|
+
return { type: "workspaceWrite", networkAccess: false, writableRoots: [this.request.cwd] };
|
|
11423
|
+
}
|
|
11424
|
+
providerText(text) {
|
|
11425
|
+
if (this.settings.mode !== "plan") return text;
|
|
11426
|
+
return [
|
|
11427
|
+
"<longleash-plan-mode>",
|
|
11428
|
+
"Plan only. Investigate and explain a concrete implementation approach, but do not modify files or run commands that change the workspace. Ask for clarification when a decision is required.",
|
|
11429
|
+
"</longleash-plan-mode>",
|
|
11430
|
+
"",
|
|
11431
|
+
text
|
|
11432
|
+
].join("\n");
|
|
11433
|
+
}
|
|
11304
11434
|
handle() {
|
|
11305
11435
|
return {
|
|
11306
11436
|
events: this.iterate(),
|
|
@@ -11309,7 +11439,7 @@ var CodexRun = class {
|
|
|
11309
11439
|
if (this.threadId !== null && this.turnId !== null) {
|
|
11310
11440
|
void this.call("turn/interrupt", { threadId: this.threadId, turnId: this.turnId }).catch(() => {
|
|
11311
11441
|
});
|
|
11312
|
-
await new Promise((
|
|
11442
|
+
await new Promise((resolve7) => setTimeout(resolve7, INTERRUPT_GRACE_MS));
|
|
11313
11443
|
}
|
|
11314
11444
|
this.child.kill("SIGTERM");
|
|
11315
11445
|
this.finish();
|
|
@@ -11335,15 +11465,15 @@ var CodexRun = class {
|
|
|
11335
11465
|
this.request.resume === void 0 ? {
|
|
11336
11466
|
cwd: this.request.cwd,
|
|
11337
11467
|
...this.settings.model === void 0 ? {} : { model: this.settings.model },
|
|
11338
|
-
approvalPolicy: this.
|
|
11339
|
-
sandbox: this.
|
|
11468
|
+
approvalPolicy: this.approvalPolicy(),
|
|
11469
|
+
sandbox: this.sandbox(),
|
|
11340
11470
|
config: MANAGED_THREAD_CONFIG
|
|
11341
11471
|
} : {
|
|
11342
11472
|
threadId: this.request.resume,
|
|
11343
11473
|
cwd: this.request.cwd,
|
|
11344
11474
|
...this.settings.model === void 0 ? {} : { model: this.settings.model },
|
|
11345
|
-
approvalPolicy: this.
|
|
11346
|
-
sandbox: this.
|
|
11475
|
+
approvalPolicy: this.approvalPolicy(),
|
|
11476
|
+
sandbox: this.sandbox(),
|
|
11347
11477
|
config: MANAGED_THREAD_CONFIG
|
|
11348
11478
|
}
|
|
11349
11479
|
);
|
|
@@ -11365,9 +11495,11 @@ var CodexRun = class {
|
|
|
11365
11495
|
try {
|
|
11366
11496
|
await this.call("turn/start", {
|
|
11367
11497
|
threadId: this.threadId,
|
|
11368
|
-
input: [{ type: "text", text }],
|
|
11498
|
+
input: [{ type: "text", text: this.providerText(text) }],
|
|
11369
11499
|
...this.settings.model === void 0 ? {} : { model: this.settings.model },
|
|
11370
|
-
...this.settings.effort === void 0 ? {} : { effort: this.settings.effort }
|
|
11500
|
+
...this.settings.effort === void 0 ? {} : { effort: this.settings.effort },
|
|
11501
|
+
approvalPolicy: this.approvalPolicy(),
|
|
11502
|
+
sandboxPolicy: this.sandboxPolicy()
|
|
11371
11503
|
});
|
|
11372
11504
|
} catch (error) {
|
|
11373
11505
|
if (fatal) throw error;
|
|
@@ -11527,8 +11659,8 @@ var CodexRun = class {
|
|
|
11527
11659
|
// -------------------------------------------------------------------- plumbing
|
|
11528
11660
|
call(method, params) {
|
|
11529
11661
|
const id2 = this.nextId++;
|
|
11530
|
-
const promise = new Promise((
|
|
11531
|
-
this.pending.set(id2, { resolve:
|
|
11662
|
+
const promise = new Promise((resolve7, reject) => {
|
|
11663
|
+
this.pending.set(id2, { resolve: resolve7, reject });
|
|
11532
11664
|
});
|
|
11533
11665
|
this.write({ jsonrpc: "2.0", id: id2, method, params });
|
|
11534
11666
|
return promise;
|
|
@@ -11580,8 +11712,8 @@ var CodexRun = class {
|
|
|
11580
11712
|
if (this.failure !== null) throw this.failure;
|
|
11581
11713
|
return;
|
|
11582
11714
|
}
|
|
11583
|
-
await new Promise((
|
|
11584
|
-
this.waiter =
|
|
11715
|
+
await new Promise((resolve7) => {
|
|
11716
|
+
this.waiter = resolve7;
|
|
11585
11717
|
});
|
|
11586
11718
|
}
|
|
11587
11719
|
}
|
|
@@ -11763,8 +11895,8 @@ var FolderIndex = class {
|
|
|
11763
11895
|
for (const root of this.roots) {
|
|
11764
11896
|
if (path === root) return basename4(root);
|
|
11765
11897
|
if (path.startsWith(root + sep4)) {
|
|
11766
|
-
const
|
|
11767
|
-
return `${basename4(root)}/${
|
|
11898
|
+
const relative3 = path.slice(root.length + 1);
|
|
11899
|
+
return `${basename4(root)}/${relative3}`;
|
|
11768
11900
|
}
|
|
11769
11901
|
}
|
|
11770
11902
|
return path;
|
|
@@ -12170,13 +12302,183 @@ function roleName2(role) {
|
|
|
12170
12302
|
return role.slice(0, 1).toUpperCase() + role.slice(1);
|
|
12171
12303
|
}
|
|
12172
12304
|
|
|
12305
|
+
// ../daemon/src/codex-session-watch.ts
|
|
12306
|
+
import { closeSync as closeSync2, openSync as openSync2, readSync as readSync2, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
|
|
12307
|
+
import { homedir as homedir3 } from "node:os";
|
|
12308
|
+
import { isAbsolute as isAbsolute2, join as join6, relative as relative2, resolve as resolve4 } from "node:path";
|
|
12309
|
+
var MAX_HEAD = 256 * 1024;
|
|
12310
|
+
var MAX_TAIL = 1024 * 1024;
|
|
12311
|
+
var MAX_INITIAL_TAIL = 64 * 1024 * 1024;
|
|
12312
|
+
function readSlice(path, offset, length) {
|
|
12313
|
+
const fd = openSync2(path, "r");
|
|
12314
|
+
try {
|
|
12315
|
+
const buffer = Buffer.alloc(length);
|
|
12316
|
+
const read = readSync2(fd, buffer, 0, length, offset);
|
|
12317
|
+
return buffer.subarray(0, read).toString("utf8");
|
|
12318
|
+
} finally {
|
|
12319
|
+
closeSync2(fd);
|
|
12320
|
+
}
|
|
12321
|
+
}
|
|
12322
|
+
function inside(root, candidate) {
|
|
12323
|
+
const rel = relative2(resolve4(root), resolve4(candidate));
|
|
12324
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
|
|
12325
|
+
}
|
|
12326
|
+
function inspectCodexTranscript(path, roots2, maxTailBytes = MAX_INITIAL_TAIL) {
|
|
12327
|
+
let size;
|
|
12328
|
+
try {
|
|
12329
|
+
size = statSync2(path).size;
|
|
12330
|
+
} catch {
|
|
12331
|
+
return null;
|
|
12332
|
+
}
|
|
12333
|
+
try {
|
|
12334
|
+
const firstLine = readSlice(path, 0, Math.min(size, MAX_HEAD)).split("\n")[0];
|
|
12335
|
+
if (!firstLine) return null;
|
|
12336
|
+
const meta = JSON.parse(firstLine);
|
|
12337
|
+
if (meta.type !== "session_meta" || !meta.payload) return null;
|
|
12338
|
+
const sessionId = typeof meta.payload.session_id === "string" ? meta.payload.session_id : typeof meta.payload.id === "string" ? meta.payload.id : null;
|
|
12339
|
+
const cwd = typeof meta.payload.cwd === "string" ? meta.payload.cwd : null;
|
|
12340
|
+
if (sessionId === null || cwd === null || !roots2.some((root) => inside(root, cwd))) return null;
|
|
12341
|
+
if (meta.payload.source !== "vscode" && meta.payload.originator !== "codex_vscode") return null;
|
|
12342
|
+
const surface = "vscode";
|
|
12343
|
+
const tailOffset = Math.max(0, size - maxTailBytes);
|
|
12344
|
+
const tailText = readSlice(path, tailOffset, size - tailOffset);
|
|
12345
|
+
const lines = tailText.split("\n");
|
|
12346
|
+
if (tailOffset > 0) lines.shift();
|
|
12347
|
+
let title;
|
|
12348
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
12349
|
+
const line = lines[index];
|
|
12350
|
+
if (!line?.includes('"role":"user"')) continue;
|
|
12351
|
+
try {
|
|
12352
|
+
const record = JSON.parse(line);
|
|
12353
|
+
if (record.type !== "response_item" || record.payload?.type !== "message" || record.payload.role !== "user") continue;
|
|
12354
|
+
const content = record.payload.content;
|
|
12355
|
+
if (!Array.isArray(content)) continue;
|
|
12356
|
+
const text = content.filter((item) => !!item && typeof item === "object" && item.type === "input_text" && typeof item.text === "string").map((item) => item.text).join("\n");
|
|
12357
|
+
title = titleFrom(text) ?? void 0;
|
|
12358
|
+
if (title !== void 0) break;
|
|
12359
|
+
} catch {
|
|
12360
|
+
}
|
|
12361
|
+
}
|
|
12362
|
+
if (title === void 0) title = titleFrom(latestUserText(tailText) ?? "") ?? void 0;
|
|
12363
|
+
return { sessionId, cwd, transcriptPath: path, surface, ...title === void 0 ? {} : { title } };
|
|
12364
|
+
} catch {
|
|
12365
|
+
return null;
|
|
12366
|
+
}
|
|
12367
|
+
}
|
|
12368
|
+
var CodexSessionWatcher = class {
|
|
12369
|
+
options;
|
|
12370
|
+
mtimes = /* @__PURE__ */ new Map();
|
|
12371
|
+
known = /* @__PURE__ */ new Map();
|
|
12372
|
+
timer = null;
|
|
12373
|
+
constructor(options) {
|
|
12374
|
+
this.options = { pollMs: 3e3, initialRecentMs: 5 * 6e4, ...options };
|
|
12375
|
+
}
|
|
12376
|
+
start() {
|
|
12377
|
+
this.scan(true);
|
|
12378
|
+
this.timer = setInterval(() => this.scan(false), this.options.pollMs);
|
|
12379
|
+
this.timer.unref?.();
|
|
12380
|
+
return () => this.stop();
|
|
12381
|
+
}
|
|
12382
|
+
stop() {
|
|
12383
|
+
if (this.timer !== null) clearInterval(this.timer);
|
|
12384
|
+
this.timer = null;
|
|
12385
|
+
}
|
|
12386
|
+
scan(initial = false) {
|
|
12387
|
+
const root = this.options.sessionsRoot ?? join6(homedir3(), ".codex", "sessions");
|
|
12388
|
+
const now = this.options.now?.() ?? Date.now();
|
|
12389
|
+
let observed = 0;
|
|
12390
|
+
for (const path of jsonlFiles(root)) {
|
|
12391
|
+
let modified;
|
|
12392
|
+
try {
|
|
12393
|
+
modified = statSync2(path).mtimeMs;
|
|
12394
|
+
} catch {
|
|
12395
|
+
continue;
|
|
12396
|
+
}
|
|
12397
|
+
const previous = this.mtimes.get(path);
|
|
12398
|
+
this.mtimes.set(path, modified);
|
|
12399
|
+
const changed = previous !== void 0 && modified > previous;
|
|
12400
|
+
const recentlyActive = initial && now - modified <= this.options.initialRecentMs;
|
|
12401
|
+
if (!changed && !recentlyActive) continue;
|
|
12402
|
+
const previousSession = this.known.get(path);
|
|
12403
|
+
const inspected = inspectCodexTranscript(
|
|
12404
|
+
path,
|
|
12405
|
+
this.options.roots,
|
|
12406
|
+
previousSession === void 0 ? MAX_INITIAL_TAIL : MAX_TAIL
|
|
12407
|
+
);
|
|
12408
|
+
const session = inspected === null ? null : inspected.title === void 0 && previousSession?.title !== void 0 ? { ...inspected, title: previousSession.title } : inspected;
|
|
12409
|
+
if (session === null) continue;
|
|
12410
|
+
this.known.set(path, session);
|
|
12411
|
+
this.options.onSession(session);
|
|
12412
|
+
observed += 1;
|
|
12413
|
+
}
|
|
12414
|
+
return observed;
|
|
12415
|
+
}
|
|
12416
|
+
};
|
|
12417
|
+
function jsonStringAt(source, quoteAt) {
|
|
12418
|
+
if (source[quoteAt] !== '"') return null;
|
|
12419
|
+
let escaped = false;
|
|
12420
|
+
for (let index = quoteAt + 1; index < source.length; index += 1) {
|
|
12421
|
+
const char = source[index];
|
|
12422
|
+
if (escaped) {
|
|
12423
|
+
escaped = false;
|
|
12424
|
+
continue;
|
|
12425
|
+
}
|
|
12426
|
+
if (char === "\\") {
|
|
12427
|
+
escaped = true;
|
|
12428
|
+
continue;
|
|
12429
|
+
}
|
|
12430
|
+
if (char === '"') {
|
|
12431
|
+
try {
|
|
12432
|
+
return JSON.parse(source.slice(quoteAt, index + 1));
|
|
12433
|
+
} catch {
|
|
12434
|
+
return null;
|
|
12435
|
+
}
|
|
12436
|
+
}
|
|
12437
|
+
}
|
|
12438
|
+
return null;
|
|
12439
|
+
}
|
|
12440
|
+
function latestUserText(source) {
|
|
12441
|
+
let before = source.length;
|
|
12442
|
+
while (before > 0) {
|
|
12443
|
+
const roleAt = source.lastIndexOf('"role":"user"', before);
|
|
12444
|
+
if (roleAt < 0) return null;
|
|
12445
|
+
const textKey = source.indexOf('"text":', roleAt);
|
|
12446
|
+
const nextRole = source.indexOf('"role":', roleAt + 1);
|
|
12447
|
+
if (textKey >= 0 && (nextRole < 0 || textKey < nextRole)) {
|
|
12448
|
+
const quoteAt = source.indexOf('"', textKey + 7);
|
|
12449
|
+
const text = quoteAt < 0 ? null : jsonStringAt(source, quoteAt);
|
|
12450
|
+
if (text !== null && titleFrom(text) !== null) return text;
|
|
12451
|
+
}
|
|
12452
|
+
before = roleAt;
|
|
12453
|
+
}
|
|
12454
|
+
return null;
|
|
12455
|
+
}
|
|
12456
|
+
function jsonlFiles(root) {
|
|
12457
|
+
const out = [];
|
|
12458
|
+
const visit = (directory) => {
|
|
12459
|
+
let entries;
|
|
12460
|
+
try {
|
|
12461
|
+
entries = readdirSync3(directory, { withFileTypes: true });
|
|
12462
|
+
} catch {
|
|
12463
|
+
return;
|
|
12464
|
+
}
|
|
12465
|
+
for (const entry of entries) {
|
|
12466
|
+
const path = join6(directory, entry.name);
|
|
12467
|
+
if (entry.isDirectory()) visit(path);
|
|
12468
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push(path);
|
|
12469
|
+
}
|
|
12470
|
+
};
|
|
12471
|
+
visit(root);
|
|
12472
|
+
return out;
|
|
12473
|
+
}
|
|
12474
|
+
|
|
12173
12475
|
// ../daemon/src/daemon.ts
|
|
12174
12476
|
var DEFAULT_READ_ONLY_TOOLS = ["Read", "Glob", "Grep"];
|
|
12175
12477
|
function executableOnPath(command) {
|
|
12176
12478
|
for (const directory of (process.env.PATH ?? "").split(delimiter)) {
|
|
12177
12479
|
if (directory === "") continue;
|
|
12178
12480
|
try {
|
|
12179
|
-
accessSync(
|
|
12481
|
+
accessSync(join7(directory, command), constants.X_OK);
|
|
12180
12482
|
return true;
|
|
12181
12483
|
} catch {
|
|
12182
12484
|
}
|
|
@@ -12184,20 +12486,20 @@ function executableOnPath(command) {
|
|
|
12184
12486
|
return false;
|
|
12185
12487
|
}
|
|
12186
12488
|
async function startDaemon(options) {
|
|
12187
|
-
const dataDir2 = options.dataDir ??
|
|
12489
|
+
const dataDir2 = options.dataDir ?? join7(homedir4(), ".longleash");
|
|
12188
12490
|
mkdirSync3(dataDir2, { recursive: true });
|
|
12189
|
-
const roots2 = options.allowedRoots.map((root) =>
|
|
12491
|
+
const roots2 = options.allowedRoots.map((root) => resolve5(root));
|
|
12190
12492
|
for (const root of roots2) {
|
|
12191
12493
|
if (!existsSync6(root)) throw new Error(`Allowed root does not exist: ${root}`);
|
|
12192
12494
|
}
|
|
12193
|
-
const eventLog = new EventLog(
|
|
12194
|
-
const registry = new DeviceRegistry(
|
|
12195
|
-
const approvals = new ApprovalStore(
|
|
12495
|
+
const eventLog = new EventLog(join7(dataDir2, "events.db"));
|
|
12496
|
+
const registry = new DeviceRegistry(join7(dataDir2, "devices.db"));
|
|
12497
|
+
const approvals = new ApprovalStore(join7(dataDir2, "approvals.db"));
|
|
12196
12498
|
const workspace = new WorkspaceLeaseManager(approvals.rawDb);
|
|
12197
|
-
const worktrees = new WorktreeManager(
|
|
12499
|
+
const worktrees = new WorktreeManager(join7(dataDir2, "worktrees"));
|
|
12198
12500
|
const push = new PushNotifier({
|
|
12199
|
-
dbPath:
|
|
12200
|
-
keysPath:
|
|
12501
|
+
dbPath: join7(dataDir2, "push.db"),
|
|
12502
|
+
keysPath: join7(dataDir2, "vapid.json"),
|
|
12201
12503
|
// Who a push service may contact about this sender: the relay origin when
|
|
12202
12504
|
// one exists, else a placeholder mailto (LAN-only installs never push far).
|
|
12203
12505
|
subject: process.env.LONGLEASH_PUSH_SUBJECT ?? (options.relayUrl !== void 0 ? options.relayUrl.replace(/^ws(s?):\/\//, "http$1://").replace(/\/ws\/?$/, "") : "mailto:longleash@localhost.invalid"),
|
|
@@ -12283,8 +12585,8 @@ async function startDaemon(options) {
|
|
|
12283
12585
|
server.attachSessions(sessions);
|
|
12284
12586
|
server.attachFolders(new FolderIndex(roots2));
|
|
12285
12587
|
server.attachPush(push);
|
|
12286
|
-
const externalApprovals = new ApprovalStore(
|
|
12287
|
-
const sessionRegistry = new SessionRegistry(
|
|
12588
|
+
const externalApprovals = new ApprovalStore(join7(dataDir2, "approvals-external.db"));
|
|
12589
|
+
const sessionRegistry = new SessionRegistry(join7(dataDir2, "live-sessions.db"));
|
|
12288
12590
|
const external = new ExternalSessions({
|
|
12289
12591
|
eventLog,
|
|
12290
12592
|
approvals: externalApprovals,
|
|
@@ -12307,6 +12609,11 @@ async function startDaemon(options) {
|
|
|
12307
12609
|
agentSessionId: info.claudeSessionId
|
|
12308
12610
|
})
|
|
12309
12611
|
});
|
|
12612
|
+
const codexWatcher = new CodexSessionWatcher({
|
|
12613
|
+
roots: roots2,
|
|
12614
|
+
onSession: (session) => external.observeCodexSession(session)
|
|
12615
|
+
});
|
|
12616
|
+
codexWatcher.start();
|
|
12310
12617
|
external.startSweeping();
|
|
12311
12618
|
const delegationStore = new DelegationStore(approvals.rawDb);
|
|
12312
12619
|
workspace.reconcile({
|
|
@@ -12332,7 +12639,7 @@ async function startDaemon(options) {
|
|
|
12332
12639
|
server.attachExternal(external, hookSecret);
|
|
12333
12640
|
const { port } = await server.listen();
|
|
12334
12641
|
writeFileSync2(
|
|
12335
|
-
|
|
12642
|
+
join7(dataDir2, "hook-endpoint.json"),
|
|
12336
12643
|
JSON.stringify({ url: `http://${options.host}:${port}/hook`, secret: hookSecret }, null, 2) + "\n",
|
|
12337
12644
|
{ mode: 384 }
|
|
12338
12645
|
);
|
|
@@ -12355,6 +12662,7 @@ async function startDaemon(options) {
|
|
|
12355
12662
|
posture: readPermissionPosture(),
|
|
12356
12663
|
stop: async () => {
|
|
12357
12664
|
stopMaintenance();
|
|
12665
|
+
codexWatcher.stop();
|
|
12358
12666
|
bridge?.stop();
|
|
12359
12667
|
await sessions.shutdown();
|
|
12360
12668
|
awake.release();
|
|
@@ -12372,9 +12680,9 @@ async function startDaemon(options) {
|
|
|
12372
12680
|
|
|
12373
12681
|
// ../daemon/src/config.ts
|
|
12374
12682
|
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
12375
|
-
import { dirname as dirname3, join as
|
|
12683
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
12376
12684
|
function resolveRelayUrl(given, dataDir2) {
|
|
12377
|
-
const path =
|
|
12685
|
+
const path = join8(dataDir2, "config.json");
|
|
12378
12686
|
if (given !== void 0 && given.trim().toLowerCase() === "off") {
|
|
12379
12687
|
const config = load(path);
|
|
12380
12688
|
delete config.relayUrl;
|
|
@@ -12636,19 +12944,115 @@ function noAddressReason(interfaces = networkInterfaces()) {
|
|
|
12636
12944
|
return `No address a phone could dial (found: ${addresses.join(", ")}).`;
|
|
12637
12945
|
}
|
|
12638
12946
|
|
|
12947
|
+
// ../daemon/src/instance-lock.ts
|
|
12948
|
+
import { closeSync as closeSync3, lstatSync, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
12949
|
+
import { randomUUID } from "node:crypto";
|
|
12950
|
+
import { join as join9 } from "node:path";
|
|
12951
|
+
var LOCK_NAME = "daemon.lock";
|
|
12952
|
+
var LOCK_KIND = "longleash-daemon";
|
|
12953
|
+
function acquireDaemonInstance(dataDir2) {
|
|
12954
|
+
mkdirSync5(dataDir2, { recursive: true, mode: 448 });
|
|
12955
|
+
const path = join9(dataDir2, LOCK_NAME);
|
|
12956
|
+
const token = randomUUID();
|
|
12957
|
+
const create = () => {
|
|
12958
|
+
const fd = openSync3(path, "wx", 384);
|
|
12959
|
+
try {
|
|
12960
|
+
writeFileSync4(fd, `${JSON.stringify({ kind: LOCK_KIND, pid: process.pid, token, startedAt: Date.now() })}
|
|
12961
|
+
`);
|
|
12962
|
+
} finally {
|
|
12963
|
+
closeSync3(fd);
|
|
12964
|
+
}
|
|
12965
|
+
};
|
|
12966
|
+
try {
|
|
12967
|
+
create();
|
|
12968
|
+
} catch (error) {
|
|
12969
|
+
if (error.code !== "EEXIST") throw error;
|
|
12970
|
+
if (lstatSync(path).isSymbolicLink()) throw new Error(`Refusing symlinked daemon lock: ${path}`);
|
|
12971
|
+
let owner = {};
|
|
12972
|
+
try {
|
|
12973
|
+
owner = JSON.parse(readFileSync5(path, "utf8"));
|
|
12974
|
+
} catch {
|
|
12975
|
+
}
|
|
12976
|
+
if (owner.kind !== LOCK_KIND || !Number.isSafeInteger(owner.pid) || owner.pid <= 0) {
|
|
12977
|
+
throw new Error(`Unrecognized daemon lock requires manual inspection: ${path}`);
|
|
12978
|
+
}
|
|
12979
|
+
if (processAlive(owner.pid)) {
|
|
12980
|
+
throw new Error(`LongLeash already owns this data directory in process ${owner.pid}. Use \`longleash service status\` before starting another instance.`);
|
|
12981
|
+
}
|
|
12982
|
+
rmSync(path);
|
|
12983
|
+
create();
|
|
12984
|
+
}
|
|
12985
|
+
let held = true;
|
|
12986
|
+
return {
|
|
12987
|
+
path,
|
|
12988
|
+
release() {
|
|
12989
|
+
if (!held) return;
|
|
12990
|
+
held = false;
|
|
12991
|
+
try {
|
|
12992
|
+
const owner = JSON.parse(readFileSync5(path, "utf8"));
|
|
12993
|
+
if (owner.token === token) rmSync(path);
|
|
12994
|
+
} catch {
|
|
12995
|
+
}
|
|
12996
|
+
}
|
|
12997
|
+
};
|
|
12998
|
+
}
|
|
12999
|
+
function processAlive(pid) {
|
|
13000
|
+
try {
|
|
13001
|
+
process.kill(pid, 0);
|
|
13002
|
+
return true;
|
|
13003
|
+
} catch (error) {
|
|
13004
|
+
return error.code === "EPERM";
|
|
13005
|
+
}
|
|
13006
|
+
}
|
|
13007
|
+
|
|
13008
|
+
// ../daemon/src/service-log.ts
|
|
13009
|
+
function persistentServiceLogLine(line) {
|
|
13010
|
+
if (/codex\s+(?:<-|->)/i.test(line)) return null;
|
|
13011
|
+
const stamp = line.match(/^\[[^\]]+\]\s*/)?.[0] ?? "";
|
|
13012
|
+
const message = line.slice(stamp.length);
|
|
13013
|
+
const started2 = message.match(/^(claude|codex) session ([A-Za-z0-9_-]{1,32}) started in .+ \((terminal|vscode|phone)\)$/i);
|
|
13014
|
+
if (started2) return `${stamp}${started2[1].toLowerCase()} session ${started2[2]} started (${started2[3].toLowerCase()})`;
|
|
13015
|
+
if (/^\? .+ in [A-Za-z0-9_-]+ .* -> /i.test(message)) return `${stamp}tool decision recorded`;
|
|
13016
|
+
if (/^(decision|gate|resume|takeOver|stop|session)\s/i.test(message)) return `${stamp}session lifecycle state changed`;
|
|
13017
|
+
if (/^(delegation|start) refused:/i.test(message)) return `${stamp}session request refused; details are available on the paired device`;
|
|
13018
|
+
if (/^(relay|relay link|relay room|dropped a relay frame)/i.test(message)) return `${stamp}relay connection state changed`;
|
|
13019
|
+
if (/^(device|dropping unresponsive connection)/i.test(message)) return `${stamp}device connection state changed`;
|
|
13020
|
+
if (/^push:/i.test(message) || /^push (subscription|test)/i.test(message)) return `${stamp}push delivery state changed`;
|
|
13021
|
+
if (/^(keeping this Mac awake|all agents idle)/i.test(message)) return `${stamp}${message}`;
|
|
13022
|
+
if (/^rebound to /i.test(message)) return `${stamp}local listener rebound`;
|
|
13023
|
+
return `${stamp}service activity recorded; details omitted`;
|
|
13024
|
+
}
|
|
13025
|
+
|
|
12639
13026
|
// ../daemon/bin/longleashd.ts
|
|
12640
13027
|
var here = dirname4(fileURLToPath(import.meta.url));
|
|
12641
|
-
var APP_DIR =
|
|
13028
|
+
var APP_DIR = resolve6(here, "../../app/dist");
|
|
13029
|
+
var serviceMode = process.env.LONGLEASH_SERVICE === "1";
|
|
13030
|
+
if (serviceMode) {
|
|
13031
|
+
const fatal = () => {
|
|
13032
|
+
console.error("LongLeash service stopped unexpectedly; details omitted. Run `longleash doctor`.");
|
|
13033
|
+
process.exit(1);
|
|
13034
|
+
};
|
|
13035
|
+
process.on("uncaughtException", fatal);
|
|
13036
|
+
process.on("unhandledRejection", fatal);
|
|
13037
|
+
}
|
|
13038
|
+
function daemonLog(line) {
|
|
13039
|
+
if (!serviceMode) {
|
|
13040
|
+
console.log(line);
|
|
13041
|
+
return;
|
|
13042
|
+
}
|
|
13043
|
+
const safe = persistentServiceLogLine(line);
|
|
13044
|
+
if (safe !== null) console.log(safe);
|
|
13045
|
+
}
|
|
12642
13046
|
var roots = process.argv.slice(2).filter((arg) => !arg.startsWith("-"));
|
|
12643
13047
|
if (roots.length === 0) {
|
|
12644
13048
|
console.error("\nUsage: longleashd <project-directory> [more directories\u2026]");
|
|
12645
13049
|
console.error("Agents may only ever work inside the directories you list.\n");
|
|
12646
13050
|
process.exit(1);
|
|
12647
13051
|
}
|
|
12648
|
-
var dataDir = process.env.LONGLEASH_DATA ??
|
|
13052
|
+
var dataDir = process.env.LONGLEASH_DATA ?? join10(homedir5(), ".longleash");
|
|
12649
13053
|
var relay = resolveRelayUrl(process.env.LONGLEASH_RELAY_URL, dataDir);
|
|
12650
13054
|
if (relay?.source === "remembered") {
|
|
12651
|
-
console.log(`Relay: ${relay.url} (remembered \u2014 LONGLEASH_RELAY_URL=off forgets it)`);
|
|
13055
|
+
console.log(serviceMode ? "Relay configuration loaded." : `Relay: ${relay.url} (remembered \u2014 LONGLEASH_RELAY_URL=off forgets it)`);
|
|
12652
13056
|
}
|
|
12653
13057
|
var candidates = findCandidates();
|
|
12654
13058
|
var best = candidates[0];
|
|
@@ -12698,15 +13102,17 @@ async function boot(host, port) {
|
|
|
12698
13102
|
allowedRoots: roots,
|
|
12699
13103
|
host,
|
|
12700
13104
|
port,
|
|
12701
|
-
...existsSync7(
|
|
13105
|
+
...existsSync7(join10(APP_DIR, "index.html")) ? { staticRoot: APP_DIR } : {},
|
|
12702
13106
|
denyOutsideRoot: process.env.LONGLEASH_STRICT !== "0",
|
|
12703
13107
|
// LONGLEASH_ASK_EVERYTHING=1 pre-approves nothing, so even reading a file comes to your phone.
|
|
12704
13108
|
...process.env.LONGLEASH_ASK_EVERYTHING === "1" ? { allowedTools: [] } : {},
|
|
12705
13109
|
dataDir,
|
|
12706
|
-
log:
|
|
13110
|
+
log: daemonLog,
|
|
12707
13111
|
...relay === null ? {} : { relayUrl: relay.url }
|
|
12708
13112
|
});
|
|
12709
13113
|
}
|
|
13114
|
+
var instanceLock = acquireDaemonInstance(dataDir);
|
|
13115
|
+
process.once("exit", () => instanceLock.release());
|
|
12710
13116
|
var started = await (async () => {
|
|
12711
13117
|
const firstPort = holder === "other" ? 0 : wantedPort;
|
|
12712
13118
|
try {
|
|
@@ -12738,7 +13144,7 @@ function relayAppOrigin(wsUrl) {
|
|
|
12738
13144
|
function freshPairingUrl() {
|
|
12739
13145
|
const challenge = daemon.registry.createPairingChallenge();
|
|
12740
13146
|
if (relay !== null) {
|
|
12741
|
-
hostPairing({ registry: daemon.registry, relayUrl: relay.url, challenge, log: (line) =>
|
|
13147
|
+
hostPairing({ registry: daemon.registry, relayUrl: relay.url, challenge, log: (line) => daemonLog(`[pair] ${line}`) });
|
|
12742
13148
|
return pairingUrl(relayAppOrigin(relay.url), challenge.challengeId, challenge.secret);
|
|
12743
13149
|
}
|
|
12744
13150
|
return pairingUrl(
|
|
@@ -12747,15 +13153,16 @@ function freshPairingUrl() {
|
|
|
12747
13153
|
challenge.secret
|
|
12748
13154
|
);
|
|
12749
13155
|
}
|
|
12750
|
-
|
|
13156
|
+
daemon.server.attachLocalPairing(freshPairingUrl);
|
|
13157
|
+
var url = serviceMode ? null : freshPairingUrl();
|
|
12751
13158
|
console.log("\n=== LongLeash ===\n");
|
|
12752
13159
|
var warn = vpnWarning();
|
|
12753
13160
|
if (warn) console.log(`!!! ${warn}
|
|
12754
13161
|
`);
|
|
12755
|
-
if (!existsSync7(
|
|
13162
|
+
if (!existsSync7(join10(APP_DIR, "index.html"))) {
|
|
12756
13163
|
console.log("The web app is not built yet \u2014 run `pnpm --filter @longleash/app build` first.\n");
|
|
12757
13164
|
}
|
|
12758
|
-
if (daemon.posture.gateWeakened) {
|
|
13165
|
+
if (daemon.posture.gateWeakened && !serviceMode) {
|
|
12759
13166
|
console.log(`Note: your Claude Code settings pre-approve ${daemon.posture.allowRuleCount} pattern(s),`);
|
|
12760
13167
|
console.log("e.g. " + daemon.posture.examples.join(", "));
|
|
12761
13168
|
console.log("Those commands run WITHOUT asking your phone. They still appear in the activity");
|
|
@@ -12776,23 +13183,29 @@ if (preApproved.length > 0) {
|
|
|
12776
13183
|
} else {
|
|
12777
13184
|
console.log("");
|
|
12778
13185
|
}
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
console.log(
|
|
12782
|
-
|
|
13186
|
+
if (serviceMode) {
|
|
13187
|
+
console.log(`Background service ready with ${roots.length} allowed project root(s).`);
|
|
13188
|
+
console.log(`Connectivity: ${relay !== null ? "encrypted relay enabled" : "LAN only"}.`);
|
|
13189
|
+
console.log("Pairing secrets are never written to service logs. Run `longleash pair` in a terminal.\n");
|
|
13190
|
+
} else {
|
|
13191
|
+
console.log("Agents may work only in:");
|
|
13192
|
+
for (const root of roots) console.log(` ${resolve6(root)}`);
|
|
13193
|
+
console.log(
|
|
13194
|
+
servedOnLan && best ? `
|
|
12783
13195
|
Listening on ${servedHost}:${daemon.port} (${best.iface}, ${best.label})` : `
|
|
12784
13196
|
Listening on ${servedHost}:${daemon.port} (no local network \u2014 relay only)`
|
|
12785
|
-
);
|
|
12786
|
-
console.log(
|
|
12787
|
-
|
|
12788
|
-
);
|
|
12789
|
-
console.log("\nScan this with your phone, then add it to your home screen:\n");
|
|
12790
|
-
console.log(terminalQr(url));
|
|
12791
|
-
console.log(`
|
|
13197
|
+
);
|
|
13198
|
+
console.log(
|
|
13199
|
+
relay !== null ? `Relay: ${relay.url} \u2014 your phone can reach this laptop from anywhere.` : "Relay: not configured (LAN only). Set LONGLEASH_RELAY_URL once to enable remote access \u2014 it is remembered."
|
|
13200
|
+
);
|
|
13201
|
+
console.log("\nScan this with your phone, then add it to your home screen:\n");
|
|
13202
|
+
console.log(terminalQr(url));
|
|
13203
|
+
console.log(`
|
|
12792
13204
|
${url}
|
|
12793
13205
|
`);
|
|
12794
|
-
if (relay !== null && servedOnLan) {
|
|
12795
|
-
|
|
13206
|
+
if (relay !== null && servedOnLan) {
|
|
13207
|
+
console.log(`(LAN fallback for pairing at home: http://${servedHost}:${daemon.port}/#c=\u2026&s=\u2026 \u2014 same code)`);
|
|
13208
|
+
}
|
|
12796
13209
|
}
|
|
12797
13210
|
var NETWORK_WATCH_MS = 5e3;
|
|
12798
13211
|
var networkWatch = setInterval(() => {
|
|
@@ -12803,23 +13216,23 @@ var networkWatch = setInterval(() => {
|
|
|
12803
13216
|
servingHost = nextHost;
|
|
12804
13217
|
void daemon.server.rebind(nextHost).then((port) => {
|
|
12805
13218
|
servingPort = port;
|
|
12806
|
-
console.log(`
|
|
13219
|
+
console.log(serviceMode ? "\nNetwork changed; the local listener recovered." : `
|
|
12807
13220
|
Network changed: ${was} -> ${nextHost}. Now serving ${nextHost}:${port}.`);
|
|
12808
13221
|
console.log(
|
|
12809
13222
|
nextBest ? "Press n + Enter for a pairing QR on this network." : "No local network now \u2014 the relay carries everything."
|
|
12810
13223
|
);
|
|
12811
13224
|
}).catch((err) => {
|
|
12812
|
-
console.log(`
|
|
13225
|
+
console.log(serviceMode ? "\nNetwork changed but local listener recovery failed; details omitted." : `
|
|
12813
13226
|
Network changed but rebinding failed (${String(err)}).`);
|
|
12814
13227
|
console.log(relay !== null ? "The relay is unaffected." : "Restart the daemon to recover.");
|
|
12815
13228
|
});
|
|
12816
13229
|
}, NETWORK_WATCH_MS);
|
|
12817
13230
|
networkWatch.unref?.();
|
|
12818
|
-
console.log("Press n + Enter for a fresh pairing QR, r + Enter to revoke every device, q + Enter to quit.\n");
|
|
13231
|
+
if (!serviceMode) console.log("Press n + Enter for a fresh pairing QR, r + Enter to revoke every device, q + Enter to quit.\n");
|
|
12819
13232
|
process.stdin.setEncoding("utf8");
|
|
12820
13233
|
process.stdin.on("data", (chunk) => {
|
|
12821
13234
|
const key = chunk.trim().toLowerCase();
|
|
12822
|
-
if (key === "n") {
|
|
13235
|
+
if (key === "n" && !serviceMode) {
|
|
12823
13236
|
const next = freshPairingUrl();
|
|
12824
13237
|
console.log("\nScan this with your phone:\n");
|
|
12825
13238
|
console.log(terminalQr(next));
|
|
@@ -12834,12 +13247,18 @@ process.stdin.on("data", (chunk) => {
|
|
|
12834
13247
|
}
|
|
12835
13248
|
if (key === "q") {
|
|
12836
13249
|
clearInterval(networkWatch);
|
|
12837
|
-
void daemon.stop().then(() =>
|
|
13250
|
+
void daemon.stop().then(() => {
|
|
13251
|
+
instanceLock.release();
|
|
13252
|
+
process.exit(0);
|
|
13253
|
+
});
|
|
12838
13254
|
}
|
|
12839
13255
|
});
|
|
12840
13256
|
var shutdown = () => {
|
|
12841
13257
|
clearInterval(networkWatch);
|
|
12842
|
-
void daemon.stop().then(() =>
|
|
13258
|
+
void daemon.stop().then(() => {
|
|
13259
|
+
instanceLock.release();
|
|
13260
|
+
process.exit(0);
|
|
13261
|
+
});
|
|
12843
13262
|
};
|
|
12844
13263
|
process.on("SIGINT", shutdown);
|
|
12845
13264
|
process.on("SIGTERM", shutdown);
|