@keyou007/agent-bot 0.1.17-alpha.0 → 0.1.17-alpha.2
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/CHANGELOG.md +14 -0
- package/README.md +3 -1
- package/README.zh.md +3 -1
- package/config.example.yaml +12 -0
- package/dist/codex/CodexProcessManager.js +2 -1
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +22 -2
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/config/loadConfig.js +10 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +6 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +61 -5
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +42 -13
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +35 -12
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/LocalImageMarkdown.js +16 -2
- package/dist/feishu/LocalImageMarkdown.js.map +1 -1
- package/dist/index.js +33 -5
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +706 -0
- package/dist/local-files/LocalFileViewerServer.js.map +1 -0
- package/dist/local-files/NetworkAddressSelector.js +97 -0
- package/dist/local-files/NetworkAddressSelector.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +43 -29
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +15 -2
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/utils/spawnCommand.js +2 -1
- package/dist/utils/spawnCommand.js.map +1 -1
- package/docs/technical-reference.md +8 -0
- package/docs/technical-reference.zh.md +8 -0
- package/npm-shrinkwrap.json +9 -9
- package/package.json +2 -2
- package/skills/agent-bot/SKILL.md +1 -1
- package/src/codex/CodexProcessManager.ts +3 -1
- package/src/codex/CodexRuntime.ts +25 -2
- package/src/config/loadConfig.ts +10 -0
- package/src/config/schema.ts +9 -0
- package/src/feishu/FeishuConnector.ts +70 -5
- package/src/feishu/FeishuMarkdown.ts +51 -12
- package/src/feishu/FeishuTurnPresenter.ts +43 -13
- package/src/feishu/LocalImageMarkdown.ts +23 -2
- package/src/index.ts +33 -4
- package/src/local-files/LocalFileViewerServer.ts +774 -0
- package/src/local-files/NetworkAddressSelector.ts +111 -0
- package/src/proxy/ProxySessionController.ts +33 -20
- package/src/supervision/SafeRestartScheduler.ts +23 -2
- package/src/utils/spawnCommand.ts +2 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
|
|
3
|
+
export type NetworkConnectionKind = "wired" | "wifi" | "other" | "vpn";
|
|
4
|
+
|
|
5
|
+
export interface PreferredNetworkAddress {
|
|
6
|
+
address: string;
|
|
7
|
+
interfaceName: string;
|
|
8
|
+
kind: NetworkConnectionKind;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type NetworkInterfaces = ReturnType<typeof os.networkInterfaces>;
|
|
12
|
+
|
|
13
|
+
const KIND_PRIORITY: Record<NetworkConnectionKind, number> = {
|
|
14
|
+
wired: 0,
|
|
15
|
+
wifi: 1,
|
|
16
|
+
other: 2,
|
|
17
|
+
vpn: 3,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const VIRTUAL_INTERFACE = /(?:loopback|hyper[- ]?v|vEthernet|vmware|virtualbox|docker|wsl|bluetooth|npcap|container|\bbridge\b|^br[-_]|^virbr|^veth|^awdl|^llw)/iu;
|
|
21
|
+
const VPN_INTERFACE = /(?:\bvpn\b|wireguard|tailscale|zerotier|hamachi|nordlynx|openvpn|anyconnect|fortinet|globalprotect|^utun\d*$|^tun\d*$|^tap\d*$|^wg\d*$|^ppp\d*$|^ipsec\d*$)/iu;
|
|
22
|
+
const WIFI_INTERFACE = /(?:wi-?fi|wireless|wlan|airport|无线|^wl[a-z0-9_-]*)/iu;
|
|
23
|
+
const WIRED_INTERFACE = /(?:ethernet|以太网|local area connection|本地连接|\blan\b|^(?:eth|enp|ens|eno|enx)[a-z0-9_-]*)/iu;
|
|
24
|
+
|
|
25
|
+
export function selectPreferredNetworkAddress(
|
|
26
|
+
interfaces: NetworkInterfaces = os.networkInterfaces(),
|
|
27
|
+
): PreferredNetworkAddress | undefined {
|
|
28
|
+
const candidates: Array<PreferredNetworkAddress & { privateAddress: boolean }> = [];
|
|
29
|
+
|
|
30
|
+
for (const [interfaceName, addresses] of Object.entries(interfaces)) {
|
|
31
|
+
if (!addresses || isExcludedInterface(interfaceName)) continue;
|
|
32
|
+
const kind = classifyInterface(interfaceName);
|
|
33
|
+
for (const address of addresses) {
|
|
34
|
+
if (address.internal || address.family !== "IPv4" || !isUsableIpv4(address.address)) continue;
|
|
35
|
+
candidates.push({
|
|
36
|
+
address: address.address,
|
|
37
|
+
interfaceName,
|
|
38
|
+
kind,
|
|
39
|
+
privateAddress: isPrivateIpv4(address.address),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
candidates.sort((left, right) => {
|
|
45
|
+
const kindDifference = KIND_PRIORITY[left.kind] - KIND_PRIORITY[right.kind];
|
|
46
|
+
if (kindDifference !== 0) return kindDifference;
|
|
47
|
+
if (left.privateAddress !== right.privateAddress) return left.privateAddress ? -1 : 1;
|
|
48
|
+
const interfaceDifference = compareText(left.interfaceName, right.interfaceName);
|
|
49
|
+
return interfaceDifference !== 0 ? interfaceDifference : compareIpv4(left.address, right.address);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const selected = candidates[0];
|
|
53
|
+
return selected
|
|
54
|
+
? { address: selected.address, interfaceName: selected.interfaceName, kind: selected.kind }
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function classifyInterface(interfaceName: string): NetworkConnectionKind {
|
|
59
|
+
if (VPN_INTERFACE.test(interfaceName)) return "vpn";
|
|
60
|
+
if (WIFI_INTERFACE.test(interfaceName)) return "wifi";
|
|
61
|
+
if (WIRED_INTERFACE.test(interfaceName)) return "wired";
|
|
62
|
+
return "other";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isExcludedInterface(interfaceName: string): boolean {
|
|
66
|
+
return /^lo\d*$/iu.test(interfaceName) || VIRTUAL_INTERFACE.test(interfaceName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isUsableIpv4(address: string): boolean {
|
|
70
|
+
const parts = ipv4Parts(address);
|
|
71
|
+
if (!parts) return false;
|
|
72
|
+
const [first, second] = parts;
|
|
73
|
+
return first !== 0
|
|
74
|
+
&& first !== 127
|
|
75
|
+
&& first < 224
|
|
76
|
+
&& !(first === 169 && second === 254);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isPrivateIpv4(address: string): boolean {
|
|
80
|
+
const parts = ipv4Parts(address);
|
|
81
|
+
if (!parts) return false;
|
|
82
|
+
const [first, second] = parts;
|
|
83
|
+
return first === 10
|
|
84
|
+
|| (first === 172 && second >= 16 && second <= 31)
|
|
85
|
+
|| (first === 192 && second === 168)
|
|
86
|
+
|| (first === 100 && second >= 64 && second <= 127);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ipv4Parts(address: string): [number, number, number, number] | undefined {
|
|
90
|
+
const values = address.split(".").map((part) => Number(part));
|
|
91
|
+
if (values.length !== 4 || values.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
return values as [number, number, number, number];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function compareText(left: string, right: string): number {
|
|
98
|
+
const normalizedLeft = left.toLocaleLowerCase("en-US");
|
|
99
|
+
const normalizedRight = right.toLocaleLowerCase("en-US");
|
|
100
|
+
return normalizedLeft < normalizedRight ? -1 : normalizedLeft > normalizedRight ? 1 : 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function compareIpv4(left: string, right: string): number {
|
|
104
|
+
const leftParts = ipv4Parts(left) ?? [0, 0, 0, 0];
|
|
105
|
+
const rightParts = ipv4Parts(right) ?? [0, 0, 0, 0];
|
|
106
|
+
for (let index = 0; index < leftParts.length; index += 1) {
|
|
107
|
+
const difference = leftParts[index]! - rightParts[index]!;
|
|
108
|
+
if (difference !== 0) return difference;
|
|
109
|
+
}
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
} from "../feishu/GroupNameFormatter.js";
|
|
42
42
|
import { normalizeFeishuPostText } from "../feishu/InboundText.js";
|
|
43
43
|
import { allowsFeishuUser } from "../feishu/ownerAccess.js";
|
|
44
|
+
import { classifyFileContent } from "../local-files/LocalFileViewerServer.js";
|
|
44
45
|
import type { OutboundRouter } from "../presentation/OutboundRouter.js";
|
|
45
46
|
import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.js";
|
|
46
47
|
import { buildTurnGraphRows } from "../presentation/turnGraph.js";
|
|
@@ -131,17 +132,12 @@ const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
|
131
132
|
const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
132
133
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
133
134
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
135
|
+
const TURN_STOP_REQUESTED_MESSAGE = "正在停止当前任务,请稍候。状态卡片将在任务停止后自动更新。";
|
|
134
136
|
const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
|
|
135
137
|
const WINDOWS_DRIVES_DIRECTORY = "agentbot://windows-drives";
|
|
136
138
|
const IMAGE_FILE_EXTENSIONS = new Set([
|
|
137
139
|
".avif", ".bmp", ".gif", ".heic", ".heif", ".ico", ".jpeg", ".jpg", ".png", ".svg", ".tif", ".tiff", ".webp",
|
|
138
140
|
]);
|
|
139
|
-
const BINARY_FILE_EXTENSIONS = new Set([
|
|
140
|
-
".7z", ".a", ".apk", ".avi", ".bin", ".bz2", ".class", ".dat", ".db", ".deb", ".dll", ".dmg", ".doc", ".docx",
|
|
141
|
-
".dylib", ".eot", ".exe", ".flac", ".gz", ".ico", ".ipa", ".iso", ".jar", ".lib", ".mdb", ".mkv", ".mov", ".mp3",
|
|
142
|
-
".mp4", ".msi", ".o", ".obj", ".ogg", ".otf", ".pdf", ".ppt", ".pptx", ".rar", ".rpm", ".so", ".sqlite", ".tar",
|
|
143
|
-
".ttf", ".wav", ".webm", ".woff", ".woff2", ".xls", ".xlsx", ".xz", ".zip", ".zst",
|
|
144
|
-
]);
|
|
145
141
|
|
|
146
142
|
interface HelpCommandDefinition {
|
|
147
143
|
command: string;
|
|
@@ -1675,9 +1671,10 @@ export class ProxySessionController {
|
|
|
1675
1671
|
await this.assertBrowsableDirectory(directory);
|
|
1676
1672
|
const entries = (await fs.readdir(directory, { withFileTypes: true }))
|
|
1677
1673
|
.map((entry) => ({
|
|
1674
|
+
dirent: entry,
|
|
1678
1675
|
name: entry.name,
|
|
1679
1676
|
path: path.join(directory, entry.name),
|
|
1680
|
-
kind:
|
|
1677
|
+
kind: entry.isDirectory() ? "directory" as const : "file" as const,
|
|
1681
1678
|
}))
|
|
1682
1679
|
.sort((left, right) => Number(right.kind === "directory") - Number(left.kind === "directory")
|
|
1683
1680
|
|| left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: "base" }));
|
|
@@ -1690,7 +1687,15 @@ export class ProxySessionController {
|
|
|
1690
1687
|
page,
|
|
1691
1688
|
totalPages,
|
|
1692
1689
|
totalEntries: entries.length,
|
|
1693
|
-
entries: entries
|
|
1690
|
+
entries: entries
|
|
1691
|
+
.slice(page * DIRECTORY_PAGE_SIZE, (page + 1) * DIRECTORY_PAGE_SIZE)
|
|
1692
|
+
.map((entry) => ({
|
|
1693
|
+
name: entry.name,
|
|
1694
|
+
path: entry.path,
|
|
1695
|
+
kind: entry.kind === "directory"
|
|
1696
|
+
? "directory" as const
|
|
1697
|
+
: directoryBrowserEntryKind(entry.dirent, entry.path),
|
|
1698
|
+
})),
|
|
1694
1699
|
};
|
|
1695
1700
|
}
|
|
1696
1701
|
|
|
@@ -4247,7 +4252,7 @@ export class ProxySessionController {
|
|
|
4247
4252
|
remoteSessionId: record.remoteSessionId,
|
|
4248
4253
|
turnId,
|
|
4249
4254
|
});
|
|
4250
|
-
await this.outbound.sendText(record.contextKey,
|
|
4255
|
+
await this.outbound.sendText(record.contextKey, TURN_STOP_REQUESTED_MESSAGE);
|
|
4251
4256
|
return;
|
|
4252
4257
|
}
|
|
4253
4258
|
|
|
@@ -5347,9 +5352,10 @@ export class ProxySessionController {
|
|
|
5347
5352
|
}
|
|
5348
5353
|
const entries = directoryEntries
|
|
5349
5354
|
.map((entry) => ({
|
|
5355
|
+
dirent: entry,
|
|
5350
5356
|
name: entry.name,
|
|
5351
5357
|
path: path.join(directory, entry.name),
|
|
5352
|
-
kind:
|
|
5358
|
+
kind: entry.isDirectory() ? "directory" as const : "file" as const,
|
|
5353
5359
|
}))
|
|
5354
5360
|
.sort((left, right) => Number(right.kind === "directory") - Number(left.kind === "directory")
|
|
5355
5361
|
|| left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: "base" }));
|
|
@@ -5377,8 +5383,9 @@ export class ProxySessionController {
|
|
|
5377
5383
|
...parentEntries,
|
|
5378
5384
|
...entries
|
|
5379
5385
|
.slice(offset, offset + pageSize)
|
|
5380
|
-
.map((entry): DirectoryBrowserCardEntry =>
|
|
5381
|
-
|
|
5386
|
+
.map((entry): DirectoryBrowserCardEntry => {
|
|
5387
|
+
if (entry.kind === "directory") {
|
|
5388
|
+
return {
|
|
5382
5389
|
name: entry.name,
|
|
5383
5390
|
kind: entry.kind,
|
|
5384
5391
|
openAction: {
|
|
@@ -5389,10 +5396,11 @@ export class ProxySessionController {
|
|
|
5389
5396
|
contextKey,
|
|
5390
5397
|
},
|
|
5391
5398
|
},
|
|
5392
|
-
}
|
|
5393
|
-
|
|
5399
|
+
};
|
|
5400
|
+
}
|
|
5401
|
+
return {
|
|
5394
5402
|
name: entry.name,
|
|
5395
|
-
kind: entry.
|
|
5403
|
+
kind: directoryBrowserEntryKind(entry.dirent, entry.path),
|
|
5396
5404
|
openAction: {
|
|
5397
5405
|
text: entry.name,
|
|
5398
5406
|
value: {
|
|
@@ -5401,7 +5409,8 @@ export class ProxySessionController {
|
|
|
5401
5409
|
contextKey,
|
|
5402
5410
|
},
|
|
5403
5411
|
},
|
|
5404
|
-
}
|
|
5412
|
+
};
|
|
5413
|
+
}),
|
|
5405
5414
|
];
|
|
5406
5415
|
const navigationActions = directoryPaginationActions(directory, page, totalPages, contextKey);
|
|
5407
5416
|
const directoryCount = entries.filter((entry) => entry.kind === "directory").length;
|
|
@@ -5903,7 +5912,7 @@ export class ProxySessionController {
|
|
|
5903
5912
|
turnId,
|
|
5904
5913
|
source: "sessions_card",
|
|
5905
5914
|
});
|
|
5906
|
-
await this.outbound.sendText(contextKey,
|
|
5915
|
+
await this.outbound.sendText(contextKey, TURN_STOP_REQUESTED_MESSAGE);
|
|
5907
5916
|
}
|
|
5908
5917
|
|
|
5909
5918
|
private async archiveSessionReference(
|
|
@@ -7114,12 +7123,16 @@ function isWindowsDriveRoot(directory: string): boolean {
|
|
|
7114
7123
|
return /^[a-z]:\\$/iu.test(path.win32.normalize(directory));
|
|
7115
7124
|
}
|
|
7116
7125
|
|
|
7117
|
-
function directoryBrowserEntryKind(entry: Dirent): DirectoryBrowserCardEntry["kind"] {
|
|
7126
|
+
function directoryBrowserEntryKind(entry: Dirent, filePath: string): DirectoryBrowserCardEntry["kind"] {
|
|
7118
7127
|
if (entry.isDirectory()) return "directory";
|
|
7128
|
+
try {
|
|
7129
|
+
if (classifyFileContent(filePath).kind === "text") return "file";
|
|
7130
|
+
} catch {
|
|
7131
|
+
return "file";
|
|
7132
|
+
}
|
|
7119
7133
|
const extension = path.extname(entry.name).toLowerCase();
|
|
7120
7134
|
if (IMAGE_FILE_EXTENSIONS.has(extension)) return "image";
|
|
7121
|
-
|
|
7122
|
-
return "file";
|
|
7135
|
+
return "binary";
|
|
7123
7136
|
}
|
|
7124
7137
|
|
|
7125
7138
|
function directoryActionPath(value: unknown): string {
|
|
@@ -35,6 +35,10 @@ export interface SafeRestartSchedulerOptions {
|
|
|
35
35
|
pollIntervalMs?: number;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export interface SafeRestartScheduleOptions {
|
|
39
|
+
restartImmediatelyIfIdle?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
export class SafeRestartScheduler {
|
|
39
43
|
private timer?: NodeJS.Timeout;
|
|
40
44
|
private polling?: Promise<void>;
|
|
@@ -44,6 +48,7 @@ export class SafeRestartScheduler {
|
|
|
44
48
|
private idleSince?: number;
|
|
45
49
|
private idleInboundAt?: string;
|
|
46
50
|
private scheduleId = 0;
|
|
51
|
+
private restartImmediatelyIfIdle = false;
|
|
47
52
|
|
|
48
53
|
constructor(private readonly options: SafeRestartSchedulerOptions) {}
|
|
49
54
|
|
|
@@ -55,10 +60,17 @@ export class SafeRestartScheduler {
|
|
|
55
60
|
return this.reason;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
schedule(
|
|
63
|
+
schedule(
|
|
64
|
+
reason: string,
|
|
65
|
+
notificationTarget?: RestartNotificationTarget,
|
|
66
|
+
options: SafeRestartScheduleOptions = {},
|
|
67
|
+
): boolean {
|
|
59
68
|
const newlyScheduled = !this.reason;
|
|
60
69
|
if (newlyScheduled) {
|
|
61
70
|
this.notificationTargets = [];
|
|
71
|
+
this.restartImmediatelyIfIdle = options.restartImmediatelyIfIdle === true;
|
|
72
|
+
} else if (options.restartImmediatelyIfIdle) {
|
|
73
|
+
this.restartImmediatelyIfIdle = true;
|
|
62
74
|
}
|
|
63
75
|
this.scheduleId += 1;
|
|
64
76
|
this.addNotificationTarget(notificationTarget, reason);
|
|
@@ -104,6 +116,7 @@ export class SafeRestartScheduler {
|
|
|
104
116
|
this.notificationTargets = [];
|
|
105
117
|
this.idleSince = undefined;
|
|
106
118
|
this.idleInboundAt = undefined;
|
|
119
|
+
this.restartImmediatelyIfIdle = false;
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
private async poll(): Promise<void> {
|
|
@@ -123,6 +136,10 @@ export class SafeRestartScheduler {
|
|
|
123
136
|
});
|
|
124
137
|
return;
|
|
125
138
|
}
|
|
139
|
+
if (this.restartImmediatelyIfIdle) {
|
|
140
|
+
await this.restart(reason, scheduleId, state);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
126
143
|
const quietPeriodMs = this.options.quietPeriodMs ?? 15_000;
|
|
127
144
|
const latestInboundAt = state.latestInboundAt ?? "";
|
|
128
145
|
if (this.idleSince === undefined || this.idleInboundAt !== latestInboundAt) {
|
|
@@ -178,6 +195,10 @@ export class SafeRestartScheduler {
|
|
|
178
195
|
}
|
|
179
196
|
return;
|
|
180
197
|
}
|
|
198
|
+
await this.restart(reason, scheduleId, confirmed);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async restart(reason: string, scheduleId: number, activity: ServerActivityState): Promise<void> {
|
|
181
202
|
const notificationTargets = this.notificationTargetsSnapshot();
|
|
182
203
|
this.clear();
|
|
183
204
|
await this.emitStatus({
|
|
@@ -185,7 +206,7 @@ export class SafeRestartScheduler {
|
|
|
185
206
|
reason,
|
|
186
207
|
notificationTargets,
|
|
187
208
|
phase: "restarting",
|
|
188
|
-
activity
|
|
209
|
+
activity,
|
|
189
210
|
remainingMs: 0,
|
|
190
211
|
});
|
|
191
212
|
await this.options.onReady(reason, notificationTargets);
|
|
@@ -20,9 +20,11 @@ export function spawnStdioCommand(
|
|
|
20
20
|
command: string,
|
|
21
21
|
args: string[],
|
|
22
22
|
env: NodeJS.ProcessEnv,
|
|
23
|
+
cwd?: string,
|
|
23
24
|
): ChildProcessWithoutNullStreams {
|
|
24
25
|
const prepared = prepareCommand(command, args);
|
|
25
26
|
return spawn(prepared.command, prepared.args, {
|
|
27
|
+
cwd,
|
|
26
28
|
env,
|
|
27
29
|
stdio: ["pipe", "pipe", "pipe"],
|
|
28
30
|
windowsHide: true,
|