terminal-commands 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -18,6 +18,11 @@ approval prompts are enabled.
18
18
 
19
19
  Command-line programs still execute with your operating-system user permissions. A program can access more than the dedicated filesystem tools, so review every command approval carefully.
20
20
 
21
+ The MCP command surface exposes process lifecycle operations separately: `start_process` starts
22
+ an approved executable and returns a session ID, `read_process_output` reads bounded output,
23
+ `list_processes` lists sessions, and `terminate_process` stops a session. This keeps terminal
24
+ workflows available while making each model-callable operation explicit.
25
+
21
26
  ## Run
22
27
 
23
28
  ```bash
package/dist/agent.js CHANGED
@@ -35480,7 +35480,11 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
35480
35480
  var wrapper_default = import_websocket.default;
35481
35481
 
35482
35482
  // src/agent-core.ts
35483
- var destructiveTools = /* @__PURE__ */ new Set(["run_program", "write_text_file"]);
35483
+ var destructiveTools = /* @__PURE__ */ new Set([
35484
+ "start_process",
35485
+ "terminate_process",
35486
+ "write_text_file"
35487
+ ]);
35484
35488
  var requestSchema = external_exports.object({
35485
35489
  type: external_exports.literal("request"),
35486
35490
  requestId: external_exports.string().uuid(),
@@ -35488,7 +35492,10 @@ var requestSchema = external_exports.object({
35488
35492
  "get_system_info",
35489
35493
  "list_directory",
35490
35494
  "read_text_file",
35491
- "run_program",
35495
+ "start_process",
35496
+ "read_process_output",
35497
+ "terminate_process",
35498
+ "list_processes",
35492
35499
  "write_text_file"
35493
35500
  ]),
35494
35501
  arguments: external_exports.record(external_exports.string(), external_exports.unknown())
@@ -35668,12 +35675,15 @@ function approvalSummary(tool, args) {
35668
35675
  const bytes = Buffer.byteLength(String(args.contents ?? ""));
35669
35676
  return `Write ${bytes} bytes to ${String(args.path ?? "an unknown path")}${args.overwrite ? " (overwrite)" : ""}`;
35670
35677
  }
35671
- if (tool === "run_program") {
35678
+ if (tool === "start_process") {
35672
35679
  const command = [
35673
35680
  String(args.program ?? ""),
35674
35681
  ...Array.isArray(args.args) ? args.args.map(String) : []
35675
35682
  ];
35676
- return `Run: ${command.join(" ")}`;
35683
+ return `Start: ${command.join(" ")}`;
35684
+ }
35685
+ if (tool === "terminate_process") {
35686
+ return `Terminate process session ${String(args.sessionId ?? "")}`;
35677
35687
  }
35678
35688
  return `Run destructive tool: ${tool}`;
35679
35689
  }
package/dist/cli.js CHANGED
@@ -35487,7 +35487,11 @@ var import_websocket_server = __toESM(require_websocket_server(), 1);
35487
35487
  var wrapper_default = import_websocket.default;
35488
35488
 
35489
35489
  // src/agent-core.ts
35490
- var destructiveTools = /* @__PURE__ */ new Set(["run_program", "write_text_file"]);
35490
+ var destructiveTools = /* @__PURE__ */ new Set([
35491
+ "start_process",
35492
+ "terminate_process",
35493
+ "write_text_file"
35494
+ ]);
35491
35495
  var requestSchema = external_exports.object({
35492
35496
  type: external_exports.literal("request"),
35493
35497
  requestId: external_exports.string().uuid(),
@@ -35495,7 +35499,10 @@ var requestSchema = external_exports.object({
35495
35499
  "get_system_info",
35496
35500
  "list_directory",
35497
35501
  "read_text_file",
35498
- "run_program",
35502
+ "start_process",
35503
+ "read_process_output",
35504
+ "terminate_process",
35505
+ "list_processes",
35499
35506
  "write_text_file"
35500
35507
  ]),
35501
35508
  arguments: external_exports.record(external_exports.string(), external_exports.unknown())
@@ -35691,12 +35698,15 @@ function approvalSummary(tool, args) {
35691
35698
  const bytes = Buffer.byteLength(String(args.contents ?? ""));
35692
35699
  return `Write ${bytes} bytes to ${String(args.path ?? "an unknown path")}${args.overwrite ? " (overwrite)" : ""}`;
35693
35700
  }
35694
- if (tool === "run_program") {
35701
+ if (tool === "start_process") {
35695
35702
  const command = [
35696
35703
  String(args.program ?? ""),
35697
35704
  ...Array.isArray(args.args) ? args.args.map(String) : []
35698
35705
  ];
35699
- return `Run: ${command.join(" ")}`;
35706
+ return `Start: ${command.join(" ")}`;
35707
+ }
35708
+ if (tool === "terminate_process") {
35709
+ return `Terminate process session ${String(args.sessionId ?? "")}`;
35700
35710
  }
35701
35711
  return `Run destructive tool: ${tool}`;
35702
35712
  }
package/dist/index.js CHANGED
@@ -62498,9 +62498,30 @@ var EMPTY_COMPLETION_RESULT = {
62498
62498
  }
62499
62499
  };
62500
62500
 
62501
- // src/runner.ts
62502
- import { execFile } from "node:child_process";
62503
- import { promisify } from "node:util";
62501
+ // src/file-reader.ts
62502
+ import { open as open2 } from "node:fs/promises";
62503
+ async function readBoundedTextFile(filename, maxCharacters) {
62504
+ const handle = await open2(filename, "r");
62505
+ try {
62506
+ const stats = await handle.stat();
62507
+ const buffer = Buffer.allocUnsafe(maxCharacters * 4 + 4);
62508
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
62509
+ const hasUnreadBytes = bytesRead < stats.size;
62510
+ const decoded = new TextDecoder().decode(buffer.subarray(0, bytesRead), {
62511
+ stream: hasUnreadBytes
62512
+ });
62513
+ return {
62514
+ contents: decoded.slice(0, maxCharacters),
62515
+ truncated: hasUnreadBytes || decoded.length > maxCharacters
62516
+ };
62517
+ } finally {
62518
+ await handle.close();
62519
+ }
62520
+ }
62521
+
62522
+ // src/process-manager.ts
62523
+ import { randomUUID } from "node:crypto";
62524
+ import { spawn } from "node:child_process";
62504
62525
 
62505
62526
  // src/security.ts
62506
62527
  import { constants as constants2 } from "node:fs";
@@ -62590,59 +62611,144 @@ function assertProgramAllowed(program) {
62590
62611
  }
62591
62612
  }
62592
62613
 
62593
- // src/runner.ts
62594
- var execFileAsync = promisify(execFile);
62595
- async function runProgram(options) {
62596
- assertProgramAllowed(options.program);
62597
- try {
62598
- const { stdout, stderr } = await execFileAsync(
62599
- options.program,
62600
- options.args,
62601
- {
62602
- cwd: options.cwd,
62603
- env: safeEnvironment(),
62604
- encoding: "utf8",
62605
- maxBuffer: options.maxOutputChars,
62606
- timeout: options.timeoutMs,
62607
- windowsHide: true
62608
- }
62609
- );
62610
- return { exitCode: 0, signal: null, stdout, stderr, timedOut: false };
62611
- } catch (error62) {
62612
- const failure2 = error62;
62613
- return {
62614
- exitCode: typeof failure2.code === "number" ? failure2.code : null,
62615
- signal: failure2.signal ?? null,
62616
- stdout: failure2.stdout ?? "",
62617
- stderr: failure2.stderr ?? failure2.message,
62618
- timedOut: failure2.killed === true && failure2.signal === "SIGTERM"
62619
- };
62620
- }
62614
+ // src/process-manager.ts
62615
+ function appendBounded(current, value, limit) {
62616
+ if (current.length >= limit) return current;
62617
+ return `${current}${value.toString("utf8")}`.slice(0, limit);
62621
62618
  }
62622
-
62623
- // src/file-reader.ts
62624
- import { open as open2 } from "node:fs/promises";
62625
- async function readBoundedTextFile(filename, maxCharacters) {
62626
- const handle = await open2(filename, "r");
62627
- try {
62628
- const stats = await handle.stat();
62629
- const buffer = Buffer.allocUnsafe(maxCharacters * 4 + 4);
62630
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
62631
- const hasUnreadBytes = bytesRead < stats.size;
62632
- const decoded = new TextDecoder().decode(buffer.subarray(0, bytesRead), {
62633
- stream: hasUnreadBytes
62619
+ function snapshot(session) {
62620
+ return {
62621
+ sessionId: session.sessionId,
62622
+ program: session.program,
62623
+ args: session.args,
62624
+ cwd: session.cwd,
62625
+ pid: session.child.pid ?? null,
62626
+ running: session.running,
62627
+ exitCode: session.exitCode,
62628
+ signal: session.signal,
62629
+ stdout: session.stdout,
62630
+ stderr: session.stderr,
62631
+ timedOut: session.timedOut
62632
+ };
62633
+ }
62634
+ var ProcessManager = class {
62635
+ #sessions = /* @__PURE__ */ new Map();
62636
+ start(options) {
62637
+ assertProgramAllowed(options.program);
62638
+ const child = spawn(options.program, options.args, {
62639
+ cwd: options.cwd,
62640
+ env: safeEnvironment(),
62641
+ stdio: ["ignore", "pipe", "pipe"],
62642
+ windowsHide: true
62643
+ });
62644
+ const session = {
62645
+ sessionId: randomUUID(),
62646
+ program: options.program,
62647
+ args: [...options.args],
62648
+ cwd: options.cwd,
62649
+ child,
62650
+ maxOutputCharacters: options.maxOutputCharacters,
62651
+ stdout: "",
62652
+ stderr: "",
62653
+ running: true,
62654
+ exitCode: null,
62655
+ signal: null,
62656
+ timedOut: false
62657
+ };
62658
+ this.#sessions.set(session.sessionId, session);
62659
+ child.stdout.on("data", (chunk) => {
62660
+ session.stdout = appendBounded(
62661
+ session.stdout,
62662
+ chunk,
62663
+ session.maxOutputCharacters
62664
+ );
62665
+ });
62666
+ child.stderr.on("data", (chunk) => {
62667
+ session.stderr = appendBounded(
62668
+ session.stderr,
62669
+ chunk,
62670
+ session.maxOutputCharacters
62671
+ );
62634
62672
  });
62673
+ child.once("error", (error62) => {
62674
+ session.stderr = appendBounded(
62675
+ session.stderr,
62676
+ error62.message,
62677
+ session.maxOutputCharacters
62678
+ );
62679
+ session.running = false;
62680
+ session.exitCode = null;
62681
+ session.signal = null;
62682
+ if (session.timeout) clearTimeout(session.timeout);
62683
+ });
62684
+ child.once("close", (exitCode, signal) => {
62685
+ session.running = false;
62686
+ session.exitCode = exitCode;
62687
+ session.signal = signal;
62688
+ if (session.timeout) clearTimeout(session.timeout);
62689
+ });
62690
+ session.timeout = setTimeout(() => {
62691
+ if (!session.running) return;
62692
+ session.timedOut = true;
62693
+ child.kill("SIGTERM");
62694
+ }, options.timeoutMs);
62695
+ const result = snapshot(session);
62635
62696
  return {
62636
- contents: decoded.slice(0, maxCharacters),
62637
- truncated: hasUnreadBytes || decoded.length > maxCharacters
62697
+ ...result,
62698
+ nextStdoutOffset: result.stdout.length,
62699
+ nextStderrOffset: result.stderr.length
62700
+ };
62701
+ }
62702
+ read(sessionId, stdoutOffset, stderrOffset, maxOutputCharacters) {
62703
+ const session = this.#sessions.get(sessionId);
62704
+ if (!session)
62705
+ throw new Error("The requested process session was not found.");
62706
+ const safeStdoutOffset = Math.max(
62707
+ 0,
62708
+ Math.min(stdoutOffset, session.stdout.length)
62709
+ );
62710
+ const safeStderrOffset = Math.max(
62711
+ 0,
62712
+ Math.min(stderrOffset, session.stderr.length)
62713
+ );
62714
+ const stdout = session.stdout.slice(
62715
+ safeStdoutOffset,
62716
+ safeStdoutOffset + maxOutputCharacters
62717
+ );
62718
+ const stderr = session.stderr.slice(
62719
+ safeStderrOffset,
62720
+ safeStderrOffset + maxOutputCharacters
62721
+ );
62722
+ return {
62723
+ ...snapshot(session),
62724
+ stdout,
62725
+ stderr,
62726
+ nextStdoutOffset: safeStdoutOffset + stdout.length,
62727
+ nextStderrOffset: safeStderrOffset + stderr.length
62638
62728
  };
62639
- } finally {
62640
- await handle.close();
62641
62729
  }
62642
- }
62730
+ terminate(sessionId) {
62731
+ const session = this.#sessions.get(sessionId);
62732
+ if (!session)
62733
+ throw new Error("The requested process session was not found.");
62734
+ if (session.running) session.child.kill("SIGTERM");
62735
+ return snapshot(session);
62736
+ }
62737
+ list() {
62738
+ return [...this.#sessions.values()].map(snapshot);
62739
+ }
62740
+ close() {
62741
+ for (const session of this.#sessions.values()) {
62742
+ if (session.timeout) clearTimeout(session.timeout);
62743
+ if (session.running) session.child.kill("SIGTERM");
62744
+ }
62745
+ this.#sessions.clear();
62746
+ }
62747
+ };
62643
62748
 
62644
62749
  // src/server.ts
62645
62750
  var roots = configuredRoots();
62751
+ var processManager = new ProcessManager();
62646
62752
  function textResult(value) {
62647
62753
  return {
62648
62754
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
@@ -62782,10 +62888,10 @@ function createServer() {
62782
62888
  }
62783
62889
  );
62784
62890
  server.registerTool(
62785
- "run_program",
62891
+ "start_process",
62786
62892
  {
62787
- title: "Run a local program",
62788
- description: "Run one executable without a shell. Shells and privilege-elevation programs are blocked unless explicitly enabled by local configuration.",
62893
+ title: "Start a process",
62894
+ description: "Start one approved executable without a shell on this machine and return a session ID for reading its output.",
62789
62895
  inputSchema: {
62790
62896
  program: external_exports.string().min(1),
62791
62897
  args: external_exports.array(external_exports.string()).max(100).default([]),
@@ -62794,14 +62900,19 @@ function createServer() {
62794
62900
  maxOutputCharacters: external_exports.number().int().min(1e3).max(2e5).default(5e4)
62795
62901
  },
62796
62902
  outputSchema: {
62903
+ sessionId: external_exports.string(),
62797
62904
  program: external_exports.string(),
62798
62905
  args: external_exports.array(external_exports.string()),
62799
62906
  cwd: external_exports.string(),
62907
+ pid: external_exports.number().int().nullable(),
62908
+ running: external_exports.boolean(),
62800
62909
  exitCode: external_exports.number().int().nullable(),
62801
62910
  signal: external_exports.string().nullable(),
62802
62911
  stdout: external_exports.string(),
62803
62912
  stderr: external_exports.string(),
62804
- timedOut: external_exports.boolean()
62913
+ timedOut: external_exports.boolean(),
62914
+ nextStdoutOffset: external_exports.number().int().nonnegative(),
62915
+ nextStderrOffset: external_exports.number().int().nonnegative()
62805
62916
  },
62806
62917
  annotations: {
62807
62918
  readOnlyHint: false,
@@ -62814,16 +62925,114 @@ function createServer() {
62814
62925
  cwd ?? roots[0],
62815
62926
  roots
62816
62927
  );
62817
- const result = await runProgram({
62818
- program,
62819
- args,
62820
- cwd: workingDirectory,
62821
- timeoutMs,
62822
- maxOutputChars: maxOutputCharacters
62823
- });
62824
- return textResult({ program, args, cwd: workingDirectory, ...result });
62928
+ return textResult(
62929
+ processManager.start({
62930
+ program,
62931
+ args,
62932
+ cwd: workingDirectory,
62933
+ timeoutMs,
62934
+ maxOutputCharacters
62935
+ })
62936
+ );
62825
62937
  }
62826
62938
  );
62939
+ server.registerTool(
62940
+ "read_process_output",
62941
+ {
62942
+ title: "Read process output",
62943
+ description: "Read new stdout and stderr from a previously started process session.",
62944
+ inputSchema: {
62945
+ sessionId: external_exports.string().uuid(),
62946
+ stdoutOffset: external_exports.number().int().min(0).default(0),
62947
+ stderrOffset: external_exports.number().int().min(0).default(0),
62948
+ maxOutputCharacters: external_exports.number().int().min(1e3).max(2e5).default(5e4)
62949
+ },
62950
+ outputSchema: {
62951
+ sessionId: external_exports.string(),
62952
+ program: external_exports.string(),
62953
+ args: external_exports.array(external_exports.string()),
62954
+ cwd: external_exports.string(),
62955
+ pid: external_exports.number().int().nullable(),
62956
+ running: external_exports.boolean(),
62957
+ exitCode: external_exports.number().int().nullable(),
62958
+ signal: external_exports.string().nullable(),
62959
+ stdout: external_exports.string(),
62960
+ stderr: external_exports.string(),
62961
+ timedOut: external_exports.boolean(),
62962
+ nextStdoutOffset: external_exports.number().int().nonnegative(),
62963
+ nextStderrOffset: external_exports.number().int().nonnegative()
62964
+ },
62965
+ annotations: {
62966
+ readOnlyHint: true,
62967
+ destructiveHint: false,
62968
+ openWorldHint: false
62969
+ }
62970
+ },
62971
+ async ({ sessionId, stdoutOffset, stderrOffset, maxOutputCharacters }) => textResult(
62972
+ processManager.read(
62973
+ sessionId,
62974
+ stdoutOffset,
62975
+ stderrOffset,
62976
+ maxOutputCharacters
62977
+ )
62978
+ )
62979
+ );
62980
+ server.registerTool(
62981
+ "terminate_process",
62982
+ {
62983
+ title: "Terminate a process",
62984
+ description: "Stop a previously started process session on this machine.",
62985
+ inputSchema: { sessionId: external_exports.string().uuid() },
62986
+ outputSchema: {
62987
+ sessionId: external_exports.string(),
62988
+ program: external_exports.string(),
62989
+ args: external_exports.array(external_exports.string()),
62990
+ cwd: external_exports.string(),
62991
+ pid: external_exports.number().int().nullable(),
62992
+ running: external_exports.boolean(),
62993
+ exitCode: external_exports.number().int().nullable(),
62994
+ signal: external_exports.string().nullable(),
62995
+ stdout: external_exports.string(),
62996
+ stderr: external_exports.string(),
62997
+ timedOut: external_exports.boolean()
62998
+ },
62999
+ annotations: {
63000
+ readOnlyHint: false,
63001
+ destructiveHint: true,
63002
+ openWorldHint: false
63003
+ }
63004
+ },
63005
+ async ({ sessionId }) => textResult(processManager.terminate(sessionId))
63006
+ );
63007
+ server.registerTool(
63008
+ "list_processes",
63009
+ {
63010
+ title: "List process sessions",
63011
+ description: "List process sessions started through Terminal Commands.",
63012
+ inputSchema: {},
63013
+ outputSchema: {
63014
+ processes: external_exports.array(
63015
+ external_exports.object({
63016
+ sessionId: external_exports.string(),
63017
+ program: external_exports.string(),
63018
+ args: external_exports.array(external_exports.string()),
63019
+ cwd: external_exports.string(),
63020
+ pid: external_exports.number().int().nullable(),
63021
+ running: external_exports.boolean(),
63022
+ exitCode: external_exports.number().int().nullable(),
63023
+ signal: external_exports.string().nullable(),
63024
+ timedOut: external_exports.boolean()
63025
+ })
63026
+ )
63027
+ },
63028
+ annotations: {
63029
+ readOnlyHint: true,
63030
+ destructiveHint: false,
63031
+ openWorldHint: false
63032
+ }
63033
+ },
63034
+ async () => textResult({ processes: processManager.list() })
63035
+ );
62827
63036
  return server;
62828
63037
  }
62829
63038
 
@@ -62884,6 +63093,7 @@ var listener = app.listen(port, host, () => {
62884
63093
  });
62885
63094
  function shutdown(signal) {
62886
63095
  console.log(`Received ${signal}; shutting down.`);
63096
+ processManager.close();
62887
63097
  listener.close((error62) => {
62888
63098
  if (error62) {
62889
63099
  console.error(error62);
package/dist/stdio.js CHANGED
@@ -36479,9 +36479,30 @@ var EMPTY_COMPLETION_RESULT = {
36479
36479
  }
36480
36480
  };
36481
36481
 
36482
- // src/runner.ts
36483
- import { execFile } from "node:child_process";
36484
- import { promisify } from "node:util";
36482
+ // src/file-reader.ts
36483
+ import { open as open2 } from "node:fs/promises";
36484
+ async function readBoundedTextFile(filename, maxCharacters) {
36485
+ const handle = await open2(filename, "r");
36486
+ try {
36487
+ const stats = await handle.stat();
36488
+ const buffer = Buffer.allocUnsafe(maxCharacters * 4 + 4);
36489
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
36490
+ const hasUnreadBytes = bytesRead < stats.size;
36491
+ const decoded = new TextDecoder().decode(buffer.subarray(0, bytesRead), {
36492
+ stream: hasUnreadBytes
36493
+ });
36494
+ return {
36495
+ contents: decoded.slice(0, maxCharacters),
36496
+ truncated: hasUnreadBytes || decoded.length > maxCharacters
36497
+ };
36498
+ } finally {
36499
+ await handle.close();
36500
+ }
36501
+ }
36502
+
36503
+ // src/process-manager.ts
36504
+ import { randomUUID } from "node:crypto";
36505
+ import { spawn } from "node:child_process";
36485
36506
 
36486
36507
  // src/security.ts
36487
36508
  import { constants } from "node:fs";
@@ -36571,59 +36592,144 @@ function assertProgramAllowed(program) {
36571
36592
  }
36572
36593
  }
36573
36594
 
36574
- // src/runner.ts
36575
- var execFileAsync = promisify(execFile);
36576
- async function runProgram(options) {
36577
- assertProgramAllowed(options.program);
36578
- try {
36579
- const { stdout, stderr } = await execFileAsync(
36580
- options.program,
36581
- options.args,
36582
- {
36583
- cwd: options.cwd,
36584
- env: safeEnvironment(),
36585
- encoding: "utf8",
36586
- maxBuffer: options.maxOutputChars,
36587
- timeout: options.timeoutMs,
36588
- windowsHide: true
36589
- }
36590
- );
36591
- return { exitCode: 0, signal: null, stdout, stderr, timedOut: false };
36592
- } catch (error62) {
36593
- const failure2 = error62;
36595
+ // src/process-manager.ts
36596
+ function appendBounded(current, value, limit) {
36597
+ if (current.length >= limit) return current;
36598
+ return `${current}${value.toString("utf8")}`.slice(0, limit);
36599
+ }
36600
+ function snapshot(session) {
36601
+ return {
36602
+ sessionId: session.sessionId,
36603
+ program: session.program,
36604
+ args: session.args,
36605
+ cwd: session.cwd,
36606
+ pid: session.child.pid ?? null,
36607
+ running: session.running,
36608
+ exitCode: session.exitCode,
36609
+ signal: session.signal,
36610
+ stdout: session.stdout,
36611
+ stderr: session.stderr,
36612
+ timedOut: session.timedOut
36613
+ };
36614
+ }
36615
+ var ProcessManager = class {
36616
+ #sessions = /* @__PURE__ */ new Map();
36617
+ start(options) {
36618
+ assertProgramAllowed(options.program);
36619
+ const child = spawn(options.program, options.args, {
36620
+ cwd: options.cwd,
36621
+ env: safeEnvironment(),
36622
+ stdio: ["ignore", "pipe", "pipe"],
36623
+ windowsHide: true
36624
+ });
36625
+ const session = {
36626
+ sessionId: randomUUID(),
36627
+ program: options.program,
36628
+ args: [...options.args],
36629
+ cwd: options.cwd,
36630
+ child,
36631
+ maxOutputCharacters: options.maxOutputCharacters,
36632
+ stdout: "",
36633
+ stderr: "",
36634
+ running: true,
36635
+ exitCode: null,
36636
+ signal: null,
36637
+ timedOut: false
36638
+ };
36639
+ this.#sessions.set(session.sessionId, session);
36640
+ child.stdout.on("data", (chunk) => {
36641
+ session.stdout = appendBounded(
36642
+ session.stdout,
36643
+ chunk,
36644
+ session.maxOutputCharacters
36645
+ );
36646
+ });
36647
+ child.stderr.on("data", (chunk) => {
36648
+ session.stderr = appendBounded(
36649
+ session.stderr,
36650
+ chunk,
36651
+ session.maxOutputCharacters
36652
+ );
36653
+ });
36654
+ child.once("error", (error62) => {
36655
+ session.stderr = appendBounded(
36656
+ session.stderr,
36657
+ error62.message,
36658
+ session.maxOutputCharacters
36659
+ );
36660
+ session.running = false;
36661
+ session.exitCode = null;
36662
+ session.signal = null;
36663
+ if (session.timeout) clearTimeout(session.timeout);
36664
+ });
36665
+ child.once("close", (exitCode, signal) => {
36666
+ session.running = false;
36667
+ session.exitCode = exitCode;
36668
+ session.signal = signal;
36669
+ if (session.timeout) clearTimeout(session.timeout);
36670
+ });
36671
+ session.timeout = setTimeout(() => {
36672
+ if (!session.running) return;
36673
+ session.timedOut = true;
36674
+ child.kill("SIGTERM");
36675
+ }, options.timeoutMs);
36676
+ const result = snapshot(session);
36594
36677
  return {
36595
- exitCode: typeof failure2.code === "number" ? failure2.code : null,
36596
- signal: failure2.signal ?? null,
36597
- stdout: failure2.stdout ?? "",
36598
- stderr: failure2.stderr ?? failure2.message,
36599
- timedOut: failure2.killed === true && failure2.signal === "SIGTERM"
36678
+ ...result,
36679
+ nextStdoutOffset: result.stdout.length,
36680
+ nextStderrOffset: result.stderr.length
36600
36681
  };
36601
36682
  }
36602
- }
36603
-
36604
- // src/file-reader.ts
36605
- import { open as open2 } from "node:fs/promises";
36606
- async function readBoundedTextFile(filename, maxCharacters) {
36607
- const handle = await open2(filename, "r");
36608
- try {
36609
- const stats = await handle.stat();
36610
- const buffer = Buffer.allocUnsafe(maxCharacters * 4 + 4);
36611
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
36612
- const hasUnreadBytes = bytesRead < stats.size;
36613
- const decoded = new TextDecoder().decode(buffer.subarray(0, bytesRead), {
36614
- stream: hasUnreadBytes
36615
- });
36683
+ read(sessionId, stdoutOffset, stderrOffset, maxOutputCharacters) {
36684
+ const session = this.#sessions.get(sessionId);
36685
+ if (!session)
36686
+ throw new Error("The requested process session was not found.");
36687
+ const safeStdoutOffset = Math.max(
36688
+ 0,
36689
+ Math.min(stdoutOffset, session.stdout.length)
36690
+ );
36691
+ const safeStderrOffset = Math.max(
36692
+ 0,
36693
+ Math.min(stderrOffset, session.stderr.length)
36694
+ );
36695
+ const stdout = session.stdout.slice(
36696
+ safeStdoutOffset,
36697
+ safeStdoutOffset + maxOutputCharacters
36698
+ );
36699
+ const stderr = session.stderr.slice(
36700
+ safeStderrOffset,
36701
+ safeStderrOffset + maxOutputCharacters
36702
+ );
36616
36703
  return {
36617
- contents: decoded.slice(0, maxCharacters),
36618
- truncated: hasUnreadBytes || decoded.length > maxCharacters
36704
+ ...snapshot(session),
36705
+ stdout,
36706
+ stderr,
36707
+ nextStdoutOffset: safeStdoutOffset + stdout.length,
36708
+ nextStderrOffset: safeStderrOffset + stderr.length
36619
36709
  };
36620
- } finally {
36621
- await handle.close();
36622
36710
  }
36623
- }
36711
+ terminate(sessionId) {
36712
+ const session = this.#sessions.get(sessionId);
36713
+ if (!session)
36714
+ throw new Error("The requested process session was not found.");
36715
+ if (session.running) session.child.kill("SIGTERM");
36716
+ return snapshot(session);
36717
+ }
36718
+ list() {
36719
+ return [...this.#sessions.values()].map(snapshot);
36720
+ }
36721
+ close() {
36722
+ for (const session of this.#sessions.values()) {
36723
+ if (session.timeout) clearTimeout(session.timeout);
36724
+ if (session.running) session.child.kill("SIGTERM");
36725
+ }
36726
+ this.#sessions.clear();
36727
+ }
36728
+ };
36624
36729
 
36625
36730
  // src/server.ts
36626
36731
  var roots = configuredRoots();
36732
+ var processManager = new ProcessManager();
36627
36733
  function textResult(value) {
36628
36734
  return {
36629
36735
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
@@ -36763,10 +36869,10 @@ function createServer() {
36763
36869
  }
36764
36870
  );
36765
36871
  server2.registerTool(
36766
- "run_program",
36872
+ "start_process",
36767
36873
  {
36768
- title: "Run a local program",
36769
- description: "Run one executable without a shell. Shells and privilege-elevation programs are blocked unless explicitly enabled by local configuration.",
36874
+ title: "Start a process",
36875
+ description: "Start one approved executable without a shell on this machine and return a session ID for reading its output.",
36770
36876
  inputSchema: {
36771
36877
  program: external_exports.string().min(1),
36772
36878
  args: external_exports.array(external_exports.string()).max(100).default([]),
@@ -36775,14 +36881,19 @@ function createServer() {
36775
36881
  maxOutputCharacters: external_exports.number().int().min(1e3).max(2e5).default(5e4)
36776
36882
  },
36777
36883
  outputSchema: {
36884
+ sessionId: external_exports.string(),
36778
36885
  program: external_exports.string(),
36779
36886
  args: external_exports.array(external_exports.string()),
36780
36887
  cwd: external_exports.string(),
36888
+ pid: external_exports.number().int().nullable(),
36889
+ running: external_exports.boolean(),
36781
36890
  exitCode: external_exports.number().int().nullable(),
36782
36891
  signal: external_exports.string().nullable(),
36783
36892
  stdout: external_exports.string(),
36784
36893
  stderr: external_exports.string(),
36785
- timedOut: external_exports.boolean()
36894
+ timedOut: external_exports.boolean(),
36895
+ nextStdoutOffset: external_exports.number().int().nonnegative(),
36896
+ nextStderrOffset: external_exports.number().int().nonnegative()
36786
36897
  },
36787
36898
  annotations: {
36788
36899
  readOnlyHint: false,
@@ -36795,16 +36906,114 @@ function createServer() {
36795
36906
  cwd ?? roots[0],
36796
36907
  roots
36797
36908
  );
36798
- const result = await runProgram({
36799
- program,
36800
- args,
36801
- cwd: workingDirectory,
36802
- timeoutMs,
36803
- maxOutputChars: maxOutputCharacters
36804
- });
36805
- return textResult({ program, args, cwd: workingDirectory, ...result });
36909
+ return textResult(
36910
+ processManager.start({
36911
+ program,
36912
+ args,
36913
+ cwd: workingDirectory,
36914
+ timeoutMs,
36915
+ maxOutputCharacters
36916
+ })
36917
+ );
36806
36918
  }
36807
36919
  );
36920
+ server2.registerTool(
36921
+ "read_process_output",
36922
+ {
36923
+ title: "Read process output",
36924
+ description: "Read new stdout and stderr from a previously started process session.",
36925
+ inputSchema: {
36926
+ sessionId: external_exports.string().uuid(),
36927
+ stdoutOffset: external_exports.number().int().min(0).default(0),
36928
+ stderrOffset: external_exports.number().int().min(0).default(0),
36929
+ maxOutputCharacters: external_exports.number().int().min(1e3).max(2e5).default(5e4)
36930
+ },
36931
+ outputSchema: {
36932
+ sessionId: external_exports.string(),
36933
+ program: external_exports.string(),
36934
+ args: external_exports.array(external_exports.string()),
36935
+ cwd: external_exports.string(),
36936
+ pid: external_exports.number().int().nullable(),
36937
+ running: external_exports.boolean(),
36938
+ exitCode: external_exports.number().int().nullable(),
36939
+ signal: external_exports.string().nullable(),
36940
+ stdout: external_exports.string(),
36941
+ stderr: external_exports.string(),
36942
+ timedOut: external_exports.boolean(),
36943
+ nextStdoutOffset: external_exports.number().int().nonnegative(),
36944
+ nextStderrOffset: external_exports.number().int().nonnegative()
36945
+ },
36946
+ annotations: {
36947
+ readOnlyHint: true,
36948
+ destructiveHint: false,
36949
+ openWorldHint: false
36950
+ }
36951
+ },
36952
+ async ({ sessionId, stdoutOffset, stderrOffset, maxOutputCharacters }) => textResult(
36953
+ processManager.read(
36954
+ sessionId,
36955
+ stdoutOffset,
36956
+ stderrOffset,
36957
+ maxOutputCharacters
36958
+ )
36959
+ )
36960
+ );
36961
+ server2.registerTool(
36962
+ "terminate_process",
36963
+ {
36964
+ title: "Terminate a process",
36965
+ description: "Stop a previously started process session on this machine.",
36966
+ inputSchema: { sessionId: external_exports.string().uuid() },
36967
+ outputSchema: {
36968
+ sessionId: external_exports.string(),
36969
+ program: external_exports.string(),
36970
+ args: external_exports.array(external_exports.string()),
36971
+ cwd: external_exports.string(),
36972
+ pid: external_exports.number().int().nullable(),
36973
+ running: external_exports.boolean(),
36974
+ exitCode: external_exports.number().int().nullable(),
36975
+ signal: external_exports.string().nullable(),
36976
+ stdout: external_exports.string(),
36977
+ stderr: external_exports.string(),
36978
+ timedOut: external_exports.boolean()
36979
+ },
36980
+ annotations: {
36981
+ readOnlyHint: false,
36982
+ destructiveHint: true,
36983
+ openWorldHint: false
36984
+ }
36985
+ },
36986
+ async ({ sessionId }) => textResult(processManager.terminate(sessionId))
36987
+ );
36988
+ server2.registerTool(
36989
+ "list_processes",
36990
+ {
36991
+ title: "List process sessions",
36992
+ description: "List process sessions started through Terminal Commands.",
36993
+ inputSchema: {},
36994
+ outputSchema: {
36995
+ processes: external_exports.array(
36996
+ external_exports.object({
36997
+ sessionId: external_exports.string(),
36998
+ program: external_exports.string(),
36999
+ args: external_exports.array(external_exports.string()),
37000
+ cwd: external_exports.string(),
37001
+ pid: external_exports.number().int().nullable(),
37002
+ running: external_exports.boolean(),
37003
+ exitCode: external_exports.number().int().nullable(),
37004
+ signal: external_exports.string().nullable(),
37005
+ timedOut: external_exports.boolean()
37006
+ })
37007
+ )
37008
+ },
37009
+ annotations: {
37010
+ readOnlyHint: true,
37011
+ destructiveHint: false,
37012
+ openWorldHint: false
37013
+ }
37014
+ },
37015
+ async () => textResult({ processes: processManager.list() })
37016
+ );
36808
37017
  return server2;
36809
37018
  }
36810
37019
 
@@ -36813,6 +37022,7 @@ var server = createServer();
36813
37022
  var transport = new StdioServerTransport();
36814
37023
  await server.connect(transport);
36815
37024
  async function shutdown() {
37025
+ processManager.close();
36816
37026
  await server.close();
36817
37027
  process.exit(0);
36818
37028
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminal-commands",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "A cross-platform, approval-gated terminal companion for ChatGPT, Claude, Codex, and any MCP-compatible AI client.",
@@ -22,7 +22,7 @@
22
22
  "start:agent": "node dist/agent.js",
23
23
  "cli": "node dist/cli.js",
24
24
  "cli:dev": "tsx src/cli.ts",
25
- "test": "tsx --test test/**/*.test.ts",
25
+ "test": "tsx --test test/*.test.ts",
26
26
  "test:smoke": "node test/smoke.mjs",
27
27
  "prepack": "npm run build"
28
28
  },