pi-interactive-shell 0.8.2 → 0.10.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.
@@ -0,0 +1,170 @@
1
+ import type { InteractiveShellConfig } from "./config.js";
2
+ import type { OutputOptions, OutputResult } from "./session-manager.js";
3
+ import type { InteractiveShellResult } from "./types.js";
4
+ import type { PtyTerminalSession } from "./pty-session.js";
5
+
6
+ /** Mutable query bookkeeping kept per active session. */
7
+ export interface SessionQueryState {
8
+ lastQueryTime: number;
9
+ incrementalReadPosition: number;
10
+ }
11
+
12
+ export const DEFAULT_STATUS_OUTPUT = 5 * 1024;
13
+ export const DEFAULT_STATUS_LINES = 20;
14
+ export const MAX_STATUS_OUTPUT = 50 * 1024;
15
+ export const MAX_STATUS_LINES = 200;
16
+
17
+ export function createSessionQueryState(): SessionQueryState {
18
+ return {
19
+ lastQueryTime: 0,
20
+ incrementalReadPosition: 0,
21
+ };
22
+ }
23
+
24
+ export function getSessionOutput(
25
+ session: PtyTerminalSession,
26
+ config: InteractiveShellConfig,
27
+ state: SessionQueryState,
28
+ options: OutputOptions | boolean = false,
29
+ completionOutput?: InteractiveShellResult["completionOutput"],
30
+ ): OutputResult {
31
+ if (completionOutput) {
32
+ return buildCompletionOutputResult(completionOutput);
33
+ }
34
+
35
+ const opts = typeof options === "boolean" ? { skipRateLimit: options } : options;
36
+ const requestedLines = clampPositive(opts.lines ?? DEFAULT_STATUS_LINES, MAX_STATUS_LINES);
37
+ const requestedMaxChars = clampPositive(opts.maxChars ?? DEFAULT_STATUS_OUTPUT, MAX_STATUS_OUTPUT);
38
+ const rateLimited = maybeRateLimitQuery(config, state, opts.skipRateLimit ?? false);
39
+ if (rateLimited) return rateLimited;
40
+
41
+ if (opts.incremental) {
42
+ return getIncrementalOutput(session, state, requestedLines, requestedMaxChars);
43
+ }
44
+
45
+ if (opts.drain) {
46
+ return buildTruncatedOutput(session.getRawStream({ sinceLast: true, stripAnsi: true }), requestedMaxChars, true);
47
+ }
48
+
49
+ if (opts.offset !== undefined) {
50
+ return getOffsetOutput(session, opts.offset, requestedLines, requestedMaxChars);
51
+ }
52
+
53
+ const tailResult = session.getTailLines({
54
+ lines: requestedLines,
55
+ ansi: false,
56
+ maxChars: requestedMaxChars,
57
+ });
58
+ const output = tailResult.lines.join("\n");
59
+ return {
60
+ output,
61
+ truncated: tailResult.lines.length < tailResult.totalLinesInBuffer || tailResult.truncatedByChars,
62
+ totalBytes: output.length,
63
+ totalLines: tailResult.totalLinesInBuffer,
64
+ };
65
+ }
66
+
67
+ function maybeRateLimitQuery(
68
+ config: InteractiveShellConfig,
69
+ state: SessionQueryState,
70
+ skipRateLimit: boolean,
71
+ ): OutputResult | null {
72
+ if (skipRateLimit) return null;
73
+ const now = Date.now();
74
+ const minIntervalMs = config.minQueryIntervalSeconds * 1000;
75
+ const elapsed = now - state.lastQueryTime;
76
+ if (state.lastQueryTime > 0 && elapsed < minIntervalMs) {
77
+ return {
78
+ output: "",
79
+ truncated: false,
80
+ totalBytes: 0,
81
+ rateLimited: true,
82
+ waitSeconds: Math.ceil((minIntervalMs - elapsed) / 1000),
83
+ };
84
+ }
85
+ state.lastQueryTime = now;
86
+ return null;
87
+ }
88
+
89
+ function getIncrementalOutput(
90
+ session: PtyTerminalSession,
91
+ state: SessionQueryState,
92
+ requestedLines: number,
93
+ requestedMaxChars: number,
94
+ ): OutputResult {
95
+ const result = session.getLogSlice({
96
+ offset: state.incrementalReadPosition,
97
+ limit: requestedLines,
98
+ stripAnsi: true,
99
+ });
100
+ const output = truncateForMaxChars(result.slice, requestedMaxChars);
101
+ state.incrementalReadPosition += result.sliceLineCount;
102
+ return {
103
+ output: output.value,
104
+ truncated: output.truncated,
105
+ totalBytes: output.value.length,
106
+ totalLines: result.totalLines,
107
+ hasMore: state.incrementalReadPosition < result.totalLines,
108
+ };
109
+ }
110
+
111
+ function getOffsetOutput(
112
+ session: PtyTerminalSession,
113
+ offset: number,
114
+ requestedLines: number,
115
+ requestedMaxChars: number,
116
+ ): OutputResult {
117
+ const result = session.getLogSlice({
118
+ offset,
119
+ limit: requestedLines,
120
+ stripAnsi: true,
121
+ });
122
+ const output = truncateForMaxChars(result.slice, requestedMaxChars);
123
+ const hasMore = (offset + result.sliceLineCount) < result.totalLines;
124
+ return {
125
+ output: output.value,
126
+ truncated: output.truncated || hasMore,
127
+ totalBytes: output.value.length,
128
+ totalLines: result.totalLines,
129
+ hasMore,
130
+ };
131
+ }
132
+
133
+ function buildCompletionOutputResult(completionOutput: NonNullable<InteractiveShellResult["completionOutput"]>): OutputResult {
134
+ const output = completionOutput.lines.join("\n");
135
+ return {
136
+ output,
137
+ truncated: completionOutput.truncated,
138
+ totalBytes: output.length,
139
+ totalLines: completionOutput.totalLines,
140
+ };
141
+ }
142
+
143
+ function buildTruncatedOutput(output: string, requestedMaxChars: number, sliceFromEnd = false): OutputResult {
144
+ const truncated = output.length > requestedMaxChars;
145
+ let value = output;
146
+ if (truncated) {
147
+ value = sliceFromEnd
148
+ ? output.slice(-requestedMaxChars)
149
+ : output.slice(0, requestedMaxChars);
150
+ }
151
+ return {
152
+ output: value,
153
+ truncated,
154
+ totalBytes: value.length,
155
+ };
156
+ }
157
+
158
+ function truncateForMaxChars(output: string, requestedMaxChars: number): { value: string; truncated: boolean } {
159
+ if (output.length <= requestedMaxChars) {
160
+ return { value: output, truncated: false };
161
+ }
162
+ return {
163
+ value: output.slice(0, requestedMaxChars),
164
+ truncated: true,
165
+ };
166
+ }
167
+
168
+ function clampPositive(value: number, max: number): number {
169
+ return Math.max(1, Math.min(max, value));
170
+ }
@@ -0,0 +1,37 @@
1
+ import { chmodSync, statSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+
5
+ const require = createRequire(import.meta.url);
6
+ let spawnHelperChecked = false;
7
+
8
+ export function ensureSpawnHelperExec(): void {
9
+ if (spawnHelperChecked) return;
10
+ spawnHelperChecked = true;
11
+ if (process.platform !== "darwin") return;
12
+
13
+ let pkgPath: string;
14
+ try {
15
+ pkgPath = require.resolve("node-pty/package.json");
16
+ } catch {
17
+ return;
18
+ }
19
+
20
+ const base = dirname(pkgPath);
21
+ const targets = [
22
+ join(base, "prebuilds", "darwin-arm64", "spawn-helper"),
23
+ join(base, "prebuilds", "darwin-x64", "spawn-helper"),
24
+ ];
25
+
26
+ for (const target of targets) {
27
+ try {
28
+ const stats = statSync(target);
29
+ const mode = stats.mode | 0o111;
30
+ if ((stats.mode & 0o111) !== 0o111) {
31
+ chmodSync(target, mode);
32
+ }
33
+ } catch {
34
+ continue;
35
+ }
36
+ }
37
+ }
package/tool-schema.ts CHANGED
@@ -14,6 +14,10 @@ MODES:
14
14
  - hands-free: Agent monitors with periodic updates, user can take over anytime by typing
15
15
  - dispatch: Agent is notified on completion via triggerTurn (no polling needed)
16
16
 
17
+ RECOMMENDED DEFAULT FOR DELEGATED TASKS:
18
+ - For fire-and-forget delegations and QA-style checks, prefer mode="dispatch".
19
+ - Dispatch is the safest choice when the agent should continue immediately and be notified automatically on completion.
20
+
17
21
  The user will see the process in an overlay. They can:
18
22
  - Watch output in real-time
19
23
  - Scroll through output (Shift+Up/Down)
@@ -234,10 +238,10 @@ export const toolParameters = Type.Object({
234
238
  Type.Number({ description: "Max interval between updates in ms (default: 60000)" }),
235
239
  ),
236
240
  quietThreshold: Type.Optional(
237
- Type.Number({ description: "Silence duration before emitting update in on-quiet mode (default: 5000ms)" }),
241
+ Type.Number({ description: "Silence duration before emitting update in on-quiet mode (default: 8000ms)" }),
238
242
  ),
239
243
  gracePeriod: Type.Optional(
240
- Type.Number({ description: "Startup grace period before autoExitOnQuiet can kill the session (default: 30000ms)" }),
244
+ Type.Number({ description: "Startup grace period before autoExitOnQuiet can kill the session (default: 15000ms)" }),
241
245
  ),
242
246
  updateMaxChars: Type.Optional(
243
247
  Type.Number({ description: "Max chars per update (default: 1500)" }),
package/types.ts CHANGED
@@ -48,11 +48,14 @@ export interface HandsFreeUpdate {
48
48
  budgetExhausted?: boolean;
49
49
  }
50
50
 
51
+ /** Options for starting or reattaching an interactive shell session. */
51
52
  export interface InteractiveShellOptions {
52
53
  command: string;
53
54
  cwd?: string;
54
55
  name?: string;
55
56
  reason?: string;
57
+ /** Original session start time in ms since epoch, preserved across background/reattach transitions. */
58
+ startedAt?: number;
56
59
  handoffPreviewEnabled?: boolean;
57
60
  handoffPreviewLines?: number;
58
61
  handoffPreviewMaxChars?: number;
@@ -73,6 +76,9 @@ export interface InteractiveShellOptions {
73
76
  autoExitGracePeriod?: number;
74
77
  // Auto-kill timeout
75
78
  timeout?: number;
79
+ // When true, unregister active session on completion (blocking tool call path).
80
+ // When false/undefined, keep registered so agent can query result later.
81
+ streamingMode?: boolean;
76
82
  // Existing PTY session (for attach flow -- skip creating a new PTY)
77
83
  existingSession?: import("./pty-session.js").PtyTerminalSession;
78
84
  }