@theokit/sdk-pty 0.1.0 → 0.2.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/dist/index.cjs CHANGED
@@ -23,6 +23,11 @@ ${buf.slice(-max)}` : buf;
23
23
  }
24
24
  var PtyInteractiveBackend = class extends interactive.InteractiveBackend {
25
25
  sessions = /* @__PURE__ */ new Map();
26
+ wrapCommand;
27
+ constructor(options = {}) {
28
+ super();
29
+ this.wrapCommand = options.wrapCommand;
30
+ }
26
31
  ptyModule;
27
32
  exitReaperArmed = false;
28
33
  /** Lazy, cached node-pty load — a failed native build must degrade, not crash at import time. */
@@ -56,24 +61,25 @@ var PtyInteractiveBackend = class extends interactive.InteractiveBackend {
56
61
  clearTimeout(session.ttlTimer);
57
62
  session.ttlTimer = setTimeout(() => this.kill(session.id), ttlMs);
58
63
  }
59
- async startInteractive(command, opts) {
64
+ /** Allocate a PTY for `command` or throw a typed {@link InteractiveUnavailableError}. Validates the
65
+ * cwd at the boundary (node-pty does NOT throw on a non-existent cwd — it spawns a broken session). */
66
+ spawnPty(command, opts) {
60
67
  const pty = this.loadPty();
61
68
  if (pty === null) {
62
69
  throw new interactive.InteractiveUnavailableError(
63
70
  "interactive shell unavailable: node-pty native module failed to load; use non-interactive exec"
64
71
  );
65
72
  }
66
- this.armExitReaper();
67
- const shell = process.env.SHELL ?? "/bin/bash";
68
- const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
69
- const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
70
73
  const cwd = opts?.cwd ?? process.cwd();
71
74
  if (!fs.existsSync(cwd)) {
72
- throw new interactive.InteractiveUnavailableError(`interactive shell unavailable: cwd does not exist: ${cwd}`);
75
+ throw new interactive.InteractiveUnavailableError(
76
+ `interactive shell unavailable: cwd does not exist: ${cwd}`
77
+ );
73
78
  }
74
- let proc;
79
+ const efetivo = this.wrapCommand?.(command, cwd) ?? command;
80
+ const shell = process.env.SHELL ?? "/bin/bash";
75
81
  try {
76
- proc = pty.spawn(shell, ["-c", command], {
82
+ return pty.spawn(shell, ["-c", efetivo], {
77
83
  name: "xterm-color",
78
84
  cols: opts?.cols ?? 80,
79
85
  rows: opts?.rows ?? 24,
@@ -85,6 +91,12 @@ var PtyInteractiveBackend = class extends interactive.InteractiveBackend {
85
91
  `interactive shell unavailable: failed to spawn a PTY (${err instanceof Error ? err.message : String(err)})`
86
92
  );
87
93
  }
94
+ }
95
+ async startInteractive(command, opts) {
96
+ this.armExitReaper();
97
+ const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
98
+ const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
99
+ const proc = this.spawnPty(command, opts);
88
100
  const id = `pty-${crypto.randomUUID()}`;
89
101
  const session = {
90
102
  id,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pty-interactive-backend.ts"],"names":["createRequire","InteractiveBackend","InteractiveUnavailableError","existsSync","randomUUID","NoSuchSessionError"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `PtyInteractiveBackend` — the LOCAL implementation of `@theokit/sdk`'s\n * `InteractiveBackend`, backed by `node-pty`. This is the terminal-surface\n * backend: it allocates a real pseudo-terminal so REPLs, `git rebase -i`, and\n * prompting commands can be driven to completion. Ported from the AgentBuilder\n * Codex clone (M11) into the ecosystem (M14) so any terminal agent injects it.\n *\n * `node-pty` is an OPTIONAL dependency of this package — the ONLY place in the\n * theokit ecosystem that touches it. Core / sdk-tools / cluster / desktop never\n * do. When the native module is unavailable (or a spawn fails), every method\n * throws the SDK's typed {@link InteractiveUnavailableError} so the caller falls\n * back to non-interactive exec.\n *\n * Safety (mirrors M11): graceful typed degradation; per-session write\n * serialization (concurrent writes never steal each other's output); idle TTL\n * reaper; process-GROUP kill so detached grandchildren die; a process-exit\n * reaper; tail-capped output (the live prompt, not the stale banner).\n */\nimport { randomUUID } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\n\nimport {\n InteractiveBackend,\n InteractiveUnavailableError,\n NoSuchSessionError,\n type StartInteractiveOptions,\n type StartInteractiveResult,\n type WriteStdinOptions,\n type WriteStdinResult,\n} from \"@theokit/sdk/interactive\";\n\nconst nodeRequire = createRequire(import.meta.url);\n\nexport const YIELD_MIN_MS = 250;\nexport const YIELD_MAX_MS = 30_000;\nconst DEFAULT_YIELD_MS = 500;\nconst DEFAULT_TTL_MS = 300_000; // 5 min idle → reap\nconst DEFAULT_MAX_BYTES = 100_000;\n\n/** The minimal `node-pty` surface driven here (kept structural so the import stays lazy). */\ninterface PtyProcess {\n readonly pid: number;\n write(data: string): void;\n kill(signal?: string): void;\n onData(cb: (data: string) => void): void;\n onExit(cb: (e: { exitCode: number }) => void): void;\n}\ninterface PtyModule {\n spawn(file: string, args: string[], opts: Record<string, unknown>): PtyProcess;\n}\n\ninterface PtySession {\n id: string;\n pty: PtyProcess;\n pending: string;\n alive: boolean;\n ttlTimer: ReturnType<typeof setTimeout>;\n writeChain: Promise<unknown>;\n}\n\n/** Bound the yield window to [{@link YIELD_MIN_MS}, {@link YIELD_MAX_MS}]. */\nexport function clampYield(ms: number | undefined): number {\n const v = ms ?? DEFAULT_YIELD_MS;\n return Math.max(YIELD_MIN_MS, Math.min(YIELD_MAX_MS, v));\n}\n\n/** Keep the TAIL of output — the newest bytes carry the live prompt. */\nfunction capTail(buf: string, max: number): string {\n return buf.length > max ? `…(truncated)\\n${buf.slice(-max)}` : buf;\n}\n\nexport class PtyInteractiveBackend extends InteractiveBackend {\n private readonly sessions = new Map<string, PtySession>();\n private ptyModule: PtyModule | null | undefined;\n private exitReaperArmed = false;\n\n /** Lazy, cached node-pty load — a failed native build must degrade, not crash at import time. */\n private loadPty(): PtyModule | null {\n if (this.ptyModule !== undefined) return this.ptyModule;\n try {\n this.ptyModule = nodeRequire(\"node-pty\") as PtyModule;\n } catch {\n this.ptyModule = null;\n }\n return this.ptyModule;\n }\n\n /** Whether the interactive (PTY) path is usable in this environment. */\n available(): boolean {\n return this.loadPty() !== null;\n }\n\n /** Reap orphaned PTYs when the host process exits. ONLY the `exit` event — installing SIGINT/SIGTERM\n * handlers would remove Node's default terminate-on-signal behavior. Armed once, lazily. */\n private armExitReaper(): void {\n if (this.exitReaperArmed) return;\n this.exitReaperArmed = true;\n process.on(\"exit\", () => this.killAll());\n }\n\n private async collect(session: PtySession, yieldMs: number, maxBytes: number): Promise<string> {\n await new Promise((r) => setTimeout(r, clampYield(yieldMs)));\n const out = session.pending;\n session.pending = \"\";\n return capTail(out, maxBytes);\n }\n\n private armTtl(session: PtySession, ttlMs: number): void {\n clearTimeout(session.ttlTimer);\n session.ttlTimer = setTimeout(() => this.kill(session.id), ttlMs);\n }\n\n async startInteractive(\n command: string,\n opts?: StartInteractiveOptions,\n ): Promise<StartInteractiveResult> {\n const pty = this.loadPty();\n if (pty === null) {\n throw new InteractiveUnavailableError(\n \"interactive shell unavailable: node-pty native module failed to load; use non-interactive exec\",\n );\n }\n this.armExitReaper();\n const shell = process.env.SHELL ?? \"/bin/bash\";\n const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;\n const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;\n // Validate cwd at the boundary: node-pty does NOT throw on a non-existent cwd (it spawns a broken\n // session), so fail-fast to the typed fallback the caller expects.\n const cwd = opts?.cwd ?? process.cwd();\n if (!existsSync(cwd)) {\n throw new InteractiveUnavailableError(`interactive shell unavailable: cwd does not exist: ${cwd}`);\n }\n let proc: PtyProcess;\n try {\n proc = pty.spawn(shell, [\"-c\", command], {\n name: \"xterm-color\",\n cols: opts?.cols ?? 80,\n rows: opts?.rows ?? 24,\n cwd,\n env: process.env,\n });\n } catch (err) {\n throw new InteractiveUnavailableError(\n `interactive shell unavailable: failed to spawn a PTY (${err instanceof Error ? err.message : String(err)})`,\n );\n }\n const id = `pty-${randomUUID()}`;\n const session: PtySession = {\n id,\n pty: proc,\n pending: \"\",\n alive: true,\n ttlTimer: setTimeout(() => this.kill(id), ttlMs),\n writeChain: Promise.resolve(),\n };\n proc.onData((data) => {\n session.pending = capTail(session.pending + data, maxBytes);\n });\n proc.onExit(() => {\n session.alive = false;\n clearTimeout(session.ttlTimer);\n this.sessions.delete(id);\n });\n this.sessions.set(id, session);\n const output = await this.collect(session, opts?.yieldMs ?? DEFAULT_YIELD_MS, maxBytes);\n return { sessionId: id, output };\n }\n\n async writeStdin(\n sessionId: string,\n chars: string,\n opts?: WriteStdinOptions,\n ): Promise<WriteStdinResult> {\n const session = this.sessions.get(sessionId);\n if (session === undefined || !session.alive) throw new NoSuchSessionError(sessionId);\n // Serialize per session: chain onto the previous write so two concurrent calls run strictly in\n // order and each reads only its own output window (no stolen output).\n const run = session.writeChain.then(async () => {\n if (!session.alive) throw new NoSuchSessionError(sessionId);\n this.armTtl(session, opts?.ttlMs ?? DEFAULT_TTL_MS);\n if (chars.length > 0) session.pty.write(chars);\n const output = await this.collect(\n session,\n opts?.yieldMs ?? DEFAULT_YIELD_MS,\n opts?.maxBytes ?? DEFAULT_MAX_BYTES,\n );\n return { output, alive: session.alive };\n });\n session.writeChain = run.catch(() => undefined);\n return run;\n }\n\n /** Kill a single session (idempotent). Kills the whole process GROUP so a detached grandchild dies too. */\n kill(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (session === undefined) return;\n clearTimeout(session.ttlTimer);\n session.alive = false;\n this.sessions.delete(sessionId);\n try {\n process.kill(-session.pty.pid, \"SIGKILL\");\n } catch {\n try {\n session.pty.kill();\n } catch {\n // already dead — nothing to do\n }\n }\n }\n\n /** Reap every session — used by the process-exit reaper; also callable on `/clear`. */\n killAll(): void {\n for (const id of [...this.sessions.keys()]) this.kill(id);\n }\n\n /** Live session count — for observability / tests. */\n activeSessionCount(): number {\n return this.sessions.size;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/pty-interactive-backend.ts"],"names":["createRequire","InteractiveBackend","InteractiveUnavailableError","existsSync","randomUUID","NoSuchSessionError"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * `PtyInteractiveBackend` — the LOCAL implementation of `@theokit/sdk`'s\n * `InteractiveBackend`, backed by `node-pty`. This is the terminal-surface\n * backend: it allocates a real pseudo-terminal so REPLs, `git rebase -i`, and\n * prompting commands can be driven to completion. Ported from the AgentBuilder\n * Codex clone (M11) into the ecosystem (M14) so any terminal agent injects it.\n *\n * `node-pty` is an OPTIONAL dependency of this package — the ONLY place in the\n * theokit ecosystem that touches it. Core / sdk-tools / cluster / desktop never\n * do. When the native module is unavailable (or a spawn fails), every method\n * throws the SDK's typed {@link InteractiveUnavailableError} so the caller falls\n * back to non-interactive exec.\n *\n * Safety (mirrors M11): graceful typed degradation; per-session write\n * serialization (concurrent writes never steal each other's output); idle TTL\n * reaper; process-GROUP kill so detached grandchildren die; a process-exit\n * reaper; tail-capped output (the live prompt, not the stale banner).\n */\nimport { randomUUID } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\n\nimport {\n InteractiveBackend,\n InteractiveUnavailableError,\n NoSuchSessionError,\n type StartInteractiveOptions,\n type StartInteractiveResult,\n type WriteStdinOptions,\n type WriteStdinResult,\n} from \"@theokit/sdk/interactive\";\n\nconst nodeRequire = createRequire(import.meta.url);\n\nexport const YIELD_MIN_MS = 250;\nexport const YIELD_MAX_MS = 30_000;\nconst DEFAULT_YIELD_MS = 500;\nconst DEFAULT_TTL_MS = 300_000; // 5 min idle → reap\nconst DEFAULT_MAX_BYTES = 100_000;\n\n/** The minimal `node-pty` surface driven here (kept structural so the import stays lazy). */\ninterface PtyProcess {\n readonly pid: number;\n write(data: string): void;\n kill(signal?: string): void;\n onData(cb: (data: string) => void): void;\n onExit(cb: (e: { exitCode: number }) => void): void;\n}\ninterface PtyModule {\n spawn(file: string, args: string[], opts: Record<string, unknown>): PtyProcess;\n}\n\ninterface PtySession {\n id: string;\n pty: PtyProcess;\n pending: string;\n alive: boolean;\n ttlTimer: ReturnType<typeof setTimeout>;\n writeChain: Promise<unknown>;\n}\n\n/** Bound the yield window to [{@link YIELD_MIN_MS}, {@link YIELD_MAX_MS}]. */\nexport function clampYield(ms: number | undefined): number {\n const v = ms ?? DEFAULT_YIELD_MS;\n return Math.max(YIELD_MIN_MS, Math.min(YIELD_MAX_MS, v));\n}\n\n/** Keep the TAIL of output — the newest bytes carry the live prompt. */\nfunction capTail(buf: string, max: number): string {\n return buf.length > max ? `…(truncated)\\n${buf.slice(-max)}` : buf;\n}\n\n/**\n * M75 T3.1 — como o chamador embrulha o comando antes do spawn.\n *\n * Existe para que confinamento (sandbox) componha com PTY sem herança: o backend continua dono do\n * spawn, o chamador continua dono da política, e nenhum dos dois conhece o tipo do outro.\n */\nexport interface PtyInteractiveBackendOptions {\n /**\n * Transforma o comando imediatamente antes do spawn. Recebe o `cwd` JÁ RESOLVIDO — o PTY spawna\n * nele, então um wrap que mirasse outro diretório produziria confinamento que não confina nada.\n *\n * Devolver `null` significa **não embrulhe** — decisão explícita, distinta de \"embrulhei e deu no\n * mesmo\". É o caso do modo sem confinamento.\n */\n readonly wrapCommand?: (command: string, cwd: string) => string | null;\n}\n\nexport class PtyInteractiveBackend extends InteractiveBackend {\n private readonly sessions = new Map<string, PtySession>();\n private readonly wrapCommand: ((command: string, cwd: string) => string | null) | undefined;\n\n constructor(options: PtyInteractiveBackendOptions = {}) {\n super();\n this.wrapCommand = options.wrapCommand;\n }\n\n private ptyModule: PtyModule | null | undefined;\n private exitReaperArmed = false;\n\n /** Lazy, cached node-pty load — a failed native build must degrade, not crash at import time. */\n private loadPty(): PtyModule | null {\n if (this.ptyModule !== undefined) return this.ptyModule;\n try {\n this.ptyModule = nodeRequire(\"node-pty\") as PtyModule;\n } catch {\n this.ptyModule = null;\n }\n return this.ptyModule;\n }\n\n /** Whether the interactive (PTY) path is usable in this environment. */\n available(): boolean {\n return this.loadPty() !== null;\n }\n\n /** Reap orphaned PTYs when the host process exits. ONLY the `exit` event — installing SIGINT/SIGTERM\n * handlers would remove Node's default terminate-on-signal behavior. Armed once, lazily. */\n private armExitReaper(): void {\n if (this.exitReaperArmed) return;\n this.exitReaperArmed = true;\n process.on(\"exit\", () => this.killAll());\n }\n\n private async collect(session: PtySession, yieldMs: number, maxBytes: number): Promise<string> {\n await new Promise((r) => setTimeout(r, clampYield(yieldMs)));\n const out = session.pending;\n session.pending = \"\";\n return capTail(out, maxBytes);\n }\n\n private armTtl(session: PtySession, ttlMs: number): void {\n clearTimeout(session.ttlTimer);\n session.ttlTimer = setTimeout(() => this.kill(session.id), ttlMs);\n }\n\n /** Allocate a PTY for `command` or throw a typed {@link InteractiveUnavailableError}. Validates the\n * cwd at the boundary (node-pty does NOT throw on a non-existent cwd — it spawns a broken session). */\n private spawnPty(command: string, opts?: StartInteractiveOptions): PtyProcess {\n const pty = this.loadPty();\n if (pty === null) {\n throw new InteractiveUnavailableError(\n \"interactive shell unavailable: node-pty native module failed to load; use non-interactive exec\",\n );\n }\n const cwd = opts?.cwd ?? process.cwd();\n if (!existsSync(cwd)) {\n throw new InteractiveUnavailableError(\n `interactive shell unavailable: cwd does not exist: ${cwd}`,\n );\n }\n // M75 T3.1 — o wrap entra AQUI: depois do cwd resolvido e validado, antes do spawn. É o ponto\n // único por onde todo comando passa, então não há caminho que escape do confinamento.\n const efetivo = this.wrapCommand?.(command, cwd) ?? command;\n\n const shell = process.env.SHELL ?? \"/bin/bash\";\n try {\n return pty.spawn(shell, [\"-c\", efetivo], {\n name: \"xterm-color\",\n cols: opts?.cols ?? 80,\n rows: opts?.rows ?? 24,\n cwd,\n env: process.env,\n });\n } catch (err) {\n throw new InteractiveUnavailableError(\n `interactive shell unavailable: failed to spawn a PTY (${err instanceof Error ? err.message : String(err)})`,\n );\n }\n }\n\n async startInteractive(\n command: string,\n opts?: StartInteractiveOptions,\n ): Promise<StartInteractiveResult> {\n this.armExitReaper();\n const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;\n const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;\n const proc = this.spawnPty(command, opts);\n const id = `pty-${randomUUID()}`;\n const session: PtySession = {\n id,\n pty: proc,\n pending: \"\",\n alive: true,\n ttlTimer: setTimeout(() => this.kill(id), ttlMs),\n writeChain: Promise.resolve(),\n };\n proc.onData((data) => {\n session.pending = capTail(session.pending + data, maxBytes);\n });\n proc.onExit(() => {\n session.alive = false;\n clearTimeout(session.ttlTimer);\n this.sessions.delete(id);\n });\n this.sessions.set(id, session);\n const output = await this.collect(session, opts?.yieldMs ?? DEFAULT_YIELD_MS, maxBytes);\n return { sessionId: id, output };\n }\n\n async writeStdin(\n sessionId: string,\n chars: string,\n opts?: WriteStdinOptions,\n ): Promise<WriteStdinResult> {\n const session = this.sessions.get(sessionId);\n if (session === undefined || !session.alive) throw new NoSuchSessionError(sessionId);\n // Serialize per session: chain onto the previous write so two concurrent calls run strictly in\n // order and each reads only its own output window (no stolen output).\n const run = session.writeChain.then(async () => {\n if (!session.alive) throw new NoSuchSessionError(sessionId);\n this.armTtl(session, opts?.ttlMs ?? DEFAULT_TTL_MS);\n if (chars.length > 0) session.pty.write(chars);\n const output = await this.collect(\n session,\n opts?.yieldMs ?? DEFAULT_YIELD_MS,\n opts?.maxBytes ?? DEFAULT_MAX_BYTES,\n );\n return { output, alive: session.alive };\n });\n session.writeChain = run.catch(() => undefined);\n return run;\n }\n\n /** Kill a single session (idempotent). Kills the whole process GROUP so a detached grandchild dies too. */\n kill(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (session === undefined) return;\n clearTimeout(session.ttlTimer);\n session.alive = false;\n this.sessions.delete(sessionId);\n try {\n process.kill(-session.pty.pid, \"SIGKILL\");\n } catch {\n try {\n session.pty.kill();\n } catch {\n // already dead — nothing to do\n }\n }\n }\n\n /** Reap every session — used by the process-exit reaper; also callable on `/clear`. */\n killAll(): void {\n for (const id of [...this.sessions.keys()]) this.kill(id);\n }\n\n /** Live session count — for observability / tests. */\n activeSessionCount(): number {\n return this.sessions.size;\n }\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -4,8 +4,26 @@ declare const YIELD_MIN_MS = 250;
4
4
  declare const YIELD_MAX_MS = 30000;
5
5
  /** Bound the yield window to [{@link YIELD_MIN_MS}, {@link YIELD_MAX_MS}]. */
6
6
  declare function clampYield(ms: number | undefined): number;
7
+ /**
8
+ * M75 T3.1 — como o chamador embrulha o comando antes do spawn.
9
+ *
10
+ * Existe para que confinamento (sandbox) componha com PTY sem herança: o backend continua dono do
11
+ * spawn, o chamador continua dono da política, e nenhum dos dois conhece o tipo do outro.
12
+ */
13
+ interface PtyInteractiveBackendOptions {
14
+ /**
15
+ * Transforma o comando imediatamente antes do spawn. Recebe o `cwd` JÁ RESOLVIDO — o PTY spawna
16
+ * nele, então um wrap que mirasse outro diretório produziria confinamento que não confina nada.
17
+ *
18
+ * Devolver `null` significa **não embrulhe** — decisão explícita, distinta de "embrulhei e deu no
19
+ * mesmo". É o caso do modo sem confinamento.
20
+ */
21
+ readonly wrapCommand?: (command: string, cwd: string) => string | null;
22
+ }
7
23
  declare class PtyInteractiveBackend extends InteractiveBackend {
8
24
  private readonly sessions;
25
+ private readonly wrapCommand;
26
+ constructor(options?: PtyInteractiveBackendOptions);
9
27
  private ptyModule;
10
28
  private exitReaperArmed;
11
29
  /** Lazy, cached node-pty load — a failed native build must degrade, not crash at import time. */
@@ -17,6 +35,9 @@ declare class PtyInteractiveBackend extends InteractiveBackend {
17
35
  private armExitReaper;
18
36
  private collect;
19
37
  private armTtl;
38
+ /** Allocate a PTY for `command` or throw a typed {@link InteractiveUnavailableError}. Validates the
39
+ * cwd at the boundary (node-pty does NOT throw on a non-existent cwd — it spawns a broken session). */
40
+ private spawnPty;
20
41
  startInteractive(command: string, opts?: StartInteractiveOptions): Promise<StartInteractiveResult>;
21
42
  writeStdin(sessionId: string, chars: string, opts?: WriteStdinOptions): Promise<WriteStdinResult>;
22
43
  /** Kill a single session (idempotent). Kills the whole process GROUP so a detached grandchild dies too. */
package/dist/index.d.ts CHANGED
@@ -4,8 +4,26 @@ declare const YIELD_MIN_MS = 250;
4
4
  declare const YIELD_MAX_MS = 30000;
5
5
  /** Bound the yield window to [{@link YIELD_MIN_MS}, {@link YIELD_MAX_MS}]. */
6
6
  declare function clampYield(ms: number | undefined): number;
7
+ /**
8
+ * M75 T3.1 — como o chamador embrulha o comando antes do spawn.
9
+ *
10
+ * Existe para que confinamento (sandbox) componha com PTY sem herança: o backend continua dono do
11
+ * spawn, o chamador continua dono da política, e nenhum dos dois conhece o tipo do outro.
12
+ */
13
+ interface PtyInteractiveBackendOptions {
14
+ /**
15
+ * Transforma o comando imediatamente antes do spawn. Recebe o `cwd` JÁ RESOLVIDO — o PTY spawna
16
+ * nele, então um wrap que mirasse outro diretório produziria confinamento que não confina nada.
17
+ *
18
+ * Devolver `null` significa **não embrulhe** — decisão explícita, distinta de "embrulhei e deu no
19
+ * mesmo". É o caso do modo sem confinamento.
20
+ */
21
+ readonly wrapCommand?: (command: string, cwd: string) => string | null;
22
+ }
7
23
  declare class PtyInteractiveBackend extends InteractiveBackend {
8
24
  private readonly sessions;
25
+ private readonly wrapCommand;
26
+ constructor(options?: PtyInteractiveBackendOptions);
9
27
  private ptyModule;
10
28
  private exitReaperArmed;
11
29
  /** Lazy, cached node-pty load — a failed native build must degrade, not crash at import time. */
@@ -17,6 +35,9 @@ declare class PtyInteractiveBackend extends InteractiveBackend {
17
35
  private armExitReaper;
18
36
  private collect;
19
37
  private armTtl;
38
+ /** Allocate a PTY for `command` or throw a typed {@link InteractiveUnavailableError}. Validates the
39
+ * cwd at the boundary (node-pty does NOT throw on a non-existent cwd — it spawns a broken session). */
40
+ private spawnPty;
20
41
  startInteractive(command: string, opts?: StartInteractiveOptions): Promise<StartInteractiveResult>;
21
42
  writeStdin(sessionId: string, chars: string, opts?: WriteStdinOptions): Promise<WriteStdinResult>;
22
43
  /** Kill a single session (idempotent). Kills the whole process GROUP so a detached grandchild dies too. */
package/dist/index.js CHANGED
@@ -20,6 +20,11 @@ ${buf.slice(-max)}` : buf;
20
20
  }
21
21
  var PtyInteractiveBackend = class extends InteractiveBackend {
22
22
  sessions = /* @__PURE__ */ new Map();
23
+ wrapCommand;
24
+ constructor(options = {}) {
25
+ super();
26
+ this.wrapCommand = options.wrapCommand;
27
+ }
23
28
  ptyModule;
24
29
  exitReaperArmed = false;
25
30
  /** Lazy, cached node-pty load — a failed native build must degrade, not crash at import time. */
@@ -53,24 +58,25 @@ var PtyInteractiveBackend = class extends InteractiveBackend {
53
58
  clearTimeout(session.ttlTimer);
54
59
  session.ttlTimer = setTimeout(() => this.kill(session.id), ttlMs);
55
60
  }
56
- async startInteractive(command, opts) {
61
+ /** Allocate a PTY for `command` or throw a typed {@link InteractiveUnavailableError}. Validates the
62
+ * cwd at the boundary (node-pty does NOT throw on a non-existent cwd — it spawns a broken session). */
63
+ spawnPty(command, opts) {
57
64
  const pty = this.loadPty();
58
65
  if (pty === null) {
59
66
  throw new InteractiveUnavailableError(
60
67
  "interactive shell unavailable: node-pty native module failed to load; use non-interactive exec"
61
68
  );
62
69
  }
63
- this.armExitReaper();
64
- const shell = process.env.SHELL ?? "/bin/bash";
65
- const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
66
- const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
67
70
  const cwd = opts?.cwd ?? process.cwd();
68
71
  if (!existsSync(cwd)) {
69
- throw new InteractiveUnavailableError(`interactive shell unavailable: cwd does not exist: ${cwd}`);
72
+ throw new InteractiveUnavailableError(
73
+ `interactive shell unavailable: cwd does not exist: ${cwd}`
74
+ );
70
75
  }
71
- let proc;
76
+ const efetivo = this.wrapCommand?.(command, cwd) ?? command;
77
+ const shell = process.env.SHELL ?? "/bin/bash";
72
78
  try {
73
- proc = pty.spawn(shell, ["-c", command], {
79
+ return pty.spawn(shell, ["-c", efetivo], {
74
80
  name: "xterm-color",
75
81
  cols: opts?.cols ?? 80,
76
82
  rows: opts?.rows ?? 24,
@@ -82,6 +88,12 @@ var PtyInteractiveBackend = class extends InteractiveBackend {
82
88
  `interactive shell unavailable: failed to spawn a PTY (${err instanceof Error ? err.message : String(err)})`
83
89
  );
84
90
  }
91
+ }
92
+ async startInteractive(command, opts) {
93
+ this.armExitReaper();
94
+ const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;
95
+ const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;
96
+ const proc = this.spawnPty(command, opts);
85
97
  const id = `pty-${randomUUID()}`;
86
98
  const session = {
87
99
  id,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pty-interactive-backend.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `PtyInteractiveBackend` — the LOCAL implementation of `@theokit/sdk`'s\n * `InteractiveBackend`, backed by `node-pty`. This is the terminal-surface\n * backend: it allocates a real pseudo-terminal so REPLs, `git rebase -i`, and\n * prompting commands can be driven to completion. Ported from the AgentBuilder\n * Codex clone (M11) into the ecosystem (M14) so any terminal agent injects it.\n *\n * `node-pty` is an OPTIONAL dependency of this package — the ONLY place in the\n * theokit ecosystem that touches it. Core / sdk-tools / cluster / desktop never\n * do. When the native module is unavailable (or a spawn fails), every method\n * throws the SDK's typed {@link InteractiveUnavailableError} so the caller falls\n * back to non-interactive exec.\n *\n * Safety (mirrors M11): graceful typed degradation; per-session write\n * serialization (concurrent writes never steal each other's output); idle TTL\n * reaper; process-GROUP kill so detached grandchildren die; a process-exit\n * reaper; tail-capped output (the live prompt, not the stale banner).\n */\nimport { randomUUID } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\n\nimport {\n InteractiveBackend,\n InteractiveUnavailableError,\n NoSuchSessionError,\n type StartInteractiveOptions,\n type StartInteractiveResult,\n type WriteStdinOptions,\n type WriteStdinResult,\n} from \"@theokit/sdk/interactive\";\n\nconst nodeRequire = createRequire(import.meta.url);\n\nexport const YIELD_MIN_MS = 250;\nexport const YIELD_MAX_MS = 30_000;\nconst DEFAULT_YIELD_MS = 500;\nconst DEFAULT_TTL_MS = 300_000; // 5 min idle → reap\nconst DEFAULT_MAX_BYTES = 100_000;\n\n/** The minimal `node-pty` surface driven here (kept structural so the import stays lazy). */\ninterface PtyProcess {\n readonly pid: number;\n write(data: string): void;\n kill(signal?: string): void;\n onData(cb: (data: string) => void): void;\n onExit(cb: (e: { exitCode: number }) => void): void;\n}\ninterface PtyModule {\n spawn(file: string, args: string[], opts: Record<string, unknown>): PtyProcess;\n}\n\ninterface PtySession {\n id: string;\n pty: PtyProcess;\n pending: string;\n alive: boolean;\n ttlTimer: ReturnType<typeof setTimeout>;\n writeChain: Promise<unknown>;\n}\n\n/** Bound the yield window to [{@link YIELD_MIN_MS}, {@link YIELD_MAX_MS}]. */\nexport function clampYield(ms: number | undefined): number {\n const v = ms ?? DEFAULT_YIELD_MS;\n return Math.max(YIELD_MIN_MS, Math.min(YIELD_MAX_MS, v));\n}\n\n/** Keep the TAIL of output — the newest bytes carry the live prompt. */\nfunction capTail(buf: string, max: number): string {\n return buf.length > max ? `…(truncated)\\n${buf.slice(-max)}` : buf;\n}\n\nexport class PtyInteractiveBackend extends InteractiveBackend {\n private readonly sessions = new Map<string, PtySession>();\n private ptyModule: PtyModule | null | undefined;\n private exitReaperArmed = false;\n\n /** Lazy, cached node-pty load — a failed native build must degrade, not crash at import time. */\n private loadPty(): PtyModule | null {\n if (this.ptyModule !== undefined) return this.ptyModule;\n try {\n this.ptyModule = nodeRequire(\"node-pty\") as PtyModule;\n } catch {\n this.ptyModule = null;\n }\n return this.ptyModule;\n }\n\n /** Whether the interactive (PTY) path is usable in this environment. */\n available(): boolean {\n return this.loadPty() !== null;\n }\n\n /** Reap orphaned PTYs when the host process exits. ONLY the `exit` event — installing SIGINT/SIGTERM\n * handlers would remove Node's default terminate-on-signal behavior. Armed once, lazily. */\n private armExitReaper(): void {\n if (this.exitReaperArmed) return;\n this.exitReaperArmed = true;\n process.on(\"exit\", () => this.killAll());\n }\n\n private async collect(session: PtySession, yieldMs: number, maxBytes: number): Promise<string> {\n await new Promise((r) => setTimeout(r, clampYield(yieldMs)));\n const out = session.pending;\n session.pending = \"\";\n return capTail(out, maxBytes);\n }\n\n private armTtl(session: PtySession, ttlMs: number): void {\n clearTimeout(session.ttlTimer);\n session.ttlTimer = setTimeout(() => this.kill(session.id), ttlMs);\n }\n\n async startInteractive(\n command: string,\n opts?: StartInteractiveOptions,\n ): Promise<StartInteractiveResult> {\n const pty = this.loadPty();\n if (pty === null) {\n throw new InteractiveUnavailableError(\n \"interactive shell unavailable: node-pty native module failed to load; use non-interactive exec\",\n );\n }\n this.armExitReaper();\n const shell = process.env.SHELL ?? \"/bin/bash\";\n const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;\n const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;\n // Validate cwd at the boundary: node-pty does NOT throw on a non-existent cwd (it spawns a broken\n // session), so fail-fast to the typed fallback the caller expects.\n const cwd = opts?.cwd ?? process.cwd();\n if (!existsSync(cwd)) {\n throw new InteractiveUnavailableError(`interactive shell unavailable: cwd does not exist: ${cwd}`);\n }\n let proc: PtyProcess;\n try {\n proc = pty.spawn(shell, [\"-c\", command], {\n name: \"xterm-color\",\n cols: opts?.cols ?? 80,\n rows: opts?.rows ?? 24,\n cwd,\n env: process.env,\n });\n } catch (err) {\n throw new InteractiveUnavailableError(\n `interactive shell unavailable: failed to spawn a PTY (${err instanceof Error ? err.message : String(err)})`,\n );\n }\n const id = `pty-${randomUUID()}`;\n const session: PtySession = {\n id,\n pty: proc,\n pending: \"\",\n alive: true,\n ttlTimer: setTimeout(() => this.kill(id), ttlMs),\n writeChain: Promise.resolve(),\n };\n proc.onData((data) => {\n session.pending = capTail(session.pending + data, maxBytes);\n });\n proc.onExit(() => {\n session.alive = false;\n clearTimeout(session.ttlTimer);\n this.sessions.delete(id);\n });\n this.sessions.set(id, session);\n const output = await this.collect(session, opts?.yieldMs ?? DEFAULT_YIELD_MS, maxBytes);\n return { sessionId: id, output };\n }\n\n async writeStdin(\n sessionId: string,\n chars: string,\n opts?: WriteStdinOptions,\n ): Promise<WriteStdinResult> {\n const session = this.sessions.get(sessionId);\n if (session === undefined || !session.alive) throw new NoSuchSessionError(sessionId);\n // Serialize per session: chain onto the previous write so two concurrent calls run strictly in\n // order and each reads only its own output window (no stolen output).\n const run = session.writeChain.then(async () => {\n if (!session.alive) throw new NoSuchSessionError(sessionId);\n this.armTtl(session, opts?.ttlMs ?? DEFAULT_TTL_MS);\n if (chars.length > 0) session.pty.write(chars);\n const output = await this.collect(\n session,\n opts?.yieldMs ?? DEFAULT_YIELD_MS,\n opts?.maxBytes ?? DEFAULT_MAX_BYTES,\n );\n return { output, alive: session.alive };\n });\n session.writeChain = run.catch(() => undefined);\n return run;\n }\n\n /** Kill a single session (idempotent). Kills the whole process GROUP so a detached grandchild dies too. */\n kill(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (session === undefined) return;\n clearTimeout(session.ttlTimer);\n session.alive = false;\n this.sessions.delete(sessionId);\n try {\n process.kill(-session.pty.pid, \"SIGKILL\");\n } catch {\n try {\n session.pty.kill();\n } catch {\n // already dead — nothing to do\n }\n }\n }\n\n /** Reap every session — used by the process-exit reaper; also callable on `/clear`. */\n killAll(): void {\n for (const id of [...this.sessions.keys()]) this.kill(id);\n }\n\n /** Live session count — for observability / tests. */\n activeSessionCount(): number {\n return this.sessions.size;\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/pty-interactive-backend.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `PtyInteractiveBackend` — the LOCAL implementation of `@theokit/sdk`'s\n * `InteractiveBackend`, backed by `node-pty`. This is the terminal-surface\n * backend: it allocates a real pseudo-terminal so REPLs, `git rebase -i`, and\n * prompting commands can be driven to completion. Ported from the AgentBuilder\n * Codex clone (M11) into the ecosystem (M14) so any terminal agent injects it.\n *\n * `node-pty` is an OPTIONAL dependency of this package — the ONLY place in the\n * theokit ecosystem that touches it. Core / sdk-tools / cluster / desktop never\n * do. When the native module is unavailable (or a spawn fails), every method\n * throws the SDK's typed {@link InteractiveUnavailableError} so the caller falls\n * back to non-interactive exec.\n *\n * Safety (mirrors M11): graceful typed degradation; per-session write\n * serialization (concurrent writes never steal each other's output); idle TTL\n * reaper; process-GROUP kill so detached grandchildren die; a process-exit\n * reaper; tail-capped output (the live prompt, not the stale banner).\n */\nimport { randomUUID } from \"node:crypto\";\nimport { existsSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\n\nimport {\n InteractiveBackend,\n InteractiveUnavailableError,\n NoSuchSessionError,\n type StartInteractiveOptions,\n type StartInteractiveResult,\n type WriteStdinOptions,\n type WriteStdinResult,\n} from \"@theokit/sdk/interactive\";\n\nconst nodeRequire = createRequire(import.meta.url);\n\nexport const YIELD_MIN_MS = 250;\nexport const YIELD_MAX_MS = 30_000;\nconst DEFAULT_YIELD_MS = 500;\nconst DEFAULT_TTL_MS = 300_000; // 5 min idle → reap\nconst DEFAULT_MAX_BYTES = 100_000;\n\n/** The minimal `node-pty` surface driven here (kept structural so the import stays lazy). */\ninterface PtyProcess {\n readonly pid: number;\n write(data: string): void;\n kill(signal?: string): void;\n onData(cb: (data: string) => void): void;\n onExit(cb: (e: { exitCode: number }) => void): void;\n}\ninterface PtyModule {\n spawn(file: string, args: string[], opts: Record<string, unknown>): PtyProcess;\n}\n\ninterface PtySession {\n id: string;\n pty: PtyProcess;\n pending: string;\n alive: boolean;\n ttlTimer: ReturnType<typeof setTimeout>;\n writeChain: Promise<unknown>;\n}\n\n/** Bound the yield window to [{@link YIELD_MIN_MS}, {@link YIELD_MAX_MS}]. */\nexport function clampYield(ms: number | undefined): number {\n const v = ms ?? DEFAULT_YIELD_MS;\n return Math.max(YIELD_MIN_MS, Math.min(YIELD_MAX_MS, v));\n}\n\n/** Keep the TAIL of output — the newest bytes carry the live prompt. */\nfunction capTail(buf: string, max: number): string {\n return buf.length > max ? `…(truncated)\\n${buf.slice(-max)}` : buf;\n}\n\n/**\n * M75 T3.1 — como o chamador embrulha o comando antes do spawn.\n *\n * Existe para que confinamento (sandbox) componha com PTY sem herança: o backend continua dono do\n * spawn, o chamador continua dono da política, e nenhum dos dois conhece o tipo do outro.\n */\nexport interface PtyInteractiveBackendOptions {\n /**\n * Transforma o comando imediatamente antes do spawn. Recebe o `cwd` JÁ RESOLVIDO — o PTY spawna\n * nele, então um wrap que mirasse outro diretório produziria confinamento que não confina nada.\n *\n * Devolver `null` significa **não embrulhe** — decisão explícita, distinta de \"embrulhei e deu no\n * mesmo\". É o caso do modo sem confinamento.\n */\n readonly wrapCommand?: (command: string, cwd: string) => string | null;\n}\n\nexport class PtyInteractiveBackend extends InteractiveBackend {\n private readonly sessions = new Map<string, PtySession>();\n private readonly wrapCommand: ((command: string, cwd: string) => string | null) | undefined;\n\n constructor(options: PtyInteractiveBackendOptions = {}) {\n super();\n this.wrapCommand = options.wrapCommand;\n }\n\n private ptyModule: PtyModule | null | undefined;\n private exitReaperArmed = false;\n\n /** Lazy, cached node-pty load — a failed native build must degrade, not crash at import time. */\n private loadPty(): PtyModule | null {\n if (this.ptyModule !== undefined) return this.ptyModule;\n try {\n this.ptyModule = nodeRequire(\"node-pty\") as PtyModule;\n } catch {\n this.ptyModule = null;\n }\n return this.ptyModule;\n }\n\n /** Whether the interactive (PTY) path is usable in this environment. */\n available(): boolean {\n return this.loadPty() !== null;\n }\n\n /** Reap orphaned PTYs when the host process exits. ONLY the `exit` event — installing SIGINT/SIGTERM\n * handlers would remove Node's default terminate-on-signal behavior. Armed once, lazily. */\n private armExitReaper(): void {\n if (this.exitReaperArmed) return;\n this.exitReaperArmed = true;\n process.on(\"exit\", () => this.killAll());\n }\n\n private async collect(session: PtySession, yieldMs: number, maxBytes: number): Promise<string> {\n await new Promise((r) => setTimeout(r, clampYield(yieldMs)));\n const out = session.pending;\n session.pending = \"\";\n return capTail(out, maxBytes);\n }\n\n private armTtl(session: PtySession, ttlMs: number): void {\n clearTimeout(session.ttlTimer);\n session.ttlTimer = setTimeout(() => this.kill(session.id), ttlMs);\n }\n\n /** Allocate a PTY for `command` or throw a typed {@link InteractiveUnavailableError}. Validates the\n * cwd at the boundary (node-pty does NOT throw on a non-existent cwd — it spawns a broken session). */\n private spawnPty(command: string, opts?: StartInteractiveOptions): PtyProcess {\n const pty = this.loadPty();\n if (pty === null) {\n throw new InteractiveUnavailableError(\n \"interactive shell unavailable: node-pty native module failed to load; use non-interactive exec\",\n );\n }\n const cwd = opts?.cwd ?? process.cwd();\n if (!existsSync(cwd)) {\n throw new InteractiveUnavailableError(\n `interactive shell unavailable: cwd does not exist: ${cwd}`,\n );\n }\n // M75 T3.1 — o wrap entra AQUI: depois do cwd resolvido e validado, antes do spawn. É o ponto\n // único por onde todo comando passa, então não há caminho que escape do confinamento.\n const efetivo = this.wrapCommand?.(command, cwd) ?? command;\n\n const shell = process.env.SHELL ?? \"/bin/bash\";\n try {\n return pty.spawn(shell, [\"-c\", efetivo], {\n name: \"xterm-color\",\n cols: opts?.cols ?? 80,\n rows: opts?.rows ?? 24,\n cwd,\n env: process.env,\n });\n } catch (err) {\n throw new InteractiveUnavailableError(\n `interactive shell unavailable: failed to spawn a PTY (${err instanceof Error ? err.message : String(err)})`,\n );\n }\n }\n\n async startInteractive(\n command: string,\n opts?: StartInteractiveOptions,\n ): Promise<StartInteractiveResult> {\n this.armExitReaper();\n const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;\n const maxBytes = opts?.maxBytes ?? DEFAULT_MAX_BYTES;\n const proc = this.spawnPty(command, opts);\n const id = `pty-${randomUUID()}`;\n const session: PtySession = {\n id,\n pty: proc,\n pending: \"\",\n alive: true,\n ttlTimer: setTimeout(() => this.kill(id), ttlMs),\n writeChain: Promise.resolve(),\n };\n proc.onData((data) => {\n session.pending = capTail(session.pending + data, maxBytes);\n });\n proc.onExit(() => {\n session.alive = false;\n clearTimeout(session.ttlTimer);\n this.sessions.delete(id);\n });\n this.sessions.set(id, session);\n const output = await this.collect(session, opts?.yieldMs ?? DEFAULT_YIELD_MS, maxBytes);\n return { sessionId: id, output };\n }\n\n async writeStdin(\n sessionId: string,\n chars: string,\n opts?: WriteStdinOptions,\n ): Promise<WriteStdinResult> {\n const session = this.sessions.get(sessionId);\n if (session === undefined || !session.alive) throw new NoSuchSessionError(sessionId);\n // Serialize per session: chain onto the previous write so two concurrent calls run strictly in\n // order and each reads only its own output window (no stolen output).\n const run = session.writeChain.then(async () => {\n if (!session.alive) throw new NoSuchSessionError(sessionId);\n this.armTtl(session, opts?.ttlMs ?? DEFAULT_TTL_MS);\n if (chars.length > 0) session.pty.write(chars);\n const output = await this.collect(\n session,\n opts?.yieldMs ?? DEFAULT_YIELD_MS,\n opts?.maxBytes ?? DEFAULT_MAX_BYTES,\n );\n return { output, alive: session.alive };\n });\n session.writeChain = run.catch(() => undefined);\n return run;\n }\n\n /** Kill a single session (idempotent). Kills the whole process GROUP so a detached grandchild dies too. */\n kill(sessionId: string): void {\n const session = this.sessions.get(sessionId);\n if (session === undefined) return;\n clearTimeout(session.ttlTimer);\n session.alive = false;\n this.sessions.delete(sessionId);\n try {\n process.kill(-session.pty.pid, \"SIGKILL\");\n } catch {\n try {\n session.pty.kill();\n } catch {\n // already dead — nothing to do\n }\n }\n }\n\n /** Reap every session — used by the process-exit reaper; also callable on `/clear`. */\n killAll(): void {\n for (const id of [...this.sessions.keys()]) this.kill(id);\n }\n\n /** Live session count — for observability / tests. */\n activeSessionCount(): number {\n return this.sessions.size;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@theokit/sdk-pty",
3
- "version": "0.1.0",
4
- "description": "Local interactive-shell backend for @theokit/sdk a node-pty implementation of InteractiveBackend. Opt-in, terminal-surface only; node-pty is optional so install never fails on a native build.",
3
+ "version": "0.2.0",
4
+ "description": "Local interactive-shell backend for @theokit/sdk \u2014 a node-pty implementation of InteractiveBackend. Opt-in, terminal-surface only; node-pty is optional so install never fails on a native build.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
@@ -32,7 +32,6 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@theokit/sdk": "workspace:*",
35
- "node-pty": "^1.1.0",
36
35
  "tsup": "^8.3.5",
37
36
  "typescript": "^5.7.2",
38
37
  "vitest": "^4.1.8"