dsh-bash-terminal-ts 0.2.6 → 0.5.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.
package/lib/terminal.d.ts CHANGED
@@ -4,14 +4,23 @@ export type TerminalSignal = (typeof TERMINAL_SIGNALS)[number];
4
4
  interface TerminalBufferRead {
5
5
  delta: string;
6
6
  nextOffset: number;
7
+ truncated: boolean;
7
8
  }
8
9
  interface TerminalBuffer {
9
10
  append(chunk: string): void;
10
11
  readFrom(offset: number): TerminalBufferRead;
11
12
  snapshot(): string;
13
+ wasTruncated(): boolean;
14
+ lastAppendAt(): number;
12
15
  }
13
16
  /** Interactive-shell argv (no -c: the terminal itself is the session). */
14
17
  export declare function terminalArgv(shell: string, paths: ResolvedPaths, distro?: string): Array<string | undefined>;
18
+ export interface TerminalSessionInfo {
19
+ sessionId: string;
20
+ shell: string;
21
+ pid: number;
22
+ closed: boolean;
23
+ }
15
24
  interface TerminalSession {
16
25
  id: string;
17
26
  handle: TerminalHandle;
@@ -19,6 +28,9 @@ interface TerminalSession {
19
28
  shell: string;
20
29
  distro?: string;
21
30
  closed: boolean;
31
+ idleMs: number;
32
+ idleTimer?: NodeJS.Timeout;
33
+ touch: () => void;
22
34
  }
23
35
  export interface TerminalOpenOptions {
24
36
  argv: string[];
@@ -29,6 +41,7 @@ export interface TerminalOpenOptions {
29
41
  cols: number;
30
42
  distro?: string;
31
43
  initial?: string;
44
+ idleMs?: number;
32
45
  }
33
46
  export interface TerminalRegistry {
34
47
  open(options: TerminalOpenOptions): Promise<TerminalSession>;
@@ -37,6 +50,7 @@ export interface TerminalRegistry {
37
50
  read(id: string): TerminalBufferRead;
38
51
  signal(id: string, sig: string): Promise<TerminalBufferRead>;
39
52
  close(id: string): Promise<boolean>;
53
+ list(): TerminalSessionInfo[];
40
54
  }
41
55
  /**
42
56
  * Terminal-session registry owned by the plugin fiber: opens PTYs through
@@ -51,6 +65,7 @@ export type ValidatedTerminalArgs = {
51
65
  command?: string;
52
66
  distro?: string;
53
67
  workdir?: string;
68
+ idleMs?: number;
54
69
  } | {
55
70
  action: "send";
56
71
  sessionId: string;
@@ -65,11 +80,14 @@ export type ValidatedTerminalArgs = {
65
80
  } | {
66
81
  action: "close";
67
82
  sessionId: string;
83
+ } | {
84
+ action: "list";
68
85
  };
69
86
  export declare function validateArgs(args: Record<string, unknown>): asserts args is ValidatedTerminalArgs;
70
87
  export interface TerminalOpenResult {
71
88
  kind: "open";
72
89
  sessionId: string;
90
+ jobId?: string;
73
91
  pid: number;
74
92
  shell: string;
75
93
  output: string;
@@ -77,11 +95,16 @@ export interface TerminalOpenResult {
77
95
  export interface TerminalSessionResult {
78
96
  kind: "session";
79
97
  output: string;
98
+ truncated?: boolean;
80
99
  }
81
100
  export interface TerminalClosedResult {
82
101
  kind: "closed";
83
102
  sessionId: string;
84
103
  }
85
- export type TerminalToolResult = TerminalOpenResult | TerminalSessionResult | TerminalClosedResult;
104
+ export interface TerminalListResult {
105
+ kind: "list";
106
+ sessions: TerminalSessionInfo[];
107
+ }
108
+ export type TerminalToolResult = TerminalOpenResult | TerminalSessionResult | TerminalClosedResult | TerminalListResult;
86
109
  export declare function terminalTool(ctx: BashTerminalContext, registry: TerminalRegistry, paths: ResolvedPaths, defaultShell: () => string): import("./dsh-types.js").ToolDefinition;
87
110
  export {};
package/lib/terminal.js CHANGED
@@ -1,29 +1,69 @@
1
- // dsh-bash-terminal-ts: interactive terminal tool over the official PTY seam.
1
+ // dsh-bash-terminal: interactive terminal tool over the official PTY seam.
2
2
  // ctx.subprocess.spawnTerminal (node-pty under the hood) allocates a real
3
3
  // terminal; this module owns model-facing sessions: open / send / read /
4
4
  // signal (Ctrl+C etc.) / close. The backend follows the user's default
5
5
  // terminal setting, exactly like the shell tool.
6
+ //
7
+ // Windows note: the official seam's process inspector is POSIX-only
8
+ // (createProcessInspector throws on win32), so on Windows the PTY is
9
+ // allocated directly through node-pty. Session readiness here is
10
+ // output-quiet based (waitSettled), which needs no process inspection.
6
11
  import { randomUUID } from "node:crypto";
7
12
  import { isAbsolute, resolve } from "node:path";
13
+ import { PassThrough } from "node:stream";
8
14
  import { TOOL_ABORTED, defineTool, HarnessError } from "./dsh.js";
15
+ // Single source of truth for backend env (msys2 MSYSTEM injection, WSL WSLENV).
16
+ // The cycle with ./index.js is safe: buildEnv is a hoisted function declaration
17
+ // and is only called at tool-execution time, never during module evaluation.
9
18
  import { buildEnv } from "./index.js";
10
19
  const MAX_BUFFER_BYTES = 1024 * 1024;
11
- const READ_SETTLE_MS = 800;
12
20
  const DEFAULT_ROWS = 30;
13
21
  const DEFAULT_COLS = 110;
22
+ const DEFAULT_IDLE_MS = 10 * 60 * 1000;
23
+ const MAX_SESSIONS = 8;
14
24
  const TERMINAL_SIGNALS = ["SIGINT", "SIGTERM", "SIGKILL", "SIGTSTP", "SIGHUP"];
15
25
  function delay(ms) {
16
26
  return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
17
27
  }
28
+ /**
29
+ * Wait until the terminal output goes quiet (no new bytes for quietMs) or the
30
+ * cap elapses. Reads after interactive input then return the complete reply
31
+ * instead of a fixed-delay slice.
32
+ */
33
+ async function waitSettled(buffer, quietMs = 300, timeoutMs = 5000) {
34
+ const start = Date.now();
35
+ let last = buffer.lastAppendAt();
36
+ while (Date.now() - start < timeoutMs) {
37
+ await delay(80);
38
+ const now = buffer.lastAppendAt();
39
+ if (now > last) {
40
+ last = now;
41
+ continue;
42
+ }
43
+ if (Date.now() - last >= quietMs)
44
+ return;
45
+ }
46
+ }
18
47
  /** Interactive-shell argv (no -c: the terminal itself is the session). */
19
48
  export function terminalArgv(shell, paths, distro) {
20
49
  switch (shell) {
21
50
  case "powershell": return [paths.pwsh, "-NoLogo", "-NoProfile"];
22
51
  case "gitbash": return [paths.gitbash, "-i"];
23
- case "msys2": return [paths.msys2, "-l"];
52
+ case "msys2":
53
+ // -l, NOT -lc: this argv carries no command (the PTY itself is the
54
+ // session), and `bash -lc` with no operand dies immediately with
55
+ // "-c: option requires an argument" (measured: exit 2). A login shell is
56
+ // also what sources /etc/profile, putting /usr/bin and /mingw64/bin on
57
+ // PATH for the interactive session.
58
+ return [paths.msys2, "-l"];
24
59
  case "wsl": {
25
- const distroArg = distro !== undefined && distro.trim().length > 0 ? ["-d", distro.trim()] : [];
26
- return [paths.wsl, ...distroArg, "-e", "bash", "-i"];
60
+ // Verified under ConPTY: 'wsl -e bash -i' on the DEFAULT distro fails with
61
+ // a WSL service RPC error (0x8007072c), while 'wsl -- bash -i' works;
62
+ // with an explicit -d distro, '-e bash -i' also works.
63
+ if (distro !== undefined && distro.trim().length > 0) {
64
+ return [paths.wsl, "-d", distro.trim(), "-e", "bash", "-i"];
65
+ }
66
+ return [paths.wsl, "--", "bash", "-i"];
27
67
  }
28
68
  default: throw new Error("invalid shell: " + JSON.stringify(shell));
29
69
  }
@@ -31,17 +71,66 @@ export function terminalArgv(shell, paths, distro) {
31
71
  /** In-memory output ring for one session (drop-oldest at the cap). */
32
72
  function createBuffer() {
33
73
  let text = "";
74
+ let truncated = false;
75
+ let lastAppendAt = Date.now();
34
76
  return {
35
77
  append(chunk) {
36
78
  text += chunk;
37
- if (text.length > MAX_BUFFER_BYTES)
79
+ lastAppendAt = Date.now();
80
+ if (text.length > MAX_BUFFER_BYTES) {
38
81
  text = text.slice(text.length - MAX_BUFFER_BYTES);
82
+ truncated = true;
83
+ }
39
84
  },
40
85
  readFrom(offset) {
41
86
  const delta = offset >= text.length ? "" : text.slice(offset);
42
- return { delta, nextOffset: text.length };
87
+ return { delta, nextOffset: text.length, truncated };
88
+ },
89
+ snapshot: () => text,
90
+ wasTruncated: () => truncated,
91
+ lastAppendAt: () => lastAppendAt
92
+ };
93
+ }
94
+ /**
95
+ * Direct node-pty handle with the same surface the official seam returns:
96
+ * output (stream), pid, done, write / signalForeground / terminate. Used on
97
+ * win32 where the official spawnTerminal's process inspector is unsupported.
98
+ */
99
+ async function spawnPtyHandle(spec) {
100
+ const { argv, cwd, env, rows, cols } = spec;
101
+ const mod = (await import("node-pty"));
102
+ const nodePty = mod.default ?? mod;
103
+ const term = nodePty.spawn(argv[0], argv.slice(1), {
104
+ name: "dumb",
105
+ cols,
106
+ rows,
107
+ cwd,
108
+ env: { ...env, TERM: "dumb", NO_COLOR: "1" }
109
+ });
110
+ const output = new PassThrough();
111
+ term.onData((chunk) => output.write(Buffer.from(chunk, "utf8")));
112
+ const done = new Promise((resolveDone) => {
113
+ term.onExit(({ exitCode, signal }) => resolveDone({ exitCode, signal: typeof signal === "string" ? signal : null }));
114
+ });
115
+ return {
116
+ pid: term.pid,
117
+ output,
118
+ done,
119
+ write: (data) => term.write(data),
120
+ // node-pty accepts no named signals on Windows: Ctrl+C is the \x03 byte
121
+ // (ConPTY delivers it to the foreground process), other signals degrade
122
+ // to terminating the session.
123
+ signalForeground: (sig) => {
124
+ if (sig === "SIGINT") {
125
+ term.write("\x03");
126
+ return;
127
+ }
128
+ term.kill();
43
129
  },
44
- snapshot: () => text
130
+ terminate: async () => { try {
131
+ term.kill();
132
+ }
133
+ catch { /* already dead */ } }
45
134
  };
46
135
  }
47
136
  /**
@@ -58,19 +147,21 @@ export function createTerminalRegistry(ctx) {
58
147
  sessions.clear();
59
148
  }, "bash-terminal: terminal sessions teardown");
60
149
  async function open(options) {
61
- const { argv, shell, cwd, env, rows, cols, distro, initial } = options;
62
- const spawnTerminal = ctx.subprocess?.spawnTerminal;
63
- if (spawnTerminal === undefined) {
64
- throw new Error("terminal: ctx.subprocess.spawnTerminal seam unavailable");
150
+ const { argv, shell, cwd, env, rows, cols, distro, initial, idleMs } = options;
151
+ if (sessions.size >= MAX_SESSIONS) {
152
+ throw new Error("terminal: too many open sessions (" + MAX_SESSIONS + "); close one before opening another");
153
+ }
154
+ let handle;
155
+ if (process.platform === "win32") {
156
+ handle = await spawnPtyHandle({ argv, cwd, env, rows, cols });
157
+ }
158
+ else {
159
+ const spawnTerminal = ctx.subprocess?.spawnTerminal;
160
+ if (spawnTerminal === undefined) {
161
+ throw new Error("terminal: ctx.subprocess.spawnTerminal seam unavailable");
162
+ }
163
+ handle = await spawnTerminal({ argv, cwd, env, rows, cols, graceMs: 3000 });
65
164
  }
66
- const handle = await spawnTerminal({
67
- argv,
68
- cwd,
69
- env,
70
- rows,
71
- cols,
72
- graceMs: 3000
73
- });
74
165
  const buffer = createBuffer();
75
166
  handle.output.on("data", (chunk) => buffer.append(typeof chunk === "string" ? chunk : String(chunk)));
76
167
  const session = {
@@ -79,12 +170,30 @@ export function createTerminalRegistry(ctx) {
79
170
  buffer,
80
171
  shell,
81
172
  distro,
82
- closed: false
173
+ closed: false,
174
+ idleMs: idleMs ?? DEFAULT_IDLE_MS,
175
+ touch: () => { }
176
+ };
177
+ // Idle guard: any read/send/signal touches the timer; expiry terminates the
178
+ // session so an abandoned PTY never leaks a process tree.
179
+ const touch = () => {
180
+ if (session.closed)
181
+ return;
182
+ clearTimeout(session.idleTimer);
183
+ session.idleTimer = setTimeout(() => {
184
+ if (session.closed)
185
+ return;
186
+ session.closed = true;
187
+ sessions.delete(session.id);
188
+ void session.handle.terminate().catch(() => { });
189
+ }, session.idleMs);
83
190
  };
191
+ session.touch = touch;
192
+ touch();
84
193
  sessions.set(session.id, session);
85
194
  if (initial !== undefined && initial.length > 0) {
86
195
  await handle.write(initial);
87
- await delay(READ_SETTLE_MS);
196
+ await waitSettled(buffer);
88
197
  }
89
198
  return session;
90
199
  }
@@ -94,41 +203,51 @@ export function createTerminalRegistry(ctx) {
94
203
  throw new Error("terminal session not found: " + JSON.stringify(id));
95
204
  if (session.closed)
96
205
  throw new Error("terminal session is closed: " + JSON.stringify(id));
206
+ session.touch();
97
207
  return session;
98
208
  }
99
209
  async function send(id, input) {
100
210
  const session = get(id);
101
211
  await session.handle.write(input);
102
- await delay(READ_SETTLE_MS);
212
+ await waitSettled(session.buffer);
103
213
  return read(id);
104
214
  }
105
- function read(id) {
106
- const session = get(id);
107
- const { delta, nextOffset } = session.buffer.readFrom(0);
108
- return { delta, nextOffset };
109
- }
110
215
  async function signal(id, sig) {
111
216
  const session = get(id);
112
217
  if (!TERMINAL_SIGNALS.includes(sig)) {
113
218
  throw new Error("invalid terminal signal: " + JSON.stringify(sig));
114
219
  }
115
220
  await session.handle.signalForeground(sig);
116
- await delay(READ_SETTLE_MS);
221
+ await waitSettled(session.buffer);
117
222
  return read(id);
118
223
  }
224
+ function read(id) {
225
+ const session = get(id);
226
+ const { delta, nextOffset, truncated } = session.buffer.readFrom(0);
227
+ return { delta, nextOffset, truncated };
228
+ }
119
229
  async function close(id) {
120
230
  const session = get(id);
121
231
  session.closed = true;
232
+ clearTimeout(session.idleTimer);
122
233
  sessions.delete(id);
123
234
  await session.handle.terminate();
124
235
  return true;
125
236
  }
126
- return { open, get, send, read, signal, close };
237
+ function list() {
238
+ return [...sessions.values()].map((session) => ({
239
+ sessionId: session.id,
240
+ shell: session.shell,
241
+ pid: session.handle.pid,
242
+ closed: session.closed
243
+ }));
244
+ }
245
+ return { open, get, send, read, signal, close, list };
127
246
  }
128
247
  export function validateArgs(args) {
129
248
  const action = args.action;
130
- if (typeof action !== "string" || ["open", "send", "read", "signal", "close"].indexOf(action) === -1) {
131
- throw new Error("invalid action: expected open, send, read, signal, or close");
249
+ if (typeof action !== "string" || ["open", "send", "read", "signal", "close", "list"].indexOf(action) === -1) {
250
+ throw new Error("invalid action: expected open, send, read, signal, close, or list");
132
251
  }
133
252
  if ((action === "send" || action === "read" || action === "signal" || action === "close") && (typeof args.sessionId !== "string" || args.sessionId.length === 0)) {
134
253
  throw new Error("invalid sessionId: required for " + action);
@@ -143,13 +262,13 @@ export function validateArgs(args) {
143
262
  export function terminalTool(ctx, registry, paths, defaultShell) {
144
263
  return defineTool({
145
264
  name: "terminal",
146
- description: "Interactive terminal session over the user's default terminal (Settings -> General -> Default terminal: powershell / gitbash / msys2 / wsl). A real PTY hosts a persistent shell: open a session, send input and read output across turns, deliver signals (Ctrl+C = SIGINT) to the foreground process, and close when done. Backend and env follow the shell tool exactly; the session survives between calls until closed. Use this for interactive programs (REPLs, ssh, databases, TUI tools) or when you need shell state (cwd, variables, aliases) to persist across calls.",
265
+ description: "Interactive terminal session over the user's default terminal (Settings -> General -> Default terminal: powershell / gitbash / msys2 / wsl). A real PTY hosts a persistent shell: open a session, send input and read output across turns, deliver signals (Ctrl+C = SIGINT) to the foreground process, and close when done. Backend and env follow the shell tool exactly; the session survives between calls until closed and is managed as a background job (job_kill / job_output work on it); idle sessions close automatically. Use this for interactive programs (REPLs, ssh, databases, TUI tools) or when you need shell state (cwd, variables, aliases) to persist across calls.",
147
266
  parameters: {
148
267
  action: {
149
268
  type: "string",
150
- enum: ["open", "send", "read", "signal", "close"],
269
+ enum: ["open", "send", "read", "signal", "close", "list"],
151
270
  required: true,
152
- description: "open: create a session and return its id. send: write input and read new output. read: read new output without writing. signal: send a signal to the foreground process (SIGINT for Ctrl+C). close: terminate the session."
271
+ description: "open: create a session and return its id. send: write input and read new output. read: read new output without writing. signal: send a signal to the foreground process (SIGINT for Ctrl+C). close: terminate the session. list: list live sessions."
153
272
  },
154
273
  sessionId: {
155
274
  type: "string",
@@ -175,6 +294,10 @@ export function terminalTool(ctx, registry, paths, defaultShell) {
175
294
  workdir: {
176
295
  type: "string",
177
296
  description: "Working directory for the session (open only). Defaults to the session workspace."
297
+ },
298
+ idleMs: {
299
+ type: "number",
300
+ description: "Idle timeout in ms (open only): the session closes automatically after this long without send/read/signal. Default 600000 (10 min)."
178
301
  }
179
302
  },
180
303
  output: {
@@ -186,6 +309,7 @@ export function terminalTool(ctx, registry, paths, defaultShell) {
186
309
  properties: {
187
310
  kind: { type: "string", required: true, const: "open" },
188
311
  sessionId: { type: "string", required: true },
312
+ jobId: { type: "string" },
189
313
  pid: { type: "integer", required: true },
190
314
  shell: { type: "string", required: true },
191
315
  output: { type: "string" }
@@ -196,7 +320,8 @@ export function terminalTool(ctx, registry, paths, defaultShell) {
196
320
  additionalProperties: false,
197
321
  properties: {
198
322
  kind: { type: "string", required: true, const: "session" },
199
- output: { type: "string", required: true }
323
+ output: { type: "string", required: true },
324
+ truncated: { type: "boolean" }
200
325
  }
201
326
  },
202
327
  {
@@ -206,6 +331,26 @@ export function terminalTool(ctx, registry, paths, defaultShell) {
206
331
  kind: { type: "string", required: true, const: "closed" },
207
332
  sessionId: { type: "string", required: true }
208
333
  }
334
+ },
335
+ {
336
+ type: "object",
337
+ additionalProperties: false,
338
+ properties: {
339
+ kind: { type: "string", required: true, const: "list" },
340
+ sessions: {
341
+ type: "array",
342
+ items: {
343
+ type: "object",
344
+ additionalProperties: false,
345
+ properties: {
346
+ sessionId: { type: "string", required: true },
347
+ shell: { type: "string", required: true },
348
+ pid: { type: "integer", required: true },
349
+ closed: { type: "boolean" }
350
+ }
351
+ }
352
+ }
353
+ }
209
354
  }
210
355
  ]
211
356
  },
@@ -215,7 +360,11 @@ export function terminalTool(ctx, registry, paths, defaultShell) {
215
360
  ? "terminal session " + value.sessionId + " (pid " + value.pid + ", " + value.shell + ")" + (value.output ? "\n" + value.output : "")
216
361
  : value.kind === "closed"
217
362
  ? "terminal session " + value.sessionId + " closed"
218
- : value.output
363
+ : value.kind === "list"
364
+ ? (value.sessions.length === 0 ? "no open terminal sessions" : value.sessions.map((s) => s.sessionId + " (" + s.shell + ", pid " + s.pid + ")").join("\n"))
365
+ : value.truncated === true
366
+ ? (value.output.length === 0 ? "[terminal buffer overflowed; oldest output dropped]" : value.output + "\n[terminal buffer overflowed; oldest output dropped]")
367
+ : value.output
219
368
  }]
220
369
  },
221
370
  async execute(args, exec) {
@@ -238,30 +387,46 @@ export function terminalTool(ctx, registry, paths, defaultShell) {
238
387
  // buildEnv, not an inline duplicate: the msys2 backend needs its
239
388
  // MSYSTEM=MINGW64 injection here too, or the login shell sources
240
389
  // /etc/profile with the default MSYS environment and /mingw64/bin
241
- // (gcc, make) never joins PATH. DSH's spawnTerminal replaces the child
242
- // environment with exactly this object (childEnv(spec.env)), so the
243
- // inherited WSLENV is passed through explicitly for the WSL allow-list;
244
- // for every other backend buildEnv ignores the third argument.
390
+ // (gcc, make) never joins PATH. It also owns the WSL WSLENV layering,
391
+ // so a terminal session's env follows the shell tool exactly.
392
+ // process.env.WSLENV is passed explicitly: spawnTerminal replaces the
393
+ // child environment wholesale (childEnv(spec.env)), so the ambient
394
+ // allow-list is not otherwise visible and inherited entries such as
395
+ // WT_SESSION / WT_PROFILE_ID would be dropped.
245
396
  const env = buildEnv(shell, ctx.shellEnv.collect(exec), process.env.WSLENV);
246
- const session = await registry.open({ argv, shell, cwd, env, rows: DEFAULT_ROWS, cols: DEFAULT_COLS, distro: args.distro, initial: args.command !== undefined ? args.command + "\r" : undefined });
247
- return { kind: "open", sessionId: session.id, pid: session.handle.pid, shell, output: session.buffer.snapshot() };
397
+ const session = await registry.open({ argv, shell, cwd, env, rows: DEFAULT_ROWS, cols: DEFAULT_COLS, distro: args.distro, initial: args.command !== undefined ? args.command + "\r" : undefined, idleMs: args.idleMs });
398
+ const jobs = ctx.get("jobs");
399
+ const jobId = jobs === undefined ? undefined : jobs.start({
400
+ kind: "terminal/session",
401
+ label: "terminal " + session.id.slice(0, 8) + " (" + shell + ")",
402
+ ...(exec.agent ? { owner: exec.agent } : {}),
403
+ run: () => ({
404
+ cancel: () => { void session.handle.terminate().catch(() => { }); },
405
+ done: session.handle.done,
406
+ readOutput: () => registry.read(session.id).delta
407
+ })
408
+ });
409
+ return { kind: "open", sessionId: session.id, ...(jobId !== undefined ? { jobId } : {}), pid: session.handle.pid, shell, output: session.buffer.snapshot() };
248
410
  }
249
411
  case "send": {
250
- const { delta } = await registry.send(args.sessionId, args.input);
251
- return { kind: "session", output: delta };
412
+ const { delta, truncated } = await registry.send(args.sessionId, args.input);
413
+ return { kind: "session", output: delta, ...(truncated ? { truncated } : {}) };
252
414
  }
253
415
  case "read": {
254
- const { delta } = registry.read(args.sessionId);
255
- return { kind: "session", output: delta };
416
+ const { delta, truncated } = registry.read(args.sessionId);
417
+ return { kind: "session", output: delta, ...(truncated ? { truncated } : {}) };
256
418
  }
257
419
  case "signal": {
258
- const { delta } = await registry.signal(args.sessionId, args.signal);
259
- return { kind: "session", output: delta };
420
+ const { delta, truncated } = await registry.signal(args.sessionId, args.signal);
421
+ return { kind: "session", output: delta, ...(truncated ? { truncated } : {}) };
260
422
  }
261
423
  case "close": {
262
424
  await registry.close(args.sessionId);
263
425
  return { kind: "closed", sessionId: args.sessionId };
264
426
  }
427
+ case "list": {
428
+ return { kind: "list", sessions: registry.list() };
429
+ }
265
430
  }
266
431
  },
267
432
  presentCall: (args) => ({
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "dsh-bash-terminal-ts",
3
- "version": "0.2.6",
3
+ "version": "0.5.0",
4
+ "publishConfig": {
5
+ "registry": "https://registry.npmjs.org",
6
+ "access": "public",
7
+ "tag": "dsh-0.1.5"
8
+ },
4
9
  "description": "DSH plugin: one shell tool that runs commands through PowerShell, Git Bash, MSYS2, or WSL on Windows, with a user-chosen default terminal in the Web UI settings. TypeScript rewrite of MAXeaglet/dsh-bash-terminal with working MSYS2/MINGW64 support.",
5
10
  "author": "drscrewdriver",
6
- "contributors": [
7
- "MAXeaglet (original dsh-bash-terminal)"
8
- ],
9
11
  "license": "MIT",
10
12
  "repository": {
11
13
  "type": "git",
@@ -23,24 +25,19 @@
23
25
  "shell",
24
26
  "powershell",
25
27
  "git-bash",
26
- "msys2",
27
- "mingw64",
28
28
  "wsl",
29
29
  "windows",
30
30
  "typescript"
31
31
  ],
32
32
  "engines": {
33
33
  "node": ">=22",
34
- "dsh": ">=0.1.2-rc.1 <0.2.0-0"
35
- },
36
- "publishConfig": {
37
- "access": "public"
34
+ "dsh": ">=0.1.5-alpha.1 <0.2.0-0"
38
35
  },
39
36
  "type": "module",
40
37
  "main": "lib/index.js",
41
38
  "exports": {
42
39
  ".": "./lib/index.js",
43
- "./client": "./dist/client.js",
40
+ "./client": "./lib/client.js",
44
41
  "./package.json": "./package.json"
45
42
  },
46
43
  "files": [
@@ -52,17 +49,15 @@
52
49
  "tsconfig.client.json",
53
50
  "tsconfig.test.json",
54
51
  "README.md",
55
- "README.en.md",
56
- "README.ja.md",
57
- "README.ko.md",
58
52
  "LICENSE",
59
53
  "cordis.patch.yml",
60
- "dsh.plugin.json"
54
+ "dsh.plugin.json",
55
+ "screenshots.json"
61
56
  ],
62
57
  "scripts": {
63
58
  "build": "tsc -p tsconfig.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs && tsc -p tsconfig.test.json",
64
59
  "build:client": "node scripts/build-client.mjs",
65
- "test": "node test-dist/unit.js && node test-dist/apply.js && node test-dist/client.js"
60
+ "test": "node test-dist/unit.js && node test-dist/apply.js && node test-dist/client.js && node test-dist/terminal.js"
66
61
  },
67
62
  "dsh": {
68
63
  "bundle": {
@@ -72,64 +67,50 @@
72
67
  "platform": "web",
73
68
  "immediately": true,
74
69
  "inject": [
75
- "@deepseek-ai/dsh-client-store",
76
70
  "@deepseek-ai/dsh-client-locale",
77
71
  "@deepseek-ai/dsh-client-ui-settings",
78
- "@deepseek-ai/dsh-client-ui-slots"
72
+ "@deepseek-ai/dsh-api-remotes"
79
73
  ]
80
74
  }
81
75
  },
82
76
  "peerDependencies": {
83
77
  "@deepseek-ai/cordis": "^4.0.2",
84
- "@deepseek-ai/dsh-tools": "^0.1.2-rc.1",
85
- "@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
86
- "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
87
- "@deepseek-ai/dsh-shell": "^0.1.2-rc.1",
88
- "@deepseek-ai/dsh-timeout": "^0.1.2-rc.1",
89
- "@deepseek-ai/dsh-client-store": "^0.1.2-rc.1",
90
- "@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
91
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-rc.1",
92
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
93
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-rc.1",
94
- "@deepseek-ai/dsh-sandbox": "^0.1.2-rc.1",
78
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.1",
79
+ "@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
80
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
81
+ "@deepseek-ai/dsh-shell": "^0.1.5-rc.1",
82
+ "@deepseek-ai/dsh-sandbox": "^0.1.5-rc.1",
83
+ "@deepseek-ai/dsh-timeout": "^0.1.5-rc.1",
84
+ "@deepseek-ai/dsh-client-store": "^0.1.5-rc.1",
85
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
86
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.1",
87
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
88
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.1",
95
89
  "react": "^18.2.0"
96
90
  },
97
- "peerDependenciesMeta": {
98
- "@deepseek-ai/cordis": { "optional": true },
99
- "@deepseek-ai/dsh-tools": { "optional": true },
100
- "@deepseek-ai/dsh-settings": { "optional": true },
101
- "@deepseek-ai/dsh-llm": { "optional": true },
102
- "@deepseek-ai/dsh-shell": { "optional": true },
103
- "@deepseek-ai/dsh-timeout": { "optional": true },
104
- "@deepseek-ai/dsh-client-store": { "optional": true },
105
- "@deepseek-ai/dsh-client-locale": { "optional": true },
106
- "@deepseek-ai/dsh-client-ui-settings": { "optional": true },
107
- "@deepseek-ai/dsh-client-ui-slots": { "optional": true },
108
- "@deepseek-ai/dsh-client-ui-primitives": { "optional": true },
109
- "@deepseek-ai/dsh-sandbox": { "optional": true },
110
- "react": { "optional": true }
111
- },
112
91
  "dependencies": {
113
- "@deepseek-ai/schemastery": "^3.18.1"
92
+ "@deepseek-ai/schemastery": "^3.18.1",
93
+ "node-pty": "^1.1.0"
114
94
  },
115
95
  "devDependencies": {
116
96
  "@deepseek-ai/cordis": "^4.0.2",
117
- "@deepseek-ai/dsh-client-locale": "^0.1.2-rc.1",
118
- "@deepseek-ai/dsh-client-store": "^0.1.2-rc.1",
119
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-rc.1",
120
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-rc.1",
121
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-rc.1",
122
- "@deepseek-ai/dsh-llm": "^0.1.2-rc.1",
123
- "@deepseek-ai/dsh-sandbox": "^0.1.2-rc.1",
124
- "@deepseek-ai/dsh-settings": "^0.1.2-rc.1",
125
- "@deepseek-ai/dsh-shell": "^0.1.2-rc.1",
126
- "@deepseek-ai/dsh-timeout": "^0.1.2-rc.1",
127
- "@deepseek-ai/dsh-tools": "^0.1.2-rc.1",
128
- "@types/node": "^24.13.4",
97
+ "@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
98
+ "@deepseek-ai/dsh-client-store": "0.1.5-rc.2",
99
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
100
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
101
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
102
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.2",
103
+ "@deepseek-ai/dsh-sandbox": "0.1.5-rc.2",
104
+ "@deepseek-ai/dsh-settings": "0.1.5-rc.2",
105
+ "@deepseek-ai/dsh-shell": "0.1.5-rc.2",
106
+ "@deepseek-ai/dsh-timeout": "0.1.5-rc.2",
107
+ "@deepseek-ai/dsh-tools": "0.1.5-rc.2",
108
+ "@types/node": "^22.10.0",
129
109
  "@types/react": "^18.2.0",
130
110
  "@types/react-dom": "^18.2.0",
131
111
  "esbuild": "^0.28.2",
132
112
  "react": "^18.2.0",
113
+ "react-dom": "^18.2.0",
133
114
  "typescript": "^5.9.0"
134
115
  }
135
116
  }