@directed/cli 0.1.3 → 0.1.4
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 +12 -4
- package/dist/cli.js +477 -490
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { realpathSync } from "fs";
|
|
4
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
5
5
|
import { basename as basename4 } from "path";
|
|
6
6
|
import { pathToFileURL } from "url";
|
|
7
7
|
import { randomBytes as randomBytes3 } from "crypto";
|
|
8
|
-
import { execFileSync, spawn as
|
|
9
|
-
import { createRequire } from "module";
|
|
8
|
+
import { execFileSync, spawn as spawn2 } from "child_process";
|
|
9
|
+
import { createRequire as createRequire2 } from "module";
|
|
10
10
|
|
|
11
11
|
// src/agent.ts
|
|
12
12
|
import { basename as basename3 } from "path";
|
|
@@ -666,12 +666,15 @@ function commaList(value) {
|
|
|
666
666
|
}
|
|
667
667
|
|
|
668
668
|
// src/hub.ts
|
|
669
|
+
import { createRequire } from "module";
|
|
669
670
|
import { gzip } from "zlib";
|
|
670
671
|
import { promisify } from "util";
|
|
671
672
|
var gzipAsync = promisify(gzip);
|
|
672
673
|
if (false)
|
|
673
674
|
throw new Error("$HUB_URL is required, e.g. HUB_URL=https://hub.directed.ai");
|
|
674
675
|
var HUB_URL = "https://hub.directed.ai";
|
|
676
|
+
var require2 = createRequire(import.meta.url);
|
|
677
|
+
var CLI_VERSION = require2("../package.json").version;
|
|
675
678
|
var TIMEOUT_MS = 15e3;
|
|
676
679
|
var UPLOAD_TIMEOUT_MS = 6e4;
|
|
677
680
|
var HubError = class extends Error {
|
|
@@ -688,6 +691,12 @@ var HubError = class extends Error {
|
|
|
688
691
|
return this.status >= 400 && this.status < 500 && this.status !== 408 && this.status !== 429;
|
|
689
692
|
}
|
|
690
693
|
};
|
|
694
|
+
var AuthenticationRequiredError = class extends HubError {
|
|
695
|
+
constructor(detail) {
|
|
696
|
+
super(detail, 401);
|
|
697
|
+
this.name = "AuthenticationRequiredError";
|
|
698
|
+
}
|
|
699
|
+
};
|
|
691
700
|
var TerminalHeldError = class extends HubError {
|
|
692
701
|
constructor(chatUrl) {
|
|
693
702
|
super("this conversation is already connected", 409);
|
|
@@ -696,6 +705,14 @@ var TerminalHeldError = class extends HubError {
|
|
|
696
705
|
}
|
|
697
706
|
chatUrl;
|
|
698
707
|
};
|
|
708
|
+
var UpgradeRequiredError = class extends HubError {
|
|
709
|
+
constructor(detail, minimumVersion) {
|
|
710
|
+
super(detail, 426);
|
|
711
|
+
this.minimumVersion = minimumVersion;
|
|
712
|
+
this.name = "UpgradeRequiredError";
|
|
713
|
+
}
|
|
714
|
+
minimumVersion;
|
|
715
|
+
};
|
|
699
716
|
var Hub = class {
|
|
700
717
|
constructor(url = HUB_URL, fetchImpl = fetch) {
|
|
701
718
|
this.url = url;
|
|
@@ -770,20 +787,25 @@ var Hub = class {
|
|
|
770
787
|
async terminalCreate(input2) {
|
|
771
788
|
const response = await this.request("/api/terminal-sessions/", {
|
|
772
789
|
method: "POST",
|
|
773
|
-
expect: [409],
|
|
790
|
+
expect: [409, 426],
|
|
774
791
|
json: {
|
|
775
792
|
source: input2.source,
|
|
776
793
|
title: input2.title ?? "",
|
|
777
794
|
resume_key: input2.resumeKey ?? "",
|
|
778
795
|
destination_chat_public_id: input2.chatPublicId ?? "",
|
|
779
796
|
member_identifiers: input2.memberList ?? [],
|
|
780
|
-
git_start: gitSnapshotWire(input2.gitStart)
|
|
797
|
+
git_start: gitSnapshotWire(input2.gitStart),
|
|
798
|
+
client_version: CLI_VERSION
|
|
781
799
|
}
|
|
782
800
|
});
|
|
783
801
|
if (response.status === 409) {
|
|
784
802
|
const held = await response.json();
|
|
785
803
|
throw new TerminalHeldError(held.chat_url);
|
|
786
804
|
}
|
|
805
|
+
if (response.status === 426) {
|
|
806
|
+
const stale = await response.json();
|
|
807
|
+
throw new UpgradeRequiredError(stale.detail, stale.minimum_version);
|
|
808
|
+
}
|
|
787
809
|
const body = await response.json();
|
|
788
810
|
return {
|
|
789
811
|
chatPublicId: body.chat_public_id,
|
|
@@ -800,7 +822,6 @@ var Hub = class {
|
|
|
800
822
|
method: "POST",
|
|
801
823
|
json: {
|
|
802
824
|
transcript_lost: state.transcriptLost,
|
|
803
|
-
recording_lost: state.recordingLost,
|
|
804
825
|
detached: state.isDetached,
|
|
805
826
|
git_end: gitSnapshotWire(state.gitEnd)
|
|
806
827
|
}
|
|
@@ -816,9 +837,9 @@ var Hub = class {
|
|
|
816
837
|
return { isEnded: response.status === 409 };
|
|
817
838
|
}
|
|
818
839
|
// A contiguous slice of one agent's transcript file, sent as the file's own
|
|
819
|
-
// lines. Compressed: transcripts are JSON text and shrink about 5x
|
|
820
|
-
//
|
|
821
|
-
// the
|
|
840
|
+
// lines. Compressed: transcripts are JSON text and shrink about 5x. A
|
|
841
|
+
// subagent's slice names the file that launched it, so lineage arrives with
|
|
842
|
+
// the data it describes.
|
|
822
843
|
async transcriptAppend(sessionId, batch) {
|
|
823
844
|
await this.request(`/api/terminal-sessions/${sessionId}/transcript/`, {
|
|
824
845
|
method: "POST",
|
|
@@ -836,19 +857,6 @@ var Hub = class {
|
|
|
836
857
|
timeoutMs: UPLOAD_TIMEOUT_MS
|
|
837
858
|
});
|
|
838
859
|
}
|
|
839
|
-
async recordingAppend(sessionId, chunk) {
|
|
840
|
-
await this.request(`/api/terminal-sessions/${sessionId}/recording/`, {
|
|
841
|
-
method: "POST",
|
|
842
|
-
json: {
|
|
843
|
-
seq: chunk.seq,
|
|
844
|
-
offset: chunk.offset,
|
|
845
|
-
events: chunk.events,
|
|
846
|
-
width: chunk.width,
|
|
847
|
-
height: chunk.height,
|
|
848
|
-
started_epoch: chunk.startedEpoch
|
|
849
|
-
}
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
860
|
// What the pane did with a steer, so the chat can show delivery rather than
|
|
853
861
|
// leaving the sender to guess.
|
|
854
862
|
async steerAck(sessionId, eventId, outcome) {
|
|
@@ -868,15 +876,31 @@ var Hub = class {
|
|
|
868
876
|
// is unauthenticated; a string is an explicit token for the login path; absent
|
|
869
877
|
// means the attached session's token, refreshed once if the hub says it is bad.
|
|
870
878
|
async request(path, spec) {
|
|
871
|
-
|
|
879
|
+
let token;
|
|
880
|
+
try {
|
|
881
|
+
token = await this.tokenFor(spec);
|
|
882
|
+
} catch (error) {
|
|
883
|
+
if (spec.token === void 0 && this.auth !== null) {
|
|
884
|
+
authenticationRequired(error);
|
|
885
|
+
}
|
|
886
|
+
throw error;
|
|
887
|
+
}
|
|
888
|
+
const response = await this.send(path, spec, token);
|
|
872
889
|
if (response.status !== 401 || spec.token !== void 0 || this.auth === null) {
|
|
873
890
|
return this.checked(path, response, spec.expect);
|
|
874
891
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
892
|
+
let refreshedToken;
|
|
893
|
+
try {
|
|
894
|
+
refreshedToken = await this.auth.refresh();
|
|
895
|
+
} catch (error) {
|
|
896
|
+
authenticationRequired(error);
|
|
897
|
+
}
|
|
898
|
+
const retried = await this.send(path, spec, refreshedToken);
|
|
899
|
+
try {
|
|
900
|
+
return await this.checked(path, retried, spec.expect);
|
|
901
|
+
} catch (error) {
|
|
902
|
+
authenticationRequired(error);
|
|
903
|
+
}
|
|
880
904
|
}
|
|
881
905
|
async tokenFor(spec) {
|
|
882
906
|
if (spec.token !== void 0) return spec.token;
|
|
@@ -919,6 +943,12 @@ var Hub = class {
|
|
|
919
943
|
);
|
|
920
944
|
}
|
|
921
945
|
};
|
|
946
|
+
function authenticationRequired(error) {
|
|
947
|
+
if (error instanceof HubError && error.status === 401) {
|
|
948
|
+
throw new AuthenticationRequiredError(error.message);
|
|
949
|
+
}
|
|
950
|
+
throw error;
|
|
951
|
+
}
|
|
922
952
|
function gitSnapshotWire(snapshot) {
|
|
923
953
|
if (!snapshot) return null;
|
|
924
954
|
return {
|
|
@@ -942,6 +972,57 @@ import { existsSync, mkdirSync, readFileSync, rmSync as rmSync2, writeFileSync }
|
|
|
942
972
|
import http from "http";
|
|
943
973
|
import { homedir as homedir3 } from "os";
|
|
944
974
|
import { dirname as dirname2, join as join4 } from "path";
|
|
975
|
+
|
|
976
|
+
// src/prompt.ts
|
|
977
|
+
import { stdin, stderr } from "process";
|
|
978
|
+
var PROMPT_TERMINAL = { input: stdin, output: stderr };
|
|
979
|
+
var PromptCancelledError = class extends Error {
|
|
980
|
+
constructor() {
|
|
981
|
+
super("cancelled");
|
|
982
|
+
this.name = "PromptCancelledError";
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
function promptIsInteractive(terminal = PROMPT_TERMINAL) {
|
|
986
|
+
return terminal.input.isTTY === true && terminal.output.isTTY === true && typeof terminal.input.setRawMode === "function";
|
|
987
|
+
}
|
|
988
|
+
function promptAction(action, hasColors) {
|
|
989
|
+
const line = `> ${action}`;
|
|
990
|
+
return hasColors ? `\x1B[1;36m${line}\x1B[0m` : line;
|
|
991
|
+
}
|
|
992
|
+
function promptConfirm(title, action, terminal = PROMPT_TERMINAL) {
|
|
993
|
+
if (!promptIsInteractive(terminal)) {
|
|
994
|
+
throw new Error("an interactive terminal is required to continue");
|
|
995
|
+
}
|
|
996
|
+
const input2 = terminal.input;
|
|
997
|
+
const wasRaw = input2.isRaw === true;
|
|
998
|
+
terminal.output.write(
|
|
999
|
+
`${title}
|
|
1000
|
+
|
|
1001
|
+
${promptAction(action, terminal.output.hasColors?.() === true)}
|
|
1002
|
+
`
|
|
1003
|
+
);
|
|
1004
|
+
return new Promise((resolve, reject) => {
|
|
1005
|
+
function finish(run) {
|
|
1006
|
+
input2.removeListener("data", keyRead);
|
|
1007
|
+
input2.setRawMode?.(wasRaw);
|
|
1008
|
+
input2.pause();
|
|
1009
|
+
run();
|
|
1010
|
+
}
|
|
1011
|
+
function keyRead(chunk) {
|
|
1012
|
+
const text = chunk.toString();
|
|
1013
|
+
if (text.includes("")) {
|
|
1014
|
+
finish(() => reject(new PromptCancelledError()));
|
|
1015
|
+
} else if (text.includes("\r") || text.includes("\n")) {
|
|
1016
|
+
finish(resolve);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
input2.setRawMode?.(true);
|
|
1020
|
+
input2.resume();
|
|
1021
|
+
input2.on("data", keyRead);
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// src/auth.ts
|
|
945
1026
|
var EXPIRY_SKEW_MS = 6e4;
|
|
946
1027
|
var LOGIN_TIMEOUT_MS = 3e5;
|
|
947
1028
|
function authPath(hubUrl) {
|
|
@@ -968,6 +1049,14 @@ function authClear(hubUrl) {
|
|
|
968
1049
|
const path = authPath(hubUrl);
|
|
969
1050
|
if (existsSync(path)) rmSync2(path);
|
|
970
1051
|
}
|
|
1052
|
+
async function authPrompt(hub, open3, terminal) {
|
|
1053
|
+
await promptConfirm(
|
|
1054
|
+
`[directed] Sign in to ${hub.url} to share this session.`,
|
|
1055
|
+
"Press Enter to sign in and continue. Ctrl-C to exit.",
|
|
1056
|
+
terminal
|
|
1057
|
+
);
|
|
1058
|
+
return authLogin(hub, open3);
|
|
1059
|
+
}
|
|
971
1060
|
var AuthSession = class {
|
|
972
1061
|
constructor(hub, state) {
|
|
973
1062
|
this.hub = hub;
|
|
@@ -1223,6 +1312,9 @@ async function chatInvite(linkCreate, emails, hostName, note) {
|
|
|
1223
1312
|
import { execFile } from "child_process";
|
|
1224
1313
|
import { promisify as promisify2 } from "util";
|
|
1225
1314
|
|
|
1315
|
+
// src/transcript.ts
|
|
1316
|
+
import { open as open2, stat as stat3 } from "fs/promises";
|
|
1317
|
+
|
|
1226
1318
|
// src/outbox.ts
|
|
1227
1319
|
var BACKOFF_MS = [500, 1500, 4e3, 1e4];
|
|
1228
1320
|
var BYTES_MAX_DEFAULT = 32 * 1024 * 1024;
|
|
@@ -1349,157 +1441,7 @@ function sleep(ms) {
|
|
|
1349
1441
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1350
1442
|
}
|
|
1351
1443
|
|
|
1352
|
-
// src/recording.ts
|
|
1353
|
-
var FLUSH_INTERVAL_MS = 250;
|
|
1354
|
-
var FLUSH_BYTES_MAX = 16 * 1024;
|
|
1355
|
-
var BACKLOG_BYTES_MAX = 4 * 1024 * 1024;
|
|
1356
|
-
var Recording = class {
|
|
1357
|
-
constructor(headerWidth, headerHeight) {
|
|
1358
|
-
this.headerWidth = headerWidth;
|
|
1359
|
-
this.headerHeight = headerHeight;
|
|
1360
|
-
this.width = headerWidth;
|
|
1361
|
-
this.height = headerHeight;
|
|
1362
|
-
}
|
|
1363
|
-
headerWidth;
|
|
1364
|
-
headerHeight;
|
|
1365
|
-
startMs = monotonicMs();
|
|
1366
|
-
startedEpoch = Math.floor(Date.now() / 1e3);
|
|
1367
|
-
buffer = [];
|
|
1368
|
-
bufferBytes = 0;
|
|
1369
|
-
droppedCount = 0;
|
|
1370
|
-
sink = null;
|
|
1371
|
-
timer = null;
|
|
1372
|
-
isStopped = false;
|
|
1373
|
-
// The pane's size now, for deciding whether a poll saw a change. The header
|
|
1374
|
-
// size below is the asciicast's opening geometry and never moves -- every
|
|
1375
|
-
// later size is an "r" event, and playback rebuilds from the two together.
|
|
1376
|
-
width;
|
|
1377
|
-
height;
|
|
1378
|
-
get header() {
|
|
1379
|
-
return { width: this.headerWidth, height: this.headerHeight, startedEpoch: this.startedEpoch };
|
|
1380
|
-
}
|
|
1381
|
-
// Events the pane produced that no stream ever took.
|
|
1382
|
-
get droppedEventCount() {
|
|
1383
|
-
return this.droppedCount;
|
|
1384
|
-
}
|
|
1385
|
-
write(data) {
|
|
1386
|
-
this.append([this.elapsed(), "o", data]);
|
|
1387
|
-
}
|
|
1388
|
-
mark(label) {
|
|
1389
|
-
this.append([this.elapsed(), "m", label]);
|
|
1390
|
-
}
|
|
1391
|
-
// Recorded as an "r" event so playback re-creates the pane geometry at the
|
|
1392
|
-
// right point in time -- a replay against stale dims mis-wraps every line the
|
|
1393
|
-
// app draws after the resize.
|
|
1394
|
-
resize(cols, rows) {
|
|
1395
|
-
if (cols === this.width && rows === this.height) return;
|
|
1396
|
-
const event = [this.elapsed(), "r", `${cols}x${rows}`];
|
|
1397
|
-
this.width = cols;
|
|
1398
|
-
this.height = rows;
|
|
1399
|
-
this.append(event);
|
|
1400
|
-
}
|
|
1401
|
-
// Attach a stream. Whatever the pane recorded while waiting goes first, so a
|
|
1402
|
-
// capture that opened three seconds late still has the whole session.
|
|
1403
|
-
sinkAttach(sink) {
|
|
1404
|
-
this.sink = sink;
|
|
1405
|
-
this.flush();
|
|
1406
|
-
}
|
|
1407
|
-
sinkDetach() {
|
|
1408
|
-
this.flush();
|
|
1409
|
-
this.sink = null;
|
|
1410
|
-
}
|
|
1411
|
-
// The last flush. Anything the pane says afterwards is past the end of the
|
|
1412
|
-
// recording and is dropped rather than published into a session already closed.
|
|
1413
|
-
stop() {
|
|
1414
|
-
this.flush();
|
|
1415
|
-
this.isStopped = true;
|
|
1416
|
-
}
|
|
1417
|
-
elapsed() {
|
|
1418
|
-
return (monotonicMs() - this.startMs) / 1e3;
|
|
1419
|
-
}
|
|
1420
|
-
append(event) {
|
|
1421
|
-
if (this.isStopped) return;
|
|
1422
|
-
this.buffer.push(event);
|
|
1423
|
-
this.bufferBytes += event[2].length;
|
|
1424
|
-
if (this.sink === null) {
|
|
1425
|
-
while (this.bufferBytes > BACKLOG_BYTES_MAX && this.buffer.length > 0) {
|
|
1426
|
-
const dropped = this.buffer.shift();
|
|
1427
|
-
this.bufferBytes -= dropped[2].length;
|
|
1428
|
-
this.droppedCount += 1;
|
|
1429
|
-
}
|
|
1430
|
-
return;
|
|
1431
|
-
}
|
|
1432
|
-
if (this.bufferBytes >= FLUSH_BYTES_MAX) {
|
|
1433
|
-
this.flush();
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
if (!this.timer) this.timer = setTimeout(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
1437
|
-
}
|
|
1438
|
-
flush() {
|
|
1439
|
-
if (this.timer) {
|
|
1440
|
-
clearTimeout(this.timer);
|
|
1441
|
-
this.timer = null;
|
|
1442
|
-
}
|
|
1443
|
-
if (this.buffer.length === 0 || this.sink === null) return;
|
|
1444
|
-
const events = this.buffer;
|
|
1445
|
-
this.buffer = [];
|
|
1446
|
-
this.bufferBytes = 0;
|
|
1447
|
-
this.sink(events);
|
|
1448
|
-
}
|
|
1449
|
-
};
|
|
1450
|
-
var RecordingStream = class {
|
|
1451
|
-
constructor(session, recording) {
|
|
1452
|
-
this.session = session;
|
|
1453
|
-
this.recording = recording;
|
|
1454
|
-
recording.sinkAttach((events) => this.chunkAdd(events));
|
|
1455
|
-
}
|
|
1456
|
-
session;
|
|
1457
|
-
recording;
|
|
1458
|
-
seq = 0;
|
|
1459
|
-
outbox = new Outbox("recording", {
|
|
1460
|
-
send: (chunk) => this.chunkPost(chunk),
|
|
1461
|
-
bytesOf: (chunk) => chunk.events.reduce((sum, event) => sum + event[2].length, 0)
|
|
1462
|
-
});
|
|
1463
|
-
// Chunks this session produced that the hub never took, plus events the pane
|
|
1464
|
-
// recorded before any stream existed.
|
|
1465
|
-
get lastError() {
|
|
1466
|
-
return this.outbox.lastError;
|
|
1467
|
-
}
|
|
1468
|
-
get unsentCount() {
|
|
1469
|
-
return this.outbox.unsentCount + this.recording.droppedEventCount;
|
|
1470
|
-
}
|
|
1471
|
-
async stop(deadline) {
|
|
1472
|
-
this.recording.sinkDetach();
|
|
1473
|
-
await this.outbox.flush(deadline);
|
|
1474
|
-
}
|
|
1475
|
-
chunkAdd(events) {
|
|
1476
|
-
const seq = this.seq;
|
|
1477
|
-
this.seq += 1;
|
|
1478
|
-
this.outbox.add({ seq, offset: events[0][0], events });
|
|
1479
|
-
}
|
|
1480
|
-
// Throws on failure so the outbox retries: the token refresh and the POST are
|
|
1481
|
-
// both worth another attempt, and a dropped chunk is a permanent gap.
|
|
1482
|
-
//
|
|
1483
|
-
// The header rides on every chunk because the hub keeps whichever arrives
|
|
1484
|
-
// first, and a retry can make that any of them.
|
|
1485
|
-
chunkPost(chunk) {
|
|
1486
|
-
const header = this.recording.header;
|
|
1487
|
-
return this.session.recordingAppend({
|
|
1488
|
-
seq: chunk.seq,
|
|
1489
|
-
offset: chunk.offset,
|
|
1490
|
-
events: chunk.events,
|
|
1491
|
-
width: header.width,
|
|
1492
|
-
height: header.height,
|
|
1493
|
-
startedEpoch: header.startedEpoch
|
|
1494
|
-
});
|
|
1495
|
-
}
|
|
1496
|
-
};
|
|
1497
|
-
function monotonicMs() {
|
|
1498
|
-
return Number(process.hrtime.bigint() / 1000000n);
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
1444
|
// src/transcript.ts
|
|
1502
|
-
import { open as open2, stat as stat3 } from "fs/promises";
|
|
1503
1445
|
var POLL_MS = 400;
|
|
1504
1446
|
var LOCATE_BACKOFF_MS = [400, 800, 1600, 3200, 5e3];
|
|
1505
1447
|
var MISSING_DEADLINE_MS = 15e3;
|
|
@@ -1782,8 +1724,8 @@ var TranscriptReader = class {
|
|
|
1782
1724
|
}
|
|
1783
1725
|
}
|
|
1784
1726
|
};
|
|
1785
|
-
var
|
|
1786
|
-
var
|
|
1727
|
+
var FLUSH_INTERVAL_MS = 250;
|
|
1728
|
+
var FLUSH_BYTES_MAX = 512 * 1024;
|
|
1787
1729
|
var FLUSH_LINES_MAX = 100;
|
|
1788
1730
|
var TranscriptStream = class {
|
|
1789
1731
|
constructor(session, reader) {
|
|
@@ -1843,12 +1785,12 @@ var TranscriptStream = class {
|
|
|
1843
1785
|
}
|
|
1844
1786
|
buffer.lines.push(text);
|
|
1845
1787
|
buffer.bytes += Buffer.byteLength(text, "utf8") + 1;
|
|
1846
|
-
if (buffer.bytes >=
|
|
1788
|
+
if (buffer.bytes >= FLUSH_BYTES_MAX || buffer.lines.length >= FLUSH_LINES_MAX) {
|
|
1847
1789
|
this.flushOne(key);
|
|
1848
1790
|
return;
|
|
1849
1791
|
}
|
|
1850
1792
|
if (!this.timer) {
|
|
1851
|
-
this.timer = setTimeout(() => this.flush(),
|
|
1793
|
+
this.timer = setTimeout(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
1852
1794
|
this.timer.unref();
|
|
1853
1795
|
}
|
|
1854
1796
|
}
|
|
@@ -2129,11 +2071,10 @@ var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
|
2129
2071
|
var SHUTDOWN_MS = 8e3;
|
|
2130
2072
|
var GIT_TIMEOUT_MS = 5e3;
|
|
2131
2073
|
var Capture = class _Capture {
|
|
2132
|
-
constructor(note, session, reader,
|
|
2074
|
+
constructor(note, session, reader, transcriptStream, steering, chatUrl, reattached, skippedMemberList) {
|
|
2133
2075
|
this.note = note;
|
|
2134
2076
|
this.session = session;
|
|
2135
2077
|
this.reader = reader;
|
|
2136
|
-
this.recordingStream = recordingStream;
|
|
2137
2078
|
this.transcriptStream = transcriptStream;
|
|
2138
2079
|
this.steering = steering;
|
|
2139
2080
|
this.chatUrl = chatUrl;
|
|
@@ -2143,7 +2084,6 @@ var Capture = class _Capture {
|
|
|
2143
2084
|
note;
|
|
2144
2085
|
session;
|
|
2145
2086
|
reader;
|
|
2146
|
-
recordingStream;
|
|
2147
2087
|
transcriptStream;
|
|
2148
2088
|
steering;
|
|
2149
2089
|
chatUrl;
|
|
@@ -2153,28 +2093,19 @@ var Capture = class _Capture {
|
|
|
2153
2093
|
get chatPublicId() {
|
|
2154
2094
|
return this.session.chatPublicId;
|
|
2155
2095
|
}
|
|
2156
|
-
//
|
|
2157
|
-
//
|
|
2158
|
-
//
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
} catch (e) {
|
|
2164
|
-
if (e instanceof TerminalHeldError) throw e;
|
|
2165
|
-
note(
|
|
2166
|
-
`could not open the chat (${e.message}); this session stays local.`
|
|
2167
|
-
);
|
|
2168
|
-
return null;
|
|
2169
|
-
}
|
|
2170
|
-
const recordingStream = new RecordingStream(session, recording);
|
|
2096
|
+
// Binds an already-open chat to what the pane is now producing.
|
|
2097
|
+
//
|
|
2098
|
+
// Opening is separate and happens first, before tmux owns the screen: every
|
|
2099
|
+
// reason a chat cannot be opened -- signed out, too old for this hub, hub
|
|
2100
|
+
// unreachable -- is a question for the person, and by here there is nowhere
|
|
2101
|
+
// left to ask it.
|
|
2102
|
+
static attach(session, auth, reader, note) {
|
|
2171
2103
|
const transcriptStream = new TranscriptStream(session, reader);
|
|
2172
2104
|
const steering = new Steering(session, auth);
|
|
2173
2105
|
const capture = new _Capture(
|
|
2174
2106
|
note,
|
|
2175
2107
|
session,
|
|
2176
2108
|
reader,
|
|
2177
|
-
recordingStream,
|
|
2178
2109
|
transcriptStream,
|
|
2179
2110
|
steering,
|
|
2180
2111
|
session.chatUrl,
|
|
@@ -2191,9 +2122,9 @@ var Capture = class _Capture {
|
|
|
2191
2122
|
linkCreate() {
|
|
2192
2123
|
return this.session.linkCreate();
|
|
2193
2124
|
}
|
|
2194
|
-
//
|
|
2195
|
-
// the
|
|
2196
|
-
//
|
|
2125
|
+
// Stop the producers, drain the stream with the time that remains, then post
|
|
2126
|
+
// the end state. `isDetached` means the agent ran on unwatched, which no loss
|
|
2127
|
+
// count can show, so the session says so.
|
|
2197
2128
|
async stop(isDetached) {
|
|
2198
2129
|
if (this.stopWork === null) this.stopWork = this.localStop(isDetached);
|
|
2199
2130
|
await this.stopWork;
|
|
@@ -2202,14 +2133,10 @@ var Capture = class _Capture {
|
|
|
2202
2133
|
const gitEnd = await gitSnapshot(process.cwd());
|
|
2203
2134
|
const deadline = Date.now() + SHUTDOWN_MS;
|
|
2204
2135
|
await this.steering.stop();
|
|
2205
|
-
await
|
|
2206
|
-
this.recordingStream.stop(deadline),
|
|
2207
|
-
this.transcriptStream.stop(deadline)
|
|
2208
|
-
]);
|
|
2136
|
+
await this.transcriptStream.stop(deadline);
|
|
2209
2137
|
this.lossReport();
|
|
2210
2138
|
await this.session.close({
|
|
2211
2139
|
transcriptLost: this.transcriptStream.unsentCount,
|
|
2212
|
-
recordingLost: this.recordingStream.unsentCount,
|
|
2213
2140
|
isDetached,
|
|
2214
2141
|
gitEnd
|
|
2215
2142
|
});
|
|
@@ -2222,38 +2149,23 @@ var Capture = class _Capture {
|
|
|
2222
2149
|
}
|
|
2223
2150
|
async remoteStopWrite() {
|
|
2224
2151
|
await this.steering.stop();
|
|
2225
|
-
await
|
|
2226
|
-
this.recordingStream.stop(Date.now()),
|
|
2227
|
-
this.transcriptStream.stop(Date.now())
|
|
2228
|
-
]);
|
|
2152
|
+
await this.transcriptStream.stop(Date.now());
|
|
2229
2153
|
await this.session.close({
|
|
2230
2154
|
transcriptLost: this.transcriptStream.unsentCount,
|
|
2231
|
-
recordingLost: this.recordingStream.unsentCount,
|
|
2232
2155
|
isDetached: true,
|
|
2233
2156
|
gitEnd: null
|
|
2234
2157
|
});
|
|
2235
2158
|
this.note("this chat connection ended; the local agent is still running");
|
|
2236
2159
|
}
|
|
2237
|
-
// Said once, after the
|
|
2238
|
-
// terminal back. The
|
|
2160
|
+
// Said once, after the stream has drained and the pane has already given the
|
|
2161
|
+
// terminal back. The count is the session's integrity state; the last error
|
|
2239
2162
|
// is the only part a person can act on.
|
|
2240
2163
|
lossReport() {
|
|
2241
|
-
const
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
this.transcriptStream.unsentCount,
|
|
2245
|
-
this.transcriptStream.lastError
|
|
2246
|
-
],
|
|
2247
|
-
[
|
|
2248
|
-
"recording chunks",
|
|
2249
|
-
this.recordingStream.unsentCount,
|
|
2250
|
-
this.recordingStream.lastError
|
|
2251
|
-
]
|
|
2252
|
-
];
|
|
2253
|
-
for (const [unit, count, why] of lostList) {
|
|
2254
|
-
if (count === 0) continue;
|
|
2164
|
+
const lost = this.transcriptStream.unsentCount;
|
|
2165
|
+
if (lost > 0) {
|
|
2166
|
+
const why = this.transcriptStream.lastError;
|
|
2255
2167
|
this.note(
|
|
2256
|
-
`${
|
|
2168
|
+
`${lost} transcript lines never reached the hub${why ? ` (${why})` : ""}`
|
|
2257
2169
|
);
|
|
2258
2170
|
}
|
|
2259
2171
|
const unresolved = this.reader.unresolvedChildList;
|
|
@@ -2317,11 +2229,6 @@ var HubSession = class _HubSession {
|
|
|
2317
2229
|
session.heartbeatTimer.unref();
|
|
2318
2230
|
return session;
|
|
2319
2231
|
}
|
|
2320
|
-
async recordingAppend(chunk) {
|
|
2321
|
-
await this.liveRequest(
|
|
2322
|
-
() => this.hub.recordingAppend(this.publicId, chunk)
|
|
2323
|
-
);
|
|
2324
|
-
}
|
|
2325
2232
|
// A batch names its own file, and a subagent's names the parent that launched
|
|
2326
2233
|
// it, so lineage arrives with the data it describes and the outbox retries
|
|
2327
2234
|
// both together.
|
|
@@ -2421,11 +2328,6 @@ async function git(args, cwd) {
|
|
|
2421
2328
|
}
|
|
2422
2329
|
|
|
2423
2330
|
// src/pane.ts
|
|
2424
|
-
import { spawn } from "child_process";
|
|
2425
|
-
import { rmSync as rmSync3 } from "fs";
|
|
2426
|
-
import { join as join5 } from "path";
|
|
2427
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
2428
|
-
import { StringDecoder } from "string_decoder";
|
|
2429
2331
|
var BANNER = `
|
|
2430
2332
|
\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591
|
|
2431
2333
|
\u2591\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591\u2591\u2591\u2591
|
|
@@ -2454,9 +2356,8 @@ var BANNER = `
|
|
|
2454
2356
|
Directed helps you collaborate with AI.
|
|
2455
2357
|
`;
|
|
2456
2358
|
var BIND_DEADLINE_MS = 2e4;
|
|
2457
|
-
var READER_DRAIN_MS = 1e3;
|
|
2458
2359
|
var HOOK_SETUP_MS = 2e3;
|
|
2459
|
-
var TRANSCRIPT_MISSING_NOTE = "no transcript found
|
|
2360
|
+
var TRANSCRIPT_MISSING_NOTE = "no transcript found; nothing reaches the chat. The agent runs normally.";
|
|
2460
2361
|
function bannerWrap(command, cols, rows) {
|
|
2461
2362
|
const lines = BANNER.replace(/^\n+|\n+$/g, "").split("\n");
|
|
2462
2363
|
const width = Math.max(...lines.map((l) => l.length));
|
|
@@ -2473,26 +2374,20 @@ function bannerWrap(command, cols, rows) {
|
|
|
2473
2374
|
];
|
|
2474
2375
|
}
|
|
2475
2376
|
var PaneSession = class {
|
|
2476
|
-
constructor(options,
|
|
2377
|
+
constructor(options, tmux, agent, note) {
|
|
2477
2378
|
this.options = options;
|
|
2478
|
-
this.recording = recording;
|
|
2479
2379
|
this.tmux = tmux;
|
|
2480
2380
|
this.agent = agent;
|
|
2481
|
-
this.
|
|
2381
|
+
this.note = note;
|
|
2482
2382
|
}
|
|
2483
2383
|
options;
|
|
2484
|
-
recording;
|
|
2485
2384
|
tmux;
|
|
2486
2385
|
agent;
|
|
2487
|
-
|
|
2488
|
-
fifo = join5(tmpdir2(), `directed-${process.pid}.fifo`);
|
|
2489
|
-
decoder = new StringDecoder("utf8");
|
|
2490
|
-
tap = null;
|
|
2491
|
-
sizeTimer = null;
|
|
2386
|
+
note;
|
|
2492
2387
|
bindTimer = null;
|
|
2493
2388
|
hook = null;
|
|
2494
2389
|
reader = null;
|
|
2495
|
-
|
|
2390
|
+
isMissingNoted = false;
|
|
2496
2391
|
// The transcript this pane is reading, for capture to attach a stream to.
|
|
2497
2392
|
// Null when the command is not an agent directed can follow.
|
|
2498
2393
|
get transcript() {
|
|
@@ -2534,14 +2429,13 @@ var PaneSession = class {
|
|
|
2534
2429
|
if (running === null) {
|
|
2535
2430
|
await this.tmux.newSession(bannerWrap(command, cols, rows), cols, rows);
|
|
2536
2431
|
}
|
|
2537
|
-
if (this.isCapturing && running === null) await this.captureStart();
|
|
2538
2432
|
if (this.agent) {
|
|
2539
2433
|
const agent = this.agent;
|
|
2540
2434
|
this.reader = new TranscriptReader(
|
|
2541
2435
|
agent,
|
|
2542
2436
|
process.cwd(),
|
|
2543
2437
|
sinceMs,
|
|
2544
|
-
() => this.
|
|
2438
|
+
() => this.transcriptMissingNote()
|
|
2545
2439
|
);
|
|
2546
2440
|
this.reader.onRootOpen((key, path) => {
|
|
2547
2441
|
void this.tmux.conversationMark(agent.kind, key, path);
|
|
@@ -2554,32 +2448,6 @@ var PaneSession = class {
|
|
|
2554
2448
|
this.reader.start();
|
|
2555
2449
|
if (running === null) this.announceBind();
|
|
2556
2450
|
}
|
|
2557
|
-
this.sizeTimer = this.sizePoll();
|
|
2558
|
-
}
|
|
2559
|
-
// Install the terminal tap only after this wrapper owns hub capture. An
|
|
2560
|
-
// attach-only wrapper must not replace or compete with the pipe feeding the
|
|
2561
|
-
// wrapper that already owns the conversation.
|
|
2562
|
-
async captureStart() {
|
|
2563
|
-
if (this.isTapped) return;
|
|
2564
|
-
try {
|
|
2565
|
-
rmSync3(this.fifo, { force: true });
|
|
2566
|
-
await exited(spawn("mkfifo", [this.fifo]));
|
|
2567
|
-
this.tap = spawn("cat", [this.fifo]);
|
|
2568
|
-
this.tap.stdout?.on(
|
|
2569
|
-
"data",
|
|
2570
|
-
(chunk) => this.recording.write(this.decoder.write(chunk))
|
|
2571
|
-
);
|
|
2572
|
-
await this.tmux.pipePane(this.fifo);
|
|
2573
|
-
this.isTapped = true;
|
|
2574
|
-
} catch (e) {
|
|
2575
|
-
this.tap?.kill();
|
|
2576
|
-
this.tap = null;
|
|
2577
|
-
rmSync3(this.fifo, { force: true });
|
|
2578
|
-
process.stderr.write(
|
|
2579
|
-
`[directed] terminal recording unavailable (${String(e)}).
|
|
2580
|
-
`
|
|
2581
|
-
);
|
|
2582
|
-
}
|
|
2583
2451
|
}
|
|
2584
2452
|
// Hands the terminal to the pane and waits. Returns the child's exit code.
|
|
2585
2453
|
async attach() {
|
|
@@ -2592,29 +2460,18 @@ var PaneSession = class {
|
|
|
2592
2460
|
async isDetached() {
|
|
2593
2461
|
return this.tmux.hasSession();
|
|
2594
2462
|
}
|
|
2595
|
-
async steer(text
|
|
2463
|
+
async steer(text) {
|
|
2596
2464
|
await this.tmux.sendText(text, this.options.steerMode === "send");
|
|
2597
|
-
this.recording.mark(`${who}: ${text}`);
|
|
2598
2465
|
}
|
|
2599
2466
|
// Unwinds everything acquired in start(), in reverse. `isDetached` decides
|
|
2600
2467
|
// whether the agent keeps running: killing a session someone detached from
|
|
2601
2468
|
// destroys work they expected to come back to.
|
|
2602
2469
|
async stop(isDetached) {
|
|
2603
|
-
if (this.sizeTimer) clearInterval(this.sizeTimer);
|
|
2604
2470
|
if (this.bindTimer) clearTimeout(this.bindTimer);
|
|
2605
2471
|
await this.reader?.stop();
|
|
2606
2472
|
if (!isDetached) await this.tmux.kill();
|
|
2607
|
-
if (this.tap) await tapDrain(this.tap);
|
|
2608
|
-
const tail = this.decoder.end();
|
|
2609
|
-
if (tail) this.recording.write(tail);
|
|
2610
|
-
this.tap?.kill();
|
|
2611
|
-
rmSync3(this.fifo, { force: true });
|
|
2612
2473
|
this.hook?.close();
|
|
2613
|
-
this.
|
|
2614
|
-
if (this.reader && !this.reader.isReading) {
|
|
2615
|
-
process.stderr.write(`[directed] ${TRANSCRIPT_MISSING_NOTE}
|
|
2616
|
-
`);
|
|
2617
|
-
}
|
|
2474
|
+
if (this.reader && !this.reader.isReading) this.transcriptMissingNote();
|
|
2618
2475
|
if (isDetached) {
|
|
2619
2476
|
process.stdout.write(
|
|
2620
2477
|
`[directed] detached; your agent is still running. Reattach with: tmux attach -t ${this.tmux.session}
|
|
@@ -2622,6 +2479,13 @@ var PaneSession = class {
|
|
|
2622
2479
|
);
|
|
2623
2480
|
}
|
|
2624
2481
|
}
|
|
2482
|
+
// Once: the reader says it after its deadline, and stop() says it for a
|
|
2483
|
+
// session that ended before the deadline came.
|
|
2484
|
+
transcriptMissingNote() {
|
|
2485
|
+
if (this.isMissingNoted) return;
|
|
2486
|
+
this.isMissingNoted = true;
|
|
2487
|
+
this.note(TRANSCRIPT_MISSING_NOTE);
|
|
2488
|
+
}
|
|
2625
2489
|
announceBind() {
|
|
2626
2490
|
const hook = this.hook;
|
|
2627
2491
|
const reader = this.reader;
|
|
@@ -2635,30 +2499,7 @@ var PaneSession = class {
|
|
|
2635
2499
|
if (!isBound) reader.locateAllow();
|
|
2636
2500
|
}, BIND_DEADLINE_MS);
|
|
2637
2501
|
}
|
|
2638
|
-
sizePoll() {
|
|
2639
|
-
let last = "";
|
|
2640
|
-
return setInterval(async () => {
|
|
2641
|
-
const { cols, rows } = await this.tmux.paneSize().catch(() => ({ cols: 0, rows: 0 }));
|
|
2642
|
-
if (!cols) return;
|
|
2643
|
-
const key = `${cols}x${rows}`;
|
|
2644
|
-
if (key !== last) {
|
|
2645
|
-
last = key;
|
|
2646
|
-
this.recording.resize(cols, rows);
|
|
2647
|
-
}
|
|
2648
|
-
}, 1e3);
|
|
2649
|
-
}
|
|
2650
2502
|
};
|
|
2651
|
-
function tapDrain(tap) {
|
|
2652
|
-
return new Promise((resolve) => {
|
|
2653
|
-
const done = () => {
|
|
2654
|
-
clearTimeout(timer);
|
|
2655
|
-
resolve();
|
|
2656
|
-
};
|
|
2657
|
-
const timer = setTimeout(done, READER_DRAIN_MS);
|
|
2658
|
-
tap.stdout?.once("end", done);
|
|
2659
|
-
tap.once("exit", done);
|
|
2660
|
-
});
|
|
2661
|
-
}
|
|
2662
2503
|
async function withDeadline(work, ms) {
|
|
2663
2504
|
let timer;
|
|
2664
2505
|
try {
|
|
@@ -2675,18 +2516,9 @@ async function withDeadline(work, ms) {
|
|
|
2675
2516
|
clearTimeout(timer);
|
|
2676
2517
|
}
|
|
2677
2518
|
}
|
|
2678
|
-
function exited(child) {
|
|
2679
|
-
return new Promise((resolve, reject) => {
|
|
2680
|
-
child.on("error", reject);
|
|
2681
|
-
child.on(
|
|
2682
|
-
"exit",
|
|
2683
|
-
(c) => c === 0 ? resolve() : reject(new Error(`exit ${c}`))
|
|
2684
|
-
);
|
|
2685
|
-
});
|
|
2686
|
-
}
|
|
2687
2519
|
|
|
2688
2520
|
// src/tmux.ts
|
|
2689
|
-
import { spawn
|
|
2521
|
+
import { spawn } from "child_process";
|
|
2690
2522
|
var SESSION_OPTIONS = [
|
|
2691
2523
|
["status", "off"],
|
|
2692
2524
|
["mouse", "on"],
|
|
@@ -2717,7 +2549,7 @@ async function conversationSession(bin, source, key, runner) {
|
|
|
2717
2549
|
}
|
|
2718
2550
|
function defaultRunner(bin) {
|
|
2719
2551
|
return (args) => new Promise((resolve, reject) => {
|
|
2720
|
-
const p =
|
|
2552
|
+
const p = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2721
2553
|
let out = "", err = "";
|
|
2722
2554
|
p.stdout.on("data", (d) => out += d);
|
|
2723
2555
|
p.stderr.on("data", (d) => err += d);
|
|
@@ -2741,7 +2573,7 @@ var Tmux = class {
|
|
|
2741
2573
|
// Detached sessions default to 80x24 until a client attaches; sizing the
|
|
2742
2574
|
// pane up front means everything the child draws before attach (a resumed
|
|
2743
2575
|
// agent repaints its whole conversation immediately) wraps at the real
|
|
2744
|
-
// terminal width
|
|
2576
|
+
// terminal width.
|
|
2745
2577
|
async newSession(command, cols, rows) {
|
|
2746
2578
|
await this.runner([
|
|
2747
2579
|
"new-session",
|
|
@@ -2768,15 +2600,6 @@ var Tmux = class {
|
|
|
2768
2600
|
await this.runner(["set-option", "-t", this.session, TAG_KEY, key]).catch(() => "");
|
|
2769
2601
|
await this.runner(["set-option", "-t", this.session, TAG_PATH, path]).catch(() => "");
|
|
2770
2602
|
}
|
|
2771
|
-
async paneSize() {
|
|
2772
|
-
const out = (await this.runner(["display-message", "-p", "-t", this.session, "#{pane_width}x#{pane_height}"])).trim();
|
|
2773
|
-
const [cols, rows] = out.split("x").map(Number);
|
|
2774
|
-
return { cols, rows };
|
|
2775
|
-
}
|
|
2776
|
-
pipePane(fifo) {
|
|
2777
|
-
return this.runner(["pipe-pane", "-t", this.session, `cat >> ${fifo}`]).then(() => {
|
|
2778
|
-
});
|
|
2779
|
-
}
|
|
2780
2603
|
sendText(text, submit) {
|
|
2781
2604
|
const write = this.inputWrite.then(() => this.textWrite(text, submit));
|
|
2782
2605
|
this.inputWrite = write.catch(() => {
|
|
@@ -2811,7 +2634,7 @@ var Tmux = class {
|
|
|
2811
2634
|
return this.runner(["has-session", "-t", this.session]).then(() => true).catch(() => false);
|
|
2812
2635
|
}
|
|
2813
2636
|
attach() {
|
|
2814
|
-
return
|
|
2637
|
+
return spawn(this.bin, ["attach", "-t", this.session], { stdio: "inherit" });
|
|
2815
2638
|
}
|
|
2816
2639
|
kill() {
|
|
2817
2640
|
return this.runner(["kill-session", "-t", this.session]).then(() => {
|
|
@@ -2821,59 +2644,65 @@ var Tmux = class {
|
|
|
2821
2644
|
};
|
|
2822
2645
|
|
|
2823
2646
|
// src/update.ts
|
|
2824
|
-
import {
|
|
2647
|
+
import { mkdtempSync as mkdtempSync2, realpathSync, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2825
2648
|
import { spawnSync } from "child_process";
|
|
2826
|
-
import { homedir as homedir4, tmpdir as
|
|
2827
|
-
import {
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
function stateLoad() {
|
|
2649
|
+
import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
|
|
2650
|
+
import { join as join5 } from "path";
|
|
2651
|
+
function installRoot() {
|
|
2652
|
+
return process.env.DIRECTED_CLI_INSTALL_ROOT ?? join5(homedir4(), ".directed");
|
|
2653
|
+
}
|
|
2654
|
+
function installChannel(entry = process.argv[1] ?? "") {
|
|
2655
|
+
let resolved;
|
|
2834
2656
|
try {
|
|
2835
|
-
|
|
2657
|
+
resolved = realpathSync(entry);
|
|
2836
2658
|
} catch {
|
|
2837
|
-
return
|
|
2659
|
+
return "unknown";
|
|
2838
2660
|
}
|
|
2661
|
+
if (resolved.startsWith(`${realpathOr(installRoot())}/`)) return "curl";
|
|
2662
|
+
if (resolved.includes("/node_modules/")) return "npm";
|
|
2663
|
+
return "unknown";
|
|
2839
2664
|
}
|
|
2840
|
-
function
|
|
2665
|
+
function realpathOr(path) {
|
|
2841
2666
|
try {
|
|
2842
|
-
|
|
2843
|
-
mkdirSync2(dirname3(path), { recursive: true });
|
|
2844
|
-
writeFileSync2(path, JSON.stringify(state));
|
|
2667
|
+
return realpathSync(path);
|
|
2845
2668
|
} catch {
|
|
2669
|
+
return path;
|
|
2846
2670
|
}
|
|
2847
2671
|
}
|
|
2848
|
-
function
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
if (x !== y) return x > y;
|
|
2855
|
-
}
|
|
2856
|
-
return false;
|
|
2672
|
+
async function updatePrompt(currentVersion, minimumVersion, terminal) {
|
|
2673
|
+
await promptConfirm(
|
|
2674
|
+
`[directed] Directed ${minimumVersion} or newer is required (you have ${currentVersion}).`,
|
|
2675
|
+
"Press Enter to upgrade and continue. Ctrl-C to exit.",
|
|
2676
|
+
terminal
|
|
2677
|
+
);
|
|
2857
2678
|
}
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2679
|
+
function updateInstructions(channel) {
|
|
2680
|
+
if (channel === "npm") return "npm install -g @directed/cli@latest";
|
|
2681
|
+
return "curl -fsSL https://hub.directed.ai/cli/install.sh | bash";
|
|
2682
|
+
}
|
|
2683
|
+
async function updateInstall(hubUrl, channel = installChannel(), fetchImpl = fetch) {
|
|
2684
|
+
if (channel === "npm") return npmInstall();
|
|
2685
|
+
if (channel === "curl") return curlInstall(hubUrl, fetchImpl);
|
|
2686
|
+
process.stderr.write(
|
|
2687
|
+
`[directed] this build was not installed by npm or the installer, so it cannot upgrade itself.
|
|
2688
|
+
Install a current one with: ${updateInstructions("curl")}
|
|
2689
|
+
`
|
|
2690
|
+
);
|
|
2691
|
+
return 1;
|
|
2692
|
+
}
|
|
2693
|
+
function npmInstall() {
|
|
2694
|
+
process.stdout.write("[directed] installing @directed/cli@latest from npm\n");
|
|
2695
|
+
const result = spawnSync("npm", ["install", "-g", "@directed/cli@latest"], {
|
|
2696
|
+
stdio: "inherit"
|
|
2697
|
+
});
|
|
2698
|
+
if (result.error) {
|
|
2699
|
+
process.stderr.write(`[directed] npm could not run (${result.error.message}).
|
|
2700
|
+
`);
|
|
2701
|
+
return 1;
|
|
2874
2702
|
}
|
|
2703
|
+
return result.status ?? 1;
|
|
2875
2704
|
}
|
|
2876
|
-
async function
|
|
2705
|
+
async function curlInstall(hubUrl, fetchImpl) {
|
|
2877
2706
|
const scriptUrl = `${hubUrl}/cli/install.sh`;
|
|
2878
2707
|
process.stdout.write(`[directed] fetching installer from ${scriptUrl}
|
|
2879
2708
|
`);
|
|
@@ -2883,23 +2712,35 @@ async function updateInstall(hubUrl, fetchImpl = fetch) {
|
|
|
2883
2712
|
return 1;
|
|
2884
2713
|
}
|
|
2885
2714
|
const script = await res.text();
|
|
2886
|
-
const dir = mkdtempSync2(
|
|
2715
|
+
const dir = mkdtempSync2(join5(tmpdir2(), "directed-upgrade-"));
|
|
2887
2716
|
try {
|
|
2888
|
-
const scriptPath =
|
|
2717
|
+
const scriptPath = join5(dir, "install.sh");
|
|
2889
2718
|
writeFileSync2(scriptPath, script, { mode: 493 });
|
|
2890
2719
|
const result = spawnSync("bash", [scriptPath], {
|
|
2891
2720
|
stdio: "inherit",
|
|
2892
|
-
env: {
|
|
2721
|
+
env: {
|
|
2722
|
+
...process.env,
|
|
2723
|
+
DIRECTED_CLI_BASE_URL: hubUrl,
|
|
2724
|
+
DIRECTED_CLI_UPGRADE: "1"
|
|
2725
|
+
}
|
|
2893
2726
|
});
|
|
2894
2727
|
return result.status ?? 1;
|
|
2895
2728
|
} finally {
|
|
2896
|
-
|
|
2729
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
2897
2730
|
}
|
|
2898
2731
|
}
|
|
2732
|
+
function updateLaunch(argv, channel = installChannel()) {
|
|
2733
|
+
const spawn3 = channel === "curl" ? { command: join5(installRoot(), "bin", "directed"), args: argv } : { command: process.execPath, args: [process.argv[1] ?? "", ...argv] };
|
|
2734
|
+
const result = spawnSync(spawn3.command, spawn3.args, {
|
|
2735
|
+
stdio: "inherit",
|
|
2736
|
+
env: { ...process.env, DIRECTED_CLI_RELAUNCHED: "1" }
|
|
2737
|
+
});
|
|
2738
|
+
if (result.error) throw result.error;
|
|
2739
|
+
return result.status ?? 1;
|
|
2740
|
+
}
|
|
2899
2741
|
|
|
2900
2742
|
// src/cli.ts
|
|
2901
|
-
var
|
|
2902
|
-
var CLI_VERSION = require2("../package.json").version;
|
|
2743
|
+
var require3 = createRequire2(import.meta.url);
|
|
2903
2744
|
async function main(argv = process.argv.slice(2)) {
|
|
2904
2745
|
if (isHelpRequest(argv)) {
|
|
2905
2746
|
process.stdout.write(`${helpText()}
|
|
@@ -2944,68 +2785,105 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
2944
2785
|
);
|
|
2945
2786
|
return 1;
|
|
2946
2787
|
}
|
|
2947
|
-
return sessionRun(options);
|
|
2788
|
+
return sessionRun(options, argv);
|
|
2948
2789
|
}
|
|
2949
|
-
async function sessionRun(options) {
|
|
2790
|
+
async function sessionRun(options, argv) {
|
|
2950
2791
|
const hub = new Hub();
|
|
2951
2792
|
const agent = agentResolve(options.command);
|
|
2952
|
-
const
|
|
2953
|
-
|
|
2793
|
+
const shouldShare = options.chatEnabled && agent !== null;
|
|
2794
|
+
let auth = shouldShare ? authSessionLoad(hub) : null;
|
|
2795
|
+
if (shouldShare && auth === null) {
|
|
2796
|
+
if (!promptIsInteractive()) {
|
|
2797
|
+
process.stderr.write(
|
|
2798
|
+
`[directed] sign-in is required to share this session; use an interactive terminal.
|
|
2799
|
+
`
|
|
2800
|
+
);
|
|
2801
|
+
return 1;
|
|
2802
|
+
}
|
|
2803
|
+
try {
|
|
2804
|
+
const state = await authPrompt(hub, browserOpen, PROMPT_TERMINAL);
|
|
2805
|
+
auth = new AuthSession(hub, state);
|
|
2806
|
+
process.stderr.write(`[directed] signed in as ${state.identity.email}
|
|
2807
|
+
`);
|
|
2808
|
+
} catch (error) {
|
|
2809
|
+
if (error instanceof PromptCancelledError) return 130;
|
|
2810
|
+
process.stderr.write(
|
|
2811
|
+
`[directed] sign-in failed (${error.message}); the shared session was not started.
|
|
2812
|
+
`
|
|
2813
|
+
);
|
|
2814
|
+
return 1;
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
const isCapturing = shouldShare && auth !== null;
|
|
2954
2818
|
let localOnly = null;
|
|
2955
2819
|
if (options.chatEnabled && agent === null) {
|
|
2956
2820
|
localOnly = "chat streaming only works for claude and codex sessions; this tool has no transcript.";
|
|
2957
|
-
} else if (options.chatEnabled && auth === null) {
|
|
2958
|
-
localOnly = `not signed in to ${HUB_URL}; run '${binName()} login' to share a session.`;
|
|
2959
2821
|
}
|
|
2960
|
-
if (localOnly !== null)
|
|
2961
|
-
process.stderr.write(`[directed] ${localOnly} Running locally.
|
|
2962
|
-
`);
|
|
2963
2822
|
let chatPublicId2 = null;
|
|
2964
|
-
if (isCapturing && options.chatTarget.kind !== "new") {
|
|
2965
|
-
|
|
2966
|
-
hub.authAttach(
|
|
2967
|
-
|
|
2968
|
-
}
|
|
2969
|
-
|
|
2823
|
+
if (isCapturing && auth !== null && options.chatTarget.kind !== "new") {
|
|
2824
|
+
const selected = await authRetry(hub, auth, async (currentAuth) => {
|
|
2825
|
+
hub.authAttach(currentAuth);
|
|
2826
|
+
return chatPick(hub, options.chatTarget);
|
|
2827
|
+
});
|
|
2828
|
+
if (selected.kind === "exit") return selected.code;
|
|
2829
|
+
auth = selected.auth;
|
|
2830
|
+
if (selected.kind === "failed") {
|
|
2831
|
+
console.error(`[directed] ${selected.error.message}`);
|
|
2970
2832
|
return 1;
|
|
2971
2833
|
}
|
|
2834
|
+
chatPublicId2 = selected.value;
|
|
2972
2835
|
}
|
|
2973
|
-
const cols = process.stdout.columns || 80;
|
|
2974
|
-
const rows = process.stdout.rows || 24;
|
|
2975
|
-
const recording = new Recording(cols, rows);
|
|
2976
|
-
const running = agent === null ? null : await runningFind(agent, options);
|
|
2977
|
-
const tmux = new Tmux(
|
|
2978
|
-
options.tmuxBin,
|
|
2979
|
-
running?.session ?? `directed-${randomBytes3(4).toString("hex")}`
|
|
2980
|
-
);
|
|
2981
|
-
const pane = new PaneSession(
|
|
2982
|
-
options,
|
|
2983
|
-
recording,
|
|
2984
|
-
tmux,
|
|
2985
|
-
isCapturing ? agent : null,
|
|
2986
|
-
isCapturing
|
|
2987
|
-
);
|
|
2988
|
-
await pane.start(running?.pane ?? null);
|
|
2989
2836
|
const held = [];
|
|
2990
2837
|
let isTerminalFree = false;
|
|
2991
2838
|
const note = (text) => {
|
|
2992
|
-
recording.mark(`directed: ${text}`);
|
|
2993
2839
|
if (isTerminalFree) process.stderr.write(`[directed] ${text}
|
|
2994
2840
|
`);
|
|
2995
2841
|
else held.push(text);
|
|
2996
2842
|
};
|
|
2997
|
-
|
|
2843
|
+
const running = agent === null ? null : await runningFind(agent, options);
|
|
2844
|
+
let session = null;
|
|
2845
|
+
let isHeldRetrying = false;
|
|
2846
|
+
if (isCapturing && auth !== null && agent !== null) {
|
|
2847
|
+
const opened = await sessionOpen(
|
|
2848
|
+
hub,
|
|
2849
|
+
auth,
|
|
2850
|
+
agent,
|
|
2851
|
+
options,
|
|
2852
|
+
chatPublicId2,
|
|
2853
|
+
argv,
|
|
2854
|
+
running !== null
|
|
2855
|
+
);
|
|
2856
|
+
if (opened.kind === "exit") return opened.code;
|
|
2857
|
+
if (opened.kind === "opened") {
|
|
2858
|
+
auth = opened.auth;
|
|
2859
|
+
session = opened.session;
|
|
2860
|
+
}
|
|
2861
|
+
if (opened.kind === "held") {
|
|
2862
|
+
auth = opened.auth;
|
|
2863
|
+
isHeldRetrying = true;
|
|
2864
|
+
}
|
|
2865
|
+
if (opened.kind === "local") localOnly = opened.why;
|
|
2866
|
+
}
|
|
2867
|
+
const isSharing = session !== null || isHeldRetrying;
|
|
2868
|
+
if (localOnly !== null)
|
|
2869
|
+
process.stderr.write(`[directed] ${localOnly} Running locally.
|
|
2870
|
+
`);
|
|
2871
|
+
const tmux = new Tmux(
|
|
2872
|
+
options.tmuxBin,
|
|
2873
|
+
running?.session ?? `directed-${randomBytes3(4).toString("hex")}`
|
|
2874
|
+
);
|
|
2875
|
+
const pane = new PaneSession(options, tmux, isSharing ? agent : null, note);
|
|
2876
|
+
await pane.start(running?.pane ?? null);
|
|
2998
2877
|
const captureAbort = new AbortController();
|
|
2999
|
-
const capturing =
|
|
2878
|
+
const capturing = isSharing && auth !== null && agent !== null ? captureBind(
|
|
2879
|
+
session,
|
|
3000
2880
|
hub,
|
|
3001
2881
|
auth,
|
|
3002
2882
|
agent,
|
|
3003
2883
|
options,
|
|
3004
|
-
recording,
|
|
3005
2884
|
pane,
|
|
3006
2885
|
chatPublicId2,
|
|
3007
2886
|
note,
|
|
3008
|
-
running !== null,
|
|
3009
2887
|
captureAbort.signal
|
|
3010
2888
|
) : Promise.resolve(null);
|
|
3011
2889
|
try {
|
|
@@ -3036,79 +2914,188 @@ async function runningFind(agent, options) {
|
|
|
3036
2914
|
function binName() {
|
|
3037
2915
|
return basename4(process.argv[1] ?? "directed");
|
|
3038
2916
|
}
|
|
3039
|
-
async function
|
|
2917
|
+
async function sessionOpen(hub, auth, agent, options, chatPublicId2, argv, isRejoiningPane) {
|
|
2918
|
+
const opened = await authRetry(
|
|
2919
|
+
hub,
|
|
2920
|
+
auth,
|
|
2921
|
+
(currentAuth) => sessionOpenOnce(hub, currentAuth, agent, options, chatPublicId2)
|
|
2922
|
+
);
|
|
2923
|
+
if (opened.kind === "exit") return opened;
|
|
2924
|
+
if (opened.kind === "succeeded") {
|
|
2925
|
+
return { kind: "opened", session: opened.value, auth: opened.auth };
|
|
2926
|
+
}
|
|
2927
|
+
const error = opened.error;
|
|
2928
|
+
if (error instanceof UpgradeRequiredError) return upgradeRun(error, argv);
|
|
2929
|
+
if (error instanceof TerminalHeldError) {
|
|
2930
|
+
if (isRejoiningPane) {
|
|
2931
|
+
process.stderr.write(
|
|
2932
|
+
`[directed] joined the session streaming to ${error.chatUrl}; the original connection keeps posting.
|
|
2933
|
+
`
|
|
2934
|
+
);
|
|
2935
|
+
return { kind: "held", auth: opened.auth };
|
|
2936
|
+
}
|
|
2937
|
+
return {
|
|
2938
|
+
kind: "local",
|
|
2939
|
+
why: `already streaming to ${error.chatUrl} from another terminal.`
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
return sharingFailed(error.message);
|
|
2943
|
+
}
|
|
2944
|
+
async function authRetry(hub, auth, operation) {
|
|
2945
|
+
try {
|
|
2946
|
+
return { kind: "succeeded", value: await operation(auth), auth };
|
|
2947
|
+
} catch (error) {
|
|
2948
|
+
if (!(error instanceof AuthenticationRequiredError)) {
|
|
2949
|
+
return { kind: "failed", error, auth };
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
authClear(hub.url);
|
|
2953
|
+
if (!promptIsInteractive()) {
|
|
2954
|
+
process.stderr.write(
|
|
2955
|
+
`[directed] the saved sign-in is no longer valid; run '${binName()} login' from an interactive terminal, or use --no-chat.
|
|
2956
|
+
`
|
|
2957
|
+
);
|
|
2958
|
+
return { kind: "exit", code: 1 };
|
|
2959
|
+
}
|
|
2960
|
+
let replacement;
|
|
2961
|
+
try {
|
|
2962
|
+
process.stderr.write(`[directed] the saved sign-in is no longer valid.
|
|
2963
|
+
`);
|
|
2964
|
+
const state = await authPrompt(hub, browserOpen, PROMPT_TERMINAL);
|
|
2965
|
+
replacement = new AuthSession(hub, state);
|
|
2966
|
+
process.stderr.write(`[directed] signed in as ${state.identity.email}
|
|
2967
|
+
`);
|
|
2968
|
+
} catch (error) {
|
|
2969
|
+
if (error instanceof PromptCancelledError) return { kind: "exit", code: 130 };
|
|
2970
|
+
process.stderr.write(
|
|
2971
|
+
`[directed] sign-in failed (${error.message}); the shared session was not started.
|
|
2972
|
+
`
|
|
2973
|
+
);
|
|
2974
|
+
return { kind: "exit", code: 1 };
|
|
2975
|
+
}
|
|
2976
|
+
try {
|
|
2977
|
+
return {
|
|
2978
|
+
kind: "succeeded",
|
|
2979
|
+
value: await operation(replacement),
|
|
2980
|
+
auth: replacement
|
|
2981
|
+
};
|
|
2982
|
+
} catch (error) {
|
|
2983
|
+
if (!(error instanceof AuthenticationRequiredError)) {
|
|
2984
|
+
return { kind: "failed", error, auth: replacement };
|
|
2985
|
+
}
|
|
2986
|
+
authClear(hub.url);
|
|
2987
|
+
process.stderr.write(
|
|
2988
|
+
`[directed] sign-in rejected. Try '${binName()} login', or --no-chat.
|
|
2989
|
+
`
|
|
2990
|
+
);
|
|
2991
|
+
return { kind: "exit", code: 1 };
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
async function sessionOpenOnce(hub, auth, agent, options, chatPublicId2) {
|
|
2995
|
+
hub.authAttach(auth);
|
|
2996
|
+
return HubSession.open(hub, {
|
|
2997
|
+
source: agent.kind,
|
|
2998
|
+
title: options.title,
|
|
2999
|
+
resumeKey: await agent.resumeKey(options.command, process.cwd()),
|
|
3000
|
+
chatPublicId: chatPublicId2,
|
|
3001
|
+
memberList: options.chatMembers,
|
|
3002
|
+
gitStart: await gitSnapshot(process.cwd())
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
async function sharingFailed(reason) {
|
|
3006
|
+
const cause = /fetch failed|abort|timeout/i.test(reason) ? "can't reach the hub" : `could not start a shared session (${reason})`;
|
|
3007
|
+
const why = `${cause}; nothing will be shared or steerable.`;
|
|
3008
|
+
if (!promptIsInteractive()) {
|
|
3009
|
+
process.stderr.write(`[directed] ${why} Run with --no-chat to work locally.
|
|
3010
|
+
`);
|
|
3011
|
+
return { kind: "exit", code: 1 };
|
|
3012
|
+
}
|
|
3013
|
+
try {
|
|
3014
|
+
await promptConfirm(
|
|
3015
|
+
`[directed] ${why}`,
|
|
3016
|
+
"Press Enter to run locally. Ctrl-C to exit.",
|
|
3017
|
+
PROMPT_TERMINAL
|
|
3018
|
+
);
|
|
3019
|
+
} catch (e) {
|
|
3020
|
+
if (e instanceof PromptCancelledError) return { kind: "exit", code: 130 };
|
|
3021
|
+
throw e;
|
|
3022
|
+
}
|
|
3023
|
+
return { kind: "local", why };
|
|
3024
|
+
}
|
|
3025
|
+
async function upgradeRun(refusal, argv) {
|
|
3026
|
+
const channel = installChannel();
|
|
3027
|
+
const version = CLI_VERSION.split("+")[0];
|
|
3028
|
+
if (process.env.DIRECTED_CLI_RELAUNCHED === "1") {
|
|
3029
|
+
process.stderr.write(
|
|
3030
|
+
`[directed] still running ${version} after upgrading, and this hub needs ${refusal.minimumVersion}.
|
|
3031
|
+
Install it with: ${updateInstructions(channel)}
|
|
3032
|
+
`
|
|
3033
|
+
);
|
|
3034
|
+
return { kind: "exit", code: 1 };
|
|
3035
|
+
}
|
|
3036
|
+
if (!promptIsInteractive() || channel === "unknown") {
|
|
3037
|
+
process.stderr.write(
|
|
3038
|
+
`[directed] ${refusal.message}
|
|
3039
|
+
Upgrade with: ${updateInstructions(channel)}
|
|
3040
|
+
`
|
|
3041
|
+
);
|
|
3042
|
+
return { kind: "exit", code: 1 };
|
|
3043
|
+
}
|
|
3044
|
+
try {
|
|
3045
|
+
await updatePrompt(version, refusal.minimumVersion, PROMPT_TERMINAL);
|
|
3046
|
+
} catch (e) {
|
|
3047
|
+
if (e instanceof PromptCancelledError) return { kind: "exit", code: 130 };
|
|
3048
|
+
throw e;
|
|
3049
|
+
}
|
|
3050
|
+
if (await updateInstall(HUB_URL, channel) !== 0) {
|
|
3051
|
+
process.stderr.write(
|
|
3052
|
+
`[directed] the upgrade did not finish; the shared session was not started.
|
|
3053
|
+
`
|
|
3054
|
+
);
|
|
3055
|
+
return { kind: "exit", code: 1 };
|
|
3056
|
+
}
|
|
3057
|
+
process.stderr.write(`[directed] restarting on the new version
|
|
3058
|
+
`);
|
|
3059
|
+
return { kind: "exit", code: updateLaunch(argv, channel) };
|
|
3060
|
+
}
|
|
3061
|
+
async function captureBind(opened, hub, auth, agent, options, pane, chatPublicId2, note, signal) {
|
|
3040
3062
|
const reader = pane.transcript;
|
|
3041
3063
|
if (reader === null) return null;
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
while (!signal.aborted) {
|
|
3064
|
+
let session = opened;
|
|
3065
|
+
while (session === null) {
|
|
3066
|
+
if (signal.aborted || !await retryWait(signal)) return null;
|
|
3046
3067
|
try {
|
|
3047
|
-
hub
|
|
3048
|
-
const capture = await Capture.start(
|
|
3049
|
-
hub,
|
|
3050
|
-
auth,
|
|
3051
|
-
recording,
|
|
3052
|
-
reader,
|
|
3053
|
-
{
|
|
3054
|
-
source: agent.kind,
|
|
3055
|
-
title: options.title,
|
|
3056
|
-
resumeKey,
|
|
3057
|
-
chatPublicId: chatPublicId2,
|
|
3058
|
-
memberList: options.chatMembers,
|
|
3059
|
-
gitStart
|
|
3060
|
-
},
|
|
3061
|
-
note
|
|
3062
|
-
);
|
|
3063
|
-
if (capture === null) return null;
|
|
3064
|
-
await pane.captureStart();
|
|
3065
|
-
note(
|
|
3066
|
-
capture.reattached ? `re-attached to its chat: ${capture.chatUrl}` : `streaming to a chat: ${capture.chatUrl}`
|
|
3067
|
-
);
|
|
3068
|
-
if (capture.skippedMemberList.length > 0) {
|
|
3069
|
-
note(
|
|
3070
|
-
`not added (not your active connections): ${capture.skippedMemberList.join(", ")}`
|
|
3071
|
-
);
|
|
3072
|
-
}
|
|
3073
|
-
if (options.chatOpen) browserOpen(capture.chatUrl);
|
|
3074
|
-
if (options.chatInviteEmails.length > 0) {
|
|
3075
|
-
const mailto = await chatInvite(
|
|
3076
|
-
() => capture.linkCreate(),
|
|
3077
|
-
options.chatInviteEmails,
|
|
3078
|
-
auth.identity.fullName || auth.identity.username,
|
|
3079
|
-
note
|
|
3080
|
-
);
|
|
3081
|
-
if (mailto) browserOpen(mailto);
|
|
3082
|
-
}
|
|
3083
|
-
capture.steerListen(async (steer) => {
|
|
3084
|
-
await pane.steer(steer.text, steer.author);
|
|
3085
|
-
return options.steerMode === "send" ? "sent" : "staged";
|
|
3086
|
-
});
|
|
3087
|
-
return capture;
|
|
3068
|
+
session = await sessionOpenOnce(hub, auth, agent, options, chatPublicId2);
|
|
3088
3069
|
} catch (e) {
|
|
3089
|
-
if (e instanceof TerminalHeldError
|
|
3090
|
-
|
|
3091
|
-
isHeldReported = true;
|
|
3092
|
-
note(
|
|
3093
|
-
`this conversation is already connected: ${e.chatUrl}. This terminal is attached without replacing its capture.`
|
|
3094
|
-
);
|
|
3095
|
-
}
|
|
3096
|
-
if (!await retryWait(signal)) return null;
|
|
3097
|
-
continue;
|
|
3098
|
-
}
|
|
3099
|
-
if (e instanceof TerminalHeldError) {
|
|
3100
|
-
note(
|
|
3101
|
-
`this conversation is already connected: ${e.chatUrl}. Messages there reach it through the run that has it.`
|
|
3102
|
-
);
|
|
3103
|
-
return null;
|
|
3104
|
-
}
|
|
3105
|
-
note(
|
|
3106
|
-
`could not start capture (${e.message}); this session stays local.`
|
|
3107
|
-
);
|
|
3070
|
+
if (e instanceof TerminalHeldError) continue;
|
|
3071
|
+
note(`could not open the chat (${e.message}); this session stays local.`);
|
|
3108
3072
|
return null;
|
|
3109
3073
|
}
|
|
3110
3074
|
}
|
|
3111
|
-
|
|
3075
|
+
const capture = Capture.attach(session, auth, reader, note);
|
|
3076
|
+
note(
|
|
3077
|
+
capture.reattached ? `re-attached to its chat: ${capture.chatUrl}` : `streaming to a chat: ${capture.chatUrl}`
|
|
3078
|
+
);
|
|
3079
|
+
if (capture.skippedMemberList.length > 0) {
|
|
3080
|
+
note(
|
|
3081
|
+
`not connections yet, so not added: ${capture.skippedMemberList.join(", ")}. Use --invite instead.`
|
|
3082
|
+
);
|
|
3083
|
+
}
|
|
3084
|
+
if (options.chatOpen) browserOpen(capture.chatUrl);
|
|
3085
|
+
if (options.chatInviteEmails.length > 0) {
|
|
3086
|
+
const mailto = await chatInvite(
|
|
3087
|
+
() => capture.linkCreate(),
|
|
3088
|
+
options.chatInviteEmails,
|
|
3089
|
+
auth.identity.fullName || auth.identity.username,
|
|
3090
|
+
note
|
|
3091
|
+
);
|
|
3092
|
+
if (mailto) browserOpen(mailto);
|
|
3093
|
+
}
|
|
3094
|
+
capture.steerListen(async (steer) => {
|
|
3095
|
+
await pane.steer(steer.text);
|
|
3096
|
+
return options.steerMode === "send" ? "sent" : "staged";
|
|
3097
|
+
});
|
|
3098
|
+
return capture;
|
|
3112
3099
|
}
|
|
3113
3100
|
function retryWait(signal) {
|
|
3114
3101
|
return new Promise((resolve) => {
|
|
@@ -3142,7 +3129,7 @@ function browserOpen(url) {
|
|
|
3142
3129
|
}
|
|
3143
3130
|
const [command, args] = opener(url);
|
|
3144
3131
|
try {
|
|
3145
|
-
const child =
|
|
3132
|
+
const child = spawn2(command, args, { detached: true, stdio: "ignore" });
|
|
3146
3133
|
child.on("error", () => {
|
|
3147
3134
|
});
|
|
3148
3135
|
child.unref();
|
|
@@ -3150,7 +3137,7 @@ function browserOpen(url) {
|
|
|
3150
3137
|
return;
|
|
3151
3138
|
}
|
|
3152
3139
|
}
|
|
3153
|
-
var argv1 = process.argv[1] ?
|
|
3140
|
+
var argv1 = process.argv[1] ? realpathSync2(process.argv[1]) : "";
|
|
3154
3141
|
if (import.meta.url === pathToFileURL(argv1).href) {
|
|
3155
3142
|
main().then((code) => process.exit(code)).catch((err) => {
|
|
3156
3143
|
console.error(err?.message ?? err);
|