@mariozechner/pi-coding-agent 0.13.0 → 0.13.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 +18 -0
- package/README.md +25 -1
- package/dist/settings-manager.d.ts +3 -0
- package/dist/settings-manager.d.ts.map +1 -1
- package/dist/settings-manager.js +7 -0
- package/dist/settings-manager.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +143 -56
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +49 -28
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +37 -9
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +28 -10
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +52 -31
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/truncate.d.ts +66 -0
- package/dist/tools/truncate.d.ts.map +1 -0
- package/dist/tools/truncate.js +195 -0
- package/dist/tools/truncate.js.map +1 -0
- package/dist/tui/tool-execution.d.ts.map +1 -1
- package/dist/tui/tool-execution.js +81 -5
- package/dist/tui/tool-execution.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.13.2] - 2025-12-07
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **Tool output truncation**: All tools now enforce consistent truncation limits with actionable notices for the LLM. ([#134](https://github.com/badlogic/pi-mono/issues/134))
|
|
10
|
+
- **Limits**: 2000 lines OR 50KB (whichever hits first), never partial lines
|
|
11
|
+
- **read**: Shows `[Showing lines X-Y of Z. Use offset=N to continue]`. If first line exceeds 50KB, suggests bash command
|
|
12
|
+
- **bash**: Tail truncation with temp file. Shows `[Showing lines X-Y of Z. Full output: /tmp/...]`
|
|
13
|
+
- **grep**: Pre-truncates match lines to 500 chars. Shows match limit and line truncation notices
|
|
14
|
+
- **find/ls**: Shows result/entry limit notices
|
|
15
|
+
- TUI displays truncation warnings in yellow at bottom of tool output (visible even when collapsed)
|
|
16
|
+
|
|
17
|
+
## [0.13.1] - 2025-12-06
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Flexible Windows shell configuration**: The bash tool now supports multiple shell sources beyond Git Bash. Resolution order: (1) custom `shellPath` in settings.json, (2) Git Bash in standard locations, (3) any bash.exe on PATH. This enables Cygwin, MSYS2, and other bash environments. Configure with `~/.pi/agent/settings.json`: `{"shellPath": "C:\\cygwin64\\bin\\bash.exe"}`.
|
|
22
|
+
|
|
5
23
|
### Fixed
|
|
6
24
|
|
|
7
25
|
- **Windows binary detection**: Fixed Bun compiled binary detection on Windows by checking for URL-encoded `%7EBUN` in addition to `$bunfs` and `~BUN` in `import.meta.url`. This ensures the binary correctly locates supporting files (package.json, themes, etc.) next to the executable.
|
package/README.md
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
A radically simple and opinionated coding agent with multi-model support (including mid-session switching), a simple yet powerful CLI for headless coding tasks, and many creature comforts you might be used to from other coding agents.
|
|
4
4
|
|
|
5
|
-
Works on Linux, macOS, and Windows (
|
|
5
|
+
Works on Linux, macOS, and Windows (needs a bash shell, see [Windows Shell Configuration](#windows-shell-configuration)).
|
|
6
6
|
|
|
7
7
|
## Table of Contents
|
|
8
8
|
|
|
9
9
|
- [Installation](#installation)
|
|
10
|
+
- [Windows Shell Configuration](#windows-shell-configuration)
|
|
10
11
|
- [Quick Start](#quick-start)
|
|
11
12
|
- [API Keys](#api-keys)
|
|
12
13
|
- [OAuth Authentication (Optional)](#oauth-authentication-optional)
|
|
@@ -81,6 +82,29 @@ npm run build:binary
|
|
|
81
82
|
./dist/pi
|
|
82
83
|
```
|
|
83
84
|
|
|
85
|
+
## Windows Shell Configuration
|
|
86
|
+
|
|
87
|
+
On Windows, pi requires a bash shell. The following locations are checked in order:
|
|
88
|
+
|
|
89
|
+
1. **Custom shell path** from `~/.pi/agent/settings.json` (if configured)
|
|
90
|
+
2. **Git Bash** in standard locations (`C:\Program Files\Git\bin\bash.exe`)
|
|
91
|
+
3. **bash.exe on PATH** (Cygwin, MSYS2, WSL, etc.)
|
|
92
|
+
|
|
93
|
+
For most users, installing [Git for Windows](https://git-scm.com/download/win) is sufficient.
|
|
94
|
+
|
|
95
|
+
### Custom Shell Path
|
|
96
|
+
|
|
97
|
+
If you use Cygwin, MSYS2, or have bash in a non-standard location, add the path to your settings:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
// ~/.pi/agent/settings.json
|
|
101
|
+
{
|
|
102
|
+
"shellPath": "C:\\cygwin64\\bin\\bash.exe"
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Alternatively, ensure your bash is on the system PATH.
|
|
107
|
+
|
|
84
108
|
## Quick Start
|
|
85
109
|
|
|
86
110
|
```bash
|
|
@@ -12,6 +12,7 @@ export interface Settings {
|
|
|
12
12
|
theme?: string;
|
|
13
13
|
compaction?: CompactionSettings;
|
|
14
14
|
hideThinkingBlock?: boolean;
|
|
15
|
+
shellPath?: string;
|
|
15
16
|
}
|
|
16
17
|
export declare class SettingsManager {
|
|
17
18
|
private settingsPath;
|
|
@@ -43,5 +44,7 @@ export declare class SettingsManager {
|
|
|
43
44
|
};
|
|
44
45
|
getHideThinkingBlock(): boolean;
|
|
45
46
|
setHideThinkingBlock(hide: boolean): void;
|
|
47
|
+
getShellPath(): string | undefined;
|
|
48
|
+
setShellPath(path: string | undefined): void;
|
|
46
49
|
}
|
|
47
50
|
//# sourceMappingURL=settings-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-manager.d.ts","sourceRoot":"","sources":["../src/settings-manager.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,kBAAkB;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,QAAQ;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrE,SAAS,CAAC,EAAE,KAAK,GAAG,eAAe,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC,iBAAiB,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"settings-manager.d.ts","sourceRoot":"","sources":["../src/settings-manager.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,kBAAkB;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,QAAQ;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrE,SAAS,CAAC,EAAE,KAAK,GAAG,eAAe,CAAC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,eAAe;IAC3B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAW;IAE3B,YAAY,OAAO,CAAC,EAAE,MAAM,EAI3B;IAED,OAAO,CAAC,IAAI;IAcZ,OAAO,CAAC,IAAI;IAcZ,uBAAuB,IAAI,MAAM,GAAG,SAAS,CAE5C;IAED,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAG7C;IAED,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAEvC;IAED,eAAe,IAAI,MAAM,GAAG,SAAS,CAEpC;IAED,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAGzC;IAED,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGrC;IAED,0BAA0B,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAIlE;IAED,YAAY,IAAI,KAAK,GAAG,eAAe,CAEtC;IAED,YAAY,CAAC,IAAI,EAAE,KAAK,GAAG,eAAe,GAAG,IAAI,CAGhD;IAED,QAAQ,IAAI,MAAM,GAAG,SAAS,CAE7B;IAED,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAG5B;IAED,uBAAuB,IAAI,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAEnF;IAED,uBAAuB,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,IAAI,CAGlF;IAED,oBAAoB,IAAI,OAAO,CAE9B;IAED,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAM3C;IAED,0BAA0B,IAAI,MAAM,CAEnC;IAED,6BAA6B,IAAI,MAAM,CAEtC;IAED,qBAAqB,IAAI;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,CAM7F;IAED,oBAAoB,IAAI,OAAO,CAE9B;IAED,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAGxC;IAED,YAAY,IAAI,MAAM,GAAG,SAAS,CAEjC;IAED,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAG3C;CACD","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport { getAgentDir } from \"./config.js\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean; // default: true\n\treserveTokens?: number; // default: 16384\n\tkeepRecentTokens?: number; // default: 20000\n}\n\nexport interface Settings {\n\tlastChangelogVersion?: string;\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\";\n\tqueueMode?: \"all\" | \"one-at-a-time\";\n\ttheme?: string;\n\tcompaction?: CompactionSettings;\n\thideThinkingBlock?: boolean;\n\tshellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)\n}\n\nexport class SettingsManager {\n\tprivate settingsPath: string;\n\tprivate settings: Settings;\n\n\tconstructor(baseDir?: string) {\n\t\tconst dir = baseDir || getAgentDir();\n\t\tthis.settingsPath = join(dir, \"settings.json\");\n\t\tthis.settings = this.load();\n\t}\n\n\tprivate load(): Settings {\n\t\tif (!existsSync(this.settingsPath)) {\n\t\t\treturn {};\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(this.settingsPath, \"utf-8\");\n\t\t\treturn JSON.parse(content);\n\t\t} catch (error) {\n\t\t\tconsole.error(`Warning: Could not read settings file: ${error}`);\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tprivate save(): void {\n\t\ttry {\n\t\t\t// Ensure directory exists\n\t\t\tconst dir = dirname(this.settingsPath);\n\t\t\tif (!existsSync(dir)) {\n\t\t\t\tmkdirSync(dir, { recursive: true });\n\t\t\t}\n\n\t\t\twriteFileSync(this.settingsPath, JSON.stringify(this.settings, null, 2), \"utf-8\");\n\t\t} catch (error) {\n\t\t\tconsole.error(`Warning: Could not save settings file: ${error}`);\n\t\t}\n\t}\n\n\tgetLastChangelogVersion(): string | undefined {\n\t\treturn this.settings.lastChangelogVersion;\n\t}\n\n\tsetLastChangelogVersion(version: string): void {\n\t\tthis.settings.lastChangelogVersion = version;\n\t\tthis.save();\n\t}\n\n\tgetDefaultProvider(): string | undefined {\n\t\treturn this.settings.defaultProvider;\n\t}\n\n\tgetDefaultModel(): string | undefined {\n\t\treturn this.settings.defaultModel;\n\t}\n\n\tsetDefaultProvider(provider: string): void {\n\t\tthis.settings.defaultProvider = provider;\n\t\tthis.save();\n\t}\n\n\tsetDefaultModel(modelId: string): void {\n\t\tthis.settings.defaultModel = modelId;\n\t\tthis.save();\n\t}\n\n\tsetDefaultModelAndProvider(provider: string, modelId: string): void {\n\t\tthis.settings.defaultProvider = provider;\n\t\tthis.settings.defaultModel = modelId;\n\t\tthis.save();\n\t}\n\n\tgetQueueMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.settings.queueMode || \"one-at-a-time\";\n\t}\n\n\tsetQueueMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.settings.queueMode = mode;\n\t\tthis.save();\n\t}\n\n\tgetTheme(): string | undefined {\n\t\treturn this.settings.theme;\n\t}\n\n\tsetTheme(theme: string): void {\n\t\tthis.settings.theme = theme;\n\t\tthis.save();\n\t}\n\n\tgetDefaultThinkingLevel(): \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | undefined {\n\t\treturn this.settings.defaultThinkingLevel;\n\t}\n\n\tsetDefaultThinkingLevel(level: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\"): void {\n\t\tthis.settings.defaultThinkingLevel = level;\n\t\tthis.save();\n\t}\n\n\tgetCompactionEnabled(): boolean {\n\t\treturn this.settings.compaction?.enabled ?? true;\n\t}\n\n\tsetCompactionEnabled(enabled: boolean): void {\n\t\tif (!this.settings.compaction) {\n\t\t\tthis.settings.compaction = {};\n\t\t}\n\t\tthis.settings.compaction.enabled = enabled;\n\t\tthis.save();\n\t}\n\n\tgetCompactionReserveTokens(): number {\n\t\treturn this.settings.compaction?.reserveTokens ?? 16384;\n\t}\n\n\tgetCompactionKeepRecentTokens(): number {\n\t\treturn this.settings.compaction?.keepRecentTokens ?? 20000;\n\t}\n\n\tgetCompactionSettings(): { enabled: boolean; reserveTokens: number; keepRecentTokens: number } {\n\t\treturn {\n\t\t\tenabled: this.getCompactionEnabled(),\n\t\t\treserveTokens: this.getCompactionReserveTokens(),\n\t\t\tkeepRecentTokens: this.getCompactionKeepRecentTokens(),\n\t\t};\n\t}\n\n\tgetHideThinkingBlock(): boolean {\n\t\treturn this.settings.hideThinkingBlock ?? false;\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.settings.hideThinkingBlock = hide;\n\t\tthis.save();\n\t}\n\n\tgetShellPath(): string | undefined {\n\t\treturn this.settings.shellPath;\n\t}\n\n\tsetShellPath(path: string | undefined): void {\n\t\tthis.settings.shellPath = path;\n\t\tthis.save();\n\t}\n}\n"]}
|
package/dist/settings-manager.js
CHANGED
|
@@ -112,5 +112,12 @@ export class SettingsManager {
|
|
|
112
112
|
this.settings.hideThinkingBlock = hide;
|
|
113
113
|
this.save();
|
|
114
114
|
}
|
|
115
|
+
getShellPath() {
|
|
116
|
+
return this.settings.shellPath;
|
|
117
|
+
}
|
|
118
|
+
setShellPath(path) {
|
|
119
|
+
this.settings.shellPath = path;
|
|
120
|
+
this.save();
|
|
121
|
+
}
|
|
115
122
|
}
|
|
116
123
|
//# sourceMappingURL=settings-manager.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings-manager.js","sourceRoot":"","sources":["../src/settings-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"settings-manager.js","sourceRoot":"","sources":["../src/settings-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport { getAgentDir } from \"./config.js\";\n\nexport interface CompactionSettings {\n\tenabled?: boolean; // default: true\n\treserveTokens?: number; // default: 16384\n\tkeepRecentTokens?: number; // default: 20000\n}\n\nexport interface Settings {\n\tlastChangelogVersion?: string;\n\tdefaultProvider?: string;\n\tdefaultModel?: string;\n\tdefaultThinkingLevel?: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\";\n\tqueueMode?: \"all\" | \"one-at-a-time\";\n\ttheme?: string;\n\tcompaction?: CompactionSettings;\n\thideThinkingBlock?: boolean;\n\tshellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)\n}\n\nexport class SettingsManager {\n\tprivate settingsPath: string;\n\tprivate settings: Settings;\n\n\tconstructor(baseDir?: string) {\n\t\tconst dir = baseDir || getAgentDir();\n\t\tthis.settingsPath = join(dir, \"settings.json\");\n\t\tthis.settings = this.load();\n\t}\n\n\tprivate load(): Settings {\n\t\tif (!existsSync(this.settingsPath)) {\n\t\t\treturn {};\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(this.settingsPath, \"utf-8\");\n\t\t\treturn JSON.parse(content);\n\t\t} catch (error) {\n\t\t\tconsole.error(`Warning: Could not read settings file: ${error}`);\n\t\t\treturn {};\n\t\t}\n\t}\n\n\tprivate save(): void {\n\t\ttry {\n\t\t\t// Ensure directory exists\n\t\t\tconst dir = dirname(this.settingsPath);\n\t\t\tif (!existsSync(dir)) {\n\t\t\t\tmkdirSync(dir, { recursive: true });\n\t\t\t}\n\n\t\t\twriteFileSync(this.settingsPath, JSON.stringify(this.settings, null, 2), \"utf-8\");\n\t\t} catch (error) {\n\t\t\tconsole.error(`Warning: Could not save settings file: ${error}`);\n\t\t}\n\t}\n\n\tgetLastChangelogVersion(): string | undefined {\n\t\treturn this.settings.lastChangelogVersion;\n\t}\n\n\tsetLastChangelogVersion(version: string): void {\n\t\tthis.settings.lastChangelogVersion = version;\n\t\tthis.save();\n\t}\n\n\tgetDefaultProvider(): string | undefined {\n\t\treturn this.settings.defaultProvider;\n\t}\n\n\tgetDefaultModel(): string | undefined {\n\t\treturn this.settings.defaultModel;\n\t}\n\n\tsetDefaultProvider(provider: string): void {\n\t\tthis.settings.defaultProvider = provider;\n\t\tthis.save();\n\t}\n\n\tsetDefaultModel(modelId: string): void {\n\t\tthis.settings.defaultModel = modelId;\n\t\tthis.save();\n\t}\n\n\tsetDefaultModelAndProvider(provider: string, modelId: string): void {\n\t\tthis.settings.defaultProvider = provider;\n\t\tthis.settings.defaultModel = modelId;\n\t\tthis.save();\n\t}\n\n\tgetQueueMode(): \"all\" | \"one-at-a-time\" {\n\t\treturn this.settings.queueMode || \"one-at-a-time\";\n\t}\n\n\tsetQueueMode(mode: \"all\" | \"one-at-a-time\"): void {\n\t\tthis.settings.queueMode = mode;\n\t\tthis.save();\n\t}\n\n\tgetTheme(): string | undefined {\n\t\treturn this.settings.theme;\n\t}\n\n\tsetTheme(theme: string): void {\n\t\tthis.settings.theme = theme;\n\t\tthis.save();\n\t}\n\n\tgetDefaultThinkingLevel(): \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | undefined {\n\t\treturn this.settings.defaultThinkingLevel;\n\t}\n\n\tsetDefaultThinkingLevel(level: \"off\" | \"minimal\" | \"low\" | \"medium\" | \"high\"): void {\n\t\tthis.settings.defaultThinkingLevel = level;\n\t\tthis.save();\n\t}\n\n\tgetCompactionEnabled(): boolean {\n\t\treturn this.settings.compaction?.enabled ?? true;\n\t}\n\n\tsetCompactionEnabled(enabled: boolean): void {\n\t\tif (!this.settings.compaction) {\n\t\t\tthis.settings.compaction = {};\n\t\t}\n\t\tthis.settings.compaction.enabled = enabled;\n\t\tthis.save();\n\t}\n\n\tgetCompactionReserveTokens(): number {\n\t\treturn this.settings.compaction?.reserveTokens ?? 16384;\n\t}\n\n\tgetCompactionKeepRecentTokens(): number {\n\t\treturn this.settings.compaction?.keepRecentTokens ?? 20000;\n\t}\n\n\tgetCompactionSettings(): { enabled: boolean; reserveTokens: number; keepRecentTokens: number } {\n\t\treturn {\n\t\t\tenabled: this.getCompactionEnabled(),\n\t\t\treserveTokens: this.getCompactionReserveTokens(),\n\t\t\tkeepRecentTokens: this.getCompactionKeepRecentTokens(),\n\t\t};\n\t}\n\n\tgetHideThinkingBlock(): boolean {\n\t\treturn this.settings.hideThinkingBlock ?? false;\n\t}\n\n\tsetHideThinkingBlock(hide: boolean): void {\n\t\tthis.settings.hideThinkingBlock = hide;\n\t\tthis.save();\n\t}\n\n\tgetShellPath(): string | undefined {\n\t\treturn this.settings.shellPath;\n\t}\n\n\tsetShellPath(path: string | undefined): void {\n\t\tthis.settings.shellPath = path;\n\t\tthis.save();\n\t}\n}\n"]}
|
package/dist/tools/bash.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AA+DrD,QAAA,MAAM,UAAU;;;EAGd,CAAC;AAEH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAAC,OAAO,UAAU,CAuHjD,CAAC","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawn } from \"child_process\";\nimport { existsSync } from \"fs\";\n\n/**\n * Get shell configuration based on platform\n */\nfunction getShellConfig(): { shell: string; args: string[] } {\n\tif (process.platform === \"win32\") {\n\t\tconst paths: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) {\n\t\t\tpaths.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) {\n\t\t\tpaths.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\n\t\tfor (const path of paths) {\n\t\t\tif (existsSync(path)) {\n\t\t\t\treturn { shell: path, args: [\"-c\"] };\n\t\t\t}\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`Git Bash not found. Please install Git for Windows from https://git-scm.com/download/win\\n` +\n\t\t\t\t`Searched in:\\n${paths.map((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t);\n\t}\n\treturn { shell: \"sh\", args: [\"-c\"] };\n}\n\n/**\n * Kill a process and all its children\n */\nfunction killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\t// Use taskkill on Windows to kill process tree\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch (e) {\n\t\t\t// Ignore errors if taskkill fails\n\t\t}\n\t} else {\n\t\t// Use SIGKILL on Unix/Linux/Mac\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch (e) {\n\t\t\t// Fallback to killing just the child if process group kill fails\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch (e2) {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport const bashTool: AgentTool<typeof bashSchema> = {\n\tname: \"bash\",\n\tlabel: \"bash\",\n\tdescription:\n\t\t\"Execute a bash command in the current working directory. Returns stdout and stderr. Optionally provide a timeout in seconds.\",\n\tparameters: bashSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\treturn new Promise((resolve, _reject) => {\n\t\t\tconst { shell, args } = getShellConfig();\n\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\tdetached: true,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\n\t\t\tlet stdout = \"\";\n\t\t\tlet stderr = \"\";\n\t\t\tlet timedOut = false;\n\n\t\t\t// Set timeout if provided\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (timeout !== undefined && timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tonAbort();\n\t\t\t\t}, timeout * 1000);\n\t\t\t}\n\n\t\t\t// Collect stdout\n\t\t\tif (child.stdout) {\n\t\t\t\tchild.stdout.on(\"data\", (data) => {\n\t\t\t\t\tstdout += data.toString();\n\t\t\t\t\t// Limit buffer size\n\t\t\t\t\tif (stdout.length > 10 * 1024 * 1024) {\n\t\t\t\t\t\tstdout = stdout.slice(0, 10 * 1024 * 1024);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Collect stderr\n\t\t\tif (child.stderr) {\n\t\t\t\tchild.stderr.on(\"data\", (data) => {\n\t\t\t\t\tstderr += data.toString();\n\t\t\t\t\t// Limit buffer size\n\t\t\t\t\tif (stderr.length > 10 * 1024 * 1024) {\n\t\t\t\t\t\tstderr = stderr.slice(0, 10 * 1024 * 1024);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Handle process exit\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) {\n\t\t\t\t\tclearTimeout(timeoutHandle);\n\t\t\t\t}\n\t\t\t\tif (signal) {\n\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tlet output = \"\";\n\t\t\t\t\tif (stdout) output += stdout;\n\t\t\t\t\tif (stderr) {\n\t\t\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\t\t\toutput += stderr;\n\t\t\t\t\t}\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\toutput += \"Command aborted\";\n\t\t\t\t\t_reject(new Error(output));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (timedOut) {\n\t\t\t\t\tlet output = \"\";\n\t\t\t\t\tif (stdout) output += stdout;\n\t\t\t\t\tif (stderr) {\n\t\t\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\t\t\toutput += stderr;\n\t\t\t\t\t}\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\toutput += `Command timed out after ${timeout} seconds`;\n\t\t\t\t\t_reject(new Error(output));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet output = \"\";\n\t\t\t\tif (stdout) output += stdout;\n\t\t\t\tif (stderr) {\n\t\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\t\toutput += stderr;\n\t\t\t\t}\n\n\t\t\t\tif (code !== 0 && code !== null) {\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\t_reject(new Error(`${output}Command exited with code ${code}`));\n\t\t\t\t} else {\n\t\t\t\t\tresolve({ content: [{ type: \"text\", text: output || \"(no output)\" }], details: undefined });\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Handle abort signal - kill entire process tree\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) {\n\t\t\t\t\tkillProcessTree(child.pid);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tif (signal) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\tonAbort();\n\t\t\t\t} else {\n\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n};\n"]}
|
|
1
|
+
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAgIrD,QAAA,MAAM,UAAU;;;EAGd,CAAC;AAOH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAAC,OAAO,UAAU,CAkKjD,CAAC","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream, existsSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { SettingsManager } from \"../settings-manager.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from \"./truncate.js\";\n\nlet cachedShellConfig: { shell: string; args: string[] } | null = null;\n\n/**\n * Find bash executable on PATH (Windows)\n */\nfunction findBashOnPath(): string | null {\n\ttry {\n\t\tconst result = spawnSync(\"where\", [\"bash.exe\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\tif (result.status === 0 && result.stdout) {\n\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\tif (firstMatch && existsSync(firstMatch)) {\n\t\t\t\treturn firstMatch;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\treturn null;\n}\n\n/**\n * Get shell configuration based on platform.\n * Resolution order:\n * 1. User-specified shellPath in settings.json\n * 2. On Windows: Git Bash in known locations\n * 3. Fallback: bash on PATH (Windows) or sh (Unix)\n */\nfunction getShellConfig(): { shell: string; args: string[] } {\n\tif (cachedShellConfig) {\n\t\treturn cachedShellConfig;\n\t}\n\n\tconst settings = new SettingsManager();\n\tconst customShellPath = settings.getShellPath();\n\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\tcachedShellConfig = { shell: customShellPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\t\tthrow new Error(\n\t\t\t`Custom shell path not found: ${customShellPath}\\n` + `Please update shellPath in ~/.pi/agent/settings.json`,\n\t\t);\n\t}\n\n\tif (process.platform === \"win32\") {\n\t\t// 2. Try Git Bash in known locations\n\t\tconst paths: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) {\n\t\t\tpaths.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) {\n\t\t\tpaths.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\n\t\tfor (const path of paths) {\n\t\t\tif (existsSync(path)) {\n\t\t\t\tcachedShellConfig = { shell: path, args: [\"-c\"] };\n\t\t\t\treturn cachedShellConfig;\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)\n\t\tconst bashOnPath = findBashOnPath();\n\t\tif (bashOnPath) {\n\t\t\tcachedShellConfig = { shell: bashOnPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`No bash shell found. Options:\\n` +\n\t\t\t\t` 1. Install Git for Windows: https://git-scm.com/download/win\\n` +\n\t\t\t\t` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\\n` +\n\t\t\t\t` 3. Set shellPath in ~/.pi/agent/settings.json\\n\\n` +\n\t\t\t\t`Searched Git Bash in:\\n${paths.map((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t);\n\t}\n\n\tcachedShellConfig = { shell: \"sh\", args: [\"-c\"] };\n\treturn cachedShellConfig;\n}\n\n/**\n * Kill a process and all its children\n */\nfunction killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\t// Use taskkill on Windows to kill process tree\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch (e) {\n\t\t\t// Ignore errors if taskkill fails\n\t\t}\n\t} else {\n\t\t// Use SIGKILL on Unix/Linux/Mac\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch (e) {\n\t\t\t// Fallback to killing just the child if process group kill fails\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch (e2) {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Generate a unique temp file path for bash output\n */\nfunction getTempFilePath(): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `pi-bash-${id}.log`);\n}\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\ninterface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\nexport const bashTool: AgentTool<typeof bashSchema> = {\n\tname: \"bash\",\n\tlabel: \"bash\",\n\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\tparameters: bashSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst { shell, args } = getShellConfig();\n\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\tdetached: true,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\n\t\t\t// We'll stream to a temp file if output gets large\n\t\t\tlet tempFilePath: string | undefined;\n\t\t\tlet tempFileStream: ReturnType<typeof createWriteStream> | undefined;\n\t\t\tlet totalBytes = 0;\n\n\t\t\t// Keep a rolling buffer of the last chunk for tail truncation\n\t\t\tconst chunks: Buffer[] = [];\n\t\t\tlet chunksBytes = 0;\n\t\t\t// Keep more than we need so we have enough for truncation\n\t\t\tconst maxChunksBytes = DEFAULT_MAX_BYTES * 2;\n\n\t\t\tlet timedOut = false;\n\n\t\t\t// Set timeout if provided\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (timeout !== undefined && timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tonAbort();\n\t\t\t\t}, timeout * 1000);\n\t\t\t}\n\n\t\t\tconst handleData = (data: Buffer) => {\n\t\t\t\ttotalBytes += data.length;\n\n\t\t\t\t// Start writing to temp file once we exceed the threshold\n\t\t\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\t\t\ttempFilePath = getTempFilePath();\n\t\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\t\t// Write all buffered chunks to the file\n\t\t\t\t\tfor (const chunk of chunks) {\n\t\t\t\t\t\ttempFileStream.write(chunk);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Write to temp file if we have one\n\t\t\t\tif (tempFileStream) {\n\t\t\t\t\ttempFileStream.write(data);\n\t\t\t\t}\n\n\t\t\t\t// Keep rolling buffer of recent data\n\t\t\t\tchunks.push(data);\n\t\t\t\tchunksBytes += data.length;\n\n\t\t\t\t// Trim old chunks if buffer is too large\n\t\t\t\twhile (chunksBytes > maxChunksBytes && chunks.length > 1) {\n\t\t\t\t\tconst removed = chunks.shift()!;\n\t\t\t\t\tchunksBytes -= removed.length;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Collect stdout and stderr together\n\t\t\tif (child.stdout) {\n\t\t\t\tchild.stdout.on(\"data\", handleData);\n\t\t\t}\n\t\t\tif (child.stderr) {\n\t\t\t\tchild.stderr.on(\"data\", handleData);\n\t\t\t}\n\n\t\t\t// Handle process exit\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) {\n\t\t\t\t\tclearTimeout(timeoutHandle);\n\t\t\t\t}\n\t\t\t\tif (signal) {\n\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\n\t\t\t\t// Close temp file stream\n\t\t\t\tif (tempFileStream) {\n\t\t\t\t\ttempFileStream.end();\n\t\t\t\t}\n\n\t\t\t\t// Combine all buffered chunks\n\t\t\t\tconst fullBuffer = Buffer.concat(chunks);\n\t\t\t\tconst fullOutput = fullBuffer.toString(\"utf-8\");\n\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tlet output = fullOutput;\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\toutput += \"Command aborted\";\n\t\t\t\t\treject(new Error(output));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (timedOut) {\n\t\t\t\t\tlet output = fullOutput;\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\toutput += `Command timed out after ${timeout} seconds`;\n\t\t\t\t\treject(new Error(output));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Apply tail truncation\n\t\t\t\tconst truncation = truncateTail(fullOutput);\n\t\t\t\tlet outputText = truncation.content || \"(no output)\";\n\n\t\t\t\t// Build details with truncation info\n\t\t\t\tlet details: BashToolDetails | undefined;\n\n\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\tdetails = {\n\t\t\t\t\t\ttruncation,\n\t\t\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t\t\t};\n\n\t\t\t\t\t// Build actionable notice\n\t\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\t\tconst endLine = truncation.totalLines;\n\n\t\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t\t// Edge case: last line alone > 30KB\n\t\t\t\t\t\tconst lastLineSize = formatSize(Buffer.byteLength(fullOutput.split(\"\\n\").pop() || \"\", \"utf-8\"));\n\t\t\t\t\t\toutputText += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (code !== 0 && code !== null) {\n\t\t\t\t\toutputText += `\\n\\nCommand exited with code ${code}`;\n\t\t\t\t\treject(new Error(outputText));\n\t\t\t\t} else {\n\t\t\t\t\tresolve({ content: [{ type: \"text\", text: outputText }], details });\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Handle abort signal - kill entire process tree\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) {\n\t\t\t\t\tkillProcessTree(child.pid);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tif (signal) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\tonAbort();\n\t\t\t\t} else {\n\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n};\n"]}
|
package/dist/tools/bash.js
CHANGED
|
@@ -1,11 +1,53 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createWriteStream, existsSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
1
5
|
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import { spawn } from "child_process";
|
|
3
|
-
import {
|
|
6
|
+
import { spawn, spawnSync } from "child_process";
|
|
7
|
+
import { SettingsManager } from "../settings-manager.js";
|
|
8
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateTail } from "./truncate.js";
|
|
9
|
+
let cachedShellConfig = null;
|
|
4
10
|
/**
|
|
5
|
-
*
|
|
11
|
+
* Find bash executable on PATH (Windows)
|
|
12
|
+
*/
|
|
13
|
+
function findBashOnPath() {
|
|
14
|
+
try {
|
|
15
|
+
const result = spawnSync("where", ["bash.exe"], { encoding: "utf-8", timeout: 5000 });
|
|
16
|
+
if (result.status === 0 && result.stdout) {
|
|
17
|
+
const firstMatch = result.stdout.trim().split(/\r?\n/)[0];
|
|
18
|
+
if (firstMatch && existsSync(firstMatch)) {
|
|
19
|
+
return firstMatch;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Ignore errors
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get shell configuration based on platform.
|
|
30
|
+
* Resolution order:
|
|
31
|
+
* 1. User-specified shellPath in settings.json
|
|
32
|
+
* 2. On Windows: Git Bash in known locations
|
|
33
|
+
* 3. Fallback: bash on PATH (Windows) or sh (Unix)
|
|
6
34
|
*/
|
|
7
35
|
function getShellConfig() {
|
|
36
|
+
if (cachedShellConfig) {
|
|
37
|
+
return cachedShellConfig;
|
|
38
|
+
}
|
|
39
|
+
const settings = new SettingsManager();
|
|
40
|
+
const customShellPath = settings.getShellPath();
|
|
41
|
+
// 1. Check user-specified shell path
|
|
42
|
+
if (customShellPath) {
|
|
43
|
+
if (existsSync(customShellPath)) {
|
|
44
|
+
cachedShellConfig = { shell: customShellPath, args: ["-c"] };
|
|
45
|
+
return cachedShellConfig;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Custom shell path not found: ${customShellPath}\n` + `Please update shellPath in ~/.pi/agent/settings.json`);
|
|
48
|
+
}
|
|
8
49
|
if (process.platform === "win32") {
|
|
50
|
+
// 2. Try Git Bash in known locations
|
|
9
51
|
const paths = [];
|
|
10
52
|
const programFiles = process.env.ProgramFiles;
|
|
11
53
|
if (programFiles) {
|
|
@@ -17,13 +59,24 @@ function getShellConfig() {
|
|
|
17
59
|
}
|
|
18
60
|
for (const path of paths) {
|
|
19
61
|
if (existsSync(path)) {
|
|
20
|
-
|
|
62
|
+
cachedShellConfig = { shell: path, args: ["-c"] };
|
|
63
|
+
return cachedShellConfig;
|
|
21
64
|
}
|
|
22
65
|
}
|
|
23
|
-
|
|
24
|
-
|
|
66
|
+
// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)
|
|
67
|
+
const bashOnPath = findBashOnPath();
|
|
68
|
+
if (bashOnPath) {
|
|
69
|
+
cachedShellConfig = { shell: bashOnPath, args: ["-c"] };
|
|
70
|
+
return cachedShellConfig;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`No bash shell found. Options:\n` +
|
|
73
|
+
` 1. Install Git for Windows: https://git-scm.com/download/win\n` +
|
|
74
|
+
` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n` +
|
|
75
|
+
` 3. Set shellPath in ~/.pi/agent/settings.json\n\n` +
|
|
76
|
+
`Searched Git Bash in:\n${paths.map((p) => ` ${p}`).join("\n")}`);
|
|
25
77
|
}
|
|
26
|
-
|
|
78
|
+
cachedShellConfig = { shell: "sh", args: ["-c"] };
|
|
79
|
+
return cachedShellConfig;
|
|
27
80
|
}
|
|
28
81
|
/**
|
|
29
82
|
* Kill a process and all its children
|
|
@@ -57,6 +110,13 @@ function killProcessTree(pid) {
|
|
|
57
110
|
}
|
|
58
111
|
}
|
|
59
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Generate a unique temp file path for bash output
|
|
115
|
+
*/
|
|
116
|
+
function getTempFilePath() {
|
|
117
|
+
const id = randomBytes(8).toString("hex");
|
|
118
|
+
return join(tmpdir(), `pi-bash-${id}.log`);
|
|
119
|
+
}
|
|
60
120
|
const bashSchema = Type.Object({
|
|
61
121
|
command: Type.String({ description: "Bash command to execute" }),
|
|
62
122
|
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
|
|
@@ -64,17 +124,24 @@ const bashSchema = Type.Object({
|
|
|
64
124
|
export const bashTool = {
|
|
65
125
|
name: "bash",
|
|
66
126
|
label: "bash",
|
|
67
|
-
description:
|
|
127
|
+
description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
|
|
68
128
|
parameters: bashSchema,
|
|
69
129
|
execute: async (_toolCallId, { command, timeout }, signal) => {
|
|
70
|
-
return new Promise((resolve,
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
71
131
|
const { shell, args } = getShellConfig();
|
|
72
132
|
const child = spawn(shell, [...args, command], {
|
|
73
133
|
detached: true,
|
|
74
134
|
stdio: ["ignore", "pipe", "pipe"],
|
|
75
135
|
});
|
|
76
|
-
|
|
77
|
-
let
|
|
136
|
+
// We'll stream to a temp file if output gets large
|
|
137
|
+
let tempFilePath;
|
|
138
|
+
let tempFileStream;
|
|
139
|
+
let totalBytes = 0;
|
|
140
|
+
// Keep a rolling buffer of the last chunk for tail truncation
|
|
141
|
+
const chunks = [];
|
|
142
|
+
let chunksBytes = 0;
|
|
143
|
+
// Keep more than we need so we have enough for truncation
|
|
144
|
+
const maxChunksBytes = DEFAULT_MAX_BYTES * 2;
|
|
78
145
|
let timedOut = false;
|
|
79
146
|
// Set timeout if provided
|
|
80
147
|
let timeoutHandle;
|
|
@@ -84,25 +151,36 @@ export const bashTool = {
|
|
|
84
151
|
onAbort();
|
|
85
152
|
}, timeout * 1000);
|
|
86
153
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
154
|
+
const handleData = (data) => {
|
|
155
|
+
totalBytes += data.length;
|
|
156
|
+
// Start writing to temp file once we exceed the threshold
|
|
157
|
+
if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
|
|
158
|
+
tempFilePath = getTempFilePath();
|
|
159
|
+
tempFileStream = createWriteStream(tempFilePath);
|
|
160
|
+
// Write all buffered chunks to the file
|
|
161
|
+
for (const chunk of chunks) {
|
|
162
|
+
tempFileStream.write(chunk);
|
|
94
163
|
}
|
|
95
|
-
}
|
|
164
|
+
}
|
|
165
|
+
// Write to temp file if we have one
|
|
166
|
+
if (tempFileStream) {
|
|
167
|
+
tempFileStream.write(data);
|
|
168
|
+
}
|
|
169
|
+
// Keep rolling buffer of recent data
|
|
170
|
+
chunks.push(data);
|
|
171
|
+
chunksBytes += data.length;
|
|
172
|
+
// Trim old chunks if buffer is too large
|
|
173
|
+
while (chunksBytes > maxChunksBytes && chunks.length > 1) {
|
|
174
|
+
const removed = chunks.shift();
|
|
175
|
+
chunksBytes -= removed.length;
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
// Collect stdout and stderr together
|
|
179
|
+
if (child.stdout) {
|
|
180
|
+
child.stdout.on("data", handleData);
|
|
96
181
|
}
|
|
97
|
-
// Collect stderr
|
|
98
182
|
if (child.stderr) {
|
|
99
|
-
child.stderr.on("data",
|
|
100
|
-
stderr += data.toString();
|
|
101
|
-
// Limit buffer size
|
|
102
|
-
if (stderr.length > 10 * 1024 * 1024) {
|
|
103
|
-
stderr = stderr.slice(0, 10 * 1024 * 1024);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
183
|
+
child.stderr.on("data", handleData);
|
|
106
184
|
}
|
|
107
185
|
// Handle process exit
|
|
108
186
|
child.on("close", (code) => {
|
|
@@ -112,51 +190,60 @@ export const bashTool = {
|
|
|
112
190
|
if (signal) {
|
|
113
191
|
signal.removeEventListener("abort", onAbort);
|
|
114
192
|
}
|
|
193
|
+
// Close temp file stream
|
|
194
|
+
if (tempFileStream) {
|
|
195
|
+
tempFileStream.end();
|
|
196
|
+
}
|
|
197
|
+
// Combine all buffered chunks
|
|
198
|
+
const fullBuffer = Buffer.concat(chunks);
|
|
199
|
+
const fullOutput = fullBuffer.toString("utf-8");
|
|
115
200
|
if (signal?.aborted) {
|
|
116
|
-
let output =
|
|
117
|
-
if (stdout)
|
|
118
|
-
output += stdout;
|
|
119
|
-
if (stderr) {
|
|
120
|
-
if (output)
|
|
121
|
-
output += "\n";
|
|
122
|
-
output += stderr;
|
|
123
|
-
}
|
|
201
|
+
let output = fullOutput;
|
|
124
202
|
if (output)
|
|
125
203
|
output += "\n\n";
|
|
126
204
|
output += "Command aborted";
|
|
127
|
-
|
|
205
|
+
reject(new Error(output));
|
|
128
206
|
return;
|
|
129
207
|
}
|
|
130
208
|
if (timedOut) {
|
|
131
|
-
let output =
|
|
132
|
-
if (stdout)
|
|
133
|
-
output += stdout;
|
|
134
|
-
if (stderr) {
|
|
135
|
-
if (output)
|
|
136
|
-
output += "\n";
|
|
137
|
-
output += stderr;
|
|
138
|
-
}
|
|
209
|
+
let output = fullOutput;
|
|
139
210
|
if (output)
|
|
140
211
|
output += "\n\n";
|
|
141
212
|
output += `Command timed out after ${timeout} seconds`;
|
|
142
|
-
|
|
213
|
+
reject(new Error(output));
|
|
143
214
|
return;
|
|
144
215
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
216
|
+
// Apply tail truncation
|
|
217
|
+
const truncation = truncateTail(fullOutput);
|
|
218
|
+
let outputText = truncation.content || "(no output)";
|
|
219
|
+
// Build details with truncation info
|
|
220
|
+
let details;
|
|
221
|
+
if (truncation.truncated) {
|
|
222
|
+
details = {
|
|
223
|
+
truncation,
|
|
224
|
+
fullOutputPath: tempFilePath,
|
|
225
|
+
};
|
|
226
|
+
// Build actionable notice
|
|
227
|
+
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
|
228
|
+
const endLine = truncation.totalLines;
|
|
229
|
+
if (truncation.lastLinePartial) {
|
|
230
|
+
// Edge case: last line alone > 30KB
|
|
231
|
+
const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8"));
|
|
232
|
+
outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
|
|
233
|
+
}
|
|
234
|
+
else if (truncation.truncatedBy === "lines") {
|
|
235
|
+
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
|
|
239
|
+
}
|
|
152
240
|
}
|
|
153
241
|
if (code !== 0 && code !== null) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
_reject(new Error(`${output}Command exited with code ${code}`));
|
|
242
|
+
outputText += `\n\nCommand exited with code ${code}`;
|
|
243
|
+
reject(new Error(outputText));
|
|
157
244
|
}
|
|
158
245
|
else {
|
|
159
|
-
resolve({ content: [{ type: "text", text:
|
|
246
|
+
resolve({ content: [{ type: "text", text: outputText }], details });
|
|
160
247
|
}
|
|
161
248
|
});
|
|
162
249
|
// Handle abort signal - kill entire process tree
|
package/dist/tools/bash.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bash.js","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawn } from \"child_process\";\nimport { existsSync } from \"fs\";\n\n/**\n * Get shell configuration based on platform\n */\nfunction getShellConfig(): { shell: string; args: string[] } {\n\tif (process.platform === \"win32\") {\n\t\tconst paths: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) {\n\t\t\tpaths.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) {\n\t\t\tpaths.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\n\t\tfor (const path of paths) {\n\t\t\tif (existsSync(path)) {\n\t\t\t\treturn { shell: path, args: [\"-c\"] };\n\t\t\t}\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`Git Bash not found. Please install Git for Windows from https://git-scm.com/download/win\\n` +\n\t\t\t\t`Searched in:\\n${paths.map((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t);\n\t}\n\treturn { shell: \"sh\", args: [\"-c\"] };\n}\n\n/**\n * Kill a process and all its children\n */\nfunction killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\t// Use taskkill on Windows to kill process tree\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch (e) {\n\t\t\t// Ignore errors if taskkill fails\n\t\t}\n\t} else {\n\t\t// Use SIGKILL on Unix/Linux/Mac\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch (e) {\n\t\t\t// Fallback to killing just the child if process group kill fails\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch (e2) {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nexport const bashTool: AgentTool<typeof bashSchema> = {\n\tname: \"bash\",\n\tlabel: \"bash\",\n\tdescription:\n\t\t\"Execute a bash command in the current working directory. Returns stdout and stderr. Optionally provide a timeout in seconds.\",\n\tparameters: bashSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\treturn new Promise((resolve, _reject) => {\n\t\t\tconst { shell, args } = getShellConfig();\n\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\tdetached: true,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\n\t\t\tlet stdout = \"\";\n\t\t\tlet stderr = \"\";\n\t\t\tlet timedOut = false;\n\n\t\t\t// Set timeout if provided\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (timeout !== undefined && timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tonAbort();\n\t\t\t\t}, timeout * 1000);\n\t\t\t}\n\n\t\t\t// Collect stdout\n\t\t\tif (child.stdout) {\n\t\t\t\tchild.stdout.on(\"data\", (data) => {\n\t\t\t\t\tstdout += data.toString();\n\t\t\t\t\t// Limit buffer size\n\t\t\t\t\tif (stdout.length > 10 * 1024 * 1024) {\n\t\t\t\t\t\tstdout = stdout.slice(0, 10 * 1024 * 1024);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Collect stderr\n\t\t\tif (child.stderr) {\n\t\t\t\tchild.stderr.on(\"data\", (data) => {\n\t\t\t\t\tstderr += data.toString();\n\t\t\t\t\t// Limit buffer size\n\t\t\t\t\tif (stderr.length > 10 * 1024 * 1024) {\n\t\t\t\t\t\tstderr = stderr.slice(0, 10 * 1024 * 1024);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\n\t\t\t// Handle process exit\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) {\n\t\t\t\t\tclearTimeout(timeoutHandle);\n\t\t\t\t}\n\t\t\t\tif (signal) {\n\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tlet output = \"\";\n\t\t\t\t\tif (stdout) output += stdout;\n\t\t\t\t\tif (stderr) {\n\t\t\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\t\t\toutput += stderr;\n\t\t\t\t\t}\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\toutput += \"Command aborted\";\n\t\t\t\t\t_reject(new Error(output));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (timedOut) {\n\t\t\t\t\tlet output = \"\";\n\t\t\t\t\tif (stdout) output += stdout;\n\t\t\t\t\tif (stderr) {\n\t\t\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\t\t\toutput += stderr;\n\t\t\t\t\t}\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\toutput += `Command timed out after ${timeout} seconds`;\n\t\t\t\t\t_reject(new Error(output));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tlet output = \"\";\n\t\t\t\tif (stdout) output += stdout;\n\t\t\t\tif (stderr) {\n\t\t\t\t\tif (output) output += \"\\n\";\n\t\t\t\t\toutput += stderr;\n\t\t\t\t}\n\n\t\t\t\tif (code !== 0 && code !== null) {\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\t_reject(new Error(`${output}Command exited with code ${code}`));\n\t\t\t\t} else {\n\t\t\t\t\tresolve({ content: [{ type: \"text\", text: output || \"(no output)\" }], details: undefined });\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Handle abort signal - kill entire process tree\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) {\n\t\t\t\t\tkillProcessTree(child.pid);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tif (signal) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\tonAbort();\n\t\t\t\t} else {\n\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n};\n"]}
|
|
1
|
+
{"version":3,"file":"bash.js","sourceRoot":"","sources":["../../src/tools/bash.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { createWriteStream, existsSync } from \"node:fs\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { SettingsManager } from \"../settings-manager.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from \"./truncate.js\";\n\nlet cachedShellConfig: { shell: string; args: string[] } | null = null;\n\n/**\n * Find bash executable on PATH (Windows)\n */\nfunction findBashOnPath(): string | null {\n\ttry {\n\t\tconst result = spawnSync(\"where\", [\"bash.exe\"], { encoding: \"utf-8\", timeout: 5000 });\n\t\tif (result.status === 0 && result.stdout) {\n\t\t\tconst firstMatch = result.stdout.trim().split(/\\r?\\n/)[0];\n\t\t\tif (firstMatch && existsSync(firstMatch)) {\n\t\t\t\treturn firstMatch;\n\t\t\t}\n\t\t}\n\t} catch {\n\t\t// Ignore errors\n\t}\n\treturn null;\n}\n\n/**\n * Get shell configuration based on platform.\n * Resolution order:\n * 1. User-specified shellPath in settings.json\n * 2. On Windows: Git Bash in known locations\n * 3. Fallback: bash on PATH (Windows) or sh (Unix)\n */\nfunction getShellConfig(): { shell: string; args: string[] } {\n\tif (cachedShellConfig) {\n\t\treturn cachedShellConfig;\n\t}\n\n\tconst settings = new SettingsManager();\n\tconst customShellPath = settings.getShellPath();\n\n\t// 1. Check user-specified shell path\n\tif (customShellPath) {\n\t\tif (existsSync(customShellPath)) {\n\t\t\tcachedShellConfig = { shell: customShellPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\t\tthrow new Error(\n\t\t\t`Custom shell path not found: ${customShellPath}\\n` + `Please update shellPath in ~/.pi/agent/settings.json`,\n\t\t);\n\t}\n\n\tif (process.platform === \"win32\") {\n\t\t// 2. Try Git Bash in known locations\n\t\tconst paths: string[] = [];\n\t\tconst programFiles = process.env.ProgramFiles;\n\t\tif (programFiles) {\n\t\t\tpaths.push(`${programFiles}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\t\tconst programFilesX86 = process.env[\"ProgramFiles(x86)\"];\n\t\tif (programFilesX86) {\n\t\t\tpaths.push(`${programFilesX86}\\\\Git\\\\bin\\\\bash.exe`);\n\t\t}\n\n\t\tfor (const path of paths) {\n\t\t\tif (existsSync(path)) {\n\t\t\t\tcachedShellConfig = { shell: path, args: [\"-c\"] };\n\t\t\t\treturn cachedShellConfig;\n\t\t\t}\n\t\t}\n\n\t\t// 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.)\n\t\tconst bashOnPath = findBashOnPath();\n\t\tif (bashOnPath) {\n\t\t\tcachedShellConfig = { shell: bashOnPath, args: [\"-c\"] };\n\t\t\treturn cachedShellConfig;\n\t\t}\n\n\t\tthrow new Error(\n\t\t\t`No bash shell found. Options:\\n` +\n\t\t\t\t` 1. Install Git for Windows: https://git-scm.com/download/win\\n` +\n\t\t\t\t` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\\n` +\n\t\t\t\t` 3. Set shellPath in ~/.pi/agent/settings.json\\n\\n` +\n\t\t\t\t`Searched Git Bash in:\\n${paths.map((p) => ` ${p}`).join(\"\\n\")}`,\n\t\t);\n\t}\n\n\tcachedShellConfig = { shell: \"sh\", args: [\"-c\"] };\n\treturn cachedShellConfig;\n}\n\n/**\n * Kill a process and all its children\n */\nfunction killProcessTree(pid: number): void {\n\tif (process.platform === \"win32\") {\n\t\t// Use taskkill on Windows to kill process tree\n\t\ttry {\n\t\t\tspawn(\"taskkill\", [\"/F\", \"/T\", \"/PID\", String(pid)], {\n\t\t\t\tstdio: \"ignore\",\n\t\t\t\tdetached: true,\n\t\t\t});\n\t\t} catch (e) {\n\t\t\t// Ignore errors if taskkill fails\n\t\t}\n\t} else {\n\t\t// Use SIGKILL on Unix/Linux/Mac\n\t\ttry {\n\t\t\tprocess.kill(-pid, \"SIGKILL\");\n\t\t} catch (e) {\n\t\t\t// Fallback to killing just the child if process group kill fails\n\t\t\ttry {\n\t\t\t\tprocess.kill(pid, \"SIGKILL\");\n\t\t\t} catch (e2) {\n\t\t\t\t// Process already dead\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Generate a unique temp file path for bash output\n */\nfunction getTempFilePath(): string {\n\tconst id = randomBytes(8).toString(\"hex\");\n\treturn join(tmpdir(), `pi-bash-${id}.log`);\n}\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\ninterface BashToolDetails {\n\ttruncation?: TruncationResult;\n\tfullOutputPath?: string;\n}\n\nexport const bashTool: AgentTool<typeof bashSchema> = {\n\tname: \"bash\",\n\tlabel: \"bash\",\n\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,\n\tparameters: bashSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ command, timeout }: { command: string; timeout?: number },\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst { shell, args } = getShellConfig();\n\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\tdetached: true,\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t});\n\n\t\t\t// We'll stream to a temp file if output gets large\n\t\t\tlet tempFilePath: string | undefined;\n\t\t\tlet tempFileStream: ReturnType<typeof createWriteStream> | undefined;\n\t\t\tlet totalBytes = 0;\n\n\t\t\t// Keep a rolling buffer of the last chunk for tail truncation\n\t\t\tconst chunks: Buffer[] = [];\n\t\t\tlet chunksBytes = 0;\n\t\t\t// Keep more than we need so we have enough for truncation\n\t\t\tconst maxChunksBytes = DEFAULT_MAX_BYTES * 2;\n\n\t\t\tlet timedOut = false;\n\n\t\t\t// Set timeout if provided\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (timeout !== undefined && timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tonAbort();\n\t\t\t\t}, timeout * 1000);\n\t\t\t}\n\n\t\t\tconst handleData = (data: Buffer) => {\n\t\t\t\ttotalBytes += data.length;\n\n\t\t\t\t// Start writing to temp file once we exceed the threshold\n\t\t\t\tif (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {\n\t\t\t\t\ttempFilePath = getTempFilePath();\n\t\t\t\t\ttempFileStream = createWriteStream(tempFilePath);\n\t\t\t\t\t// Write all buffered chunks to the file\n\t\t\t\t\tfor (const chunk of chunks) {\n\t\t\t\t\t\ttempFileStream.write(chunk);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Write to temp file if we have one\n\t\t\t\tif (tempFileStream) {\n\t\t\t\t\ttempFileStream.write(data);\n\t\t\t\t}\n\n\t\t\t\t// Keep rolling buffer of recent data\n\t\t\t\tchunks.push(data);\n\t\t\t\tchunksBytes += data.length;\n\n\t\t\t\t// Trim old chunks if buffer is too large\n\t\t\t\twhile (chunksBytes > maxChunksBytes && chunks.length > 1) {\n\t\t\t\t\tconst removed = chunks.shift()!;\n\t\t\t\t\tchunksBytes -= removed.length;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\t// Collect stdout and stderr together\n\t\t\tif (child.stdout) {\n\t\t\t\tchild.stdout.on(\"data\", handleData);\n\t\t\t}\n\t\t\tif (child.stderr) {\n\t\t\t\tchild.stderr.on(\"data\", handleData);\n\t\t\t}\n\n\t\t\t// Handle process exit\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) {\n\t\t\t\t\tclearTimeout(timeoutHandle);\n\t\t\t\t}\n\t\t\t\tif (signal) {\n\t\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t}\n\n\t\t\t\t// Close temp file stream\n\t\t\t\tif (tempFileStream) {\n\t\t\t\t\ttempFileStream.end();\n\t\t\t\t}\n\n\t\t\t\t// Combine all buffered chunks\n\t\t\t\tconst fullBuffer = Buffer.concat(chunks);\n\t\t\t\tconst fullOutput = fullBuffer.toString(\"utf-8\");\n\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\tlet output = fullOutput;\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\toutput += \"Command aborted\";\n\t\t\t\t\treject(new Error(output));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (timedOut) {\n\t\t\t\t\tlet output = fullOutput;\n\t\t\t\t\tif (output) output += \"\\n\\n\";\n\t\t\t\t\toutput += `Command timed out after ${timeout} seconds`;\n\t\t\t\t\treject(new Error(output));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\t// Apply tail truncation\n\t\t\t\tconst truncation = truncateTail(fullOutput);\n\t\t\t\tlet outputText = truncation.content || \"(no output)\";\n\n\t\t\t\t// Build details with truncation info\n\t\t\t\tlet details: BashToolDetails | undefined;\n\n\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\tdetails = {\n\t\t\t\t\t\ttruncation,\n\t\t\t\t\t\tfullOutputPath: tempFilePath,\n\t\t\t\t\t};\n\n\t\t\t\t\t// Build actionable notice\n\t\t\t\t\tconst startLine = truncation.totalLines - truncation.outputLines + 1;\n\t\t\t\t\tconst endLine = truncation.totalLines;\n\n\t\t\t\t\tif (truncation.lastLinePartial) {\n\t\t\t\t\t\t// Edge case: last line alone > 30KB\n\t\t\t\t\t\tconst lastLineSize = formatSize(Buffer.byteLength(fullOutput.split(\"\\n\").pop() || \"\", \"utf-8\"));\n\t\t\t\t\t\toutputText += `\\n\\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;\n\t\t\t\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;\n\t\t\t\t\t} else {\n\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (code !== 0 && code !== null) {\n\t\t\t\t\toutputText += `\\n\\nCommand exited with code ${code}`;\n\t\t\t\t\treject(new Error(outputText));\n\t\t\t\t} else {\n\t\t\t\t\tresolve({ content: [{ type: \"text\", text: outputText }], details });\n\t\t\t\t}\n\t\t\t});\n\n\t\t\t// Handle abort signal - kill entire process tree\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) {\n\t\t\t\t\tkillProcessTree(child.pid);\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tif (signal) {\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\tonAbort();\n\t\t\t\t} else {\n\t\t\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n};\n"]}
|
package/dist/tools/find.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"find.d.ts","sourceRoot":"","sources":["../../src/tools/find.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"find.d.ts","sourceRoot":"","sources":["../../src/tools/find.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAuBrD,QAAA,MAAM,UAAU;;;;EAMd,CAAC;AASH,eAAO,MAAM,QAAQ,EAAE,SAAS,CAAC,OAAO,UAAU,CAoKjD,CAAC","sourcesContent":["import type { AgentTool } from \"@mariozechner/pi-ai\";\nimport { Type } from \"@sinclair/typebox\";\nimport { spawnSync } from \"child_process\";\nimport { existsSync } from \"fs\";\nimport { globSync } from \"glob\";\nimport { homedir } from \"os\";\nimport path from \"path\";\nimport { ensureTool } from \"../tools-manager.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\n/**\n * Expand ~ to home directory\n */\nfunction expandPath(filePath: string): string {\n\tif (filePath === \"~\") {\n\t\treturn homedir();\n\t}\n\tif (filePath.startsWith(\"~/\")) {\n\t\treturn homedir() + filePath.slice(1);\n\t}\n\treturn filePath;\n}\n\nconst findSchema = Type.Object({\n\tpattern: Type.String({\n\t\tdescription: \"Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'\",\n\t}),\n\tpath: Type.Optional(Type.String({ description: \"Directory to search in (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results (default: 1000)\" })),\n});\n\nconst DEFAULT_LIMIT = 1000;\n\ninterface FindToolDetails {\n\ttruncation?: TruncationResult;\n\tresultLimitReached?: number;\n}\n\nexport const findTool: AgentTool<typeof findSchema> = {\n\tname: \"find\",\n\tlabel: \"find\",\n\tdescription: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\tparameters: findSchema,\n\texecute: async (\n\t\t_toolCallId: string,\n\t\t{ pattern, path: searchDir, limit }: { pattern: string; path?: string; limit?: number },\n\t\tsignal?: AbortSignal,\n\t) => {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tif (signal?.aborted) {\n\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t(async () => {\n\t\t\t\ttry {\n\t\t\t\t\t// Ensure fd is available\n\t\t\t\t\tconst fdPath = await ensureTool(\"fd\", true);\n\t\t\t\t\tif (!fdPath) {\n\t\t\t\t\t\treject(new Error(\"fd is not available and could not be downloaded\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst searchPath = path.resolve(expandPath(searchDir || \".\"));\n\t\t\t\t\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t\t// Build fd arguments\n\t\t\t\t\tconst args: string[] = [\n\t\t\t\t\t\t\"--glob\", // Use glob pattern\n\t\t\t\t\t\t\"--color=never\", // No ANSI colors\n\t\t\t\t\t\t\"--hidden\", // Search hidden files (but still respect .gitignore)\n\t\t\t\t\t\t\"--max-results\",\n\t\t\t\t\t\tString(effectiveLimit),\n\t\t\t\t\t];\n\n\t\t\t\t\t// Include .gitignore files (root + nested) so fd respects them even outside git repos\n\t\t\t\t\tconst gitignoreFiles = new Set<string>();\n\t\t\t\t\tconst rootGitignore = path.join(searchPath, \".gitignore\");\n\t\t\t\t\tif (existsSync(rootGitignore)) {\n\t\t\t\t\t\tgitignoreFiles.add(rootGitignore);\n\t\t\t\t\t}\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst nestedGitignores = globSync(\"**/.gitignore\", {\n\t\t\t\t\t\t\tcwd: searchPath,\n\t\t\t\t\t\t\tdot: true,\n\t\t\t\t\t\t\tabsolute: true,\n\t\t\t\t\t\t\tignore: [\"**/node_modules/**\", \"**/.git/**\"],\n\t\t\t\t\t\t});\n\t\t\t\t\t\tfor (const file of nestedGitignores) {\n\t\t\t\t\t\t\tgitignoreFiles.add(file);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Ignore glob errors\n\t\t\t\t\t}\n\n\t\t\t\t\tfor (const gitignorePath of gitignoreFiles) {\n\t\t\t\t\t\targs.push(\"--ignore-file\", gitignorePath);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Pattern and path\n\t\t\t\t\targs.push(pattern, searchPath);\n\n\t\t\t\t\t// Run fd\n\t\t\t\t\tconst result = spawnSync(fdPath, args, {\n\t\t\t\t\t\tencoding: \"utf-8\",\n\t\t\t\t\t\tmaxBuffer: 10 * 1024 * 1024, // 10MB\n\t\t\t\t\t});\n\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\t\tif (result.error) {\n\t\t\t\t\t\treject(new Error(`Failed to run fd: ${result.error.message}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst output = result.stdout?.trim() || \"\";\n\n\t\t\t\t\tif (result.status !== 0) {\n\t\t\t\t\t\tconst errorMsg = result.stderr?.trim() || `fd exited with code ${result.status}`;\n\t\t\t\t\t\t// fd returns non-zero for some errors but may still have partial output\n\t\t\t\t\t\tif (!output) {\n\t\t\t\t\t\t\treject(new Error(errorMsg));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif (!output) {\n\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: \"No files found matching pattern\" }],\n\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\t\tconst relativized: string[] = [];\n\n\t\t\t\t\tfor (const rawLine of lines) {\n\t\t\t\t\t\tconst line = rawLine.replace(/\\r$/, \"\").trim();\n\t\t\t\t\t\tif (!line) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst hadTrailingSlash = line.endsWith(\"/\") || line.endsWith(\"\\\\\");\n\t\t\t\t\t\tlet relativePath = line;\n\t\t\t\t\t\tif (line.startsWith(searchPath)) {\n\t\t\t\t\t\t\trelativePath = line.slice(searchPath.length + 1); // +1 for the /\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trelativePath = path.relative(searchPath, line);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (hadTrailingSlash && !relativePath.endsWith(\"/\")) {\n\t\t\t\t\t\t\trelativePath += \"/\";\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trelativized.push(relativePath);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check if we hit the result limit\n\t\t\t\t\tconst resultLimitReached = relativized.length >= effectiveLimit;\n\n\t\t\t\t\t// Apply byte truncation (no line limit since we already have result limit)\n\t\t\t\t\tconst rawOutput = relativized.join(\"\\n\");\n\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\n\t\t\t\t\tlet resultOutput = truncation.content;\n\t\t\t\t\tconst details: FindToolDetails = {};\n\n\t\t\t\t\t// Build notices\n\t\t\t\t\tconst notices: string[] = [];\n\n\t\t\t\t\tif (resultLimitReached) {\n\t\t\t\t\t\tnotices.push(\n\t\t\t\t\t\t\t`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tdetails.resultLimitReached = effectiveLimit;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\tresultOutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t}\n\n\t\t\t\t\tresolve({\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: resultOutput }],\n\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t});\n\t\t\t\t} catch (e: any) {\n\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\treject(e);\n\t\t\t\t}\n\t\t\t})();\n\t\t});\n\t},\n};\n"]}
|