linkshell-cli 0.2.34 → 0.2.35

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.
@@ -15,7 +15,6 @@ import {
15
15
  import type { Envelope } from "@linkshell/protocol";
16
16
  import type { ProviderConfig } from "../providers.js";
17
17
  import { ScrollbackBuffer } from "./scrollback.js";
18
- import { AcpRelay } from "./acp-relay.js";
19
18
  import { ScreenFallback } from "./screen-fallback.js";
20
19
  import { ScreenShare } from "./screen-share.js";
21
20
  import { getLanIp } from "../utils/lan-ip.js";
@@ -42,9 +41,7 @@ const DEFAULT_TERMINAL_ID = "default";
42
41
 
43
42
  interface TerminalInstance {
44
43
  id: string;
45
- pty: pty.IPty | null;
46
- acpRelay: AcpRelay | null;
47
- mode: "pty" | "acp";
44
+ pty: pty.IPty;
48
45
  cwd: string;
49
46
  projectName: string;
50
47
  provider: string;
@@ -284,29 +281,21 @@ export class BridgeSession {
284
281
  case "terminal.input": {
285
282
  const p = parseTypedPayload("terminal.input", envelope.payload);
286
283
  const term = this.terminals.get(tid);
287
- if (term && term.status === "running" && term.pty) term.pty.write(p.data);
284
+ if (term && term.status === "running") term.pty.write(p.data);
288
285
  break;
289
286
  }
290
287
  case "terminal.resize": {
291
288
  const p = parseTypedPayload("terminal.resize", envelope.payload);
292
289
  const term = this.terminals.get(tid);
293
- if (term && term.status === "running" && term.pty) term.pty.resize(p.cols, p.rows);
294
- break;
295
- }
296
- case "acp.rpc": {
297
- const term = this.terminals.get(tid);
298
- if (term && term.status === "running" && term.acpRelay) {
299
- term.acpRelay.write(envelope.payload);
300
- }
290
+ if (term && term.status === "running") term.pty.resize(p.cols, p.rows);
301
291
  break;
302
292
  }
303
293
  case "terminal.spawn": {
304
294
  const p = parseTypedPayload("terminal.spawn", envelope.payload);
305
295
  const normalizedCwd = resolve(p.cwd);
306
- const mode = p.mode ?? "pty";
307
- // Dedup: if a running terminal already exists for this cwd and mode, return it
296
+ // Dedup: if a running terminal already exists for this cwd, return it
308
297
  const existing = [...this.terminals.values()].find(
309
- (t) => t.status === "running" && resolve(t.cwd) === normalizedCwd && t.mode === mode,
298
+ (t) => t.status === "running" && resolve(t.cwd) === normalizedCwd,
310
299
  );
311
300
  if (existing) {
312
301
  this.send(createEnvelope({
@@ -318,7 +307,7 @@ export class BridgeSession {
318
307
  } else {
319
308
  const newId = `term-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
320
309
  try {
321
- await this.spawnTerminal(newId, normalizedCwd, p.provider, mode);
310
+ await this.spawnTerminal(newId, normalizedCwd, p.provider);
322
311
  this.send(createEnvelope({
323
312
  type: "terminal.spawned",
324
313
  sessionId: this.sessionId,
@@ -342,8 +331,7 @@ export class BridgeSession {
342
331
  const p = parseTypedPayload("terminal.kill", envelope.payload);
343
332
  const term = this.terminals.get(p.terminalId);
344
333
  if (term && term.status === "running") {
345
- if (term.pty) term.pty.kill();
346
- if (term.acpRelay) term.acpRelay.kill();
334
+ term.pty.kill();
347
335
  }
348
336
  break;
349
337
  }
@@ -425,7 +413,7 @@ export class BridgeSession {
425
413
  writeFileSync(tempPath, Buffer.from(p.data, "base64"));
426
414
  this.log(`image saved to ${tempPath}`);
427
415
  const term = this.terminals.get(tid);
428
- if (term && term.status === "running" && term.pty) {
416
+ if (term && term.status === "running") {
429
417
  term.pty.write(`\x1b[200~${tempPath}\x1b[201~`);
430
418
  }
431
419
  break;
@@ -442,7 +430,6 @@ export class BridgeSession {
442
430
  projectName: t.projectName,
443
431
  provider: t.provider,
444
432
  status: t.status,
445
- mode: t.mode,
446
433
  }));
447
434
  this.send(createEnvelope({
448
435
  type: "terminal.list",
@@ -473,7 +460,7 @@ export class BridgeSession {
473
460
  }
474
461
  }
475
462
 
476
- private async spawnTerminal(terminalId: string, cwd: string, providerOverride?: string, mode: "pty" | "acp" = "pty"): Promise<void> {
463
+ private async spawnTerminal(terminalId: string, cwd: string, providerOverride?: string): Promise<void> {
477
464
  const cleanEnv: Record<string, string> = {};
478
465
  for (const [k, v] of Object.entries(this.options.providerConfig.env)) {
479
466
  if (v !== undefined) cleanEnv[k] = v;
@@ -482,58 +469,6 @@ export class BridgeSession {
482
469
  const provider = providerOverride ?? this.options.providerConfig.provider;
483
470
  const args = [...this.options.providerConfig.args];
484
471
 
485
- // ACP mode: spawn as child process with JSON-RPC over stdio
486
- if (mode === "acp") {
487
- const term: TerminalInstance = {
488
- id: terminalId,
489
- pty: null,
490
- acpRelay: new AcpRelay({
491
- command: this.options.providerConfig.command,
492
- args,
493
- cwd,
494
- env: cleanEnv,
495
- sessionId: this.sessionId,
496
- terminalId,
497
- send: (envelope) => this.send(envelope),
498
- log: (msg) => this.log(msg),
499
- }),
500
- mode: "acp",
501
- cwd,
502
- projectName: basename(cwd),
503
- provider,
504
- scrollback: new ScrollbackBuffer(1000),
505
- outputSeq: 0,
506
- statusSeq: 0,
507
- status: "running",
508
- };
509
-
510
- term.acpRelay!.onExit((exitCode, signal) => {
511
- term.status = "exited";
512
- this.send(createEnvelope({
513
- type: "terminal.exit",
514
- sessionId: this.sessionId,
515
- terminalId,
516
- payload: { exitCode, signal: signal ? parseInt(signal, 10) || 0 : 0 },
517
- }));
518
- this.sendTerminalList();
519
-
520
- const allExited = [...this.terminals.values()].every((t) => t.status === "exited");
521
- if (allExited) {
522
- this.exited = true;
523
- setTimeout(() => {
524
- this.stopHeartbeat();
525
- this.socket?.close();
526
- }, 500);
527
- process.exitCode = exitCode ?? 0;
528
- }
529
- });
530
-
531
- this.terminals.set(terminalId, term);
532
- this.log(`spawned ACP terminal ${terminalId} in ${cwd}`);
533
- return;
534
- }
535
-
536
- // PTY mode: existing behavior
537
472
  // Set up hook server for structured status (all supported providers)
538
473
  // For "custom" shell, set up hooks for all providers since user may launch any of them
539
474
  let hookServer: http.Server | undefined;
@@ -570,8 +505,6 @@ export class BridgeSession {
570
505
  env: cleanEnv,
571
506
  },
572
507
  ),
573
- acpRelay: null,
574
- mode: "pty",
575
508
  cwd,
576
509
  projectName: basename(cwd),
577
510
  provider,
@@ -584,7 +517,7 @@ export class BridgeSession {
584
517
  hookConfigPath,
585
518
  };
586
519
 
587
- term.pty!.onData((data) => {
520
+ term.pty.onData((data) => {
588
521
  const seq = term.outputSeq++;
589
522
  const envelope = createEnvelope({
590
523
  type: "terminal.output",
@@ -603,7 +536,7 @@ export class BridgeSession {
603
536
  this.send(envelope);
604
537
  });
605
538
 
606
- term.pty!.onExit(({ exitCode, signal }) => {
539
+ term.pty.onExit(({ exitCode, signal }) => {
607
540
  term.status = "exited";
608
541
  this.cleanupHookServer(term);
609
542
  this.send(createEnvelope({
@@ -1191,10 +1124,7 @@ export class BridgeSession {
1191
1124
  this.socket = undefined;
1192
1125
  for (const term of this.terminals.values()) {
1193
1126
  this.cleanupHookServer(term);
1194
- if (term.status === "running") {
1195
- if (term.pty) term.pty.kill();
1196
- if (term.acpRelay) term.acpRelay.kill();
1197
- }
1127
+ if (term.status === "running") term.pty.kill();
1198
1128
  }
1199
1129
  this.terminals.clear();
1200
1130
  process.exitCode = exitCode;
@@ -1,92 +0,0 @@
1
- import { spawn, type ChildProcess } from "node:child_process";
2
- import { createInterface } from "node:readline";
3
- import { createEnvelope, serializeEnvelope } from "@linkshell/protocol";
4
- import type { Envelope } from "@linkshell/protocol";
5
-
6
- export interface AcpRelayOptions {
7
- command: string;
8
- args: string[];
9
- cwd: string;
10
- env: Record<string, string>;
11
- sessionId: string;
12
- terminalId: string;
13
- send: (envelope: Envelope) => void;
14
- log: (msg: string) => void;
15
- }
16
-
17
- export class AcpRelay {
18
- private child: ChildProcess;
19
- private readonly options: AcpRelayOptions;
20
- private exited = false;
21
-
22
- constructor(options: AcpRelayOptions) {
23
- this.options = options;
24
-
25
- this.child = spawn(options.command, options.args, {
26
- cwd: options.cwd,
27
- env: options.env,
28
- stdio: ["pipe", "pipe", "pipe"],
29
- });
30
-
31
- // Read newline-delimited JSON from stdout
32
- const rl = createInterface({ input: this.child.stdout! });
33
- rl.on("line", (line) => {
34
- if (!line.trim()) return;
35
- try {
36
- const msg = JSON.parse(line);
37
- // Wrap JSON-RPC message in acp.rpc envelope and send to gateway
38
- this.options.send(createEnvelope({
39
- type: "acp.rpc",
40
- sessionId: this.options.sessionId,
41
- terminalId: this.options.terminalId,
42
- payload: msg,
43
- }));
44
- } catch (e) {
45
- this.options.log(`acp-relay: failed to parse stdout line: ${e}`);
46
- }
47
- });
48
-
49
- // Log stderr
50
- this.child.stderr?.on("data", (chunk: Buffer) => {
51
- this.options.log(`acp-relay stderr: ${chunk.toString().trimEnd()}`);
52
- });
53
-
54
- this.child.on("exit", (code, signal) => {
55
- this.exited = true;
56
- this.options.log(`acp-relay: process exited (code=${code}, signal=${signal})`);
57
- });
58
-
59
- this.child.on("error", (err) => {
60
- this.options.log(`acp-relay: process error: ${err.message}`);
61
- });
62
- }
63
-
64
- /** Forward an acp.rpc envelope payload to the child's stdin */
65
- write(payload: unknown): void {
66
- if (this.exited || !this.child.stdin?.writable) return;
67
- try {
68
- const line = JSON.stringify(payload) + "\n";
69
- this.child.stdin.write(line);
70
- } catch (e) {
71
- this.options.log(`acp-relay: failed to write to stdin: ${e}`);
72
- }
73
- }
74
-
75
- kill(): void {
76
- if (!this.exited) {
77
- this.child.kill("SIGTERM");
78
- }
79
- }
80
-
81
- get isExited(): boolean {
82
- return this.exited;
83
- }
84
-
85
- get pid(): number | undefined {
86
- return this.child.pid;
87
- }
88
-
89
- onExit(cb: (code: number | null, signal: string | null) => void): void {
90
- this.child.on("exit", cb);
91
- }
92
- }