@sealant/mend 0.34.2 → 0.34.5

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.
Files changed (79) hide show
  1. package/dist/{chunk-JJ66V76Y.js → chunk-5DORQXQT.js} +106 -7
  2. package/dist/{dashboard-OADX3PHI.js → dashboard-KZ4XGZ6W.js} +32 -13
  3. package/dist/main.js +378 -89
  4. package/man/mend-accounts.1 +1 -1
  5. package/man/mend-adopt.1 +1 -1
  6. package/man/mend-attach.1 +1 -1
  7. package/man/mend-codex.1 +1 -1
  8. package/man/mend-completions.1 +1 -1
  9. package/man/mend-connect.1 +1 -1
  10. package/man/mend-continue.1 +1 -1
  11. package/man/mend-doctor.1 +1 -1
  12. package/man/mend-dotfiles-repo.1 +1 -1
  13. package/man/mend-dotfiles-sync.1 +1 -1
  14. package/man/mend-dotfiles.1 +1 -1
  15. package/man/mend-env-cluster.1 +1 -1
  16. package/man/mend-env-load.1 +1 -1
  17. package/man/mend-env-show.1 +1 -1
  18. package/man/mend-folder-create.1 +1 -1
  19. package/man/mend-folder-list.1 +1 -1
  20. package/man/mend-folder-push.1 +1 -1
  21. package/man/mend-folder-rm.1 +1 -1
  22. package/man/mend-git-author.1 +1 -1
  23. package/man/mend-help.1 +1 -1
  24. package/man/mend-invite.1 +1 -1
  25. package/man/mend-keys-init.1 +1 -1
  26. package/man/mend-keys-mode.1 +1 -1
  27. package/man/mend-keys-share.1 +1 -1
  28. package/man/mend-keys-show.1 +1 -1
  29. package/man/mend-land.1 +1 -1
  30. package/man/mend-login.1 +1 -1
  31. package/man/mend-logout.1 +1 -1
  32. package/man/mend-man.1 +1 -1
  33. package/man/mend-members.1 +1 -1
  34. package/man/mend-operator-exposure.1 +1 -1
  35. package/man/mend-operator-gate.1 +1 -1
  36. package/man/mend-operator-grant-owner.1 +1 -1
  37. package/man/mend-operator-org-create.1 +1 -1
  38. package/man/mend-operator-org-invite-owner.1 +1 -1
  39. package/man/mend-operator-org-list.1 +1 -1
  40. package/man/mend-operator-org-rename.1 +1 -1
  41. package/man/mend-operator-reset-link.1 +1 -1
  42. package/man/mend-pair.1 +1 -1
  43. package/man/mend-projects.1 +1 -1
  44. package/man/mend-pull.1 +1 -1
  45. package/man/mend-refresh.1 +1 -1
  46. package/man/mend-rejoin.1 +1 -1
  47. package/man/mend-resume.1 +1 -1
  48. package/man/mend-run.1 +1 -1
  49. package/man/mend-server-logs.1 +1 -1
  50. package/man/mend-server-restart.1 +1 -1
  51. package/man/mend-server-setup.1 +1 -1
  52. package/man/mend-server-start.1 +1 -1
  53. package/man/mend-server-status.1 +1 -1
  54. package/man/mend-server-stop.1 +1 -1
  55. package/man/mend-server-upgrade.1 +1 -1
  56. package/man/mend-server.1 +1 -1
  57. package/man/mend-service-add.1 +1 -1
  58. package/man/mend-service-connect.1 +1 -1
  59. package/man/mend-service-init.1 +1 -1
  60. package/man/mend-service-list.1 +1 -1
  61. package/man/mend-service-logs.1 +1 -1
  62. package/man/mend-service-restart.1 +1 -1
  63. package/man/mend-service-run.1 +1 -1
  64. package/man/mend-service-stop.1 +1 -1
  65. package/man/mend-session-share.1 +1 -1
  66. package/man/mend-sessions.1 +1 -1
  67. package/man/mend-shell.1 +1 -1
  68. package/man/mend-skills-push.1 +1 -1
  69. package/man/mend-skills.1 +1 -1
  70. package/man/mend-snake.1 +1 -1
  71. package/man/mend-ssh-setup.1 +1 -1
  72. package/man/mend-ssh.1 +1 -1
  73. package/man/mend-stop.1 +1 -1
  74. package/man/mend-ui.1 +1 -1
  75. package/man/mend-uninstall.1 +1 -1
  76. package/man/mend-version.1 +1 -1
  77. package/man/mend-worktrees.1 +1 -1
  78. package/man/mend.1 +1 -1
  79. package/package.json +3 -3
package/dist/main.js CHANGED
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ GATEWAY_STATUSES,
3
4
  HARNESS_COMMANDS,
4
5
  LIVE_STATUSES,
6
+ MendRequestError,
5
7
  agentIsLive,
6
8
  agentOutcome,
9
+ agentStartingFacts,
10
+ agentStartingLine,
7
11
  captureLineOf,
8
12
  claudeGrantFacts,
9
13
  createSseParser,
@@ -18,6 +22,7 @@ import {
18
22
  landingFactsFromWire,
19
23
  matchProjectByCwd,
20
24
  narrowCredential,
25
+ noAnswerError,
21
26
  normalizeGitAuthor,
22
27
  normalizeProjectName,
23
28
  normalizeRemoteUrl,
@@ -27,8 +32,9 @@ import {
27
32
  sameGrant,
28
33
  servicesHoldOf,
29
34
  sessionDisplayName,
35
+ spoken,
30
36
  trackBracketedPaste
31
- } from "./chunk-JJ66V76Y.js";
37
+ } from "./chunk-5DORQXQT.js";
32
38
 
33
39
  // src/main.ts
34
40
  import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
@@ -171,6 +177,45 @@ var startAgentShare = (options) => {
171
177
  };
172
178
  };
173
179
 
180
+ // src/attach-starting.ts
181
+ var startingLabelOf = (harness) => harness === "run" ? "the command" : harness === "shell" ? "the shell" : harness;
182
+ var CLEAR_LINE = "\r\x1B[2K";
183
+ var startingLine = (label, facts, elapsedMs, cancelHint) => `${CLEAR_LINE} ${agentStartingLine(label, facts, elapsedMs)}${cancelHint === "" ? "" : ` \xB7 ${cancelHint}`}`;
184
+ var firstOutputGate = (options) => {
185
+ const attachedAt = options.now();
186
+ let facts = { startedAt: null, freshMachine: false };
187
+ let shown = false;
188
+ let done = false;
189
+ let interval = null;
190
+ const draw = () => {
191
+ if (done) return;
192
+ shown = true;
193
+ const since = facts.startedAt ?? attachedAt;
194
+ options.write(startingLine(options.label, facts, options.now() - since, options.cancelHint));
195
+ };
196
+ const grace = setTimeout(() => {
197
+ draw();
198
+ interval = setInterval(draw, options.intervalMs ?? 1e3);
199
+ }, options.graceMs ?? 400);
200
+ const finish = () => {
201
+ if (done) return;
202
+ done = true;
203
+ clearTimeout(grace);
204
+ if (interval !== null) clearInterval(interval);
205
+ if (shown) options.write(CLEAR_LINE);
206
+ shown = false;
207
+ };
208
+ return {
209
+ pass: finish,
210
+ stop: finish,
211
+ update: (next) => {
212
+ facts = next;
213
+ if (shown) draw();
214
+ },
215
+ shown: () => shown
216
+ };
217
+ };
218
+
174
219
  // src/claude-grant.ts
175
220
  import { spawnSync } from "node:child_process";
176
221
  import { createHash } from "node:crypto";
@@ -3655,6 +3700,78 @@ var pullCommand = async (api2, download2, args, cwd = process.cwd()) => {
3655
3700
  for (const line of fetchedLines(fetched)) say(line);
3656
3701
  };
3657
3702
 
3703
+ // src/launch-follow.ts
3704
+ var SETTLED = /* @__PURE__ */ new Set(["completed", "failed", "stopped", "stopping"]);
3705
+ var agentRuns = (agent) => agent !== null && agent.kind !== "agent-protocol" && agent.exitedAt === null && agent.status === "running" && agent.sealantSessionId !== null;
3706
+ var startingLineOf = (session) => {
3707
+ const summary = session.summary?.trim() ?? "";
3708
+ if (summary === "") return session.status;
3709
+ if (/^[a-z][a-z -]*·/u.test(summary)) return summary;
3710
+ return `${session.status} \xB7 ${summary}`;
3711
+ };
3712
+ var refusalOf = (error) => error instanceof MendRequestError && error.kind === "http" && (error.status === null || !GATEWAY_STATUSES.has(error.status)) ? error.message : null;
3713
+ var followStart = async (options) => {
3714
+ const pollMs = options.pollMs ?? 2e3;
3715
+ const unreachableAfterMs = options.unreachableAfterMs ?? 6e4;
3716
+ const launch2 = {
3717
+ state: options.start === null ? { kind: "no-answer" } : { kind: "pending" }
3718
+ };
3719
+ const start = options.start;
3720
+ const launched = (async () => {
3721
+ if (start === null) return;
3722
+ try {
3723
+ launch2.state = { kind: "answered", session: await start };
3724
+ } catch (error) {
3725
+ const refusal = refusalOf(error);
3726
+ launch2.state = refusal === null ? { kind: "no-answer" } : { kind: "refused", message: refusal };
3727
+ }
3728
+ })();
3729
+ let lastLine = null;
3730
+ let failingSince = null;
3731
+ let seenStarting = false;
3732
+ for (; ; ) {
3733
+ if (launch2.state.kind === "pending") await Promise.race([launched, options.sleep(pollMs)]);
3734
+ const state = launch2.state;
3735
+ if (state.kind === "refused") return { kind: "refused", message: state.message };
3736
+ if (state.kind === "answered" && state.session.status === "running") {
3737
+ return { kind: "live", session: state.session };
3738
+ }
3739
+ let detail;
3740
+ try {
3741
+ detail = await options.read();
3742
+ } catch (error) {
3743
+ const refusal = refusalOf(error);
3744
+ if (refusal !== null) return { kind: "refused", message: refusal };
3745
+ failingSince ??= options.now();
3746
+ if (state.kind !== "pending" && options.now() - failingSince >= unreachableAfterMs) {
3747
+ return {
3748
+ kind: "unreachable",
3749
+ message: error instanceof Error ? error.message : String(error)
3750
+ };
3751
+ }
3752
+ if (state.kind !== "pending") await options.sleep(pollMs);
3753
+ continue;
3754
+ }
3755
+ failingSince = null;
3756
+ const { session, currentAgent } = detail;
3757
+ if (agentRuns(currentAgent)) return { kind: "live", session };
3758
+ if (SETTLED.has(session.status)) {
3759
+ if (seenStarting || state.kind !== "pending") return { kind: "settled", session };
3760
+ } else {
3761
+ seenStarting = true;
3762
+ }
3763
+ if (currentAgent === null && state.kind === "answered" && state.session.status === "running" && session.status === "running") {
3764
+ return { kind: "live", session };
3765
+ }
3766
+ const line = startingLineOf(session);
3767
+ if (line !== lastLine) {
3768
+ lastLine = line;
3769
+ options.onLine(line);
3770
+ }
3771
+ if (state.kind !== "pending") await options.sleep(pollMs);
3772
+ }
3773
+ };
3774
+
3658
3775
  // src/login.ts
3659
3776
  import { spawn as spawn2 } from "node:child_process";
3660
3777
  import * as os5 from "node:os";
@@ -7483,9 +7600,10 @@ var herdrAgentOf = (harness) => {
7483
7600
  if (harness === "codex" || harness === "claude" || harness === "opencode") return harness;
7484
7601
  return null;
7485
7602
  };
7603
+ var noHerdrHint = () => void 0;
7486
7604
  var hintHerdrAttachment = (harness) => {
7487
7605
  const agent = herdrAgentOf(harness);
7488
- if (process.env["HERDR_ENV"] !== "1" || agent === null) return () => void 0;
7606
+ if (process.env["HERDR_ENV"] !== "1" || agent === null) return noHerdrHint;
7489
7607
  const carrier = spawn3(
7490
7608
  process.execPath,
7491
7609
  ["-e", "process.stdin.resume();process.stdin.once('end',()=>process.exit(0))"],
@@ -7511,6 +7629,8 @@ var request = async (config, method, route, body) => {
7511
7629
  const headers = { "content-type": "application/json" };
7512
7630
  if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
7513
7631
  let response;
7632
+ const started = Date.now();
7633
+ const call = `${method} ${route}`;
7514
7634
  try {
7515
7635
  response = await fetch(`${config.url}/api${route}`, {
7516
7636
  method,
@@ -7518,23 +7638,30 @@ var request = async (config, method, route, body) => {
7518
7638
  // Spread, not `body: null` — fresh oxlint rejects a body key on GETs.
7519
7639
  ...body === void 0 ? {} : { body: JSON.stringify(body) }
7520
7640
  });
7521
- } catch {
7522
- throw new Error(`cannot reach the Mend server at ${config.url} \u2014 is it running?`);
7641
+ } catch (error) {
7642
+ throw noAnswerError(error, config.url, call, Date.now() - started);
7523
7643
  }
7524
7644
  if (response.status === 401) {
7525
- throw new Error(
7526
- config.token === null ? `not signed in to ${config.url} \u2014 run: mend login` : `unauthorized at ${config.url} \u2014 the saved token was rejected; run: mend login`
7645
+ throw new MendRequestError(
7646
+ "http",
7647
+ config.token === null ? `not signed in to ${config.url} \u2014 run: mend login` : `unauthorized at ${config.url} \u2014 the saved token was rejected; run: mend login`,
7648
+ 401
7527
7649
  );
7528
7650
  }
7529
- const text = await response.text();
7651
+ let text;
7652
+ try {
7653
+ text = await response.text();
7654
+ } catch (error) {
7655
+ throw noAnswerError(error, config.url, call, Date.now() - started);
7656
+ }
7530
7657
  if (!response.ok) {
7531
- let message = `${method} ${route} \u2192 ${response.status}`;
7658
+ let message = `${call} \u2192 ${response.status}`;
7532
7659
  try {
7533
7660
  const parsed = JSON.parse(text);
7534
7661
  message = parsed.message ?? message;
7535
7662
  } catch {
7536
7663
  }
7537
- throw new Error(message);
7664
+ throw new MendRequestError("http", message, response.status);
7538
7665
  }
7539
7666
  try {
7540
7667
  return JSON.parse(text);
@@ -7553,10 +7680,11 @@ var download = (config) => async (route) => {
7553
7680
  const headers = {};
7554
7681
  if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
7555
7682
  let response;
7683
+ const started = Date.now();
7556
7684
  try {
7557
7685
  response = await fetch(`${config.url}/api${route}`, { headers });
7558
- } catch {
7559
- return fail6(`cannot reach the Mend server at ${config.url} \u2014 is it running?`);
7686
+ } catch (error) {
7687
+ return fail6(noAnswerError(error, config.url, `GET ${route}`, Date.now() - started).message);
7560
7688
  }
7561
7689
  if (response.status === 401) {
7562
7690
  return fail6(
@@ -7567,13 +7695,14 @@ var download = (config) => async (route) => {
7567
7695
  return { status: response.status, header: (name) => response.headers.get(name), bytes };
7568
7696
  };
7569
7697
  var warnedLegacyBearer = false;
7570
- var mintTicket = (config) => async (target, params) => {
7698
+ var mintTicket = (config, signal) => async (target, params) => {
7571
7699
  const headers = { "content-type": "application/json" };
7572
7700
  if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
7573
7701
  const response = await fetch(`${config.url}/api/upgrade-tickets`, {
7574
7702
  method: "POST",
7575
7703
  headers,
7576
- body: JSON.stringify({ target, ...params })
7704
+ body: JSON.stringify({ target, ...params }),
7705
+ ...signal === void 0 ? {} : { signal }
7577
7706
  });
7578
7707
  if (response.status === 404) {
7579
7708
  if (await mintRefusedInTransit(config.url)) throw new Error(MINT_REFUSED_IN_TRANSIT);
@@ -7591,12 +7720,12 @@ var mintTicket = (config) => async (target, params) => {
7591
7720
  throw new Error("upgrade ticket missing from the answer");
7592
7721
  return { kind: "ticket", ticket: minted.ticket };
7593
7722
  };
7594
- var socketUrl = (config, target, params, extra) => upgradeUrl({
7723
+ var socketUrl = (config, target, params, extra, signal) => upgradeUrl({
7595
7724
  serverUrl: parseMendUrl(config.url).toString(),
7596
7725
  target,
7597
7726
  params,
7598
7727
  ...extra === void 0 ? {} : { extra },
7599
- mint: mintTicket(config),
7728
+ mint: mintTicket(config, signal),
7600
7729
  legacyToken: config.token
7601
7730
  });
7602
7731
  var boundApi = (config) => (method, route, body) => api(config, method, route, body);
@@ -7607,7 +7736,10 @@ var withSpinner = async (label, work) => {
7607
7736
  let frame = 0;
7608
7737
  const timer = setInterval(() => {
7609
7738
  const seconds2 = Math.round((Date.now() - started) / 1e3);
7610
- process.stdout.write(`\r ${frames[frame % frames.length]} ${label} ${dim6(`${seconds2}s`)} `);
7739
+ const text = typeof label === "string" ? label : label();
7740
+ process.stdout.write(
7741
+ `\r\x1B[2K ${frames[frame % frames.length]} ${text} ${dim6(`${seconds2}s`)} `
7742
+ );
7611
7743
  frame += 1;
7612
7744
  }, 120);
7613
7745
  try {
@@ -7617,6 +7749,52 @@ var withSpinner = async (label, work) => {
7617
7749
  process.stdout.write("\r\x1B[2K");
7618
7750
  }
7619
7751
  };
7752
+ var followStarting = async (config, sessionId, label, start) => {
7753
+ let line = label;
7754
+ const timers = /* @__PURE__ */ new Set();
7755
+ const sleep3 = (ms) => new Promise((resolve7) => {
7756
+ const timer = setTimeout(() => {
7757
+ timers.delete(timer);
7758
+ resolve7();
7759
+ }, ms);
7760
+ timers.add(timer);
7761
+ });
7762
+ try {
7763
+ return await withSpinner(
7764
+ () => line,
7765
+ followStart({
7766
+ start,
7767
+ read: () => request(config, "GET", `/sessions/${sessionId}`),
7768
+ onLine: (next) => {
7769
+ line = next === "starting" ? label : next;
7770
+ if (process.stdout.isTTY !== true) say6(dim6(` ${next}`));
7771
+ },
7772
+ sleep: sleep3,
7773
+ now: Date.now
7774
+ })
7775
+ );
7776
+ } finally {
7777
+ for (const timer of timers) clearTimeout(timer);
7778
+ }
7779
+ };
7780
+ var startedOrExit = (config, sessionId, outcome) => {
7781
+ const id8 = sessionId.slice(0, 8);
7782
+ switch (outcome.kind) {
7783
+ case "live":
7784
+ return outcome.session;
7785
+ case "refused":
7786
+ return fail6(outcome.message);
7787
+ case "settled":
7788
+ return fail6(
7789
+ `session ${id8} \xB7 ${startingLineOf(outcome.session)} before its agent ran \xB7 ${config.url}/sessions/${sessionId}`
7790
+ );
7791
+ case "unreachable":
7792
+ return fail6(
7793
+ `${outcome.message} \xB7 the session may still be starting \xB7 follow it: mend attach ${id8}`
7794
+ );
7795
+ }
7796
+ };
7797
+ var startAndFollow = async (config, sessionId, label, start) => startedOrExit(config, sessionId, await followStarting(config, sessionId, label, start));
7620
7798
  var adopt = async (config, args) => {
7621
7799
  const nameFlag = args.indexOf("--name");
7622
7800
  const name = nameFlag !== -1 && args[nameFlag + 1] !== void 0 ? String(args[nameFlag + 1]) : null;
@@ -7818,9 +7996,11 @@ var launch = async (config, harness, args) => {
7818
7996
  ...parsed.ask ? { permissionMode: "ask" } : {},
7819
7997
  ...parsed.fast ? { speed: "fast" } : {}
7820
7998
  } : { argv };
7821
- await withSpinner(
7999
+ await startAndFollow(
8000
+ config,
8001
+ session.id,
7822
8002
  "provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
7823
- api(config, "POST", `/sessions/${session.id}/launch`, launchBody)
8003
+ request(config, "POST", `/sessions/${session.id}/launch`, launchBody)
7824
8004
  );
7825
8005
  if (lifecycle === "detach") {
7826
8006
  say6(`${green6("\u2713 recording")} \xB7 running detached`);
@@ -7842,62 +8022,74 @@ var autoLandLine = (override, project2) => {
7842
8022
  }
7843
8023
  return `${green6("\u2713")} automatic landing \xB7 ${override ? "on" : "off"} for this session ${dim6("\xB7 from this terminal, land with mend land")}`;
7844
8024
  };
7845
- var attachTty = async (config, sessionId, harness, from, processId, options) => {
7846
- let url;
7847
- try {
7848
- url = await socketUrl(
7849
- config,
7850
- "tty",
7851
- processId === void 0 ? { session: sessionId } : { process: processId },
7852
- { from: from.toString() }
8025
+ var ATTACH_CONNECT_TIMEOUT_MS = (() => {
8026
+ const configured = Number(process.env["MEND_ATTACH_TIMEOUT_MS"]);
8027
+ return Number.isFinite(configured) && configured > 0 ? configured : 3e4;
8028
+ })();
8029
+ var connectingLine = (sessionId, elapsedMs) => `\r\x1B[2K connecting to ${sessionId.slice(0, 8)} \xB7 ${Math.round(elapsedMs / 1e3)}s \xB7 ${detachKeyEnabled ? "Ctrl+] or " : ""}Ctrl+C cancels`;
8030
+ var takeTerminal = () => {
8031
+ if (process.stdin.isTTY !== true) {
8032
+ console.error(
8033
+ "mend: stdin is not a terminal, so it cannot be put in raw mode \u2014 keys reach the session a line at a time and echo here"
7853
8034
  );
7854
- } catch {
7855
- return "unavailable";
8035
+ return { raw: false };
7856
8036
  }
7857
- const ws = new WebSocket(url);
7858
- ws.binaryType = "arraybuffer";
7859
8037
  try {
7860
- await new Promise((resolve7, reject) => {
7861
- ws.addEventListener("open", () => resolve7(), { once: true });
7862
- ws.addEventListener("error", () => reject(new Error("could not connect")), { once: true });
7863
- });
7864
- } catch {
7865
- return "unavailable";
8038
+ process.stdin.setRawMode(true);
8039
+ } catch (error) {
8040
+ console.error(
8041
+ `mend: could not put this terminal in raw mode (${error instanceof Error ? error.message : String(error)}) \u2014 keys reach the session a line at a time and echo here`
8042
+ );
8043
+ return { raw: false };
7866
8044
  }
7867
- const stopHerdrHint = hintHerdrAttachment(harness);
7868
- let finishAttachment;
7869
- const finished = new Promise((resolve7) => {
7870
- finishAttachment = resolve7;
7871
- });
7872
- const onClose = () => finishAttachment?.();
7873
- if (ws.readyState === WebSocket.CLOSED) finishAttachment?.();
7874
- else ws.addEventListener("close", onClose, { once: true });
7875
- const sendResize = () => {
7876
- if (ws.readyState !== WebSocket.OPEN) return;
7877
- ws.send(
7878
- JSON.stringify({
7879
- t: "resize",
7880
- cols: process.stdout.columns ?? 80,
7881
- rows: process.stdout.rows ?? 24
7882
- })
8045
+ if (!process.stdin.isRaw) {
8046
+ console.error(
8047
+ "mend: this terminal did not accept raw mode \u2014 keys reach the session a line at a time and echo here"
7883
8048
  );
8049
+ return { raw: false };
8050
+ }
8051
+ return { raw: true };
8052
+ };
8053
+ var terminalSize = () => ({
8054
+ cols: process.stdout.columns || 80,
8055
+ rows: process.stdout.rows || 24
8056
+ });
8057
+ var attachTty = async (config, sessionId, harness, from, processId, options) => {
8058
+ const interactive = options?.readOnly !== true;
8059
+ const rawModeEnabled = interactive ? takeTerminal().raw : false;
8060
+ const connecting = new AbortController();
8061
+ let abortReason = null;
8062
+ const abort = (reason) => {
8063
+ abortReason ??= reason;
8064
+ connecting.abort();
7884
8065
  };
7885
- const onWinch = () => sendResize();
7886
- const rawTty = process.stdin.isTTY === true;
7887
- let rawModeEnabled = false;
8066
+ let ws = null;
7888
8067
  let detached = false;
7889
8068
  let sawEnd = false;
7890
8069
  let interrupted = false;
8070
+ let finishAttachment;
8071
+ const finished = new Promise((resolve7) => {
8072
+ finishAttachment = resolve7;
8073
+ });
7891
8074
  const onSignal = () => {
7892
8075
  interrupted = true;
7893
- ws.close();
8076
+ abort("interrupted");
8077
+ ws?.close();
7894
8078
  finishAttachment?.();
7895
8079
  };
7896
8080
  const signals = ["SIGHUP", "SIGINT", "SIGTERM"];
8081
+ const sendResize = (size = terminalSize()) => {
8082
+ if (ws === null || ws.readyState !== WebSocket.OPEN) return;
8083
+ ws.send(JSON.stringify({ t: "resize", cols: size.cols, rows: size.rows }));
8084
+ };
8085
+ const onWinch = () => sendResize();
7897
8086
  let bracketedPaste = false;
8087
+ let starting = null;
7898
8088
  const onTtyFrame = (event) => {
7899
8089
  if (typeof event.data !== "string") {
7900
8090
  const bytes = Buffer.from(event.data);
8091
+ if (bytes.length === 0) return;
8092
+ starting?.pass();
7901
8093
  bracketedPaste = trackBracketedPaste(bytes, bracketedPaste);
7902
8094
  process.stdout.write(bytes);
7903
8095
  return;
@@ -7908,15 +8100,24 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
7908
8100
  return;
7909
8101
  }
7910
8102
  sawEnd = true;
7911
- ws.close();
8103
+ ws?.close();
7912
8104
  finishAttachment?.();
7913
8105
  } catch {
7914
8106
  }
7915
8107
  };
8108
+ const forward = (bytes) => {
8109
+ if (ws !== null && ws.readyState === WebSocket.OPEN) ws.send(new Uint8Array(bytes).buffer);
8110
+ };
7916
8111
  const onKeys = (data) => {
8112
+ if (ws === null) {
8113
+ if (detachKeyEnabled && isDetachChunk(data)) abort("detached");
8114
+ else if (data.includes(3)) abort("interrupted");
8115
+ return;
8116
+ }
7917
8117
  if (detachKeyEnabled && isDetachChunk(data)) {
7918
8118
  detached = true;
7919
8119
  ws.close();
8120
+ finishAttachment?.();
7920
8121
  return;
7921
8122
  }
7922
8123
  if (ws.readyState !== WebSocket.OPEN) return;
@@ -7926,9 +8127,6 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
7926
8127
  }
7927
8128
  ws.send(new Uint8Array(data).buffer);
7928
8129
  };
7929
- const forward = (bytes) => {
7930
- if (ws.readyState === WebSocket.OPEN) ws.send(new Uint8Array(bytes).buffer);
7931
- };
7932
8130
  const pasteClipboardImage = async (keystroke) => {
7933
8131
  const image = await readClipboardImage();
7934
8132
  if (image === null) {
@@ -7947,36 +8145,102 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
7947
8145
  process.stdout.write("\x07");
7948
8146
  }
7949
8147
  };
8148
+ const connect2 = async () => {
8149
+ let url;
8150
+ try {
8151
+ url = await socketUrl(
8152
+ config,
8153
+ "tty",
8154
+ processId === void 0 ? { session: sessionId } : { process: processId },
8155
+ { from: from.toString() },
8156
+ connecting.signal
8157
+ );
8158
+ } catch {
8159
+ return connecting.signal.aborted ? "aborted" : "unavailable";
8160
+ }
8161
+ if (connecting.signal.aborted) return "aborted";
8162
+ const socket = new WebSocket(url);
8163
+ socket.binaryType = "arraybuffer";
8164
+ const opened = await new Promise((resolve7) => {
8165
+ const onAbort = () => resolve7(false);
8166
+ connecting.signal.addEventListener("abort", onAbort, { once: true });
8167
+ const settle = (value) => {
8168
+ connecting.signal.removeEventListener("abort", onAbort);
8169
+ resolve7(value);
8170
+ };
8171
+ socket.addEventListener("open", () => settle(true), { once: true });
8172
+ socket.addEventListener("error", () => settle(false), { once: true });
8173
+ });
8174
+ if (opened && !connecting.signal.aborted) return socket;
8175
+ socket.close();
8176
+ return connecting.signal.aborted ? "aborted" : "unavailable";
8177
+ };
8178
+ const started = Date.now();
8179
+ let connectingShown = false;
8180
+ const connectingTimer = setInterval(() => {
8181
+ if (process.stdout.isTTY !== true) return;
8182
+ connectingShown = true;
8183
+ process.stdout.write(connectingLine(sessionId, Date.now() - started));
8184
+ }, 1e3);
8185
+ const deadline = setTimeout(() => abort("no-answer"), ATTACH_CONNECT_TIMEOUT_MS);
8186
+ const stopConnecting = () => {
8187
+ for (const timer of [connectingTimer, deadline]) clearTimeout(timer);
8188
+ if (connectingShown) process.stdout.write("\r\x1B[2K");
8189
+ connectingShown = false;
8190
+ };
8191
+ let stopHerdrHint = noHerdrHint;
7950
8192
  try {
7951
- sendResize();
7952
- process.stdout.on("resize", onWinch);
7953
8193
  if (options?.handleSignals === true) for (const signal of signals) process.on(signal, onSignal);
7954
- if (options?.readOnly !== true) {
7955
- if (rawTty) {
7956
- process.stdin.setRawMode(true);
7957
- rawModeEnabled = true;
7958
- }
7959
- process.stdin.resume();
8194
+ if (interactive) {
7960
8195
  process.stdin.on("data", onKeys);
8196
+ process.stdin.resume();
7961
8197
  }
8198
+ const connected = await connect2();
8199
+ stopConnecting();
8200
+ if (connected === "unavailable") return "unavailable";
8201
+ if (connected === "aborted") return abortReason ?? "interrupted";
8202
+ ws = connected;
8203
+ if (interactive && processId === void 0 && process.stdout.isTTY === true) {
8204
+ const gate = firstOutputGate({
8205
+ label: startingLabelOf(harness),
8206
+ write: (text) => process.stdout.write(text),
8207
+ now: Date.now,
8208
+ cancelHint: detachKeyEnabled ? "Ctrl+] detaches" : ""
8209
+ });
8210
+ starting = gate;
8211
+ void request(config, "GET", `/sessions/${sessionId}`).then(
8212
+ (detail) => gate.update(agentStartingFacts(detail.currentAgent, detail.processes, Date.now())),
8213
+ () => void 0
8214
+ );
8215
+ }
8216
+ stopHerdrHint = hintHerdrAttachment(harness);
8217
+ const onClose = () => finishAttachment?.();
8218
+ if (ws.readyState === WebSocket.CLOSED) finishAttachment?.();
8219
+ else ws.addEventListener("close", onClose, { once: true });
7962
8220
  ws.addEventListener("message", onTtyFrame);
8221
+ const size = terminalSize();
8222
+ if (size.rows > 1) sendResize({ cols: size.cols, rows: size.rows - 1 });
8223
+ setTimeout(() => sendResize(), 80);
8224
+ process.stdout.on("resize", onWinch);
7963
8225
  await finished;
7964
8226
  return detached ? "detached" : interrupted ? "interrupted" : sawEnd ? "ended" : "dropped";
7965
8227
  } finally {
8228
+ stopConnecting();
8229
+ starting?.stop();
7966
8230
  for (const signal of signals) process.off(signal, onSignal);
7967
8231
  process.stdin.off("data", onKeys);
7968
8232
  process.stdout.off("resize", onWinch);
7969
- ws.removeEventListener("message", onTtyFrame);
7970
- ws.removeEventListener("close", onClose);
8233
+ ws?.removeEventListener("message", onTtyFrame);
7971
8234
  if (rawModeEnabled) process.stdin.setRawMode(false);
7972
- if (options?.readOnly !== true && process.stdout.isTTY === true) {
8235
+ if (interactive && ws !== null && process.stdout.isTTY === true) {
7973
8236
  process.stdout.write(
7974
8237
  "\x1B[<u\x1B[=0;1u\x1B[?2004l\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?1049l\x1B[?25h"
7975
8238
  // show the cursor
7976
8239
  );
7977
8240
  }
7978
8241
  process.stdin.pause();
7979
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close();
8242
+ if (ws !== null && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING))
8243
+ ws.close();
7980
8244
  stopHerdrHint();
7981
8245
  }
7982
8246
  };
@@ -8004,11 +8268,18 @@ var stopAndExit = async (config, sessionId) => {
8004
8268
  say6(`${amber2("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
8005
8269
  process.exit(1);
8006
8270
  };
8271
+ var noAnswerLine = (config, id8) => `${amber2("no answer")} \u2014 ${config.url} did not open the terminal within ${spoken(ATTACH_CONNECT_TIMEOUT_MS)}; the session keeps running \xB7 try again: mend attach ${id8} \xB7 check the server: mend doctor`;
8007
8272
  var finishAttach = async (config, sessionId, outcome, mode = "background") => {
8008
8273
  const id8 = sessionId.slice(0, 8);
8009
8274
  if (outcome === "unavailable") {
8010
8275
  return fail6(`tty attach unavailable: could not connect to ${config.url}`);
8011
8276
  }
8277
+ if (outcome === "no-answer") {
8278
+ say6("");
8279
+ say6(noAnswerLine(config, id8));
8280
+ if (mode === "foreground") return stopAndExit(config, sessionId);
8281
+ process.exit(1);
8282
+ }
8012
8283
  if (outcome === "detached") {
8013
8284
  say6("");
8014
8285
  say6(`${amber2("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
@@ -8070,6 +8341,9 @@ var attachPicked = async (config, session, tunnels) => {
8070
8341
  say6(
8071
8342
  `${green6("\u2713")} attaching to ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim6(session.id.slice(0, 8))}${detachHint()}`
8072
8343
  );
8344
+ if (session.status === "starting") {
8345
+ await startAndFollow(config, session.id, "starting \u2014 attaching once the agent runs\u2026", null);
8346
+ }
8073
8347
  await tunnels?.start(session.id);
8074
8348
  say6("");
8075
8349
  const outcome = await attachWithTunnels(
@@ -8082,9 +8356,11 @@ var attachPicked = async (config, session, tunnels) => {
8082
8356
  const detail = await api(config, "GET", `/sessions/${session.id}`);
8083
8357
  if (detail.currentAgent?.kind === "agent-protocol" && agentIsLive(detail.session, detail.currentAgent)) {
8084
8358
  say6(`${amber2("taking over")} from the protocol session`);
8085
- await withSpinner(
8359
+ await startAndFollow(
8360
+ config,
8361
+ session.id,
8086
8362
  "reopening as a terminal \u2014 same conversation\u2026",
8087
- api(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
8363
+ request(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
8088
8364
  );
8089
8365
  say6("");
8090
8366
  await attachOrExit(config, session.id, session.harness, "background", tunnels);
@@ -8240,6 +8516,10 @@ var shellCommand = async (config, args) => {
8240
8516
  return fail6(`tty attach unavailable: could not connect to ${config.url}`);
8241
8517
  }
8242
8518
  say6("");
8519
+ if (outcome === "no-answer") {
8520
+ say6(noAnswerLine(config, session.id.slice(0, 8)));
8521
+ process.exit(1);
8522
+ }
8243
8523
  if (outcome === "detached" || outcome === "interrupted" || outcome === "dropped") {
8244
8524
  say6(`${amber2("detached")} \u2014 the shell keeps running and holds the workspace open`);
8245
8525
  process.exit(0);
@@ -9627,9 +9907,11 @@ var completionsCommand = (args) => {
9627
9907
  }
9628
9908
  };
9629
9909
  var supervisedRun = async (config, session, argv) => {
9630
- const launched = await withSpinner(
9910
+ const launched = await startAndFollow(
9911
+ config,
9912
+ session.id,
9631
9913
  "provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
9632
- api(config, "POST", `/sessions/${session.id}/launch`, { argv })
9914
+ request(config, "POST", `/sessions/${session.id}/launch`, { argv })
9633
9915
  );
9634
9916
  say6(
9635
9917
  `${green6("\u2713 recording")} \xB7 run ${dim6(launched.id.slice(0, 8))} \xB7 workspace mounts the worktree`
@@ -9765,9 +10047,13 @@ var resumeCommand = async (config, args) => {
9765
10047
  say6(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${match.id}`);
9766
10048
  const priorAgent = detail.annotations.find((annotation) => annotation.sessionId === match.id)?.currentAgent ?? null;
9767
10049
  const protocolPrior = priorAgent?.kind === "agent-protocol" && withHarness === null;
9768
- await withSpinner(
10050
+ await startAndFollow(
10051
+ config,
10052
+ match.id,
9769
10053
  protocolPrior ? "reopening as a terminal \u2014 same conversation\u2026" : "resuming \u2014 a fresh workspace restores the saved session state\u2026",
9770
- protocolPrior ? api(config, "POST", `/sessions/${match.id}/handoff`, { to: "pty" }) : api(config, "POST", `/sessions/${match.id}/resume`, { harness: withHarness })
10054
+ protocolPrior ? request(config, "POST", `/sessions/${match.id}/handoff`, { to: "pty" }) : request(config, "POST", `/sessions/${match.id}/resume`, {
10055
+ harness: withHarness
10056
+ })
9771
10057
  );
9772
10058
  say6(`${green6("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}`);
9773
10059
  say6("");
@@ -9775,17 +10061,18 @@ var resumeCommand = async (config, args) => {
9775
10061
  exitAfterSessionEnd(config, match.id);
9776
10062
  };
9777
10063
  var resumeForRejoin = async (config, sessionId, label) => {
9778
- try {
9779
- await withSpinner(
9780
- label,
9781
- request(config, "POST", `/sessions/${sessionId}/resume`, { harness: null })
9782
- );
9783
- return true;
9784
- } catch (error) {
10064
+ const outcome = await followStarting(
10065
+ config,
10066
+ sessionId,
10067
+ label,
10068
+ request(config, "POST", `/sessions/${sessionId}/resume`, { harness: null })
10069
+ );
10070
+ if (outcome.kind === "refused") {
9785
10071
  const refreshed = await api(config, "GET", `/sessions/${sessionId}`);
9786
10072
  if (agentIsLive(refreshed.session, refreshed.currentAgent)) return false;
9787
- return fail6(error instanceof Error ? error.message : String(error));
9788
10073
  }
10074
+ startedOrExit(config, sessionId, outcome);
10075
+ return true;
9789
10076
  };
9790
10077
  var rejoinCommand = async (config, args) => {
9791
10078
  const harnessFlag = args.indexOf("--harness");
@@ -9816,9 +10103,11 @@ var rejoinCommand = async (config, args) => {
9816
10103
  let restored = false;
9817
10104
  const currentAgent = detail.annotations.find((annotation) => annotation.sessionId === session.id)?.currentAgent ?? null;
9818
10105
  if (currentAgent?.kind === "agent-protocol") {
9819
- await withSpinner(
10106
+ await startAndFollow(
10107
+ config,
10108
+ session.id,
9820
10109
  alreadyLive ? "taking over from the protocol session \u2014 same conversation\u2026" : "reopening as a terminal \u2014 same conversation\u2026",
9821
- api(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
10110
+ request(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
9822
10111
  );
9823
10112
  restored = true;
9824
10113
  } else if (!alreadyLive) {
@@ -10149,7 +10438,7 @@ var dashboard = async (config, options = {}) => {
10149
10438
  );
10150
10439
  process.exit(rerun.status ?? 0);
10151
10440
  }
10152
- const { runDashboard } = await import("./dashboard-OADX3PHI.js");
10441
+ const { runDashboard } = await import("./dashboard-KZ4XGZ6W.js");
10153
10442
  const agentShare = await startShareIfBridge(config);
10154
10443
  const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
10155
10444
  listServices: () => fetchServices(config),