@vybestack/llxprt-code-core 0.5.0-nightly.251125.6be1c4fa2 → 0.5.0-nightly.251126.ec44835c5
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/src/agents/invocation.js +11 -3
- package/dist/src/agents/invocation.js.map +1 -1
- package/dist/src/agents/registry.d.ts +2 -0
- package/dist/src/agents/registry.js +9 -6
- package/dist/src/agents/registry.js.map +1 -1
- package/dist/src/config/config.d.ts +3 -9
- package/dist/src/config/config.js +0 -25
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +0 -6
- package/dist/src/index.js.map +1 -1
- package/dist/src/providers/anthropic/AnthropicProvider.js +24 -0
- package/dist/src/providers/anthropic/AnthropicProvider.js.map +1 -1
- package/dist/src/providers/openai/OpenAIProvider.d.ts +3 -0
- package/dist/src/providers/openai/OpenAIProvider.js +8 -2
- package/dist/src/providers/openai/OpenAIProvider.js.map +1 -1
- package/dist/src/providers/utils/localEndpoint.d.ts +39 -0
- package/dist/src/providers/utils/localEndpoint.js +117 -0
- package/dist/src/providers/utils/localEndpoint.js.map +1 -0
- package/dist/src/services/shellExecutionService.d.ts +3 -46
- package/dist/src/services/shellExecutionService.js +49 -189
- package/dist/src/services/shellExecutionService.js.map +1 -1
- package/dist/src/tools/shell.js +2 -7
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/tools.d.ts +1 -2
- package/dist/src/tools/tools.js.map +1 -1
- package/package.json +1 -1
- package/dist/src/utils/terminalSerializer.d.ts +0 -28
- package/dist/src/utils/terminalSerializer.js +0 -418
- package/dist/src/utils/terminalSerializer.js.map +0 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2025 Vybestack LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* @plan PLAN-20251125-LOCALAUTH.P01
|
|
18
|
+
* @requirement REQ-LOCAL-001
|
|
19
|
+
*
|
|
20
|
+
* Utility to detect local/private network endpoints that don't require authentication.
|
|
21
|
+
* Used to fix Issue #598 where Ollama and other local AI servers were incorrectly
|
|
22
|
+
* requiring authentication.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Checks if a URL points to a local or private network endpoint.
|
|
26
|
+
*
|
|
27
|
+
* Local endpoints include:
|
|
28
|
+
* - localhost
|
|
29
|
+
* - 127.0.0.0/8 (IPv4 loopback range - 127.0.0.0 - 127.255.255.255)
|
|
30
|
+
* - [::1] (IPv6 loopback)
|
|
31
|
+
* - Private IP ranges (RFC 1918):
|
|
32
|
+
* - 10.0.0.0/8 (10.0.0.0 - 10.255.255.255)
|
|
33
|
+
* - 172.16.0.0/12 (172.16.0.0 - 172.31.255.255)
|
|
34
|
+
* - 192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
|
|
35
|
+
*
|
|
36
|
+
* @param url - The URL to check (can be undefined or empty)
|
|
37
|
+
* @returns true if the URL points to a local/private endpoint, false otherwise
|
|
38
|
+
*/
|
|
39
|
+
export declare function isLocalEndpoint(url: string | undefined): boolean;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2025 Vybestack LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* @plan PLAN-20251125-LOCALAUTH.P01
|
|
18
|
+
* @requirement REQ-LOCAL-001
|
|
19
|
+
*
|
|
20
|
+
* Utility to detect local/private network endpoints that don't require authentication.
|
|
21
|
+
* Used to fix Issue #598 where Ollama and other local AI servers were incorrectly
|
|
22
|
+
* requiring authentication.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Checks if a URL points to a local or private network endpoint.
|
|
26
|
+
*
|
|
27
|
+
* Local endpoints include:
|
|
28
|
+
* - localhost
|
|
29
|
+
* - 127.0.0.0/8 (IPv4 loopback range - 127.0.0.0 - 127.255.255.255)
|
|
30
|
+
* - [::1] (IPv6 loopback)
|
|
31
|
+
* - Private IP ranges (RFC 1918):
|
|
32
|
+
* - 10.0.0.0/8 (10.0.0.0 - 10.255.255.255)
|
|
33
|
+
* - 172.16.0.0/12 (172.16.0.0 - 172.31.255.255)
|
|
34
|
+
* - 192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
|
|
35
|
+
*
|
|
36
|
+
* @param url - The URL to check (can be undefined or empty)
|
|
37
|
+
* @returns true if the URL points to a local/private endpoint, false otherwise
|
|
38
|
+
*/
|
|
39
|
+
export function isLocalEndpoint(url) {
|
|
40
|
+
if (!url || url.trim() === '') {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const parsed = new URL(url);
|
|
45
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
46
|
+
// Check for localhost
|
|
47
|
+
if (hostname === 'localhost') {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
// Check for IPv4 loopback range (127.0.0.0/8)
|
|
51
|
+
// The entire 127.x.x.x range is reserved for loopback
|
|
52
|
+
if (isIPv4LoopbackRange(hostname)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
// Check for IPv6 loopback (::1)
|
|
56
|
+
// URL parser represents [::1] as just ::1 in hostname
|
|
57
|
+
if (hostname === '::1' || hostname === '[::1]') {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
// Check for private IP ranges
|
|
61
|
+
if (isPrivateIPv4(hostname)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Invalid URL
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Checks if an IPv4 address is in the loopback range (127.0.0.0/8).
|
|
73
|
+
* The entire 127.x.x.x range is reserved for loopback (RFC 1122).
|
|
74
|
+
*/
|
|
75
|
+
function isIPv4LoopbackRange(ip) {
|
|
76
|
+
const ipv4Match = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
77
|
+
if (!ipv4Match) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const firstOctet = Number(ipv4Match[1]);
|
|
81
|
+
return firstOctet === 127;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Checks if an IPv4 address is in a private range.
|
|
85
|
+
*
|
|
86
|
+
* Private ranges (RFC 1918):
|
|
87
|
+
* - 10.0.0.0/8 (10.0.0.0 - 10.255.255.255)
|
|
88
|
+
* - 172.16.0.0/12 (172.16.0.0 - 172.31.255.255)
|
|
89
|
+
* - 192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
|
|
90
|
+
*/
|
|
91
|
+
function isPrivateIPv4(ip) {
|
|
92
|
+
// Match IPv4 pattern
|
|
93
|
+
const ipv4Match = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
94
|
+
if (!ipv4Match) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const octets = ipv4Match.slice(1).map(Number);
|
|
98
|
+
// Validate octets are in valid range
|
|
99
|
+
if (octets.some((octet) => octet < 0 || octet > 255)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const [first, second] = octets;
|
|
103
|
+
// 10.0.0.0/8
|
|
104
|
+
if (first === 10) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
// 172.16.0.0/12 (172.16.x.x - 172.31.x.x)
|
|
108
|
+
if (first === 172 && second >= 16 && second <= 31) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
// 192.168.0.0/16
|
|
112
|
+
if (first === 192 && second === 168) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=localEndpoint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"localEndpoint.js","sourceRoot":"","sources":["../../../../src/providers/utils/localEndpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;GAOG;AAEH;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,GAAuB;IACrD,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAC5B,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;QAE/C,sBAAsB;QACtB,IAAI,QAAQ,KAAK,WAAW,EAAE,CAAC;YAC7B,OAAO,IAAI,CAAC;QACd,CAAC;QAED,8CAA8C;QAC9C,sDAAsD;QACtD,IAAI,mBAAmB,CAAC,QAAQ,CAAC,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC;QACd,CAAC;QAED,gCAAgC;QAChC,sDAAsD;QACtD,IAAI,QAAQ,KAAK,KAAK,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;YAC/C,OAAO,IAAI,CAAC;QACd,CAAC;QAED,8BAA8B;QAC9B,IAAI,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5B,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,cAAc;QACd,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAS,mBAAmB,CAAC,EAAU;IACrC,MAAM,SAAS,GAAG,EAAE,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAC3E,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;IACxC,OAAO,UAAU,KAAK,GAAG,CAAC;AAC5B,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,aAAa,CAAC,EAAU;IAC/B,qBAAqB;IACrB,MAAM,SAAS,GAAG,EAAE,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAC3E,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IAE9C,qCAAqC;IACrC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,GAAG,CAAC,EAAE,CAAC;QACrD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,MAAM,CAAC;IAE/B,aAAa;IACb,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,0CAA0C;IAC1C,IAAI,KAAK,KAAK,GAAG,IAAI,MAAM,IAAI,EAAE,IAAI,MAAM,IAAI,EAAE,EAAE,CAAC;QAClD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,iBAAiB;IACjB,IAAI,KAAK,KAAK,GAAG,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;QACpC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Copyright 2025 Vybestack LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
import { type AnsiOutput } from '../utils/terminalSerializer.js';
|
|
7
6
|
/** A structured result from a shell command execution. */
|
|
8
7
|
export interface ShellExecutionResult {
|
|
9
8
|
rawOutput: Buffer;
|
|
@@ -28,25 +27,11 @@ export interface ShellExecutionHandle {
|
|
|
28
27
|
pid: number | undefined;
|
|
29
28
|
result: Promise<ShellExecutionResult>;
|
|
30
29
|
}
|
|
31
|
-
export interface ShellExecutionConfig {
|
|
32
|
-
terminalWidth?: number;
|
|
33
|
-
terminalHeight?: number;
|
|
34
|
-
pager?: string;
|
|
35
|
-
showColor?: boolean;
|
|
36
|
-
defaultFg?: string;
|
|
37
|
-
defaultBg?: string;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Describes a structured event emitted during shell command execution.
|
|
41
|
-
*/
|
|
42
30
|
export type ShellOutputEvent = {
|
|
43
31
|
/** The event contains a chunk of output data. */
|
|
44
32
|
type: 'data';
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
* (in color/structured mode).
|
|
48
|
-
*/
|
|
49
|
-
chunk: string | AnsiOutput;
|
|
33
|
+
/** The decoded string chunk. */
|
|
34
|
+
chunk: string;
|
|
50
35
|
} | {
|
|
51
36
|
/** Signals that the output stream has been identified as binary. */
|
|
52
37
|
type: 'binary_detected';
|
|
@@ -56,13 +41,7 @@ export type ShellOutputEvent = {
|
|
|
56
41
|
/** The total number of bytes received so far. */
|
|
57
42
|
bytesReceived: number;
|
|
58
43
|
};
|
|
59
|
-
/**
|
|
60
|
-
* A centralized service for executing shell commands with robust process
|
|
61
|
-
* management, cross-platform compatibility, and streaming output capabilities.
|
|
62
|
-
*
|
|
63
|
-
*/
|
|
64
44
|
export declare class ShellExecutionService {
|
|
65
|
-
private static activePtys;
|
|
66
45
|
/**
|
|
67
46
|
* Executes a shell command using `node-pty`, capturing all output and lifecycle events.
|
|
68
47
|
*
|
|
@@ -75,29 +54,7 @@ export declare class ShellExecutionService {
|
|
|
75
54
|
* @returns An object containing the process ID (pid) and a promise that
|
|
76
55
|
* resolves with the complete execution result.
|
|
77
56
|
*/
|
|
78
|
-
static execute(commandToExecute: string, cwd: string, onOutputEvent: (event: ShellOutputEvent) => void, abortSignal: AbortSignal, shouldUseNodePty: boolean,
|
|
57
|
+
static execute(commandToExecute: string, cwd: string, onOutputEvent: (event: ShellOutputEvent) => void, abortSignal: AbortSignal, shouldUseNodePty: boolean, terminalColumns?: number, terminalRows?: number): Promise<ShellExecutionHandle>;
|
|
79
58
|
private static childProcessFallback;
|
|
80
59
|
private static executeWithPty;
|
|
81
|
-
/**
|
|
82
|
-
* Writes a string to the pseudo-terminal (PTY) of a running process.
|
|
83
|
-
*
|
|
84
|
-
* @param pid The process ID of the target PTY.
|
|
85
|
-
* @param input The string to write to the terminal.
|
|
86
|
-
*/
|
|
87
|
-
static writeToPty(pid: number, input: string): void;
|
|
88
|
-
/**
|
|
89
|
-
* Resizes the pseudo-terminal (PTY) of a running process.
|
|
90
|
-
*
|
|
91
|
-
* @param pid The process ID of the target PTY.
|
|
92
|
-
* @param cols The new number of columns.
|
|
93
|
-
* @param rows The new number of rows.
|
|
94
|
-
*/
|
|
95
|
-
static resizePty(pid: number, cols: number, rows: number): void;
|
|
96
|
-
/**
|
|
97
|
-
* Scrolls the pseudo-terminal (PTY) of a running process.
|
|
98
|
-
*
|
|
99
|
-
* @param pid The process ID of the target PTY.
|
|
100
|
-
* @param lines The number of lines to scroll.
|
|
101
|
-
*/
|
|
102
|
-
static scrollPty(pid: number, lines: number): void;
|
|
103
60
|
}
|
|
@@ -3,44 +3,27 @@
|
|
|
3
3
|
* Copyright 2025 Vybestack LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
import stripAnsi from 'strip-ansi';
|
|
7
6
|
import { getPty } from '../utils/getPty.js';
|
|
8
|
-
import { spawn as cpSpawn } from '
|
|
9
|
-
import { TextDecoder } from '
|
|
10
|
-
import os from '
|
|
7
|
+
import { spawn as cpSpawn } from 'child_process';
|
|
8
|
+
import { TextDecoder } from 'util';
|
|
9
|
+
import os from 'os';
|
|
11
10
|
import { getCachedEncodingForBuffer } from '../utils/systemEncoding.js';
|
|
12
11
|
import { isBinary } from '../utils/textUtils.js';
|
|
13
12
|
import pkg from '@xterm/headless';
|
|
14
|
-
import
|
|
13
|
+
import stripAnsi from 'strip-ansi';
|
|
15
14
|
const { Terminal } = pkg;
|
|
16
15
|
const SIGKILL_TIMEOUT_MS = 200;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const lines = [];
|
|
20
|
-
for (let i = 0; i < terminal.rows; i++) {
|
|
21
|
-
const line = buffer.getLine(buffer.viewportY + i);
|
|
22
|
-
const lineContent = line ? line.translateToString(true) : '';
|
|
23
|
-
lines.push(lineContent);
|
|
24
|
-
}
|
|
25
|
-
return lines.join('\n').trimEnd();
|
|
26
|
-
};
|
|
27
|
-
const getFullBufferText = (terminal) => {
|
|
16
|
+
// @ts-expect-error getFullText is not a public API.
|
|
17
|
+
const getFullText = (terminal) => {
|
|
28
18
|
const buffer = terminal.buffer.active;
|
|
29
19
|
const lines = [];
|
|
30
20
|
for (let i = 0; i < buffer.length; i++) {
|
|
31
21
|
const line = buffer.getLine(i);
|
|
32
|
-
|
|
33
|
-
lines.push(lineContent);
|
|
22
|
+
lines.push(line ? line.translateToString(true) : '');
|
|
34
23
|
}
|
|
35
|
-
return lines.join('\n').
|
|
24
|
+
return lines.join('\n').trim();
|
|
36
25
|
};
|
|
37
|
-
/**
|
|
38
|
-
* A centralized service for executing shell commands with robust process
|
|
39
|
-
* management, cross-platform compatibility, and streaming output capabilities.
|
|
40
|
-
*
|
|
41
|
-
*/
|
|
42
26
|
export class ShellExecutionService {
|
|
43
|
-
static activePtys = new Map();
|
|
44
27
|
/**
|
|
45
28
|
* Executes a shell command using `node-pty`, capturing all output and lifecycle events.
|
|
46
29
|
*
|
|
@@ -53,28 +36,28 @@ export class ShellExecutionService {
|
|
|
53
36
|
* @returns An object containing the process ID (pid) and a promise that
|
|
54
37
|
* resolves with the complete execution result.
|
|
55
38
|
*/
|
|
56
|
-
static async execute(commandToExecute, cwd, onOutputEvent, abortSignal, shouldUseNodePty,
|
|
39
|
+
static async execute(commandToExecute, cwd, onOutputEvent, abortSignal, shouldUseNodePty, terminalColumns, terminalRows) {
|
|
57
40
|
if (shouldUseNodePty) {
|
|
58
41
|
const ptyInfo = await getPty();
|
|
59
42
|
if (ptyInfo) {
|
|
60
43
|
try {
|
|
61
|
-
return this.executeWithPty(commandToExecute, cwd, onOutputEvent, abortSignal,
|
|
44
|
+
return this.executeWithPty(commandToExecute, cwd, onOutputEvent, abortSignal, terminalColumns, terminalRows, ptyInfo);
|
|
62
45
|
}
|
|
63
46
|
catch (_e) {
|
|
64
47
|
// Fallback to child_process
|
|
65
48
|
}
|
|
66
49
|
}
|
|
67
50
|
}
|
|
68
|
-
return this.childProcessFallback(commandToExecute, cwd, onOutputEvent, abortSignal
|
|
51
|
+
return this.childProcessFallback(commandToExecute, cwd, onOutputEvent, abortSignal);
|
|
69
52
|
}
|
|
70
|
-
static childProcessFallback(commandToExecute, cwd, onOutputEvent, abortSignal
|
|
53
|
+
static childProcessFallback(commandToExecute, cwd, onOutputEvent, abortSignal) {
|
|
71
54
|
try {
|
|
72
55
|
const isWindows = os.platform() === 'win32';
|
|
73
56
|
const envVars = {
|
|
74
57
|
...process.env,
|
|
75
58
|
LLXPRT_CODE: '1',
|
|
76
59
|
TERM: 'xterm-256color',
|
|
77
|
-
PAGER:
|
|
60
|
+
PAGER: 'cat',
|
|
78
61
|
};
|
|
79
62
|
delete envVars.BASH_ENV;
|
|
80
63
|
const child = cpSpawn(commandToExecute, [], {
|
|
@@ -114,17 +97,27 @@ export class ShellExecutionService {
|
|
|
114
97
|
sniffedBytes = sniffBuffer.length;
|
|
115
98
|
if (isBinary(sniffBuffer)) {
|
|
116
99
|
isStreamingRawContent = false;
|
|
100
|
+
onOutputEvent({ type: 'binary_detected' });
|
|
117
101
|
}
|
|
118
102
|
}
|
|
103
|
+
const decoder = stream === 'stdout' ? stdoutDecoder : stderrDecoder;
|
|
104
|
+
const decodedChunk = decoder.decode(data, { stream: true });
|
|
105
|
+
const strippedChunk = stripAnsi(decodedChunk);
|
|
106
|
+
if (stream === 'stdout') {
|
|
107
|
+
stdout += strippedChunk;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
stderr += strippedChunk;
|
|
111
|
+
}
|
|
119
112
|
if (isStreamingRawContent) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
113
|
+
onOutputEvent({ type: 'data', chunk: strippedChunk });
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const totalBytes = outputChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
117
|
+
onOutputEvent({
|
|
118
|
+
type: 'binary_progress',
|
|
119
|
+
bytesReceived: totalBytes,
|
|
120
|
+
});
|
|
128
121
|
}
|
|
129
122
|
};
|
|
130
123
|
const handleExit = (code, signal) => {
|
|
@@ -132,25 +125,16 @@ export class ShellExecutionService {
|
|
|
132
125
|
// Ensure we don't add an extra newline if stdout already ends with one.
|
|
133
126
|
const separator = stdout.endsWith('\n') ? '' : '\n';
|
|
134
127
|
const combinedOutput = stdout + (stderr ? (stdout ? separator : '') + stderr : '');
|
|
135
|
-
const finalStrippedOutput = stripAnsi(combinedOutput).trim();
|
|
136
|
-
if (isStreamingRawContent) {
|
|
137
|
-
if (finalStrippedOutput) {
|
|
138
|
-
onOutputEvent({ type: 'data', chunk: finalStrippedOutput });
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
else {
|
|
142
|
-
onOutputEvent({ type: 'binary_detected' });
|
|
143
|
-
}
|
|
144
128
|
resolve({
|
|
145
129
|
rawOutput: finalBuffer,
|
|
146
|
-
output:
|
|
130
|
+
output: combinedOutput.trim(),
|
|
147
131
|
stdout,
|
|
148
132
|
stderr,
|
|
149
133
|
exitCode: code,
|
|
150
134
|
signal: signal ? (os.constants.signals[signal] ?? null) : null,
|
|
151
135
|
error,
|
|
152
136
|
aborted: abortSignal.aborted,
|
|
153
|
-
pid:
|
|
137
|
+
pid: child.pid,
|
|
154
138
|
executionMethod: 'child_process',
|
|
155
139
|
});
|
|
156
140
|
};
|
|
@@ -182,9 +166,6 @@ export class ShellExecutionService {
|
|
|
182
166
|
};
|
|
183
167
|
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
184
168
|
child.on('exit', (code, signal) => {
|
|
185
|
-
if (child.pid) {
|
|
186
|
-
this.activePtys.delete(child.pid);
|
|
187
|
-
}
|
|
188
169
|
handleExit(code, signal);
|
|
189
170
|
});
|
|
190
171
|
function cleanup() {
|
|
@@ -193,20 +174,20 @@ export class ShellExecutionService {
|
|
|
193
174
|
if (stdoutDecoder) {
|
|
194
175
|
const remaining = stdoutDecoder.decode();
|
|
195
176
|
if (remaining) {
|
|
196
|
-
stdout += remaining;
|
|
177
|
+
stdout += stripAnsi(remaining);
|
|
197
178
|
}
|
|
198
179
|
}
|
|
199
180
|
if (stderrDecoder) {
|
|
200
181
|
const remaining = stderrDecoder.decode();
|
|
201
182
|
if (remaining) {
|
|
202
|
-
stderr += remaining;
|
|
183
|
+
stderr += stripAnsi(remaining);
|
|
203
184
|
}
|
|
204
185
|
}
|
|
205
186
|
const finalBuffer = Buffer.concat(outputChunks);
|
|
206
187
|
return { stdout, stderr, finalBuffer };
|
|
207
188
|
}
|
|
208
189
|
});
|
|
209
|
-
return { pid:
|
|
190
|
+
return { pid: child.pid, result };
|
|
210
191
|
}
|
|
211
192
|
catch (e) {
|
|
212
193
|
const error = e;
|
|
@@ -227,14 +208,10 @@ export class ShellExecutionService {
|
|
|
227
208
|
};
|
|
228
209
|
}
|
|
229
210
|
}
|
|
230
|
-
static executeWithPty(commandToExecute, cwd, onOutputEvent, abortSignal,
|
|
231
|
-
if (!ptyInfo) {
|
|
232
|
-
// This should not happen, but as a safeguard...
|
|
233
|
-
throw new Error('PTY implementation not found');
|
|
234
|
-
}
|
|
211
|
+
static executeWithPty(commandToExecute, cwd, onOutputEvent, abortSignal, terminalColumns, terminalRows, ptyInfo) {
|
|
235
212
|
try {
|
|
236
|
-
const cols =
|
|
237
|
-
const rows =
|
|
213
|
+
const cols = terminalColumns ?? 80;
|
|
214
|
+
const rows = terminalRows ?? 30;
|
|
238
215
|
const isWindows = os.platform() === 'win32';
|
|
239
216
|
const shell = isWindows ? 'cmd.exe' : 'bash';
|
|
240
217
|
const args = isWindows
|
|
@@ -243,14 +220,13 @@ export class ShellExecutionService {
|
|
|
243
220
|
const envVars = {
|
|
244
221
|
...process.env,
|
|
245
222
|
LLXPRT_CODE: '1',
|
|
246
|
-
GEMINI_CLI: '1',
|
|
247
223
|
TERM: 'xterm-256color',
|
|
248
|
-
PAGER:
|
|
224
|
+
PAGER: 'cat',
|
|
249
225
|
};
|
|
250
226
|
delete envVars.BASH_ENV;
|
|
251
|
-
const ptyProcess = ptyInfo
|
|
227
|
+
const ptyProcess = ptyInfo?.module.spawn(shell, args, {
|
|
252
228
|
cwd,
|
|
253
|
-
name: 'xterm',
|
|
229
|
+
name: 'xterm-color',
|
|
254
230
|
cols,
|
|
255
231
|
rows,
|
|
256
232
|
env: envVars,
|
|
@@ -262,54 +238,14 @@ export class ShellExecutionService {
|
|
|
262
238
|
cols,
|
|
263
239
|
rows,
|
|
264
240
|
});
|
|
265
|
-
this.activePtys.set(ptyProcess.pid, { ptyProcess, headlessTerminal });
|
|
266
241
|
let processingChain = Promise.resolve();
|
|
267
242
|
let decoder = null;
|
|
268
|
-
let output = null;
|
|
269
243
|
const outputChunks = [];
|
|
270
244
|
const error = null;
|
|
271
245
|
let exited = false;
|
|
272
246
|
let isStreamingRawContent = true;
|
|
273
247
|
const MAX_SNIFF_SIZE = 4096;
|
|
274
248
|
let sniffedBytes = 0;
|
|
275
|
-
let isWriting = false;
|
|
276
|
-
let renderTimeout = null;
|
|
277
|
-
const render = (finalRender = false) => {
|
|
278
|
-
if (renderTimeout) {
|
|
279
|
-
clearTimeout(renderTimeout);
|
|
280
|
-
}
|
|
281
|
-
const renderFn = () => {
|
|
282
|
-
if (!isStreamingRawContent) {
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
const newOutput = shellExecutionConfig.showColor
|
|
286
|
-
? serializeTerminalToObject(headlessTerminal, {
|
|
287
|
-
defaultFg: shellExecutionConfig.defaultFg,
|
|
288
|
-
defaultBg: shellExecutionConfig.defaultBg,
|
|
289
|
-
})
|
|
290
|
-
: getVisibleText(headlessTerminal);
|
|
291
|
-
// console.log(newOutput)
|
|
292
|
-
// Using stringify for a quick deep comparison.
|
|
293
|
-
if (JSON.stringify(output) !== JSON.stringify(newOutput)) {
|
|
294
|
-
output = newOutput;
|
|
295
|
-
onOutputEvent({
|
|
296
|
-
type: 'data',
|
|
297
|
-
chunk: newOutput,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
if (finalRender) {
|
|
302
|
-
renderFn();
|
|
303
|
-
}
|
|
304
|
-
else {
|
|
305
|
-
renderTimeout = setTimeout(renderFn, 17);
|
|
306
|
-
}
|
|
307
|
-
};
|
|
308
|
-
headlessTerminal.onScroll(() => {
|
|
309
|
-
if (!isWriting) {
|
|
310
|
-
render();
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
249
|
const handleOutput = (data) => {
|
|
314
250
|
processingChain = processingChain.then(() => new Promise((resolve) => {
|
|
315
251
|
if (!decoder) {
|
|
@@ -332,10 +268,11 @@ export class ShellExecutionService {
|
|
|
332
268
|
}
|
|
333
269
|
if (isStreamingRawContent) {
|
|
334
270
|
const decodedChunk = decoder.decode(data, { stream: true });
|
|
335
|
-
isWriting = true;
|
|
336
271
|
headlessTerminal.write(decodedChunk, () => {
|
|
337
|
-
|
|
338
|
-
|
|
272
|
+
onOutputEvent({
|
|
273
|
+
type: 'data',
|
|
274
|
+
chunk: stripAnsi(decodedChunk),
|
|
275
|
+
});
|
|
339
276
|
resolve();
|
|
340
277
|
});
|
|
341
278
|
}
|
|
@@ -356,11 +293,9 @@ export class ShellExecutionService {
|
|
|
356
293
|
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
357
294
|
exited = true;
|
|
358
295
|
abortSignal.removeEventListener('abort', abortHandler);
|
|
359
|
-
this.activePtys.delete(ptyProcess.pid);
|
|
360
296
|
processingChain.then(() => {
|
|
361
|
-
render(true);
|
|
362
297
|
const finalBuffer = Buffer.concat(outputChunks);
|
|
363
|
-
const fullOutput =
|
|
298
|
+
const fullOutput = getFullText(headlessTerminal);
|
|
364
299
|
resolve({
|
|
365
300
|
rawOutput: finalBuffer,
|
|
366
301
|
output: fullOutput,
|
|
@@ -371,26 +306,13 @@ export class ShellExecutionService {
|
|
|
371
306
|
error,
|
|
372
307
|
aborted: abortSignal.aborted,
|
|
373
308
|
pid: ptyProcess.pid,
|
|
374
|
-
executionMethod: ptyInfo?.name ??
|
|
375
|
-
'node-pty',
|
|
309
|
+
executionMethod: ptyInfo?.name ?? 'node-pty',
|
|
376
310
|
});
|
|
377
311
|
});
|
|
378
312
|
});
|
|
379
313
|
const abortHandler = async () => {
|
|
380
314
|
if (ptyProcess.pid && !exited) {
|
|
381
|
-
|
|
382
|
-
ptyProcess.kill();
|
|
383
|
-
}
|
|
384
|
-
else {
|
|
385
|
-
try {
|
|
386
|
-
// Kill the entire process group
|
|
387
|
-
process.kill(-ptyProcess.pid, 'SIGINT');
|
|
388
|
-
}
|
|
389
|
-
catch (_e) {
|
|
390
|
-
// Fallback to killing just the process if the group kill fails
|
|
391
|
-
ptyProcess.kill('SIGINT');
|
|
392
|
-
}
|
|
393
|
-
}
|
|
315
|
+
ptyProcess.kill('SIGHUP');
|
|
394
316
|
}
|
|
395
317
|
};
|
|
396
318
|
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
@@ -416,67 +338,5 @@ export class ShellExecutionService {
|
|
|
416
338
|
};
|
|
417
339
|
}
|
|
418
340
|
}
|
|
419
|
-
/**
|
|
420
|
-
* Writes a string to the pseudo-terminal (PTY) of a running process.
|
|
421
|
-
*
|
|
422
|
-
* @param pid The process ID of the target PTY.
|
|
423
|
-
* @param input The string to write to the terminal.
|
|
424
|
-
*/
|
|
425
|
-
static writeToPty(pid, input) {
|
|
426
|
-
const activePty = this.activePtys.get(pid);
|
|
427
|
-
if (activePty) {
|
|
428
|
-
activePty.ptyProcess.write(input);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* Resizes the pseudo-terminal (PTY) of a running process.
|
|
433
|
-
*
|
|
434
|
-
* @param pid The process ID of the target PTY.
|
|
435
|
-
* @param cols The new number of columns.
|
|
436
|
-
* @param rows The new number of rows.
|
|
437
|
-
*/
|
|
438
|
-
static resizePty(pid, cols, rows) {
|
|
439
|
-
const activePty = this.activePtys.get(pid);
|
|
440
|
-
if (activePty) {
|
|
441
|
-
try {
|
|
442
|
-
activePty.ptyProcess.resize(cols, rows);
|
|
443
|
-
activePty.headlessTerminal.resize(cols, rows);
|
|
444
|
-
}
|
|
445
|
-
catch (e) {
|
|
446
|
-
// Ignore errors if the pty has already exited, which can happen
|
|
447
|
-
// due to a race condition between the exit event and this call.
|
|
448
|
-
if (e instanceof Error && 'code' in e && e.code === 'ESRCH') {
|
|
449
|
-
// ignore
|
|
450
|
-
}
|
|
451
|
-
else {
|
|
452
|
-
throw e;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Scrolls the pseudo-terminal (PTY) of a running process.
|
|
459
|
-
*
|
|
460
|
-
* @param pid The process ID of the target PTY.
|
|
461
|
-
* @param lines The number of lines to scroll.
|
|
462
|
-
*/
|
|
463
|
-
static scrollPty(pid, lines) {
|
|
464
|
-
const activePty = this.activePtys.get(pid);
|
|
465
|
-
if (activePty) {
|
|
466
|
-
try {
|
|
467
|
-
activePty.headlessTerminal.scrollLines(lines);
|
|
468
|
-
}
|
|
469
|
-
catch (e) {
|
|
470
|
-
// Ignore errors if the pty has already exited, which can happen
|
|
471
|
-
// due to a race condition between the exit event and this call.
|
|
472
|
-
if (e instanceof Error && 'code' in e && e.code === 'ESRCH') {
|
|
473
|
-
// ignore
|
|
474
|
-
}
|
|
475
|
-
else {
|
|
476
|
-
throw e;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
341
|
}
|
|
482
342
|
//# sourceMappingURL=shellExecutionService.js.map
|