@aerode/pish 0.8.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/app.js ADDED
@@ -0,0 +1,473 @@
1
+ /**
2
+ * App — core application object.
3
+ *
4
+ * Owns all mutable session state (mode, FIFO, reverse tracking).
5
+ * Receives events from Recorder and AgentManager, drives rendering
6
+ * and FIFO responses. Created by main.ts after all resources are ready.
7
+ */
8
+ import * as fs from 'node:fs';
9
+ import * as os from 'node:os';
10
+ import * as path from 'node:path';
11
+ import { closeLog, log } from './log.js';
12
+ import { printBanner, printControl, printControlResult, printExit, printNotice, StreamRenderer, startSpinner, } from './render.js';
13
+ // ═══════════════════════════════════════
14
+ // Pure helpers (module-level, no `this`)
15
+ // ═══════════════════════════════════════
16
+ function formatContext(entries) {
17
+ return entries
18
+ .map((e, i) => {
19
+ const parts = [];
20
+ const num = `[${i + 1}]`;
21
+ if (e.prompt) {
22
+ parts.push(`${num} ${e.prompt}`);
23
+ }
24
+ else {
25
+ parts.push(num);
26
+ }
27
+ if (e.output)
28
+ parts.push(e.output);
29
+ if (e.rc !== 0)
30
+ parts.push(`[exit code: ${e.rc}]`);
31
+ return parts.join('\n');
32
+ })
33
+ .join('\n\n');
34
+ }
35
+ /** pi agent directory, respects PI_CODING_AGENT_DIR env var. */
36
+ function getAgentDir() {
37
+ const envDir = process.env.PI_CODING_AGENT_DIR;
38
+ if (envDir) {
39
+ if (envDir === '~')
40
+ return os.homedir();
41
+ if (envDir.startsWith('~/'))
42
+ return os.homedir() + envDir.slice(1);
43
+ return envDir;
44
+ }
45
+ return path.join(os.homedir(), '.pi', 'agent');
46
+ }
47
+ /** CWD encoding rule, matching pi's getDefaultSessionDir. */
48
+ function cwdToSessionSubdir(cwd) {
49
+ return `--${cwd.replace(/^[\/\\]/, '').replace(/[\/\\:]/g, '-')}--`;
50
+ }
51
+ /**
52
+ * Find the latest session file in the CWD session directory with mtime > since.
53
+ */
54
+ async function findLatestSession(since, debug) {
55
+ const cwdSessionDir = path.join(getAgentDir(), 'sessions', cwdToSessionSubdir(process.cwd()));
56
+ let files;
57
+ try {
58
+ files = await fs.promises.readdir(cwdSessionDir);
59
+ }
60
+ catch {
61
+ return null; // directory doesn't exist
62
+ }
63
+ let latest = null;
64
+ for (const file of files) {
65
+ if (!file.endsWith('.jsonl'))
66
+ continue;
67
+ try {
68
+ const filePath = path.join(cwdSessionDir, file);
69
+ const fstat = await fs.promises.stat(filePath);
70
+ const mtime = fstat.mtimeMs;
71
+ if (mtime > since && (!latest || mtime > latest.mtime)) {
72
+ latest = { path: filePath, mtime };
73
+ }
74
+ }
75
+ catch {
76
+ debug('findLatestSession: stat error for', file);
77
+ }
78
+ }
79
+ return latest?.path ?? null;
80
+ }
81
+ // ═══════════════════════════════════════
82
+ // App
83
+ // ═══════════════════════════════════════
84
+ export class App {
85
+ // ── Injected dependencies ──
86
+ cfg;
87
+ pty;
88
+ recorder;
89
+ agent;
90
+ // ── Infrastructure (immutable after construction) ──
91
+ fifoPath;
92
+ tmpDir;
93
+ rcPath;
94
+ debugFd;
95
+ // ── FIFO ──
96
+ fifoFd = null;
97
+ cleaned = false;
98
+ // ── Agent mode state ──
99
+ mode = 'normal';
100
+ agentCmd = '';
101
+ agentStartTime = 0;
102
+ stdinBuffer = [];
103
+ renderer = null;
104
+ // ── Reverse session recovery ──
105
+ sessionEpoch = Date.now();
106
+ reverseStartTime = 0;
107
+ preReverseSessionFile;
108
+ constructor(deps, infra) {
109
+ this.cfg = deps.cfg;
110
+ this.pty = deps.pty;
111
+ this.recorder = deps.recorder;
112
+ this.agent = deps.agent;
113
+ this.fifoPath = infra.fifoPath;
114
+ this.tmpDir = infra.tmpDir;
115
+ this.rcPath = infra.rcPath;
116
+ // Open debug log file (same file shell hooks append to)
117
+ const debugPath = process.env.PISH_DEBUG;
118
+ this.debugFd = debugPath ? fs.openSync(debugPath, 'a') : null;
119
+ // Wire internal event handlers
120
+ this.agent.onEvent((event) => this.onAgentEvent(event));
121
+ this.recorder.onEvent((evt) => this.onRecorderEvent(evt));
122
+ }
123
+ // ═══════════════════════════════════════
124
+ // Public I/O interface (called by main.ts wiring)
125
+ // ═══════════════════════════════════════
126
+ /** PTY stdout data → recorder + terminal. */
127
+ onPtyData(data) {
128
+ const clean = this.recorder.feed(data);
129
+ process.stdout.write(clean);
130
+ }
131
+ /** PTY process exited. */
132
+ onPtyExit(code) {
133
+ this.debugLog('PTY exited, code:', code);
134
+ printExit();
135
+ log('exit', { context_count: this.recorder.context.length, code });
136
+ closeLog();
137
+ this.cleanup();
138
+ process.exit(code);
139
+ }
140
+ /** Terminal stdin data → mode routing. */
141
+ onStdin(data) {
142
+ if (this.mode === 'agent') {
143
+ if (data.length === 1 && data[0] === 0x03) {
144
+ this.abortAgent();
145
+ }
146
+ else {
147
+ this.stdinBuffer.push(Buffer.from(data));
148
+ }
149
+ return;
150
+ }
151
+ // Ctrl+L: clear screen + reset context + full agent reset (including session)
152
+ if (data.length === 1 && data[0] === 0x0c) {
153
+ const cleared = this.recorder.drain();
154
+ log('context_clear', { discarded: cleared.length });
155
+ this.agent.reset();
156
+ this.sessionEpoch = Date.now();
157
+ this.preReverseSessionFile = undefined;
158
+ this.pty.write(data.toString());
159
+ return;
160
+ }
161
+ this.pty.write(data.toString());
162
+ }
163
+ /** Terminal resized. */
164
+ onResize(cols, rows) {
165
+ this.pty.resize(cols, rows);
166
+ }
167
+ /** Cleanup all resources. Public — called by signal handlers in main.ts. */
168
+ cleanup() {
169
+ if (this.cleaned)
170
+ return;
171
+ this.cleaned = true;
172
+ process.stderr.write('\x1b[?25h'); // Restore cursor visibility
173
+ this.agent.kill();
174
+ if (this.fifoFd !== null) {
175
+ try {
176
+ fs.closeSync(this.fifoFd);
177
+ }
178
+ catch {
179
+ /* fd may already be closed */
180
+ }
181
+ this.fifoFd = null;
182
+ }
183
+ try {
184
+ fs.unlinkSync(this.fifoPath);
185
+ }
186
+ catch {
187
+ /* already removed or never created */
188
+ }
189
+ try {
190
+ fs.rmSync(this.tmpDir, { recursive: true, force: true });
191
+ }
192
+ catch {
193
+ /* non-empty or already removed */
194
+ }
195
+ try {
196
+ fs.unlinkSync(this.rcPath);
197
+ fs.rmSync(path.dirname(this.rcPath), { recursive: true, force: true });
198
+ }
199
+ catch {
200
+ /* zsh rcdir may not exist or already cleaned */
201
+ }
202
+ if (this.debugFd !== null) {
203
+ try {
204
+ fs.closeSync(this.debugFd);
205
+ }
206
+ catch {
207
+ /* fd may already be closed */
208
+ }
209
+ }
210
+ }
211
+ // ═══════════════════════════════════════
212
+ // Agent event handler
213
+ // ═══════════════════════════════════════
214
+ onAgentEvent(event) {
215
+ this.renderer?.handleEvent(event);
216
+ if (event.type === 'agent_done' && this.mode === 'agent') {
217
+ log('agent_done', {
218
+ cmd: this.agentCmd,
219
+ duration_ms: Date.now() - this.agentStartTime,
220
+ });
221
+ this.exitAgentMode();
222
+ }
223
+ if (event.type === 'agent_error' && this.mode === 'agent') {
224
+ log('agent_error', { cmd: this.agentCmd, error: event.error });
225
+ this.exitAgentMode();
226
+ }
227
+ }
228
+ // ═══════════════════════════════════════
229
+ // Recorder event handler
230
+ // ═══════════════════════════════════════
231
+ onRecorderEvent(evt) {
232
+ switch (evt.type) {
233
+ case 'shell_ready':
234
+ this.debugLog('EVENT: shell_ready');
235
+ this.fifoFd = fs.openSync(this.fifoPath, 'w');
236
+ this.debugLog('FIFO write fd opened');
237
+ log('shell_ready', { pid: this.pty.pid });
238
+ printBanner(this.cfg);
239
+ break;
240
+ case 'context':
241
+ log('context', {
242
+ prompt: evt.entry.prompt,
243
+ output: evt.entry.output,
244
+ rc: evt.entry.rc,
245
+ kept: this.recorder.context.length,
246
+ });
247
+ break;
248
+ case 'context_skip':
249
+ log('context_skip', { reason: evt.reason });
250
+ break;
251
+ case 'agent':
252
+ this.handleAgentCmd(evt.cmd);
253
+ break;
254
+ case 'reverse':
255
+ this.handleReverse();
256
+ break;
257
+ case 'reverse_done':
258
+ this.handleReverseDone().catch((err) => {
259
+ log('reverse_done_error', { error: String(err) });
260
+ });
261
+ break;
262
+ case 'error':
263
+ log('error', { msg: evt.msg });
264
+ process.stderr.write(`\x1b[31mpish: ${evt.msg}\x1b[0m\n`);
265
+ this.cleanup();
266
+ process.exit(1);
267
+ break;
268
+ }
269
+ }
270
+ // ═══════════════════════════════════════
271
+ // Agent mode transitions
272
+ // ═══════════════════════════════════════
273
+ enterAgentMode(cmd) {
274
+ this.mode = 'agent';
275
+ this.agentCmd = cmd;
276
+ this.agentStartTime = Date.now();
277
+ this.stdinBuffer = [];
278
+ this.debugLog('enterAgentMode:', cmd);
279
+ const entries = this.recorder.drain();
280
+ log('agent', { cmd, context_count: entries.length });
281
+ this.renderer = new StreamRenderer(this.cfg.toolResultLines);
282
+ if (this.agent.crashInfo) {
283
+ printNotice(this.agent.crashInfo);
284
+ this.agent.crashInfo = undefined;
285
+ }
286
+ this.renderer.showSpinner();
287
+ let message = cmd;
288
+ const ctx = formatContext(entries);
289
+ if (ctx) {
290
+ message = `Here is my recent shell activity:\n\n${ctx}\n\n${cmd}`;
291
+ }
292
+ this.agent.submit(message);
293
+ }
294
+ exitAgentMode() {
295
+ this.debugLog('exitAgentMode, stdinBuffer:', this.stdinBuffer.length, 'chunks');
296
+ this.mode = 'normal';
297
+ this.renderer = null;
298
+ this.fifoWrite('PROCEED');
299
+ // Request state to get session file (for subsequent reverse).
300
+ // Only if agent process is alive — don't respawn after crash.
301
+ if (this.agent.alive) {
302
+ this.agent
303
+ .rpcWait({ type: 'get_state' }, 5000)
304
+ .then((response) => {
305
+ if (response.success && response.data?.sessionFile) {
306
+ this.agent.sessionFile = response.data.sessionFile;
307
+ }
308
+ })
309
+ .catch((err) => {
310
+ log('get_state_error', { error: String(err) });
311
+ });
312
+ }
313
+ const buffered = this.stdinBuffer;
314
+ this.stdinBuffer = [];
315
+ if (buffered.length > 0) {
316
+ setTimeout(() => {
317
+ for (const chunk of buffered) {
318
+ this.pty.write(chunk.toString());
319
+ }
320
+ this.debugLog('replayed', buffered.length, 'stdin chunks');
321
+ }, 50);
322
+ }
323
+ }
324
+ abortAgent() {
325
+ this.debugLog('abortAgent');
326
+ this.agent.abort();
327
+ this.renderer?.printInterrupted();
328
+ log('agent_abort', {
329
+ cmd: this.agentCmd,
330
+ duration_ms: Date.now() - this.agentStartTime,
331
+ });
332
+ this.mode = 'normal';
333
+ this.renderer = null;
334
+ this.stdinBuffer = [];
335
+ this.fifoWrite('PROCEED');
336
+ }
337
+ // ═══════════════════════════════════════
338
+ // Agent / control command dispatch
339
+ // ═══════════════════════════════════════
340
+ handleAgentCmd(cmd) {
341
+ this.debugLog('EVENT: agent cmd=', cmd);
342
+ if (cmd.startsWith('/')) {
343
+ log('control', { cmd });
344
+ printControl(cmd);
345
+ if (this.cfg.noAgent) {
346
+ this.fifoWrite('PROCEED');
347
+ return;
348
+ }
349
+ this.handleControlAsync(cmd)
350
+ .then((response) => {
351
+ if (response)
352
+ printControlResult(cmd, response);
353
+ })
354
+ .catch((err) => {
355
+ log('control_error', { cmd, error: String(err) });
356
+ })
357
+ .finally(() => {
358
+ this.fifoWrite('PROCEED');
359
+ });
360
+ return;
361
+ }
362
+ if (this.cfg.noAgent) {
363
+ log('agent_skip', { cmd, reason: 'no-agent' });
364
+ printNotice(`agent disabled, skipped: ${cmd}`);
365
+ this.fifoWrite('PROCEED');
366
+ return;
367
+ }
368
+ this.enterAgentMode(cmd);
369
+ }
370
+ async handleControlAsync(cmd) {
371
+ const parts = cmd.trim().split(/\s+/);
372
+ const name = parts[0];
373
+ const arg = parts.slice(1).join(' ');
374
+ switch (name) {
375
+ case '/compact': {
376
+ const stopSpinner = startSpinner('Compacting...');
377
+ try {
378
+ return await this.agent.rpcWait({ type: 'compact', ...(arg ? { customInstructions: arg } : {}) }, 60000);
379
+ }
380
+ finally {
381
+ stopSpinner();
382
+ }
383
+ }
384
+ case '/model': {
385
+ if (!arg) {
386
+ const state = await this.agent.rpcWait({ type: 'get_state' }, 5000);
387
+ if (state.success && state.data?.model) {
388
+ const m = state.data.model;
389
+ const prov = m.provider;
390
+ const provider = typeof prov === 'object' && prov !== null
391
+ ? (prov.id ?? '')
392
+ : String(prov ?? '');
393
+ const modelId = m.id ?? '';
394
+ return {
395
+ type: 'response',
396
+ command: 'set_model',
397
+ success: true,
398
+ data: { provider: { id: provider }, id: modelId },
399
+ };
400
+ }
401
+ return {
402
+ type: 'response',
403
+ command: 'set_model',
404
+ success: false,
405
+ error: 'no model info available',
406
+ };
407
+ }
408
+ const slashIdx = arg.indexOf('/');
409
+ if (slashIdx > 0) {
410
+ return await this.agent.rpcWait({
411
+ type: 'set_model',
412
+ provider: arg.slice(0, slashIdx),
413
+ modelId: arg.slice(slashIdx + 1),
414
+ }, 10000);
415
+ }
416
+ else {
417
+ return await this.agent.rpcWait({ type: 'set_model', provider: '', modelId: arg }, 10000);
418
+ }
419
+ }
420
+ case '/think': {
421
+ const level = arg || 'medium';
422
+ return await this.agent.rpcWait({ type: 'set_thinking_level', level }, 5000);
423
+ }
424
+ default:
425
+ return null;
426
+ }
427
+ }
428
+ // ═══════════════════════════════════════
429
+ // Reverse session recovery
430
+ // ═══════════════════════════════════════
431
+ handleReverse() {
432
+ this.debugLog('EVENT: reverse');
433
+ const sessionFile = this.agent.sessionFile;
434
+ this.agent.kill();
435
+ this.reverseStartTime = Date.now();
436
+ this.preReverseSessionFile = sessionFile;
437
+ log('reverse', {
438
+ context_count: this.recorder.context.length,
439
+ session: sessionFile || null,
440
+ });
441
+ if (sessionFile) {
442
+ this.fifoWrite(`SESSION:${sessionFile}`);
443
+ }
444
+ else {
445
+ this.fifoWrite('SESSION:');
446
+ }
447
+ }
448
+ async handleReverseDone() {
449
+ this.debugLog('EVENT: reverse_done');
450
+ const since = Math.max(this.reverseStartTime, this.sessionEpoch);
451
+ const recovered = await findLatestSession(since, (...a) => this.debugLog(...a));
452
+ this.agent.sessionFile = recovered ?? this.preReverseSessionFile;
453
+ log('reverse_done', { session: this.agent.sessionFile ?? null });
454
+ }
455
+ // ═══════════════════════════════════════
456
+ // FIFO + debug
457
+ // ═══════════════════════════════════════
458
+ fifoWrite(data) {
459
+ if (this.fifoFd !== null) {
460
+ fs.writeSync(this.fifoFd, `${data}\n`);
461
+ this.debugLog('FIFO wrote:', data);
462
+ }
463
+ else {
464
+ this.debugLog('FIFO not ready, dropping:', data);
465
+ }
466
+ }
467
+ debugLog(...args) {
468
+ if (this.debugFd !== null) {
469
+ const ts = new Date().toISOString().slice(11, 23);
470
+ fs.writeSync(this.debugFd, `[${ts}] PISH ${args.map(String).join(' ')}\n`);
471
+ }
472
+ }
473
+ }
package/dist/config.js ADDED
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Unified configuration.
3
+ *
4
+ * Priority: CLI args > ENV > defaults.
5
+ * All tunable parameters live here; other modules access via cfg.xxx.
6
+ */
7
+ import { execFileSync } from 'node:child_process';
8
+ import * as fs from 'node:fs';
9
+ import { createRequire } from 'node:module';
10
+ const require = createRequire(import.meta.url);
11
+ // ── Defaults ──
12
+ export const DEFAULTS = {
13
+ shell: 'bash',
14
+ maxContext: 20,
15
+ headLines: 50,
16
+ tailLines: 30,
17
+ lineWidth: 512,
18
+ toolResultLines: 10,
19
+ };
20
+ // ── Version ──
21
+ function readVersion() {
22
+ try {
23
+ return require('../package.json').version || '0.0.0';
24
+ }
25
+ catch {
26
+ return '0.0.0';
27
+ }
28
+ }
29
+ // ── Parse helpers ──
30
+ export function envInt(key, fallback) {
31
+ const v = process.env[key];
32
+ if (!v)
33
+ return fallback;
34
+ const n = parseInt(v, 10);
35
+ return Number.isFinite(n) && n > 0 ? n : fallback;
36
+ }
37
+ /**
38
+ * Resolve a binary name or path to its full path.
39
+ * Contains '/' → validate as path; otherwise → which lookup.
40
+ * Returns null if not found.
41
+ */
42
+ export function resolveBinary(nameOrPath) {
43
+ if (nameOrPath.includes('/')) {
44
+ try {
45
+ fs.accessSync(nameOrPath, fs.constants.X_OK);
46
+ return nameOrPath;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ try {
53
+ return execFileSync('which', [nameOrPath], {
54
+ encoding: 'utf-8',
55
+ stdio: ['pipe', 'pipe', 'pipe'],
56
+ }).trim();
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ /** Infer shell type from binary path basename. */
63
+ export function inferShellType(shellPath) {
64
+ const base = shellPath.split('/').pop() ?? '';
65
+ if (base === 'bash' || base.startsWith('bash'))
66
+ return 'bash';
67
+ if (base === 'zsh' || base.startsWith('zsh'))
68
+ return 'zsh';
69
+ return null;
70
+ }
71
+ export function parseArgs(argv) {
72
+ const args = argv.slice(2); // skip node, script
73
+ const result = { noAgent: false, help: false, version: false };
74
+ let i = 0;
75
+ while (i < args.length) {
76
+ const a = args[i];
77
+ if (a === '--help' || a === '-h') {
78
+ result.help = true;
79
+ }
80
+ else if (a === '--version' || a === '-v') {
81
+ result.version = true;
82
+ }
83
+ else if (a === '--shell' || a === '-s') {
84
+ result.shell = args[++i];
85
+ }
86
+ else if (a.startsWith('--shell=')) {
87
+ result.shell = a.slice('--shell='.length);
88
+ }
89
+ else if (a === '--pi') {
90
+ result.pi = args[++i];
91
+ }
92
+ else if (a.startsWith('--pi=')) {
93
+ result.pi = a.slice('--pi='.length);
94
+ }
95
+ else if (a === '--no-agent') {
96
+ result.noAgent = true;
97
+ }
98
+ else if (!a.startsWith('-') && !result.shell) {
99
+ // Positional argument = shell
100
+ result.shell = a;
101
+ }
102
+ i++;
103
+ }
104
+ return result;
105
+ }
106
+ // ── Help ──
107
+ function printHelp(version) {
108
+ process.stderr.write(`pish v${version} — Pi-Integrated Shell
109
+
110
+ Usage: pish [options] [shell]
111
+
112
+ Arguments:
113
+ shell bash, zsh, or path (default: bash)
114
+
115
+ Options:
116
+ -s, --shell <name> Shell name or path
117
+ --pi <path> Path to pi binary (default: pi in PATH)
118
+ --no-agent Disable agent (CNF passes through, for debugging)
119
+ -v, --version Show version
120
+ -h, --help Show help
121
+
122
+ Environment variables:
123
+ PISH_SHELL Shell name or path (default: $SHELL or bash)
124
+ PISH_PI Path to pi binary
125
+ PISH_MAX_CONTEXT Max context entries (default: ${DEFAULTS.maxContext})
126
+ PISH_HEAD_LINES Output head lines (default: ${DEFAULTS.headLines})
127
+ PISH_TAIL_LINES Output tail lines (default: ${DEFAULTS.tailLines})
128
+ PISH_LINE_WIDTH Max line width (default: ${DEFAULTS.lineWidth})
129
+ PISH_TOOL_LINES Tool result lines (default: ${DEFAULTS.toolResultLines})
130
+ PISH_LOG Event log (stderr or file path)
131
+ PISH_DEBUG Debug log file path
132
+ PISH_NO_BANNER Hide startup banner (set to 1)
133
+
134
+ Priority: CLI args > ENV > defaults
135
+ `);
136
+ }
137
+ // ── Entry point ──
138
+ export function loadConfig() {
139
+ const cli = parseArgs(process.argv);
140
+ const version = readVersion();
141
+ if (cli.help) {
142
+ printHelp(version);
143
+ process.exit(0);
144
+ }
145
+ if (cli.version) {
146
+ process.stderr.write(`pish v${version}\n`);
147
+ process.exit(0);
148
+ }
149
+ // ── Shell resolution (CLI > ENV > default) ──
150
+ const shellSpec = cli.shell ?? process.env.PISH_SHELL ?? process.env.SHELL ?? DEFAULTS.shell;
151
+ const shellPath = resolveBinary(shellSpec);
152
+ if (!shellPath) {
153
+ process.stderr.write(`pish: shell not found: ${shellSpec}\n`);
154
+ process.exit(1);
155
+ }
156
+ const shell = inferShellType(shellPath);
157
+ if (!shell) {
158
+ process.stderr.write(`pish: unsupported shell: ${shellSpec} (only bash and zsh)\n`);
159
+ process.exit(1);
160
+ }
161
+ // ── Pi resolution (skipped in --no-agent mode) ──
162
+ const noAgent = cli.noAgent;
163
+ let piPath = '';
164
+ if (!noAgent) {
165
+ const piSpec = cli.pi ?? process.env.PISH_PI ?? 'pi';
166
+ const resolved = resolveBinary(piSpec);
167
+ if (!resolved) {
168
+ process.stderr.write(`pish: pi not found: ${piSpec}\n`);
169
+ process.stderr.write(` Install pi or set --pi <path> / PISH_PI\n`);
170
+ process.exit(1);
171
+ }
172
+ piPath = resolved;
173
+ }
174
+ return {
175
+ shell,
176
+ shellPath,
177
+ piPath,
178
+ version,
179
+ noAgent,
180
+ maxContext: envInt('PISH_MAX_CONTEXT', DEFAULTS.maxContext),
181
+ headLines: envInt('PISH_HEAD_LINES', DEFAULTS.headLines),
182
+ tailLines: envInt('PISH_TAIL_LINES', DEFAULTS.tailLines),
183
+ lineWidth: envInt('PISH_LINE_WIDTH', DEFAULTS.lineWidth),
184
+ toolResultLines: envInt('PISH_TOOL_LINES', DEFAULTS.toolResultLines),
185
+ noBanner: process.env.PISH_NO_BANNER === '1',
186
+ };
187
+ }