@rind-ai/cli 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,12 @@
1
1
  import { wrapTextWithAnsi } from "../text-width.js";
2
2
  import {
3
3
  codeOpenLabel,
4
+ consumeTableLine,
5
+ createTableState,
6
+ finishTableState,
4
7
  dim,
5
8
  isPlainLine,
6
- isTableLine,
7
- parseTableRow,
8
- renderInline,
9
+ renderTableBlock,
9
10
  renderMarkdownishLine,
10
11
  styled,
11
12
  } from "../markdown-lines.js";
@@ -18,6 +19,7 @@ export class AssistantMessage {
18
19
  this.finalized = [];
19
20
  this.pending = "";
20
21
  this.inCodeBlock = false;
22
+ this.tableState = createTableState();
21
23
  this.cacheWidth = -1;
22
24
  this.cacheItems = [];
23
25
  this.cacheLines = null;
@@ -32,7 +34,9 @@ export class AssistantMessage {
32
34
  }
33
35
  const line = this.pending.slice(0, newlineIndex);
34
36
  this.pending = this.pending.slice(newlineIndex + 1);
35
- this.classifyFinalize(line);
37
+ if (this.classifyFinalize(line)) {
38
+ this.cacheItems.length = 0;
39
+ }
36
40
  }
37
41
  this.cacheLines = null;
38
42
  }
@@ -43,18 +47,48 @@ export class AssistantMessage {
43
47
  this.pending = "";
44
48
  this.classifyFinalize(line);
45
49
  }
50
+ this.flushTableState();
51
+ this.cacheItems.length = 0;
46
52
  this.cacheLines = null;
47
53
  }
48
54
 
49
55
  get isEmpty() {
50
- return !this.finalized.length && !this.pending;
56
+ return !this.finalized.length && !this.pending && !this.tableState.candidate.length && !this.tableState.rows;
51
57
  }
52
58
 
53
59
  classifyFinalize(line) {
54
- if (isTableLine(line, this.inCodeBlock)) {
55
- this.finalizeTable(line);
56
- return;
60
+ const result = consumeTableLine(this.tableState, line, this.inCodeBlock);
61
+ if (result.type === "hold") {
62
+ return false;
63
+ }
64
+ if (result.type === "start") {
65
+ for (const line of result.lines || []) {
66
+ this.classifyNonTable(line);
67
+ }
68
+ this.pushItem({ kind: "table", rows: this.tableState.rows });
69
+ return true;
70
+ }
71
+ if (result.type === "append") {
72
+ return true;
73
+ }
74
+ if (result.type === "flush") {
75
+ this.classifyNonTable(result.line);
76
+ return true;
57
77
  }
78
+ if (result.type === "flush_candidate") {
79
+ for (const candidate of result.lines) {
80
+ this.classifyNonTable(candidate);
81
+ }
82
+ if (result.line !== undefined) {
83
+ this.classifyNonTable(result.line);
84
+ }
85
+ return false;
86
+ }
87
+ this.classifyNonTable(line);
88
+ return false;
89
+ }
90
+
91
+ classifyNonTable(line) {
58
92
  if (line.trim().startsWith("```")) {
59
93
  this.finalizeCodeFence(line);
60
94
  return;
@@ -74,12 +108,16 @@ export class AssistantMessage {
74
108
  this.pushItem({ kind: "markdown", raw: line });
75
109
  }
76
110
 
77
- finalizeTable(line) {
78
- const cells = parseTableRow(line);
79
- if (!cells.length || cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()))) {
111
+ flushTableState() {
112
+ const result = finishTableState(this.tableState);
113
+ if (!result) {
80
114
  return;
81
115
  }
82
- this.pushItem({ kind: "table", raw: line });
116
+ if (result.type === "flush_candidate") {
117
+ for (const line of result.lines) {
118
+ this.classifyNonTable(line);
119
+ }
120
+ }
83
121
  }
84
122
 
85
123
  finalizeCodeFence(line) {
@@ -95,15 +133,10 @@ export class AssistantMessage {
95
133
  this.finalized.push(item);
96
134
  }
97
135
 
98
- styleItem(item) {
136
+ styleItem(item, width) {
99
137
  switch (item.kind) {
100
- case "table": {
101
- const cells = parseTableRow(item.raw);
102
- const rendered = cells.map((cell, index) =>
103
- renderInline(cell, this.color, index === 0 ? "tableHeader" : "")
104
- );
105
- return rendered.join(dim(" | ", this.color));
106
- }
138
+ case "table":
139
+ return renderTableBlock(item.rows, this.color, Math.max(1, width - CONTENT_PREFIX.length));
107
140
  case "fence":
108
141
  return dim(item.label ? codeOpenLabel(item.label) : "└ end", this.color);
109
142
  case "code":
@@ -125,7 +158,7 @@ export class AssistantMessage {
125
158
  }
126
159
  while (this.cacheItems.length < this.finalized.length) {
127
160
  const item = this.finalized[this.cacheItems.length];
128
- const logical = typeof item === "string" ? item : this.styleItem(item);
161
+ const logical = typeof item === "string" ? item : this.styleItem(item, width);
129
162
  this.cacheItems.push(this.wrapLogical(logical, width));
130
163
  }
131
164
  const lines = [];
@@ -1,8 +1,8 @@
1
+ import { ANSI_SEQUENCE } from "./text-width.js";
1
2
  import { graphemes, stripAnsi, textWidth, wrapTextCells } from "./text-width.js";
2
3
 
3
4
  const DEFAULT_COLUMNS = 80;
4
5
  const INPUT_MARKER = "\n ▷ ";
5
- const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
6
6
  const SGR_SEQUENCE = /^\x1b\[([0-9;]*)m$/;
7
7
 
8
8
  export function prepareComposerFrame(frame = {}, columns = DEFAULT_COLUMNS) {
@@ -3,7 +3,6 @@ import {
3
3
  cancelledText,
4
4
  contextBuiltLine,
5
5
  errorLine,
6
- goalContinuedLine,
7
6
  planUpdatedLine,
8
7
  turnCompletedLine,
9
8
  } from "./rendering.js";
@@ -32,8 +31,14 @@ export function createEventController({
32
31
  }
33
32
  switch (eventType) {
34
33
  case "assistant_delta":
34
+ output.setActivityLabel?.("Working");
35
35
  output.assistantAppend?.(event.text || "");
36
36
  return;
37
+ case "turn_step_retry": {
38
+ const attempt = Number(event.attempt);
39
+ output.setActivityLabel?.(attempt > 0 ? `Retrying ${attempt}` : "Retrying");
40
+ return;
41
+ }
37
42
  case "context_built": {
38
43
  if (output.handleContextBuilt?.(event)) {
39
44
  output.resetContextUsage?.();
@@ -113,19 +118,12 @@ export function createEventController({
113
118
  await input.answerQuestion?.(event);
114
119
  return;
115
120
  case "queued_input_delivered":
116
- output.setGoalChasing?.(false);
117
121
  output.deliverQueuedInput?.(event.input || "", event.mode || "steering", event.input_id || "");
118
122
  return;
119
- case "goal_continued":
120
- output.closeAssistant?.();
121
- output.setGoalChasing?.(true);
122
- output.log?.(() => goalContinuedLine(event.round));
123
- return;
124
123
  case "turn_failed":
125
124
  output.clearQueuedInputs?.();
126
125
  output.clearCompactContext?.();
127
126
  output.closeAssistant?.();
128
- output.setGoalChasing?.(false);
129
127
  output.log?.(() => errorLine(event.error));
130
128
  resetTurnState();
131
129
  return;
@@ -133,7 +131,6 @@ export function createEventController({
133
131
  output.clearQueuedInputs?.();
134
132
  output.clearCompactContext?.();
135
133
  output.closeAssistant?.();
136
- output.setGoalChasing?.(false);
137
134
  output.log?.(() => cancelledText());
138
135
  resetTurnState();
139
136
  return;
@@ -141,8 +138,9 @@ export function createEventController({
141
138
  output.clearQueuedInputs?.();
142
139
  output.clearCompactContext?.();
143
140
  output.closeAssistant?.();
144
- output.setGoalChasing?.(false);
145
- output.log?.(turnCompletedLine(event, toolStats));
141
+ if (state.activeGoal?.status !== "active") {
142
+ output.log?.(turnCompletedLine(event, toolStats));
143
+ }
146
144
  resetTurnState();
147
145
  return;
148
146
  default:
@@ -16,6 +16,7 @@ import {
16
16
  } from "./runtime-protocol.js";
17
17
  import { executeLocalSlashCommand, loadLocalSettings } from "./local-slash-commands.js";
18
18
  import { loadCliState, saveCliState } from "./cli-state-store.js";
19
+ import { loadPromptHistory, savePromptHistory } from "./prompt-history-store.js";
19
20
  import { setTheme } from "./theme.js";
20
21
  import { createTurnController } from "./turn-controller.js";
21
22
  import { createCommandController } from "./command-controller.js";
@@ -30,6 +31,8 @@ import { createCliRuntimeController } from "./cli-runtime-controller.js";
30
31
  import { createCliOutputController } from "./cli-output-controller.js";
31
32
  import { createCliInputActions } from "./cli-input-actions.js";
32
33
  import { cliHelp, oneShotHelp, runOneShot } from "./one-shot.js";
34
+ import { runSend, sendHelp } from "./send.js";
35
+ import { listenIpc } from "./ipc.js";
33
36
  import { createTui } from "./tui/tui.js";
34
37
  import { Container } from "./tui/component.js";
35
38
  import { ComposerArea } from "./components/composer-area.js";
@@ -72,6 +75,19 @@ if (cliArgs[0] === "run" && cliArgs.some((arg) => arg === "--help" || arg === "-
72
75
  process.stdout.write(`${oneShotHelp}\n`);
73
76
  return;
74
77
  }
78
+ if (cliArgs[0] === "send" && cliArgs.some((arg) => arg === "--help" || arg === "-h")) {
79
+ process.stdout.write(`${sendHelp}\n`);
80
+ return;
81
+ }
82
+ if (cliArgs[0] === "send") {
83
+ try {
84
+ process.exitCode = await runSend({ args: cliArgs });
85
+ } catch (error) {
86
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
87
+ process.exitCode = 2;
88
+ }
89
+ return;
90
+ }
75
91
  if (cliArgs.some((arg) => arg === "--version" || arg === "--help" || arg === "-h")) {
76
92
  if (cliArgs.includes("--help") || cliArgs.includes("-h")) {
77
93
  process.stdout.write(`${cliHelp}\n\n`);
@@ -102,7 +118,9 @@ const sessionState = cliState.session;
102
118
  const turnStateData = cliState.turn;
103
119
  const inputStateData = cliState.input;
104
120
  const displayState = cliState.display;
121
+ const promptHistory = loadPromptHistory();
105
122
  let input = null;
123
+ let ipcServer = null;
106
124
  const compactContextState = createCompactContextState();
107
125
  const isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
108
126
  const tui = isTty
@@ -143,6 +161,7 @@ const {
143
161
  writeError: writeErrorOutput,
144
162
  closeAssistant,
145
163
  renderHistory,
164
+ setTurnContext,
146
165
  } = outputController;
147
166
 
148
167
  const runtimeClient = createRuntimeClient({
@@ -168,6 +187,10 @@ const runtimeClient = createRuntimeClient({
168
187
  displayState.lastEventSequence = 0;
169
188
  turnStateData.active = false;
170
189
  turnStateData.interruptRequested = false;
190
+ displayState.activityLabel = "";
191
+ displayState.lastTurnId = "";
192
+ setTurnContext("");
193
+ clearActivityTimer();
171
194
  inputActions?.clearPendingInputs();
172
195
  if (!wasClosing) {
173
196
  runtimeState.failure = error;
@@ -206,15 +229,17 @@ const runtimeController = createCliRuntimeController({
206
229
  requireInitialization: requireRuntimeInitialization,
207
230
  state: cliState,
208
231
  getCommands: () => commandController,
209
- getTurnController: () => turnController,
210
232
  getTaskMonitor: () => taskMonitorController,
211
233
  getCompactContextState: () => compactContextState,
212
234
  askModelMenu: (...args) => inputActions.askModelMenu(...args),
213
235
  askEffortMenu: (...args) => inputActions.askEffortMenu(...args),
214
236
  askSessionMenu: (...args) => inputActions.askSessionMenu(...args),
237
+ askForkPointMenu: (...args) => inputActions.askForkPointMenu(...args),
215
238
  askTeamBlueprint: (...args) => inputActions.askTeamBlueprint(...args),
239
+ askContextBoard: (...args) => inputActions.askContextBoard(...args),
216
240
  restoreLiveTurn,
217
241
  renderHistory,
242
+ onSessionRestored: rebindSendEndpoint,
218
243
  clearPendingInputs: (...args) => inputActions.clearPendingInputs(...args),
219
244
  closeAssistant,
220
245
  refreshInputState,
@@ -227,7 +252,6 @@ const request = runtimeController.request;
227
252
  turnController = createTurnController({
228
253
  request,
229
254
  state: turnState,
230
- refreshGoalState: runtimeController.refreshGoalState,
231
255
  onTurnStart: () => {
232
256
  displayState.assistantHeaderShown = false;
233
257
  },
@@ -257,6 +281,9 @@ commandController = createCommandController({
257
281
  },
258
282
  startCompactCommand: runtimeController.startCompactCommand,
259
283
  runSessionsSelector: runtimeController.runSessionsSelector,
284
+ runForkSelector: runtimeController.runForkSelector,
285
+ runContextBoard: runtimeController.runContextBoard,
286
+ printContextReport: runtimeController.printContextReport,
260
287
  runLocalCommand: async (text) => {
261
288
  if (!Object.keys(sessionState.settings).length) {
262
289
  sessionState.settings = await loadLocalSettings(undefined, sessionState.info.workspace_root || sessionState.info.cwd || process.cwd());
@@ -322,6 +349,9 @@ const eventController = createEventController({
322
349
  get activeTurn() {
323
350
  return turnStateData.active;
324
351
  },
352
+ get activeGoal() {
353
+ return sessionState.info.goal;
354
+ },
325
355
  debug: cliArgs.includes("--debug"),
326
356
  },
327
357
  input: { answerQuestion: (...args) => inputActions.answerQuestion(...args) },
@@ -337,12 +367,10 @@ const eventController = createEventController({
337
367
  log: logOutput,
338
368
  debug: (text) => writeErrorOutput(`${text}\n`),
339
369
  updateGoal: updateGoalState,
340
- setGoalChasing: (enabled) => {
341
- displayState.goalChasing = Boolean(enabled);
342
- },
343
370
  setStats: (stats) => {
344
371
  displayState.stats = stats;
345
372
  },
373
+ setActivityLabel: outputController.setActivityLabel,
346
374
  redraw: redrawInput,
347
375
  clearCompactContext: () => compactContextState.clear(),
348
376
  deliverQueuedInput: (...args) => inputActions.deliverQueuedInput(...args),
@@ -353,7 +381,10 @@ inputActions = createCliInputActions({
353
381
  state: cliState,
354
382
  request,
355
383
  output: outputController,
384
+ promptHistory,
385
+ onPromptHistory: (history) => savePromptHistory(history),
356
386
  getTurnController: () => turnController,
387
+ getCommandController: () => commandController,
357
388
  getTaskMonitor: () => taskMonitorController,
358
389
  getLineInput: () => input,
359
390
  pausePrompt: () => inputController.pause(),
@@ -427,6 +458,26 @@ function updateGoalState(goal) {
427
458
  sessionState.info = { ...sessionState.info, goal: goal && typeof goal === "object" ? goal : null };
428
459
  redrawInput();
429
460
  }
461
+
462
+ async function rebindSendEndpoint() {
463
+ try {
464
+ await ipcServer?.close();
465
+ ipcServer = null;
466
+ const sessionId = String(sessionState.info.session_id || "");
467
+ if (!sessionId) {
468
+ return;
469
+ }
470
+ ipcServer = await listenIpc({
471
+ sessionId,
472
+ getSessionId: () => sessionState.info.session_id,
473
+ dispatch: (text) => inputActions.dispatchExternal(text),
474
+ onUnavailable: () => logOutput(`Send endpoint unavailable: another rind process owns session ${sessionId}.`),
475
+ });
476
+ } catch (error) {
477
+ ipcServer = null;
478
+ writeErrorOutput(`${error instanceof Error ? error.message : String(error)}\n`);
479
+ }
480
+ }
430
481
  function resetContextUsage() {
431
482
  displayState.stats = { context_usage_percent: 0 };
432
483
  redrawInput();
@@ -442,25 +493,57 @@ async function renderEvent(message) {
442
493
  return;
443
494
  }
444
495
  if (message?.event?.type === "turn_started") {
445
- if (turnStateData.id && String(message.turn_id || "") !== turnStateData.id) {
496
+ const nextTurnId = String(message.turn_id || "");
497
+ if (!nextTurnId || (!turnStateData.id && displayState.lastTurnId === nextTurnId)
498
+ || (turnStateData.id && nextTurnId !== turnStateData.id)) {
446
499
  return;
447
500
  }
448
- turnStateData.id = String(message.turn_id || "");
501
+ clearActivityTimer();
502
+ turnStateData.id = nextTurnId;
503
+ displayState.lastTurnId = nextTurnId;
504
+ setTurnContext(nextTurnId);
449
505
  turnStateData.active = Boolean(turnStateData.id);
506
+ turnStateData.interruptRequested = false;
507
+ displayState.activityLabel = "Working";
508
+ displayState.assistantHeaderShown = false;
509
+ refreshInputState();
450
510
  }
451
511
  const result = await eventController.handle(message);
452
512
  if (["turn_completed", "turn_failed", "turn_cancelled"].includes(message?.event?.type)) {
453
513
  turnStateData.id = "";
514
+ setTurnContext("");
515
+ displayState.activityLabel = "";
516
+ await runtimeController.refreshGoalState();
517
+ const goalActive = sessionState.info.goal?.status === "active";
518
+ turnStateData.active = goalActive;
519
+ turnStateData.interruptRequested = false;
520
+ if (!goalActive) {
521
+ clearActivityTimer();
522
+ }
523
+ refreshInputState();
454
524
  }
455
525
  return result;
456
526
  }
457
527
 
458
528
  function restoreLiveTurn(value) {
459
- if (!value || typeof value !== "object") return;
529
+ if (!value || typeof value !== "object" || String(value.status || "") !== "running") {
530
+ displayState.lastTurnId = "";
531
+ setTurnContext("");
532
+ return;
533
+ }
460
534
  const turnId = String(value.turn_id || "");
461
- if (!turnId) return;
535
+ if (!turnId) {
536
+ displayState.lastTurnId = "";
537
+ setTurnContext("");
538
+ return;
539
+ }
540
+ clearActivityTimer();
462
541
  turnStateData.id = turnId;
463
542
  turnStateData.active = true;
543
+ turnStateData.interruptRequested = false;
544
+ displayState.activityLabel = "Working";
545
+ displayState.lastTurnId = turnId;
546
+ setTurnContext(turnId);
464
547
  displayState.assistantHeaderShown = false;
465
548
  const text = String(value.assistant_text || "");
466
549
  if (text) outputController.assistantAppend(text);
@@ -471,7 +554,7 @@ function composeFrame(width = process.stdout.columns || 80) {
471
554
  if (!session) {
472
555
  return null;
473
556
  }
474
- const choiceMenu = ["model", "theme", "sessions", "team-blueprints"].includes(session.mode);
557
+ const choiceMenu = ["model", "theme", "sessions", "team-blueprints", "fork"].includes(session.mode);
475
558
  if (session.mode === "prompt" && session.menuState) {
476
559
  session.menuState.setInput(session.editor.input());
477
560
  }
@@ -500,6 +583,8 @@ function composeFrame(width = process.stdout.columns || 80) {
500
583
  }
501
584
  if (session.mode === "question") {
502
585
  const editing = session.questionState.isEditing();
586
+ session.editor.setViewportWidth(width);
587
+ const editorCursor = editing ? session.editor.cursorPosition() : null;
503
588
  const menu = questionMenuFrame(
504
589
  session.questionState.options(),
505
590
  session.questionState.selectedIndex(),
@@ -507,17 +592,18 @@ function composeFrame(width = process.stdout.columns || 80) {
507
592
  editing,
508
593
  CUSTOM_ANSWER_LABEL,
509
594
  width,
595
+ editorCursor,
510
596
  );
511
597
  return {
512
598
  showCaret,
513
599
  prompt: mainPromptText(width),
514
600
  inputText: session.question,
515
- cursor: editing ? session.editor.cursorPosition() : { line: 0, column: session.question.length },
601
+ cursor: editorCursor ?? { line: 0, column: session.question.length },
516
602
  menuText: menu.text.trimEnd(),
517
603
  menuCursor: editing ? menu.cursor : null,
518
604
  };
519
605
  }
520
- if (session.mode === "sessions") {
606
+ if (session.mode === "sessions" || session.mode === "team-blueprints" || session.mode === "fork") {
521
607
  return {
522
608
  showCaret,
523
609
  prompt: mainPromptText(width),
@@ -526,13 +612,15 @@ function composeFrame(width = process.stdout.columns || 80) {
526
612
  menuText: sessionMenuText(session.choiceState.options(), session.choiceState.selectedIndex()).trimEnd(),
527
613
  };
528
614
  }
529
- if (session.mode === "team-blueprints") {
615
+ if (session.mode === "context-board") {
530
616
  return {
531
- showCaret,
532
- prompt: mainPromptText(width),
533
- inputText: session.inputText,
534
- cursor: { line: 0, column: session.inputText.length },
535
- menuText: sessionMenuText(session.choiceState.options(), session.choiceState.selectedIndex()).trimEnd(),
617
+ showCaret: false,
618
+ prompt: "",
619
+ inputText: "",
620
+ cursor: { line: 0, column: 0 },
621
+ menuText: typeof session.board?.render === "function"
622
+ ? session.board.render(session.pageIndex, width)
623
+ : "",
536
624
  };
537
625
  }
538
626
  const matches = session.menuState
@@ -590,6 +678,7 @@ function closeRuntime() {
590
678
  runtimeState.status = "closing";
591
679
  clearActivityTimer();
592
680
  taskMonitorController.stop();
681
+ void ipcServer?.close();
593
682
  void runtimeClient.shutdown();
594
683
  closeInput();
595
684
  }
@@ -598,6 +687,7 @@ function forceCloseRuntime() {
598
687
  runtimeState.status = "closing";
599
688
  clearActivityTimer();
600
689
  taskMonitorController.stop();
690
+ void ipcServer?.close();
601
691
  closeInput();
602
692
  runtimeClient.forceShutdown();
603
693
  }
package/lib/ipc.js ADDED
@@ -0,0 +1,186 @@
1
+ import { mkdir, unlink } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import net from "node:net";
5
+
6
+ const CONNECT_TIMEOUT_MS = 2000;
7
+
8
+ export function ipcEndpointName(sessionId) {
9
+ const id = String(sessionId || "");
10
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) {
11
+ throw new Error("Invalid session id.");
12
+ }
13
+ if (process.platform === "win32") {
14
+ return `\\\\.\\pipe\\rind-${id}`;
15
+ }
16
+ return path.join(process.env.RIND_HOME || path.join(homedir(), ".rind"), "ipc", `${id}.sock`);
17
+ }
18
+
19
+ export async function listenIpc({ sessionId, getSessionId, dispatch, net: netImpl = net, onUnavailable = () => {} }) {
20
+ const endpoint = ipcEndpointName(sessionId);
21
+ const server = netImpl.createServer();
22
+ let listening = false;
23
+ let closing = false;
24
+ let settled = false;
25
+ let recovering = false;
26
+
27
+ server.on("connection", (socket) => {
28
+ socket.setEncoding("utf8");
29
+ let buffer = "";
30
+ socket.on("data", (chunk) => {
31
+ buffer += chunk;
32
+ const newlineIndex = buffer.indexOf("\n");
33
+ if (newlineIndex === -1) {
34
+ return;
35
+ }
36
+ socket.removeAllListeners("data");
37
+ respond(socket, buffer.slice(0, newlineIndex));
38
+ });
39
+ socket.on("error", () => {});
40
+ });
41
+
42
+ const ready = new Promise((resolve) => {
43
+ const settle = () => {
44
+ if (settled) {
45
+ return;
46
+ }
47
+ settled = true;
48
+ resolve();
49
+ };
50
+ server.on("listening", () => {
51
+ listening = true;
52
+ settle();
53
+ });
54
+ server.on("error", () => {
55
+ if (settled || recovering) {
56
+ return;
57
+ }
58
+ recovering = true;
59
+ void recover().then(settle, settle).finally(() => {
60
+ recovering = false;
61
+ });
62
+ });
63
+ });
64
+
65
+ function respond(socket, line) {
66
+ let input = "";
67
+ try {
68
+ const message = JSON.parse(line);
69
+ if (typeof message.input === "string") {
70
+ input = message.input;
71
+ }
72
+ } catch {
73
+ return socket.end(`${JSON.stringify({ ok: false, message: "Invalid request." })}\n`);
74
+ }
75
+ const payload = closing
76
+ ? { ok: false, message: "Session is shutting down." }
77
+ : input.trim()
78
+ ? { ok: true, session_id: String(getSessionId() || "") }
79
+ : { ok: false, message: "Empty input." };
80
+ socket.end(`${JSON.stringify(payload)}\n`);
81
+ if (payload.ok) {
82
+ dispatch(input);
83
+ }
84
+ }
85
+
86
+ async function recover() {
87
+ if (process.platform === "win32") {
88
+ onUnavailable();
89
+ return;
90
+ }
91
+ if (await probeEndpoint(netImpl, endpoint)) {
92
+ onUnavailable();
93
+ return;
94
+ }
95
+ await unlink(endpoint).catch(() => {});
96
+ await new Promise((resolve) => {
97
+ const onRetryError = () => {
98
+ onUnavailable();
99
+ resolve();
100
+ };
101
+ server.once("error", onRetryError);
102
+ server.listen(endpoint, () => {
103
+ server.removeListener("error", onRetryError);
104
+ listening = true;
105
+ resolve();
106
+ });
107
+ });
108
+ }
109
+
110
+ if (process.platform !== "win32") {
111
+ await mkdir(path.dirname(endpoint), { recursive: true }).catch(() => {});
112
+ }
113
+ server.listen(endpoint);
114
+ await ready;
115
+ return {
116
+ close() {
117
+ closing = true;
118
+ return new Promise((resolve) => {
119
+ server.close(() => {
120
+ if (process.platform !== "win32" && listening) {
121
+ void unlink(endpoint).catch(() => {});
122
+ }
123
+ resolve();
124
+ });
125
+ });
126
+ },
127
+ };
128
+ }
129
+
130
+ function probeEndpoint(netImpl, endpoint) {
131
+ return new Promise((resolve) => {
132
+ const probe = netImpl.connect(endpoint);
133
+ probe.once("connect", () => {
134
+ probe.destroy();
135
+ resolve(true);
136
+ });
137
+ probe.once("error", () => resolve(false));
138
+ });
139
+ }
140
+
141
+ export function sendIpc({ session, input, timeoutMs = CONNECT_TIMEOUT_MS, net: netImpl = net }) {
142
+ const endpoint = ipcEndpointName(session);
143
+ return new Promise((resolve) => {
144
+ const socket = netImpl.connect(endpoint);
145
+ let timer = null;
146
+ let settled = false;
147
+ let buffer = "";
148
+ const finish = (result) => {
149
+ if (settled) {
150
+ return;
151
+ }
152
+ settled = true;
153
+ if (timer) {
154
+ clearTimeout(timer);
155
+ }
156
+ socket.destroy();
157
+ resolve(result);
158
+ };
159
+ socket.setEncoding("utf8");
160
+ socket.on("connect", () => {
161
+ socket.write(`${JSON.stringify({ input: String(input || "") })}\n`);
162
+ });
163
+ socket.on("data", (chunk) => {
164
+ buffer += chunk;
165
+ const newlineIndex = buffer.indexOf("\n");
166
+ if (newlineIndex === -1) {
167
+ return;
168
+ }
169
+ let payload = null;
170
+ try {
171
+ payload = JSON.parse(buffer.slice(0, newlineIndex));
172
+ } catch {
173
+ payload = null;
174
+ }
175
+ finish(payload && typeof payload === "object" ? payload : { ok: false, message: "Invalid response from the rind session." });
176
+ });
177
+ socket.on("error", () => finish({ ok: false, message: notRunningMessage(session) }));
178
+ timer = setTimeout(() => {
179
+ finish({ ok: false, message: notRunningMessage(session) });
180
+ }, timeoutMs);
181
+ });
182
+ }
183
+
184
+ function notRunningMessage(sessionId) {
185
+ return `rind session ${sessionId} is not running — the id is shown in the target session's banner and /status.`;
186
+ }