codepiper 0.1.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.
Files changed (149) hide show
  1. package/.env.example +28 -0
  2. package/CHANGELOG.md +10 -0
  3. package/LEGAL_NOTICE.md +39 -0
  4. package/LICENSE +21 -0
  5. package/README.md +524 -0
  6. package/package.json +90 -0
  7. package/packages/cli/package.json +13 -0
  8. package/packages/cli/src/commands/analytics.ts +157 -0
  9. package/packages/cli/src/commands/attach.ts +299 -0
  10. package/packages/cli/src/commands/audit.ts +50 -0
  11. package/packages/cli/src/commands/auth.ts +261 -0
  12. package/packages/cli/src/commands/daemon.ts +162 -0
  13. package/packages/cli/src/commands/doctor.ts +303 -0
  14. package/packages/cli/src/commands/env-set.ts +162 -0
  15. package/packages/cli/src/commands/hook-forward.ts +268 -0
  16. package/packages/cli/src/commands/keys.ts +77 -0
  17. package/packages/cli/src/commands/kill.ts +19 -0
  18. package/packages/cli/src/commands/logs.ts +419 -0
  19. package/packages/cli/src/commands/model.ts +172 -0
  20. package/packages/cli/src/commands/policy-set.ts +185 -0
  21. package/packages/cli/src/commands/policy.ts +227 -0
  22. package/packages/cli/src/commands/providers.ts +114 -0
  23. package/packages/cli/src/commands/resize.ts +34 -0
  24. package/packages/cli/src/commands/send.ts +184 -0
  25. package/packages/cli/src/commands/sessions.ts +202 -0
  26. package/packages/cli/src/commands/slash.ts +92 -0
  27. package/packages/cli/src/commands/start.ts +243 -0
  28. package/packages/cli/src/commands/stop.ts +19 -0
  29. package/packages/cli/src/commands/tail.ts +137 -0
  30. package/packages/cli/src/commands/workflow.ts +786 -0
  31. package/packages/cli/src/commands/workspace.ts +127 -0
  32. package/packages/cli/src/lib/api.ts +78 -0
  33. package/packages/cli/src/lib/args.ts +72 -0
  34. package/packages/cli/src/lib/format.ts +93 -0
  35. package/packages/cli/src/main.ts +563 -0
  36. package/packages/core/package.json +7 -0
  37. package/packages/core/src/config.ts +30 -0
  38. package/packages/core/src/errors.ts +38 -0
  39. package/packages/core/src/eventBus.ts +56 -0
  40. package/packages/core/src/eventBusAdapter.ts +143 -0
  41. package/packages/core/src/index.ts +10 -0
  42. package/packages/core/src/sqliteEventBus.ts +336 -0
  43. package/packages/core/src/types.ts +63 -0
  44. package/packages/daemon/package.json +11 -0
  45. package/packages/daemon/src/api/analyticsRoutes.ts +343 -0
  46. package/packages/daemon/src/api/authRoutes.ts +344 -0
  47. package/packages/daemon/src/api/bodyLimit.ts +133 -0
  48. package/packages/daemon/src/api/envSetRoutes.ts +170 -0
  49. package/packages/daemon/src/api/gitRoutes.ts +409 -0
  50. package/packages/daemon/src/api/hooks.ts +588 -0
  51. package/packages/daemon/src/api/inputPolicy.ts +249 -0
  52. package/packages/daemon/src/api/notificationRoutes.ts +532 -0
  53. package/packages/daemon/src/api/policyRoutes.ts +234 -0
  54. package/packages/daemon/src/api/policySetRoutes.ts +445 -0
  55. package/packages/daemon/src/api/routeUtils.ts +28 -0
  56. package/packages/daemon/src/api/routes.ts +1004 -0
  57. package/packages/daemon/src/api/server.ts +1388 -0
  58. package/packages/daemon/src/api/settingsRoutes.ts +367 -0
  59. package/packages/daemon/src/api/sqliteErrors.ts +47 -0
  60. package/packages/daemon/src/api/stt.ts +143 -0
  61. package/packages/daemon/src/api/terminalRoutes.ts +200 -0
  62. package/packages/daemon/src/api/validation.ts +287 -0
  63. package/packages/daemon/src/api/validationRoutes.ts +174 -0
  64. package/packages/daemon/src/api/workflowRoutes.ts +567 -0
  65. package/packages/daemon/src/api/workspaceRoutes.ts +151 -0
  66. package/packages/daemon/src/api/ws.ts +1588 -0
  67. package/packages/daemon/src/auth/apiRateLimiter.ts +73 -0
  68. package/packages/daemon/src/auth/authMiddleware.ts +305 -0
  69. package/packages/daemon/src/auth/authService.ts +496 -0
  70. package/packages/daemon/src/auth/rateLimiter.ts +137 -0
  71. package/packages/daemon/src/config/pricing.ts +79 -0
  72. package/packages/daemon/src/crypto/encryption.ts +196 -0
  73. package/packages/daemon/src/db/db.ts +2745 -0
  74. package/packages/daemon/src/db/index.ts +16 -0
  75. package/packages/daemon/src/db/migrations.ts +182 -0
  76. package/packages/daemon/src/db/policyDb.ts +349 -0
  77. package/packages/daemon/src/db/schema.sql +408 -0
  78. package/packages/daemon/src/db/workflowDb.ts +464 -0
  79. package/packages/daemon/src/git/gitUtils.ts +544 -0
  80. package/packages/daemon/src/index.ts +6 -0
  81. package/packages/daemon/src/main.ts +525 -0
  82. package/packages/daemon/src/notifications/pushNotifier.ts +369 -0
  83. package/packages/daemon/src/providers/codexAppServerScaffold.ts +49 -0
  84. package/packages/daemon/src/providers/registry.ts +111 -0
  85. package/packages/daemon/src/providers/types.ts +82 -0
  86. package/packages/daemon/src/sessions/auditLogger.ts +103 -0
  87. package/packages/daemon/src/sessions/policyEngine.ts +165 -0
  88. package/packages/daemon/src/sessions/policyMatcher.ts +114 -0
  89. package/packages/daemon/src/sessions/policyTypes.ts +94 -0
  90. package/packages/daemon/src/sessions/ptyProcess.ts +141 -0
  91. package/packages/daemon/src/sessions/sessionManager.ts +1770 -0
  92. package/packages/daemon/src/sessions/tmuxSession.ts +1073 -0
  93. package/packages/daemon/src/sessions/transcriptManager.ts +110 -0
  94. package/packages/daemon/src/sessions/transcriptParser.ts +149 -0
  95. package/packages/daemon/src/sessions/transcriptTailer.ts +214 -0
  96. package/packages/daemon/src/tracking/tokenTracker.ts +168 -0
  97. package/packages/daemon/src/workflows/contextManager.ts +83 -0
  98. package/packages/daemon/src/workflows/index.ts +31 -0
  99. package/packages/daemon/src/workflows/resultExtractor.ts +118 -0
  100. package/packages/daemon/src/workflows/waitConditionPoller.ts +131 -0
  101. package/packages/daemon/src/workflows/workflowParser.ts +217 -0
  102. package/packages/daemon/src/workflows/workflowRunner.ts +969 -0
  103. package/packages/daemon/src/workflows/workflowTypes.ts +188 -0
  104. package/packages/daemon/src/workflows/workflowValidator.ts +533 -0
  105. package/packages/providers/claude-code/package.json +11 -0
  106. package/packages/providers/claude-code/src/index.ts +7 -0
  107. package/packages/providers/claude-code/src/overlaySettings.ts +198 -0
  108. package/packages/providers/claude-code/src/provider.ts +311 -0
  109. package/packages/web/dist/android-chrome-192x192.png +0 -0
  110. package/packages/web/dist/android-chrome-512x512.png +0 -0
  111. package/packages/web/dist/apple-touch-icon.png +0 -0
  112. package/packages/web/dist/assets/AnalyticsPage-BIopKWRf.js +17 -0
  113. package/packages/web/dist/assets/PoliciesPage-CjdLN3dl.js +11 -0
  114. package/packages/web/dist/assets/SessionDetailPage-BtSA0V0M.js +179 -0
  115. package/packages/web/dist/assets/SettingsPage-Dbbz4Ca5.js +37 -0
  116. package/packages/web/dist/assets/WorkflowsPage-Dv6f3GgU.js +1 -0
  117. package/packages/web/dist/assets/chart-vendor-DlOHLaCG.js +49 -0
  118. package/packages/web/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  119. package/packages/web/dist/assets/css.worker-BvV5MPou.js +93 -0
  120. package/packages/web/dist/assets/editor.worker-CKy7Pnvo.js +26 -0
  121. package/packages/web/dist/assets/html.worker-BLJhxQJQ.js +470 -0
  122. package/packages/web/dist/assets/index-BbdhRfr2.css +1 -0
  123. package/packages/web/dist/assets/index-hgphORiw.js +204 -0
  124. package/packages/web/dist/assets/json.worker-usMZ-FED.js +58 -0
  125. package/packages/web/dist/assets/monaco-core-B_19GPAS.css +1 -0
  126. package/packages/web/dist/assets/monaco-core-DQ5Mk8AK.js +1234 -0
  127. package/packages/web/dist/assets/monaco-react-DfZNWvtW.js +11 -0
  128. package/packages/web/dist/assets/monacoSetup-DvBj52bT.js +1 -0
  129. package/packages/web/dist/assets/pencil-Dbczxz59.js +11 -0
  130. package/packages/web/dist/assets/react-vendor-B5MgMUHH.js +136 -0
  131. package/packages/web/dist/assets/refresh-cw-B0MGsYPL.js +6 -0
  132. package/packages/web/dist/assets/tabs-C8LsWiR5.js +1 -0
  133. package/packages/web/dist/assets/terminal-vendor-Cs8KPbV3.js +9 -0
  134. package/packages/web/dist/assets/terminal-vendor-LcAfv9l9.css +32 -0
  135. package/packages/web/dist/assets/trash-2-Btlg0d4l.js +6 -0
  136. package/packages/web/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
  137. package/packages/web/dist/favicon.ico +0 -0
  138. package/packages/web/dist/icon.svg +1 -0
  139. package/packages/web/dist/index.html +29 -0
  140. package/packages/web/dist/manifest.json +29 -0
  141. package/packages/web/dist/og-image.png +0 -0
  142. package/packages/web/dist/originals/android-chrome-192x192.png +0 -0
  143. package/packages/web/dist/originals/android-chrome-512x512.png +0 -0
  144. package/packages/web/dist/originals/apple-touch-icon.png +0 -0
  145. package/packages/web/dist/originals/favicon.ico +0 -0
  146. package/packages/web/dist/piper.svg +1 -0
  147. package/packages/web/dist/sounds/codepiper-soft-chime.wav +0 -0
  148. package/packages/web/dist/sw.js +257 -0
  149. package/scripts/postinstall-link-workspaces.mjs +58 -0
@@ -0,0 +1,165 @@
1
+ /**
2
+ * PolicyEngine - Evaluates PermissionRequest events against policy rules
3
+ *
4
+ * Features:
5
+ * - Pattern matching (glob, negation, arrays)
6
+ * - Rule precedence (priority + order)
7
+ * - Context matching (tool, args, cwd, session)
8
+ * - Default policy fallback
9
+ */
10
+
11
+ import { matchPattern, valueToString } from "./policyMatcher";
12
+ import type { PermissionRequest, Policy, PolicyDecision, PolicyRule } from "./policyTypes";
13
+
14
+ export type DefaultPolicyAction = "ask" | "deny";
15
+
16
+ export interface PolicyEngineOptions {
17
+ /** Default action when no rules match (only "ask" or "deny" — "allow" requires explicit rules) */
18
+ defaultAction?: DefaultPolicyAction;
19
+
20
+ /** Default reason when no rules match */
21
+ defaultReason?: string;
22
+ }
23
+
24
+ export class PolicyEngine {
25
+ private defaultAction: DefaultPolicyAction;
26
+ private defaultReason: string;
27
+
28
+ constructor(options: PolicyEngineOptions = {}) {
29
+ this.defaultAction = options.defaultAction ?? "ask";
30
+ this.defaultReason = options.defaultReason ?? "No matching policy rule found (default ask)";
31
+ }
32
+
33
+ /**
34
+ * Update the default action at runtime (e.g., when daemon settings change)
35
+ */
36
+ setDefaultAction(action: DefaultPolicyAction): void {
37
+ this.defaultAction = action;
38
+ this.defaultReason =
39
+ action === "ask"
40
+ ? "No matching policy rule found (default ask)"
41
+ : `No matching policy rule found (default ${action})`;
42
+ }
43
+
44
+ /**
45
+ * Get the current default action
46
+ */
47
+ getDefaultAction(): DefaultPolicyAction {
48
+ return this.defaultAction;
49
+ }
50
+
51
+ /**
52
+ * Evaluate a permission request against a list of policies
53
+ *
54
+ * Algorithm:
55
+ * 1. Filter policies by session ID and enabled status
56
+ * 2. Sort by priority (higher = more specific, evaluated first)
57
+ * 3. Evaluate each rule in priority order
58
+ * 4. First match wins
59
+ * 5. If no match, return default policy
60
+ *
61
+ * @param request The permission request to evaluate
62
+ * @param policies All available policies
63
+ * @returns The policy decision
64
+ */
65
+ async evaluate(request: PermissionRequest, policies: Policy[]): Promise<PolicyDecision> {
66
+ // Filter and sort policies
67
+ const applicablePolicies = policies
68
+ .filter((p) => this.isPolicyApplicable(p, request))
69
+ .sort((a, b) => b.priority - a.priority); // Higher priority first
70
+
71
+ // Evaluate each policy's rules in order
72
+ for (const policy of applicablePolicies) {
73
+ for (const rule of policy.rules) {
74
+ if (this.ruleMatches(rule, request)) {
75
+ // First match wins
76
+ return {
77
+ action: rule.action,
78
+ reason: rule.reason,
79
+ policyId: policy.id,
80
+ ruleId: rule.id,
81
+ };
82
+ }
83
+ }
84
+ }
85
+
86
+ // No match - return default policy
87
+ return this.getDefaultPolicy();
88
+ }
89
+
90
+ /**
91
+ * Check if a policy is applicable to a request
92
+ */
93
+ private isPolicyApplicable(policy: Policy, request: PermissionRequest): boolean {
94
+ // Only enabled policies
95
+ if (!policy.enabled) {
96
+ return false;
97
+ }
98
+
99
+ // Check session ID match
100
+ if (policy.sessionId !== undefined) {
101
+ if (policy.sessionId !== request.sessionId) {
102
+ return false;
103
+ }
104
+ }
105
+
106
+ return true;
107
+ }
108
+
109
+ /**
110
+ * Check if a rule matches a permission request
111
+ *
112
+ * All criteria must match for the rule to match:
113
+ * - Tool name (if specified)
114
+ * - Arguments (if specified, all must match)
115
+ * - CWD (if specified)
116
+ * - Session (if specified)
117
+ */
118
+ private ruleMatches(rule: PolicyRule, request: PermissionRequest): boolean {
119
+ // Match tool name
120
+ if (rule.tool !== undefined) {
121
+ if (!matchPattern(request.tool, rule.tool)) {
122
+ return false;
123
+ }
124
+ }
125
+
126
+ // Match arguments
127
+ if (rule.args !== undefined) {
128
+ for (const [key, pattern] of Object.entries(rule.args)) {
129
+ const value = request.args[key];
130
+ const valueStr = valueToString(value);
131
+
132
+ if (!(valueStr && matchPattern(valueStr, pattern))) {
133
+ return false;
134
+ }
135
+ }
136
+ }
137
+
138
+ // Match CWD
139
+ if (rule.cwd !== undefined) {
140
+ if (!matchPattern(request.cwd, rule.cwd)) {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ // Match session
146
+ if (rule.session !== undefined) {
147
+ if (!matchPattern(request.sessionId, rule.session)) {
148
+ return false;
149
+ }
150
+ }
151
+
152
+ // All criteria matched
153
+ return true;
154
+ }
155
+
156
+ /**
157
+ * Get the default policy when no rules match
158
+ */
159
+ private getDefaultPolicy(): PolicyDecision {
160
+ return {
161
+ action: this.defaultAction,
162
+ reason: this.defaultReason,
163
+ };
164
+ }
165
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * PolicyMatcher - Pattern matching utilities for policy rules
3
+ * Supports glob patterns, negation, and arrays
4
+ */
5
+
6
+ import micromatch from "micromatch";
7
+
8
+ /**
9
+ * Match a value against a pattern or array of patterns
10
+ *
11
+ * Supports:
12
+ * - Exact matching: "Read" matches "Read"
13
+ * - Glob patterns: "Read*" matches "ReadFile"
14
+ * - Wildcards: "*Read*" matches "FileRead"
15
+ * - Negation: "!*.env" matches anything except *.env
16
+ * - Arrays: ["Read", "Write"] matches either
17
+ *
18
+ * @param value The value to match
19
+ * @param pattern The pattern(s) to match against
20
+ * @returns true if the value matches the pattern
21
+ */
22
+ export function matchPattern(value: string, pattern: string | string[]): boolean {
23
+ const patterns = Array.isArray(pattern) ? pattern : [pattern];
24
+
25
+ // Track if we have any negative patterns
26
+ const negativePatterns: string[] = [];
27
+ const positivePatterns: string[] = [];
28
+
29
+ for (const p of patterns) {
30
+ // Skip empty patterns
31
+ if (p === "") {
32
+ // Empty pattern matches empty value
33
+ if (value === "") {
34
+ return true;
35
+ }
36
+ continue;
37
+ }
38
+
39
+ if (p.startsWith("!")) {
40
+ const negPattern = p.slice(1);
41
+ if (negPattern !== "") {
42
+ negativePatterns.push(negPattern);
43
+ }
44
+ } else {
45
+ positivePatterns.push(p);
46
+ }
47
+ }
48
+
49
+ // Micromatch options for better pattern matching
50
+ // bash: true treats patterns as bash globs (not file paths)
51
+ const options = { bash: true };
52
+
53
+ // If value matches any negative pattern, fail immediately
54
+ if (negativePatterns.length > 0) {
55
+ if (micromatch.isMatch(value, negativePatterns, options)) {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ // If we have positive patterns, check if any match
61
+ if (positivePatterns.length > 0) {
62
+ return micromatch.isMatch(value, positivePatterns, options);
63
+ }
64
+
65
+ // If we only had negative patterns and they didn't match, still fail
66
+ // because we have no positive pattern to indicate what SHOULD match
67
+ // Negative patterns alone mean "deny these" but don't specify what to allow
68
+ if (negativePatterns.length > 0) {
69
+ return false;
70
+ }
71
+
72
+ // No patterns at all
73
+ return false;
74
+ }
75
+
76
+ /**
77
+ * Normalize a path for consistent matching
78
+ * Removes trailing slashes and resolves relative paths
79
+ */
80
+ export function normalizePath(path: string): string {
81
+ // Remove trailing slash
82
+ let normalized = path.replace(/\/+$/, "");
83
+
84
+ // If empty after removing slashes, it was root
85
+ if (normalized === "") {
86
+ normalized = "/";
87
+ }
88
+
89
+ return normalized;
90
+ }
91
+
92
+ /**
93
+ * Convert a value to string for pattern matching
94
+ * Handles non-string values gracefully
95
+ */
96
+ export function valueToString(value: unknown): string {
97
+ if (value === null || value === undefined) {
98
+ return "";
99
+ }
100
+
101
+ if (typeof value === "string") {
102
+ return value;
103
+ }
104
+
105
+ if (typeof value === "number" || typeof value === "boolean") {
106
+ return String(value);
107
+ }
108
+
109
+ if (typeof value === "object") {
110
+ return JSON.stringify(value);
111
+ }
112
+
113
+ return String(value);
114
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * PolicyTypes - Core types for the policy engine
3
+ */
4
+
5
+ /**
6
+ * Policy rule action types
7
+ */
8
+ export type PolicyAction = "allow" | "deny" | "ask";
9
+
10
+ /**
11
+ * A single policy rule that matches against PermissionRequest criteria
12
+ */
13
+ export interface PolicyRule {
14
+ /** Unique identifier for this rule */
15
+ id: string;
16
+
17
+ /** Action to take if this rule matches */
18
+ action: PolicyAction;
19
+
20
+ /** Tool name pattern(s) to match against (glob supported) */
21
+ tool?: string | string[];
22
+
23
+ /** Argument patterns to match (key -> pattern) */
24
+ args?: Record<string, string | string[]>;
25
+
26
+ /** CWD pattern(s) to match against */
27
+ cwd?: string | string[];
28
+
29
+ /** Session ID pattern(s) to match against */
30
+ session?: string | string[];
31
+
32
+ /** Human-readable explanation for the decision */
33
+ reason?: string;
34
+ }
35
+
36
+ /**
37
+ * A policy contains multiple rules and metadata
38
+ */
39
+ export interface Policy {
40
+ /** Unique identifier */
41
+ id: string;
42
+
43
+ /** Human-readable name */
44
+ name: string;
45
+
46
+ /** Optional description */
47
+ description?: string;
48
+
49
+ /** Whether this policy is active */
50
+ enabled: boolean;
51
+
52
+ /** Higher priority = evaluated first (more specific) */
53
+ priority: number;
54
+
55
+ /** Session ID this policy applies to (null = global) */
56
+ sessionId?: string;
57
+
58
+ /** Rules to evaluate in order */
59
+ rules: PolicyRule[];
60
+ }
61
+
62
+ /**
63
+ * The result of evaluating a permission request against policies
64
+ */
65
+ export interface PolicyDecision {
66
+ /** The action to take */
67
+ action: PolicyAction;
68
+
69
+ /** Explanation for the decision */
70
+ reason?: string;
71
+
72
+ /** ID of the policy that matched */
73
+ policyId?: string;
74
+
75
+ /** ID of the specific rule that matched */
76
+ ruleId?: string;
77
+ }
78
+
79
+ /**
80
+ * A permission request from Claude Code hooks
81
+ */
82
+ export interface PermissionRequest {
83
+ /** Session ID making the request */
84
+ sessionId: string;
85
+
86
+ /** Tool name being invoked */
87
+ tool: string;
88
+
89
+ /** Tool arguments */
90
+ args: Record<string, unknown>;
91
+
92
+ /** Current working directory */
93
+ cwd: string;
94
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * PTY (Pseudo-Terminal) process wrapper for Bun
3
+ */
4
+
5
+ import type { Subprocess } from "bun";
6
+
7
+ export interface PTYProcessOptions {
8
+ command: string[];
9
+ cwd: string;
10
+ env?: Record<string, string>;
11
+ cols?: number;
12
+ rows?: number;
13
+ onData?: (data: string) => void;
14
+ onExit?: (exitCode: number, signal: string | null) => void;
15
+ onDrain?: () => void;
16
+ }
17
+
18
+ export class PTYProcess {
19
+ private proc: Subprocess;
20
+ public pid: number;
21
+ public cols: number;
22
+ public rows: number;
23
+ public closed: boolean = false;
24
+
25
+ constructor(options: PTYProcessOptions) {
26
+ const { command, cwd, env, cols = 80, rows = 24, onData, onExit, onDrain } = options;
27
+
28
+ this.cols = cols;
29
+ this.rows = rows;
30
+
31
+ try {
32
+ this.proc = Bun.spawn(command, {
33
+ cwd,
34
+ env: env || process.env,
35
+ terminal: {
36
+ cols,
37
+ rows,
38
+ data: (_terminal, data) => {
39
+ if (onData) {
40
+ const text = new TextDecoder().decode(data);
41
+ onData(text);
42
+ }
43
+ },
44
+ exit: (_terminal, exitCode, signal) => {
45
+ this.closed = true;
46
+ if (onExit) {
47
+ onExit(exitCode, signal);
48
+ }
49
+ },
50
+ drain: (_terminal) => {
51
+ if (onDrain) {
52
+ onDrain();
53
+ }
54
+ },
55
+ },
56
+ });
57
+
58
+ if (!this.proc.terminal) {
59
+ throw new Error("Failed to create PTY - terminal object is undefined");
60
+ }
61
+
62
+ this.pid = this.proc.pid;
63
+ } catch (error) {
64
+ throw new Error(`Failed to spawn process: ${error}`);
65
+ }
66
+ }
67
+
68
+ write(data: string): void {
69
+ if (this.closed) {
70
+ throw new Error("Cannot write to closed PTY");
71
+ }
72
+
73
+ if (!this.proc.terminal) {
74
+ throw new Error("PTY terminal is not available");
75
+ }
76
+
77
+ this.proc.terminal.write(data);
78
+ }
79
+
80
+ resize(cols: number, rows: number): void {
81
+ if (this.closed) {
82
+ throw new Error("Cannot resize closed PTY");
83
+ }
84
+
85
+ if (!this.proc.terminal) {
86
+ throw new Error("PTY terminal is not available");
87
+ }
88
+
89
+ this.cols = cols;
90
+ this.rows = rows;
91
+ this.proc.terminal.resize(cols, rows);
92
+ }
93
+
94
+ setRawMode(enabled: boolean): void {
95
+ if (this.closed) {
96
+ throw new Error("Cannot set raw mode on closed PTY");
97
+ }
98
+
99
+ if (!this.proc.terminal) {
100
+ throw new Error("PTY terminal is not available");
101
+ }
102
+
103
+ this.proc.terminal.setRawMode(enabled);
104
+ }
105
+
106
+ async kill(signal?: string): Promise<void> {
107
+ if (this.closed) {
108
+ return;
109
+ }
110
+
111
+ try {
112
+ // Close terminal first to trigger clean shutdown
113
+ if (this.proc.terminal && !this.proc.terminal.closed) {
114
+ this.proc.terminal.close();
115
+ }
116
+
117
+ // Then kill the process
118
+ this.proc.kill(signal as any);
119
+
120
+ // Wait for process to exit with timeout
121
+ await Promise.race([this.proc.exited, new Promise((resolve) => setTimeout(resolve, 1000))]);
122
+
123
+ this.closed = true;
124
+ } catch (_error) {
125
+ // Process might already be dead
126
+ this.closed = true;
127
+ }
128
+ }
129
+
130
+ ref(): void {
131
+ if (this.proc.terminal) {
132
+ this.proc.terminal.ref();
133
+ }
134
+ }
135
+
136
+ unref(): void {
137
+ if (this.proc.terminal) {
138
+ this.proc.terminal.unref();
139
+ }
140
+ }
141
+ }