@sealant/mend 0.34.2 → 0.34.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.
Files changed (79) hide show
  1. package/dist/{chunk-JJ66V76Y.js → chunk-AAAXHRWL.js} +72 -0
  2. package/dist/{dashboard-OADX3PHI.js → dashboard-DWXXYFPE.js} +32 -13
  3. package/dist/main.js +320 -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 +2 -2
package/dist/main.js CHANGED
@@ -1,7 +1,9 @@
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,
7
9
  captureLineOf,
@@ -18,6 +20,7 @@ import {
18
20
  landingFactsFromWire,
19
21
  matchProjectByCwd,
20
22
  narrowCredential,
23
+ noAnswerError,
21
24
  normalizeGitAuthor,
22
25
  normalizeProjectName,
23
26
  normalizeRemoteUrl,
@@ -27,8 +30,9 @@ import {
27
30
  sameGrant,
28
31
  servicesHoldOf,
29
32
  sessionDisplayName,
33
+ spoken,
30
34
  trackBracketedPaste
31
- } from "./chunk-JJ66V76Y.js";
35
+ } from "./chunk-AAAXHRWL.js";
32
36
 
33
37
  // src/main.ts
34
38
  import { spawn as spawn3, spawnSync as spawnSync5 } from "node:child_process";
@@ -3655,6 +3659,78 @@ var pullCommand = async (api2, download2, args, cwd = process.cwd()) => {
3655
3659
  for (const line of fetchedLines(fetched)) say(line);
3656
3660
  };
3657
3661
 
3662
+ // src/launch-follow.ts
3663
+ var SETTLED = /* @__PURE__ */ new Set(["completed", "failed", "stopped", "stopping"]);
3664
+ var agentRuns = (agent) => agent !== null && agent.kind !== "agent-protocol" && agent.exitedAt === null && agent.status === "running" && agent.sealantSessionId !== null;
3665
+ var startingLineOf = (session) => {
3666
+ const summary = session.summary?.trim() ?? "";
3667
+ if (summary === "") return session.status;
3668
+ if (/^[a-z][a-z -]*·/u.test(summary)) return summary;
3669
+ return `${session.status} \xB7 ${summary}`;
3670
+ };
3671
+ var refusalOf = (error) => error instanceof MendRequestError && error.kind === "http" && (error.status === null || !GATEWAY_STATUSES.has(error.status)) ? error.message : null;
3672
+ var followStart = async (options) => {
3673
+ const pollMs = options.pollMs ?? 2e3;
3674
+ const unreachableAfterMs = options.unreachableAfterMs ?? 6e4;
3675
+ const launch2 = {
3676
+ state: options.start === null ? { kind: "no-answer" } : { kind: "pending" }
3677
+ };
3678
+ const start = options.start;
3679
+ const launched = (async () => {
3680
+ if (start === null) return;
3681
+ try {
3682
+ launch2.state = { kind: "answered", session: await start };
3683
+ } catch (error) {
3684
+ const refusal = refusalOf(error);
3685
+ launch2.state = refusal === null ? { kind: "no-answer" } : { kind: "refused", message: refusal };
3686
+ }
3687
+ })();
3688
+ let lastLine = null;
3689
+ let failingSince = null;
3690
+ let seenStarting = false;
3691
+ for (; ; ) {
3692
+ if (launch2.state.kind === "pending") await Promise.race([launched, options.sleep(pollMs)]);
3693
+ const state = launch2.state;
3694
+ if (state.kind === "refused") return { kind: "refused", message: state.message };
3695
+ if (state.kind === "answered" && state.session.status === "running") {
3696
+ return { kind: "live", session: state.session };
3697
+ }
3698
+ let detail;
3699
+ try {
3700
+ detail = await options.read();
3701
+ } catch (error) {
3702
+ const refusal = refusalOf(error);
3703
+ if (refusal !== null) return { kind: "refused", message: refusal };
3704
+ failingSince ??= options.now();
3705
+ if (state.kind !== "pending" && options.now() - failingSince >= unreachableAfterMs) {
3706
+ return {
3707
+ kind: "unreachable",
3708
+ message: error instanceof Error ? error.message : String(error)
3709
+ };
3710
+ }
3711
+ if (state.kind !== "pending") await options.sleep(pollMs);
3712
+ continue;
3713
+ }
3714
+ failingSince = null;
3715
+ const { session, currentAgent } = detail;
3716
+ if (agentRuns(currentAgent)) return { kind: "live", session };
3717
+ if (SETTLED.has(session.status)) {
3718
+ if (seenStarting || state.kind !== "pending") return { kind: "settled", session };
3719
+ } else {
3720
+ seenStarting = true;
3721
+ }
3722
+ if (currentAgent === null && state.kind === "answered" && state.session.status === "running" && session.status === "running") {
3723
+ return { kind: "live", session };
3724
+ }
3725
+ const line = startingLineOf(session);
3726
+ if (line !== lastLine) {
3727
+ lastLine = line;
3728
+ options.onLine(line);
3729
+ }
3730
+ if (state.kind !== "pending") await options.sleep(pollMs);
3731
+ }
3732
+ };
3733
+
3658
3734
  // src/login.ts
3659
3735
  import { spawn as spawn2 } from "node:child_process";
3660
3736
  import * as os5 from "node:os";
@@ -7483,9 +7559,10 @@ var herdrAgentOf = (harness) => {
7483
7559
  if (harness === "codex" || harness === "claude" || harness === "opencode") return harness;
7484
7560
  return null;
7485
7561
  };
7562
+ var noHerdrHint = () => void 0;
7486
7563
  var hintHerdrAttachment = (harness) => {
7487
7564
  const agent = herdrAgentOf(harness);
7488
- if (process.env["HERDR_ENV"] !== "1" || agent === null) return () => void 0;
7565
+ if (process.env["HERDR_ENV"] !== "1" || agent === null) return noHerdrHint;
7489
7566
  const carrier = spawn3(
7490
7567
  process.execPath,
7491
7568
  ["-e", "process.stdin.resume();process.stdin.once('end',()=>process.exit(0))"],
@@ -7511,6 +7588,8 @@ var request = async (config, method, route, body) => {
7511
7588
  const headers = { "content-type": "application/json" };
7512
7589
  if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
7513
7590
  let response;
7591
+ const started = Date.now();
7592
+ const call = `${method} ${route}`;
7514
7593
  try {
7515
7594
  response = await fetch(`${config.url}/api${route}`, {
7516
7595
  method,
@@ -7518,23 +7597,30 @@ var request = async (config, method, route, body) => {
7518
7597
  // Spread, not `body: null` — fresh oxlint rejects a body key on GETs.
7519
7598
  ...body === void 0 ? {} : { body: JSON.stringify(body) }
7520
7599
  });
7521
- } catch {
7522
- throw new Error(`cannot reach the Mend server at ${config.url} \u2014 is it running?`);
7600
+ } catch (error) {
7601
+ throw noAnswerError(error, config.url, call, Date.now() - started);
7523
7602
  }
7524
7603
  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`
7604
+ throw new MendRequestError(
7605
+ "http",
7606
+ 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`,
7607
+ 401
7527
7608
  );
7528
7609
  }
7529
- const text = await response.text();
7610
+ let text;
7611
+ try {
7612
+ text = await response.text();
7613
+ } catch (error) {
7614
+ throw noAnswerError(error, config.url, call, Date.now() - started);
7615
+ }
7530
7616
  if (!response.ok) {
7531
- let message = `${method} ${route} \u2192 ${response.status}`;
7617
+ let message = `${call} \u2192 ${response.status}`;
7532
7618
  try {
7533
7619
  const parsed = JSON.parse(text);
7534
7620
  message = parsed.message ?? message;
7535
7621
  } catch {
7536
7622
  }
7537
- throw new Error(message);
7623
+ throw new MendRequestError("http", message, response.status);
7538
7624
  }
7539
7625
  try {
7540
7626
  return JSON.parse(text);
@@ -7553,10 +7639,11 @@ var download = (config) => async (route) => {
7553
7639
  const headers = {};
7554
7640
  if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
7555
7641
  let response;
7642
+ const started = Date.now();
7556
7643
  try {
7557
7644
  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?`);
7645
+ } catch (error) {
7646
+ return fail6(noAnswerError(error, config.url, `GET ${route}`, Date.now() - started).message);
7560
7647
  }
7561
7648
  if (response.status === 401) {
7562
7649
  return fail6(
@@ -7567,13 +7654,14 @@ var download = (config) => async (route) => {
7567
7654
  return { status: response.status, header: (name) => response.headers.get(name), bytes };
7568
7655
  };
7569
7656
  var warnedLegacyBearer = false;
7570
- var mintTicket = (config) => async (target, params) => {
7657
+ var mintTicket = (config, signal) => async (target, params) => {
7571
7658
  const headers = { "content-type": "application/json" };
7572
7659
  if (config.token !== null) headers["authorization"] = `Bearer ${config.token}`;
7573
7660
  const response = await fetch(`${config.url}/api/upgrade-tickets`, {
7574
7661
  method: "POST",
7575
7662
  headers,
7576
- body: JSON.stringify({ target, ...params })
7663
+ body: JSON.stringify({ target, ...params }),
7664
+ ...signal === void 0 ? {} : { signal }
7577
7665
  });
7578
7666
  if (response.status === 404) {
7579
7667
  if (await mintRefusedInTransit(config.url)) throw new Error(MINT_REFUSED_IN_TRANSIT);
@@ -7591,12 +7679,12 @@ var mintTicket = (config) => async (target, params) => {
7591
7679
  throw new Error("upgrade ticket missing from the answer");
7592
7680
  return { kind: "ticket", ticket: minted.ticket };
7593
7681
  };
7594
- var socketUrl = (config, target, params, extra) => upgradeUrl({
7682
+ var socketUrl = (config, target, params, extra, signal) => upgradeUrl({
7595
7683
  serverUrl: parseMendUrl(config.url).toString(),
7596
7684
  target,
7597
7685
  params,
7598
7686
  ...extra === void 0 ? {} : { extra },
7599
- mint: mintTicket(config),
7687
+ mint: mintTicket(config, signal),
7600
7688
  legacyToken: config.token
7601
7689
  });
7602
7690
  var boundApi = (config) => (method, route, body) => api(config, method, route, body);
@@ -7607,7 +7695,10 @@ var withSpinner = async (label, work) => {
7607
7695
  let frame = 0;
7608
7696
  const timer = setInterval(() => {
7609
7697
  const seconds2 = Math.round((Date.now() - started) / 1e3);
7610
- process.stdout.write(`\r ${frames[frame % frames.length]} ${label} ${dim6(`${seconds2}s`)} `);
7698
+ const text = typeof label === "string" ? label : label();
7699
+ process.stdout.write(
7700
+ `\r\x1B[2K ${frames[frame % frames.length]} ${text} ${dim6(`${seconds2}s`)} `
7701
+ );
7611
7702
  frame += 1;
7612
7703
  }, 120);
7613
7704
  try {
@@ -7617,6 +7708,52 @@ var withSpinner = async (label, work) => {
7617
7708
  process.stdout.write("\r\x1B[2K");
7618
7709
  }
7619
7710
  };
7711
+ var followStarting = async (config, sessionId, label, start) => {
7712
+ let line = label;
7713
+ const timers = /* @__PURE__ */ new Set();
7714
+ const sleep3 = (ms) => new Promise((resolve7) => {
7715
+ const timer = setTimeout(() => {
7716
+ timers.delete(timer);
7717
+ resolve7();
7718
+ }, ms);
7719
+ timers.add(timer);
7720
+ });
7721
+ try {
7722
+ return await withSpinner(
7723
+ () => line,
7724
+ followStart({
7725
+ start,
7726
+ read: () => request(config, "GET", `/sessions/${sessionId}`),
7727
+ onLine: (next) => {
7728
+ line = next === "starting" ? label : next;
7729
+ if (process.stdout.isTTY !== true) say6(dim6(` ${next}`));
7730
+ },
7731
+ sleep: sleep3,
7732
+ now: Date.now
7733
+ })
7734
+ );
7735
+ } finally {
7736
+ for (const timer of timers) clearTimeout(timer);
7737
+ }
7738
+ };
7739
+ var startedOrExit = (config, sessionId, outcome) => {
7740
+ const id8 = sessionId.slice(0, 8);
7741
+ switch (outcome.kind) {
7742
+ case "live":
7743
+ return outcome.session;
7744
+ case "refused":
7745
+ return fail6(outcome.message);
7746
+ case "settled":
7747
+ return fail6(
7748
+ `session ${id8} \xB7 ${startingLineOf(outcome.session)} before its agent ran \xB7 ${config.url}/sessions/${sessionId}`
7749
+ );
7750
+ case "unreachable":
7751
+ return fail6(
7752
+ `${outcome.message} \xB7 the session may still be starting \xB7 follow it: mend attach ${id8}`
7753
+ );
7754
+ }
7755
+ };
7756
+ var startAndFollow = async (config, sessionId, label, start) => startedOrExit(config, sessionId, await followStarting(config, sessionId, label, start));
7620
7757
  var adopt = async (config, args) => {
7621
7758
  const nameFlag = args.indexOf("--name");
7622
7759
  const name = nameFlag !== -1 && args[nameFlag + 1] !== void 0 ? String(args[nameFlag + 1]) : null;
@@ -7818,9 +7955,11 @@ var launch = async (config, harness, args) => {
7818
7955
  ...parsed.ask ? { permissionMode: "ask" } : {},
7819
7956
  ...parsed.fast ? { speed: "fast" } : {}
7820
7957
  } : { argv };
7821
- await withSpinner(
7958
+ await startAndFollow(
7959
+ config,
7960
+ session.id,
7822
7961
  "provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
7823
- api(config, "POST", `/sessions/${session.id}/launch`, launchBody)
7962
+ request(config, "POST", `/sessions/${session.id}/launch`, launchBody)
7824
7963
  );
7825
7964
  if (lifecycle === "detach") {
7826
7965
  say6(`${green6("\u2713 recording")} \xB7 running detached`);
@@ -7842,58 +7981,67 @@ var autoLandLine = (override, project2) => {
7842
7981
  }
7843
7982
  return `${green6("\u2713")} automatic landing \xB7 ${override ? "on" : "off"} for this session ${dim6("\xB7 from this terminal, land with mend land")}`;
7844
7983
  };
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() }
7984
+ var ATTACH_CONNECT_TIMEOUT_MS = (() => {
7985
+ const configured = Number(process.env["MEND_ATTACH_TIMEOUT_MS"]);
7986
+ return Number.isFinite(configured) && configured > 0 ? configured : 3e4;
7987
+ })();
7988
+ 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`;
7989
+ var takeTerminal = () => {
7990
+ if (process.stdin.isTTY !== true) {
7991
+ console.error(
7992
+ "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
7993
  );
7854
- } catch {
7855
- return "unavailable";
7994
+ return { raw: false };
7856
7995
  }
7857
- const ws = new WebSocket(url);
7858
- ws.binaryType = "arraybuffer";
7859
7996
  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";
7997
+ process.stdin.setRawMode(true);
7998
+ } catch (error) {
7999
+ console.error(
8000
+ `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`
8001
+ );
8002
+ return { raw: false };
7866
8003
  }
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
- })
8004
+ if (!process.stdin.isRaw) {
8005
+ console.error(
8006
+ "mend: this terminal did not accept raw mode \u2014 keys reach the session a line at a time and echo here"
7883
8007
  );
8008
+ return { raw: false };
8009
+ }
8010
+ return { raw: true };
8011
+ };
8012
+ var terminalSize = () => ({
8013
+ cols: process.stdout.columns || 80,
8014
+ rows: process.stdout.rows || 24
8015
+ });
8016
+ var attachTty = async (config, sessionId, harness, from, processId, options) => {
8017
+ const interactive = options?.readOnly !== true;
8018
+ const rawModeEnabled = interactive ? takeTerminal().raw : false;
8019
+ const connecting = new AbortController();
8020
+ let abortReason = null;
8021
+ const abort = (reason) => {
8022
+ abortReason ??= reason;
8023
+ connecting.abort();
7884
8024
  };
7885
- const onWinch = () => sendResize();
7886
- const rawTty = process.stdin.isTTY === true;
7887
- let rawModeEnabled = false;
8025
+ let ws = null;
7888
8026
  let detached = false;
7889
8027
  let sawEnd = false;
7890
8028
  let interrupted = false;
8029
+ let finishAttachment;
8030
+ const finished = new Promise((resolve7) => {
8031
+ finishAttachment = resolve7;
8032
+ });
7891
8033
  const onSignal = () => {
7892
8034
  interrupted = true;
7893
- ws.close();
8035
+ abort("interrupted");
8036
+ ws?.close();
7894
8037
  finishAttachment?.();
7895
8038
  };
7896
8039
  const signals = ["SIGHUP", "SIGINT", "SIGTERM"];
8040
+ const sendResize = (size = terminalSize()) => {
8041
+ if (ws === null || ws.readyState !== WebSocket.OPEN) return;
8042
+ ws.send(JSON.stringify({ t: "resize", cols: size.cols, rows: size.rows }));
8043
+ };
8044
+ const onWinch = () => sendResize();
7897
8045
  let bracketedPaste = false;
7898
8046
  const onTtyFrame = (event) => {
7899
8047
  if (typeof event.data !== "string") {
@@ -7908,15 +8056,24 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
7908
8056
  return;
7909
8057
  }
7910
8058
  sawEnd = true;
7911
- ws.close();
8059
+ ws?.close();
7912
8060
  finishAttachment?.();
7913
8061
  } catch {
7914
8062
  }
7915
8063
  };
8064
+ const forward = (bytes) => {
8065
+ if (ws !== null && ws.readyState === WebSocket.OPEN) ws.send(new Uint8Array(bytes).buffer);
8066
+ };
7916
8067
  const onKeys = (data) => {
8068
+ if (ws === null) {
8069
+ if (detachKeyEnabled && isDetachChunk(data)) abort("detached");
8070
+ else if (data.includes(3)) abort("interrupted");
8071
+ return;
8072
+ }
7917
8073
  if (detachKeyEnabled && isDetachChunk(data)) {
7918
8074
  detached = true;
7919
8075
  ws.close();
8076
+ finishAttachment?.();
7920
8077
  return;
7921
8078
  }
7922
8079
  if (ws.readyState !== WebSocket.OPEN) return;
@@ -7926,9 +8083,6 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
7926
8083
  }
7927
8084
  ws.send(new Uint8Array(data).buffer);
7928
8085
  };
7929
- const forward = (bytes) => {
7930
- if (ws.readyState === WebSocket.OPEN) ws.send(new Uint8Array(bytes).buffer);
7931
- };
7932
8086
  const pasteClipboardImage = async (keystroke) => {
7933
8087
  const image = await readClipboardImage();
7934
8088
  if (image === null) {
@@ -7947,36 +8101,88 @@ var attachTty = async (config, sessionId, harness, from, processId, options) =>
7947
8101
  process.stdout.write("\x07");
7948
8102
  }
7949
8103
  };
8104
+ const connect2 = async () => {
8105
+ let url;
8106
+ try {
8107
+ url = await socketUrl(
8108
+ config,
8109
+ "tty",
8110
+ processId === void 0 ? { session: sessionId } : { process: processId },
8111
+ { from: from.toString() },
8112
+ connecting.signal
8113
+ );
8114
+ } catch {
8115
+ return connecting.signal.aborted ? "aborted" : "unavailable";
8116
+ }
8117
+ if (connecting.signal.aborted) return "aborted";
8118
+ const socket = new WebSocket(url);
8119
+ socket.binaryType = "arraybuffer";
8120
+ const opened = await new Promise((resolve7) => {
8121
+ const onAbort = () => resolve7(false);
8122
+ connecting.signal.addEventListener("abort", onAbort, { once: true });
8123
+ const settle = (value) => {
8124
+ connecting.signal.removeEventListener("abort", onAbort);
8125
+ resolve7(value);
8126
+ };
8127
+ socket.addEventListener("open", () => settle(true), { once: true });
8128
+ socket.addEventListener("error", () => settle(false), { once: true });
8129
+ });
8130
+ if (opened && !connecting.signal.aborted) return socket;
8131
+ socket.close();
8132
+ return connecting.signal.aborted ? "aborted" : "unavailable";
8133
+ };
8134
+ const started = Date.now();
8135
+ let connectingShown = false;
8136
+ const connectingTimer = setInterval(() => {
8137
+ if (process.stdout.isTTY !== true) return;
8138
+ connectingShown = true;
8139
+ process.stdout.write(connectingLine(sessionId, Date.now() - started));
8140
+ }, 1e3);
8141
+ const deadline = setTimeout(() => abort("no-answer"), ATTACH_CONNECT_TIMEOUT_MS);
8142
+ const stopConnecting = () => {
8143
+ for (const timer of [connectingTimer, deadline]) clearTimeout(timer);
8144
+ if (connectingShown) process.stdout.write("\r\x1B[2K");
8145
+ connectingShown = false;
8146
+ };
8147
+ let stopHerdrHint = noHerdrHint;
7950
8148
  try {
7951
- sendResize();
7952
- process.stdout.on("resize", onWinch);
7953
8149
  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();
8150
+ if (interactive) {
7960
8151
  process.stdin.on("data", onKeys);
8152
+ process.stdin.resume();
7961
8153
  }
8154
+ const connected = await connect2();
8155
+ stopConnecting();
8156
+ if (connected === "unavailable") return "unavailable";
8157
+ if (connected === "aborted") return abortReason ?? "interrupted";
8158
+ ws = connected;
8159
+ stopHerdrHint = hintHerdrAttachment(harness);
8160
+ const onClose = () => finishAttachment?.();
8161
+ if (ws.readyState === WebSocket.CLOSED) finishAttachment?.();
8162
+ else ws.addEventListener("close", onClose, { once: true });
7962
8163
  ws.addEventListener("message", onTtyFrame);
8164
+ const size = terminalSize();
8165
+ if (size.rows > 1) sendResize({ cols: size.cols, rows: size.rows - 1 });
8166
+ setTimeout(() => sendResize(), 80);
8167
+ process.stdout.on("resize", onWinch);
7963
8168
  await finished;
7964
8169
  return detached ? "detached" : interrupted ? "interrupted" : sawEnd ? "ended" : "dropped";
7965
8170
  } finally {
8171
+ stopConnecting();
7966
8172
  for (const signal of signals) process.off(signal, onSignal);
7967
8173
  process.stdin.off("data", onKeys);
7968
8174
  process.stdout.off("resize", onWinch);
7969
- ws.removeEventListener("message", onTtyFrame);
7970
- ws.removeEventListener("close", onClose);
8175
+ ws?.removeEventListener("message", onTtyFrame);
7971
8176
  if (rawModeEnabled) process.stdin.setRawMode(false);
7972
- if (options?.readOnly !== true && process.stdout.isTTY === true) {
8177
+ if (interactive && ws !== null && process.stdout.isTTY === true) {
7973
8178
  process.stdout.write(
7974
8179
  "\x1B[<u\x1B[=0;1u\x1B[?2004l\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1006l\x1B[?1049l\x1B[?25h"
7975
8180
  // show the cursor
7976
8181
  );
7977
8182
  }
7978
8183
  process.stdin.pause();
7979
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close();
8184
+ if (ws !== null && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING))
8185
+ ws.close();
7980
8186
  stopHerdrHint();
7981
8187
  }
7982
8188
  };
@@ -8004,11 +8210,18 @@ var stopAndExit = async (config, sessionId) => {
8004
8210
  say6(`${amber2("could not stop")} \u2014 the session may still run \xB7 mend stop ${id8}`);
8005
8211
  process.exit(1);
8006
8212
  };
8213
+ 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
8214
  var finishAttach = async (config, sessionId, outcome, mode = "background") => {
8008
8215
  const id8 = sessionId.slice(0, 8);
8009
8216
  if (outcome === "unavailable") {
8010
8217
  return fail6(`tty attach unavailable: could not connect to ${config.url}`);
8011
8218
  }
8219
+ if (outcome === "no-answer") {
8220
+ say6("");
8221
+ say6(noAnswerLine(config, id8));
8222
+ if (mode === "foreground") return stopAndExit(config, sessionId);
8223
+ process.exit(1);
8224
+ }
8012
8225
  if (outcome === "detached") {
8013
8226
  say6("");
8014
8227
  say6(`${amber2("detached")} \u2014 the session keeps running; reattach: mend attach ${id8}`);
@@ -8070,6 +8283,9 @@ var attachPicked = async (config, session, tunnels) => {
8070
8283
  say6(
8071
8284
  `${green6("\u2713")} attaching to ${sessionDisplayName(session)} \xB7 ${session.harness} ${dim6(session.id.slice(0, 8))}${detachHint()}`
8072
8285
  );
8286
+ if (session.status === "starting") {
8287
+ await startAndFollow(config, session.id, "starting \u2014 attaching once the agent runs\u2026", null);
8288
+ }
8073
8289
  await tunnels?.start(session.id);
8074
8290
  say6("");
8075
8291
  const outcome = await attachWithTunnels(
@@ -8082,9 +8298,11 @@ var attachPicked = async (config, session, tunnels) => {
8082
8298
  const detail = await api(config, "GET", `/sessions/${session.id}`);
8083
8299
  if (detail.currentAgent?.kind === "agent-protocol" && agentIsLive(detail.session, detail.currentAgent)) {
8084
8300
  say6(`${amber2("taking over")} from the protocol session`);
8085
- await withSpinner(
8301
+ await startAndFollow(
8302
+ config,
8303
+ session.id,
8086
8304
  "reopening as a terminal \u2014 same conversation\u2026",
8087
- api(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
8305
+ request(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
8088
8306
  );
8089
8307
  say6("");
8090
8308
  await attachOrExit(config, session.id, session.harness, "background", tunnels);
@@ -8240,6 +8458,10 @@ var shellCommand = async (config, args) => {
8240
8458
  return fail6(`tty attach unavailable: could not connect to ${config.url}`);
8241
8459
  }
8242
8460
  say6("");
8461
+ if (outcome === "no-answer") {
8462
+ say6(noAnswerLine(config, session.id.slice(0, 8)));
8463
+ process.exit(1);
8464
+ }
8243
8465
  if (outcome === "detached" || outcome === "interrupted" || outcome === "dropped") {
8244
8466
  say6(`${amber2("detached")} \u2014 the shell keeps running and holds the workspace open`);
8245
8467
  process.exit(0);
@@ -9627,9 +9849,11 @@ var completionsCommand = (args) => {
9627
9849
  }
9628
9850
  };
9629
9851
  var supervisedRun = async (config, session, argv) => {
9630
- const launched = await withSpinner(
9852
+ const launched = await startAndFollow(
9853
+ config,
9854
+ session.id,
9631
9855
  "provisioning workspace \u2014 a first launch builds the harness image (can take minutes)\u2026",
9632
- api(config, "POST", `/sessions/${session.id}/launch`, { argv })
9856
+ request(config, "POST", `/sessions/${session.id}/launch`, { argv })
9633
9857
  );
9634
9858
  say6(
9635
9859
  `${green6("\u2713 recording")} \xB7 run ${dim6(launched.id.slice(0, 8))} \xB7 workspace mounts the worktree`
@@ -9765,9 +9989,13 @@ var resumeCommand = async (config, args) => {
9765
9989
  say6(`${cobalt2(" watch")} \xB7 ${config.url}/sessions/${match.id}`);
9766
9990
  const priorAgent = detail.annotations.find((annotation) => annotation.sessionId === match.id)?.currentAgent ?? null;
9767
9991
  const protocolPrior = priorAgent?.kind === "agent-protocol" && withHarness === null;
9768
- await withSpinner(
9992
+ await startAndFollow(
9993
+ config,
9994
+ match.id,
9769
9995
  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 })
9996
+ protocolPrior ? request(config, "POST", `/sessions/${match.id}/handoff`, { to: "pty" }) : request(config, "POST", `/sessions/${match.id}/resume`, {
9997
+ harness: withHarness
9998
+ })
9771
9999
  );
9772
10000
  say6(`${green6("\u2713 recording")} \xB7 same worktree, conversation restored${detachHint()}`);
9773
10001
  say6("");
@@ -9775,17 +10003,18 @@ var resumeCommand = async (config, args) => {
9775
10003
  exitAfterSessionEnd(config, match.id);
9776
10004
  };
9777
10005
  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) {
10006
+ const outcome = await followStarting(
10007
+ config,
10008
+ sessionId,
10009
+ label,
10010
+ request(config, "POST", `/sessions/${sessionId}/resume`, { harness: null })
10011
+ );
10012
+ if (outcome.kind === "refused") {
9785
10013
  const refreshed = await api(config, "GET", `/sessions/${sessionId}`);
9786
10014
  if (agentIsLive(refreshed.session, refreshed.currentAgent)) return false;
9787
- return fail6(error instanceof Error ? error.message : String(error));
9788
10015
  }
10016
+ startedOrExit(config, sessionId, outcome);
10017
+ return true;
9789
10018
  };
9790
10019
  var rejoinCommand = async (config, args) => {
9791
10020
  const harnessFlag = args.indexOf("--harness");
@@ -9816,9 +10045,11 @@ var rejoinCommand = async (config, args) => {
9816
10045
  let restored = false;
9817
10046
  const currentAgent = detail.annotations.find((annotation) => annotation.sessionId === session.id)?.currentAgent ?? null;
9818
10047
  if (currentAgent?.kind === "agent-protocol") {
9819
- await withSpinner(
10048
+ await startAndFollow(
10049
+ config,
10050
+ session.id,
9820
10051
  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" })
10052
+ request(config, "POST", `/sessions/${session.id}/handoff`, { to: "pty" })
9822
10053
  );
9823
10054
  restored = true;
9824
10055
  } else if (!alreadyLive) {
@@ -10149,7 +10380,7 @@ var dashboard = async (config, options = {}) => {
10149
10380
  );
10150
10381
  process.exit(rerun.status ?? 0);
10151
10382
  }
10152
- const { runDashboard } = await import("./dashboard-OADX3PHI.js");
10383
+ const { runDashboard } = await import("./dashboard-DWXXYFPE.js");
10153
10384
  const agentShare = await startShareIfBridge(config);
10154
10385
  const tunnels = options.noTunnel === true || serverIsLocal(config) ? null : createServiceTunnels({
10155
10386
  listServices: () => fetchServices(config),
@@ -1,4 +1,4 @@
1
- .TH MEND-ACCOUNTS 1 "" "mend 0.34.2" "mend manual"
1
+ .TH MEND-ACCOUNTS 1 "" "mend 0.34.4" "mend manual"
2
2
  .SH NAME
3
3
  mend\-accounts \- your connected accounts on the platform
4
4
  .SH SYNOPSIS
package/man/mend-adopt.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-ADOPT 1 "" "mend 0.34.2" "mend manual"
1
+ .TH MEND-ADOPT 1 "" "mend 0.34.4" "mend manual"
2
2
  .SH NAME
3
3
  mend\-adopt \- adopt a repository into the store
4
4
  .SH SYNOPSIS
package/man/mend-attach.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH MEND-ATTACH 1 "" "mend 0.34.2" "mend manual"
1
+ .TH MEND-ATTACH 1 "" "mend 0.34.4" "mend manual"
2
2
  .SH NAME
3
3
  mend\-attach \- reattach this terminal to a running session
4
4
  .SH SYNOPSIS