dont-hallucinate 0.1.3
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/README.md +154 -0
- package/bin/dont-hallucinate.js +5 -0
- package/dont_hallucinate/hooks/hook.ps1 +272 -0
- package/dont_hallucinate/hooks/hook.sh +150 -0
- package/dont_hallucinate/hooks/hook_noninteractive.sh +151 -0
- package/dont_hallucinate/snark.json +198 -0
- package/package.json +33 -0
- package/src/burn.js +48 -0
- package/src/cli.js +599 -0
- package/src/parser.js +123 -0
- package/src/runtime.js +47 -0
- package/src/ui.js +240 -0
- package/src/verifier.js +156 -0
- package/src/watcher.js +123 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# dont-hallucinate
|
|
2
|
+
|
|
3
|
+
`dont-hallucinate` intercepts agent shell failures, classifies what went wrong, suggests fixes when it can, and mocks your coding agent. By giving a model immediate negative reinforcement helps them to stop foolishly repeating the same mistake.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- Watches shell failures in real time with a live TUI dashboard
|
|
8
|
+
- Classifies errors: hallucinated flags, phantom packages, ghost branches, dirty working trees, network failures, and more
|
|
9
|
+
- Roasts the agent with context-aware snark injected directly into stderr
|
|
10
|
+
- Suggests fixes: corrected commands and valid flag alternatives
|
|
11
|
+
- Detects the agent (Claude, Cursor, Aider, Copilot, GPT, Windsurf) and personalizes the roast
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
Recommended:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install dont-hallucinate
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
From source:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/alexgaoth/dont-hallucinate
|
|
25
|
+
cd dont-hallucinate
|
|
26
|
+
pip install -e .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Hook Setup
|
|
30
|
+
|
|
31
|
+
There are now three separate install commands because the integration points are different.
|
|
32
|
+
|
|
33
|
+
### Claude Code
|
|
34
|
+
|
|
35
|
+
Global install (recommended):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
dont-hallucinate install-hook-claude --global
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Project-local install:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
dont-hallucinate install-hook-claude
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Codex CLI
|
|
48
|
+
|
|
49
|
+
Global install (recommended):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
dont-hallucinate install-hook-codex
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Project-local install:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
dont-hallucinate install-hook-codex --project
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This updates `~/.codex/config.toml` by default and installs a `shell_environment_policy` block that sets:
|
|
62
|
+
|
|
63
|
+
- `BASH_ENV` to the noninteractive `dont-hallucinate` bash hook
|
|
64
|
+
- `HALLUCINATE_ACTOR=agent`
|
|
65
|
+
- `HALLUCINATE_STREAM_FILE=~/.dont-hallucinate/codex/stream.jsonl`
|
|
66
|
+
|
|
67
|
+
Important scope note:
|
|
68
|
+
|
|
69
|
+
- This currently covers Codex bash subprocess failures
|
|
70
|
+
- It is not a PowerShell-native Codex hook
|
|
71
|
+
- If you already use inline `set = {...}` inside `[shell_environment_policy]`, migrate that to `[shell_environment_policy.set]` first
|
|
72
|
+
|
|
73
|
+
### Cursor
|
|
74
|
+
|
|
75
|
+
Install the Cursor hook:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
dont-hallucinate install-hook-cursor
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This appends shell-profile snippets that activate only when Cursor sets `CURSOR_AGENT`.
|
|
82
|
+
|
|
83
|
+
Files updated:
|
|
84
|
+
|
|
85
|
+
- PowerShell: `~/Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1`
|
|
86
|
+
- Bash: `~/.bashrc`
|
|
87
|
+
- Zsh: `~/.zshrc`
|
|
88
|
+
|
|
89
|
+
The Cursor stream file is written to:
|
|
90
|
+
|
|
91
|
+
```text
|
|
92
|
+
~/.dont-hallucinate/cursor/stream.jsonl
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Important scope note:
|
|
96
|
+
|
|
97
|
+
- This is a shell-profile install, not a Cursor settings.json tool hook
|
|
98
|
+
- It covers Cursor agent terminal sessions that inherit your normal shell startup files
|
|
99
|
+
- Normal terminals are not affected because the snippet checks `CURSOR_AGENT`
|
|
100
|
+
|
|
101
|
+
## Watch UI
|
|
102
|
+
|
|
103
|
+
By just running:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
dont-hallucinate
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
It will shows a live dashboard with the failure type, failing command, suggested fix, and roast.
|
|
110
|
+
This will work anywhere if global setup. If local setup this will only work in the project root.
|
|
111
|
+
|
|
112
|
+
## Commands
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
dont-hallucinate
|
|
116
|
+
dont-hallucinate exec [--shell] --actor agent -- <command>
|
|
117
|
+
dont-hallucinate install-hook-claude [--global]
|
|
118
|
+
dont-hallucinate install-hook-codex [--project]
|
|
119
|
+
dont-hallucinate install-hook-cursor
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Error types caught
|
|
123
|
+
|
|
124
|
+
| Type | What it means |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `invalid_flag` | hallucinated CLI flag that does not exist |
|
|
127
|
+
| `missing_package` | command not found / phantom package |
|
|
128
|
+
| `git_branch_missing` | branch or ref that does not exist |
|
|
129
|
+
| `git_dirty` | uncommitted changes blocking the operation |
|
|
130
|
+
| `git_conflict` | merge conflict |
|
|
131
|
+
| `npm_missing_script` | script not defined in `package.json` |
|
|
132
|
+
| `file_not_found` | path does not exist |
|
|
133
|
+
| `permission_denied` | insufficient permissions |
|
|
134
|
+
| `port_in_use` | address already bound |
|
|
135
|
+
| `network_error` | DNS failure, connection refused, SSL error |
|
|
136
|
+
| `no_space` | disk full |
|
|
137
|
+
| `syntax_error` | shell or language syntax error |
|
|
138
|
+
| `generic_error` | everything else |
|
|
139
|
+
|
|
140
|
+
## What the agent sees
|
|
141
|
+
|
|
142
|
+
When active, the relevant shell command path is wrapped or instrumented so failures are decorated. On failure, stderr gets an extra line like:
|
|
143
|
+
|
|
144
|
+
```text
|
|
145
|
+
[dont-hallucinate/agent] caught: npm run tset
|
|
146
|
+
[dont-hallucinate/agent] type: npm_missing_script
|
|
147
|
+
[dont-hallucinate/agent] roast: package.json had a menu. GPT ordered something imaginary.
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Platform support
|
|
151
|
+
|
|
152
|
+
- Linux/macOS: fully supported for bash/zsh-based flows
|
|
153
|
+
- Windows: supported for PowerShell and bash-based flows
|
|
154
|
+
- Codex install currently targets bash subprocess interception
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# hallucinate PowerShell hook
|
|
2
|
+
# Dot-source from your profile or from `hallucinate activate --shell powershell`.
|
|
3
|
+
|
|
4
|
+
if (-not $env:HALLUCINATE_STREAM_FILE -or [string]::IsNullOrWhiteSpace($env:HALLUCINATE_STREAM_FILE)) {
|
|
5
|
+
$env:HALLUCINATE_STREAM_FILE = Join-Path $env:TEMP "hallucinate_stream.json"
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
$global:HallucinateSessionId = $PID
|
|
9
|
+
$global:HallucinateLastHistorySignature = ""
|
|
10
|
+
$global:HallucinateLastErrorCount = $Error.Count
|
|
11
|
+
$global:HallucinatePendingCommand = ""
|
|
12
|
+
$global:HallucinatePrevAddToHistoryHandler = $null
|
|
13
|
+
$global:HallucinateEnterHandlerInstalled = $false
|
|
14
|
+
|
|
15
|
+
if (-not $global:HallucinateOriginalPromptScript) {
|
|
16
|
+
$promptCmd = Get-Command prompt -ErrorAction SilentlyContinue
|
|
17
|
+
if ($null -ne $promptCmd -and $promptCmd.ScriptBlock) {
|
|
18
|
+
$global:HallucinateOriginalPromptScript = $promptCmd.ScriptBlock
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function Invoke-HallucinateHistoryHandler {
|
|
23
|
+
param(
|
|
24
|
+
[object]$Handler,
|
|
25
|
+
[string]$Line
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if ($null -eq $Handler) {
|
|
29
|
+
return $true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if ($Handler -is [scriptblock]) {
|
|
33
|
+
return [bool](& $Handler $Line)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
$invoke = $Handler.PSObject.Methods["Invoke"]
|
|
37
|
+
if ($null -ne $invoke) {
|
|
38
|
+
return [bool]($Handler.Invoke($Line))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return $true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Add-HallucinateUtf8Line {
|
|
45
|
+
param(
|
|
46
|
+
[string]$Path,
|
|
47
|
+
[string]$Line
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
$directory = Split-Path -Parent $Path
|
|
51
|
+
if ($directory) {
|
|
52
|
+
New-Item -ItemType Directory -Path $directory -Force -ErrorAction SilentlyContinue | Out-Null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
$encoding = [System.Text.UTF8Encoding]::new($false)
|
|
56
|
+
$writer = [System.IO.StreamWriter]::new($Path, $true, $encoding)
|
|
57
|
+
try {
|
|
58
|
+
$writer.WriteLine($Line)
|
|
59
|
+
} finally {
|
|
60
|
+
$writer.Dispose()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function Install-HallucinatePSReadLineCapture {
|
|
65
|
+
if ($global:HallucinatePSRLInstalled) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
Import-Module PSReadLine -ErrorAction SilentlyContinue | Out-Null
|
|
69
|
+
$setOpt = Get-Command Set-PSReadLineOption -ErrorAction SilentlyContinue
|
|
70
|
+
if ($null -eq $setOpt) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
$opts = Get-PSReadLineOption -ErrorAction SilentlyContinue
|
|
76
|
+
if ($opts -and $opts.AddToHistoryHandler) {
|
|
77
|
+
$global:HallucinatePrevAddToHistoryHandler = $opts.AddToHistoryHandler
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
$global:HallucinatePrevAddToHistoryHandler = $null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
Set-PSReadLineKeyHandler -Chord Enter -BriefDescription "HallucinateAcceptLine" -ScriptBlock {
|
|
85
|
+
param($key, $arg)
|
|
86
|
+
$line = $null
|
|
87
|
+
$cursor = $null
|
|
88
|
+
try {
|
|
89
|
+
[Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
|
|
90
|
+
} catch {
|
|
91
|
+
$line = $null
|
|
92
|
+
}
|
|
93
|
+
if (-not [string]::IsNullOrWhiteSpace($line)) {
|
|
94
|
+
$global:HallucinatePendingCommand = [string]$line
|
|
95
|
+
}
|
|
96
|
+
[Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
|
|
97
|
+
}
|
|
98
|
+
$global:HallucinateEnterHandlerInstalled = $true
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
Set-PSReadLineOption -AddToHistoryHandler {
|
|
104
|
+
param($line)
|
|
105
|
+
$allow = $true
|
|
106
|
+
if ($global:HallucinatePrevAddToHistoryHandler) {
|
|
107
|
+
try {
|
|
108
|
+
$allow = Invoke-HallucinateHistoryHandler -Handler $global:HallucinatePrevAddToHistoryHandler -Line $line
|
|
109
|
+
} catch {
|
|
110
|
+
$allow = $true
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if ($allow) {
|
|
114
|
+
$global:HallucinatePendingCommand = [string]$line
|
|
115
|
+
}
|
|
116
|
+
return $allow
|
|
117
|
+
}
|
|
118
|
+
$global:HallucinatePSRLInstalled = $true
|
|
119
|
+
} catch {
|
|
120
|
+
if ($global:HallucinateEnterHandlerInstalled) {
|
|
121
|
+
$global:HallucinatePSRLInstalled = $true
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function Write-HallucinateDebug {
|
|
127
|
+
param([string]$Message)
|
|
128
|
+
if ($env:HALLUCINATE_HOOK_DEBUG -ne "1") {
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
$log = Join-Path $env:TEMP "hallucinate_hook_debug.log"
|
|
132
|
+
Add-HallucinateUtf8Line -Path $log -Line ("{0} {1}" -f (Get-Date).ToString("o"), $Message)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function Test-HallucinatePathUnderRoot {
|
|
136
|
+
param(
|
|
137
|
+
[string]$Path,
|
|
138
|
+
[string]$Root
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if ([string]::IsNullOrWhiteSpace($Root)) {
|
|
142
|
+
return $true
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
$fullPath = [System.IO.Path]::GetFullPath($Path)
|
|
147
|
+
$fullRoot = [System.IO.Path]::GetFullPath($Root)
|
|
148
|
+
} catch {
|
|
149
|
+
return $false
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if ($fullPath -eq $fullRoot) {
|
|
153
|
+
return $true
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
$normalizedRoot = $fullRoot.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
|
|
157
|
+
return $fullPath.StartsWith($normalizedRoot, [System.StringComparison]::OrdinalIgnoreCase)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function Write-HallucinateEvent {
|
|
161
|
+
param(
|
|
162
|
+
[int]$ExitCode,
|
|
163
|
+
[string]$Command,
|
|
164
|
+
[string]$StderrText
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
$cwd = (Get-Location).Path
|
|
168
|
+
if (-not (Test-HallucinatePathUnderRoot -Path $cwd -Root $env:HALLUCINATE_WORKSPACE_ROOT)) {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
$payload = [ordered]@{
|
|
173
|
+
ts = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
174
|
+
shell = "powershell"
|
|
175
|
+
actor = $(if ($env:HALLUCINATE_ACTOR) { $env:HALLUCINATE_ACTOR } else { "human" })
|
|
176
|
+
session = $global:HallucinateSessionId
|
|
177
|
+
pid = $PID
|
|
178
|
+
cwd = $cwd
|
|
179
|
+
workspace_root = $env:HALLUCINATE_WORKSPACE_ROOT
|
|
180
|
+
exit_code = $ExitCode
|
|
181
|
+
command = $Command
|
|
182
|
+
stderr = $StderrText
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
$line = $payload | ConvertTo-Json -Compress
|
|
186
|
+
Add-HallucinateUtf8Line -Path $env:HALLUCINATE_STREAM_FILE -Line $line
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function Get-HallucinateExitCode {
|
|
190
|
+
param(
|
|
191
|
+
[bool]$LastSucceeded,
|
|
192
|
+
[int]$LastNativeExit,
|
|
193
|
+
[string]$CommandLine
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
$trimmed = if ($CommandLine) { $CommandLine.Trim() } else { "" }
|
|
197
|
+
$firstToken = if ($trimmed) { ($trimmed -split "\s+")[0].Trim("'`"") } else { "" }
|
|
198
|
+
$commandInfo = if ($firstToken) { Get-Command $firstToken -ErrorAction SilentlyContinue } else { $null }
|
|
199
|
+
$isNative = $false
|
|
200
|
+
if ($null -ne $commandInfo) {
|
|
201
|
+
$isNative = $commandInfo.CommandType -in @("Application", "ExternalScript")
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if ($isNative) {
|
|
205
|
+
if ($null -ne $LastNativeExit) {
|
|
206
|
+
return [int]$LastNativeExit
|
|
207
|
+
}
|
|
208
|
+
return $(if ($LastSucceeded) { 0 } else { 1 })
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if ($LastSucceeded) {
|
|
212
|
+
return 0
|
|
213
|
+
}
|
|
214
|
+
return 1
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function Get-HallucinateRecentErrors {
|
|
218
|
+
if ($Error.Count -le 0) {
|
|
219
|
+
$global:HallucinateLastErrorCount = 0
|
|
220
|
+
return ""
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
$delta = $Error.Count - $global:HallucinateLastErrorCount
|
|
224
|
+
if ($delta -le 0) {
|
|
225
|
+
$global:HallucinateLastErrorCount = $Error.Count
|
|
226
|
+
return ""
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
$upper = [Math]::Min($delta, $Error.Count)
|
|
230
|
+
$recent = for ($i = 0; $i -lt $upper; $i++) { $Error[$i].ToString() }
|
|
231
|
+
$global:HallucinateLastErrorCount = $Error.Count
|
|
232
|
+
return ($recent -join [Environment]::NewLine)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function global:prompt {
|
|
236
|
+
$lastSucceeded = $?
|
|
237
|
+
$lastNativeExit = if ($null -eq $global:LASTEXITCODE) { 0 } else { [int]$global:LASTEXITCODE }
|
|
238
|
+
|
|
239
|
+
$cmd = ""
|
|
240
|
+
if (-not [string]::IsNullOrWhiteSpace($global:HallucinatePendingCommand)) {
|
|
241
|
+
$cmd = [string]$global:HallucinatePendingCommand
|
|
242
|
+
$global:HallucinatePendingCommand = ""
|
|
243
|
+
} else {
|
|
244
|
+
$history = Get-History -Count 1 -ErrorAction SilentlyContinue
|
|
245
|
+
if ($null -ne $history) {
|
|
246
|
+
$start = if ($history.StartExecutionTime) { $history.StartExecutionTime.ToUniversalTime().ToString("o") } else { "" }
|
|
247
|
+
$sig = "{0}|{1}|{2}" -f $history.Id, $start, [string]$history.CommandLine
|
|
248
|
+
if ($sig -ne $global:HallucinateLastHistorySignature) {
|
|
249
|
+
$cmd = [string]$history.CommandLine
|
|
250
|
+
$global:HallucinateLastHistorySignature = $sig
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (-not [string]::IsNullOrWhiteSpace($cmd)) {
|
|
256
|
+
$exitCode = Get-HallucinateExitCode -LastSucceeded $lastSucceeded -LastNativeExit $lastNativeExit -CommandLine $cmd
|
|
257
|
+
$stderrText = Get-HallucinateRecentErrors
|
|
258
|
+
Write-HallucinateDebug ("log cmd=`"{0}`" exit={1} native={2} success={3}" -f $cmd, $exitCode, $lastNativeExit, $lastSucceeded)
|
|
259
|
+
Write-HallucinateEvent -ExitCode $exitCode -Command $cmd -StderrText $stderrText
|
|
260
|
+
} else {
|
|
261
|
+
Write-HallucinateDebug ("skip cmd; pending empty, history unchanged; native={0} success={1}" -f $lastNativeExit, $lastSucceeded)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if ($global:HallucinateOriginalPromptScript) {
|
|
265
|
+
& $global:HallucinateOriginalPromptScript
|
|
266
|
+
} else {
|
|
267
|
+
"PS $($executionContext.SessionState.Path.CurrentLocation)> "
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
$global:HallucinateHookLoaded = $true
|
|
272
|
+
Install-HallucinatePSReadLineCapture
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
if [ -n "${HALLUCINATE_HOOK_LOADED:-}" ]; then
|
|
4
|
+
return 0 2>/dev/null || exit 0
|
|
5
|
+
fi
|
|
6
|
+
HALLUCINATE_HOOK_LOADED=1
|
|
7
|
+
|
|
8
|
+
: "${HALLUCINATE_STREAM_FILE:=/tmp/hallucinate_stream.json}"
|
|
9
|
+
|
|
10
|
+
HALLUCINATE_SESSION_ID="${HALLUCINATE_SESSION_ID:-$$}"
|
|
11
|
+
HALLUCINATE_STATE_FILE="/tmp/hallucinate_state_${HALLUCINATE_SESSION_ID}.target"
|
|
12
|
+
: > "$HALLUCINATE_STATE_FILE"
|
|
13
|
+
|
|
14
|
+
exec {HALLUCINATE_ORIG_STDERR_FD}>&2
|
|
15
|
+
|
|
16
|
+
_hallucinate_json_escape() {
|
|
17
|
+
local s="$1"
|
|
18
|
+
s=${s//\\/\\\\}
|
|
19
|
+
s=${s//\"/\\\"}
|
|
20
|
+
s=${s//$'\n'/\\n}
|
|
21
|
+
s=${s//$'\r'/\\r}
|
|
22
|
+
s=${s//$'\t'/\\t}
|
|
23
|
+
printf '%s' "$s"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_hallucinate_path_within_workspace() {
|
|
27
|
+
if [ -z "${HALLUCINATE_WORKSPACE_ROOT:-}" ]; then
|
|
28
|
+
return 0
|
|
29
|
+
fi
|
|
30
|
+
case "$PWD/" in
|
|
31
|
+
"$HALLUCINATE_WORKSPACE_ROOT"/|"$HALLUCINATE_WORKSPACE_ROOT"/*)
|
|
32
|
+
return 0
|
|
33
|
+
;;
|
|
34
|
+
*)
|
|
35
|
+
return 1
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_hallucinate_log_event() {
|
|
41
|
+
local exit_code="$1"
|
|
42
|
+
local cmd="$2"
|
|
43
|
+
local stderr_text="$3"
|
|
44
|
+
local ts
|
|
45
|
+
ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
|
46
|
+
local actor="${HALLUCINATE_ACTOR:-human}"
|
|
47
|
+
|
|
48
|
+
_hallucinate_path_within_workspace || return 0
|
|
49
|
+
|
|
50
|
+
mkdir -p "$(dirname "$HALLUCINATE_STREAM_FILE")"
|
|
51
|
+
|
|
52
|
+
local cmd_esc stderr_esc cwd_esc workspace_esc
|
|
53
|
+
cmd_esc="$(_hallucinate_json_escape "$cmd")"
|
|
54
|
+
stderr_esc="$(_hallucinate_json_escape "$stderr_text")"
|
|
55
|
+
cwd_esc="$(_hallucinate_json_escape "$PWD")"
|
|
56
|
+
workspace_esc="$(_hallucinate_json_escape "${HALLUCINATE_WORKSPACE_ROOT:-}")"
|
|
57
|
+
|
|
58
|
+
printf '{"ts":"%s","shell":"%s","actor":"%s","session":%s,"pid":%s,"cwd":"%s","workspace_root":"%s","exit_code":%s,"command":"%s","stderr":"%s"}\n' \
|
|
59
|
+
"$ts" "${ZSH_VERSION:+zsh}${BASH_VERSION:+bash}" "$actor" "$HALLUCINATE_SESSION_ID" "$$" "$cwd_esc" "$workspace_esc" "$exit_code" "$cmd_esc" "$stderr_esc" \
|
|
60
|
+
>> "$HALLUCINATE_STREAM_FILE"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_hallucinate_stderr_router() {
|
|
64
|
+
local line target
|
|
65
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
66
|
+
printf '%s\n' "$line" >&"$HALLUCINATE_ORIG_STDERR_FD"
|
|
67
|
+
target="$(cat "$HALLUCINATE_STATE_FILE" 2>/dev/null)"
|
|
68
|
+
if [ -n "$target" ]; then
|
|
69
|
+
printf '%s\n' "$line" >> "$target"
|
|
70
|
+
fi
|
|
71
|
+
done
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
exec 2> >(_hallucinate_stderr_router)
|
|
75
|
+
|
|
76
|
+
if [ -n "${ZSH_VERSION:-}" ]; then
|
|
77
|
+
autoload -Uz add-zsh-hook 2>/dev/null || true
|
|
78
|
+
|
|
79
|
+
_hallucinate_zsh_preexec() {
|
|
80
|
+
HALLUCINATE_LAST_CMD="$1"
|
|
81
|
+
HALLUCINATE_ERR_FILE="/tmp/hallucinate_stderr_${HALLUCINATE_SESSION_ID}.log"
|
|
82
|
+
: > "$HALLUCINATE_ERR_FILE"
|
|
83
|
+
printf '%s' "$HALLUCINATE_ERR_FILE" > "$HALLUCINATE_STATE_FILE"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_hallucinate_zsh_precmd() {
|
|
87
|
+
local exit_code="$?"
|
|
88
|
+
local stderr_text=""
|
|
89
|
+
|
|
90
|
+
printf '' > "$HALLUCINATE_STATE_FILE"
|
|
91
|
+
if [ -n "${HALLUCINATE_ERR_FILE:-}" ] && [ -f "$HALLUCINATE_ERR_FILE" ]; then
|
|
92
|
+
stderr_text="$(cat "$HALLUCINATE_ERR_FILE")"
|
|
93
|
+
rm -f "$HALLUCINATE_ERR_FILE"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
if [ -n "${HALLUCINATE_LAST_CMD:-}" ]; then
|
|
97
|
+
_hallucinate_log_event "$exit_code" "$HALLUCINATE_LAST_CMD" "$stderr_text"
|
|
98
|
+
HALLUCINATE_LAST_CMD=""
|
|
99
|
+
fi
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
add-zsh-hook preexec _hallucinate_zsh_preexec
|
|
103
|
+
add-zsh-hook precmd _hallucinate_zsh_precmd
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
if [ -n "${BASH_VERSION:-}" ]; then
|
|
107
|
+
HALLUCINATE_BASH_IN_CMD=0
|
|
108
|
+
HALLUCINATE_BASH_IN_PROMPT=0
|
|
109
|
+
|
|
110
|
+
_hallucinate_bash_preexec() {
|
|
111
|
+
[ "${HALLUCINATE_BASH_IN_PROMPT:-0}" -eq 1 ] && return 0
|
|
112
|
+
|
|
113
|
+
if [ "${HALLUCINATE_BASH_IN_CMD:-0}" -eq 0 ]; then
|
|
114
|
+
HALLUCINATE_BASH_IN_CMD=1
|
|
115
|
+
HALLUCINATE_LAST_CMD="$(history 1 | sed 's/^ *[0-9][0-9]* *//')"
|
|
116
|
+
HALLUCINATE_ERR_FILE="/tmp/hallucinate_stderr_${HALLUCINATE_SESSION_ID}.log"
|
|
117
|
+
: > "$HALLUCINATE_ERR_FILE"
|
|
118
|
+
printf '%s' "$HALLUCINATE_ERR_FILE" > "$HALLUCINATE_STATE_FILE"
|
|
119
|
+
fi
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_hallucinate_bash_precmd() {
|
|
123
|
+
local exit_code="$?"
|
|
124
|
+
local stderr_text=""
|
|
125
|
+
|
|
126
|
+
HALLUCINATE_BASH_IN_PROMPT=1
|
|
127
|
+
|
|
128
|
+
if [ "${HALLUCINATE_BASH_IN_CMD:-0}" -eq 1 ]; then
|
|
129
|
+
printf '' > "$HALLUCINATE_STATE_FILE"
|
|
130
|
+
if [ -n "${HALLUCINATE_ERR_FILE:-}" ] && [ -f "$HALLUCINATE_ERR_FILE" ]; then
|
|
131
|
+
stderr_text="$(cat "$HALLUCINATE_ERR_FILE")"
|
|
132
|
+
rm -f "$HALLUCINATE_ERR_FILE"
|
|
133
|
+
fi
|
|
134
|
+
_hallucinate_log_event "$exit_code" "${HALLUCINATE_LAST_CMD:-}" "$stderr_text"
|
|
135
|
+
HALLUCINATE_BASH_IN_CMD=0
|
|
136
|
+
HALLUCINATE_LAST_CMD=""
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
HALLUCINATE_BASH_IN_PROMPT=0
|
|
140
|
+
return "$exit_code"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
trap '_hallucinate_bash_preexec' DEBUG
|
|
144
|
+
|
|
145
|
+
if [ -n "${PROMPT_COMMAND:-}" ]; then
|
|
146
|
+
PROMPT_COMMAND="_hallucinate_bash_precmd; ${PROMPT_COMMAND}"
|
|
147
|
+
else
|
|
148
|
+
PROMPT_COMMAND="_hallucinate_bash_precmd"
|
|
149
|
+
fi
|
|
150
|
+
fi
|