@mariozechner/pi-coding-agent 0.6.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.
Files changed (78) hide show
  1. package/README.md +485 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +21 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/export-html.d.ts +7 -0
  7. package/dist/export-html.d.ts.map +1 -0
  8. package/dist/export-html.js +650 -0
  9. package/dist/export-html.js.map +1 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +4 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/main.d.ts +2 -0
  15. package/dist/main.d.ts.map +1 -0
  16. package/dist/main.js +514 -0
  17. package/dist/main.js.map +1 -0
  18. package/dist/session-manager.d.ts +70 -0
  19. package/dist/session-manager.d.ts.map +1 -0
  20. package/dist/session-manager.js +323 -0
  21. package/dist/session-manager.js.map +1 -0
  22. package/dist/tools/bash.d.ts +7 -0
  23. package/dist/tools/bash.d.ts.map +1 -0
  24. package/dist/tools/bash.js +130 -0
  25. package/dist/tools/bash.js.map +1 -0
  26. package/dist/tools/edit.d.ts +9 -0
  27. package/dist/tools/edit.d.ts.map +1 -0
  28. package/dist/tools/edit.js +207 -0
  29. package/dist/tools/edit.js.map +1 -0
  30. package/dist/tools/index.d.ts +19 -0
  31. package/dist/tools/index.d.ts.map +1 -0
  32. package/dist/tools/index.js +10 -0
  33. package/dist/tools/index.js.map +1 -0
  34. package/dist/tools/read.d.ts +9 -0
  35. package/dist/tools/read.d.ts.map +1 -0
  36. package/dist/tools/read.js +165 -0
  37. package/dist/tools/read.js.map +1 -0
  38. package/dist/tools/write.d.ts +8 -0
  39. package/dist/tools/write.d.ts.map +1 -0
  40. package/dist/tools/write.js +81 -0
  41. package/dist/tools/write.js.map +1 -0
  42. package/dist/tui/assistant-message.d.ts +11 -0
  43. package/dist/tui/assistant-message.d.ts.map +1 -0
  44. package/dist/tui/assistant-message.js +53 -0
  45. package/dist/tui/assistant-message.js.map +1 -0
  46. package/dist/tui/custom-editor.d.ts +10 -0
  47. package/dist/tui/custom-editor.d.ts.map +1 -0
  48. package/dist/tui/custom-editor.js +24 -0
  49. package/dist/tui/custom-editor.js.map +1 -0
  50. package/dist/tui/footer.d.ts +11 -0
  51. package/dist/tui/footer.d.ts.map +1 -0
  52. package/dist/tui/footer.js +101 -0
  53. package/dist/tui/footer.js.map +1 -0
  54. package/dist/tui/model-selector.d.ts +23 -0
  55. package/dist/tui/model-selector.d.ts.map +1 -0
  56. package/dist/tui/model-selector.js +157 -0
  57. package/dist/tui/model-selector.js.map +1 -0
  58. package/dist/tui/session-selector.d.ts +37 -0
  59. package/dist/tui/session-selector.d.ts.map +1 -0
  60. package/dist/tui/session-selector.js +176 -0
  61. package/dist/tui/session-selector.js.map +1 -0
  62. package/dist/tui/thinking-selector.d.ts +11 -0
  63. package/dist/tui/thinking-selector.d.ts.map +1 -0
  64. package/dist/tui/thinking-selector.js +48 -0
  65. package/dist/tui/thinking-selector.js.map +1 -0
  66. package/dist/tui/tool-execution.d.ts +26 -0
  67. package/dist/tui/tool-execution.d.ts.map +1 -0
  68. package/dist/tui/tool-execution.js +246 -0
  69. package/dist/tui/tool-execution.js.map +1 -0
  70. package/dist/tui/tui-renderer.d.ts +44 -0
  71. package/dist/tui/tui-renderer.d.ts.map +1 -0
  72. package/dist/tui/tui-renderer.js +539 -0
  73. package/dist/tui/tui-renderer.js.map +1 -0
  74. package/dist/tui/user-message.d.ts +9 -0
  75. package/dist/tui/user-message.d.ts.map +1 -0
  76. package/dist/tui/user-message.js +18 -0
  77. package/dist/tui/user-message.js.map +1 -0
  78. package/package.json +53 -0
package/README.md ADDED
@@ -0,0 +1,485 @@
1
+ # pi
2
+
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
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @mariozechner/pi-coding-agent
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # Set your API key (see API Keys section)
15
+ export ANTHROPIC_API_KEY=sk-ant-...
16
+
17
+ # Start the interactive CLI
18
+ pi
19
+ ```
20
+
21
+ Once in the CLI, you can chat with the AI:
22
+
23
+ ```
24
+ You: Create a simple Express server in src/server.ts
25
+ ```
26
+
27
+ The agent will use its tools to read, write, and edit files as needed, and execute commands via Bash.
28
+
29
+ ## API Keys
30
+
31
+ The CLI supports multiple LLM providers. Set the appropriate environment variable for your chosen provider:
32
+
33
+ ```bash
34
+ # Anthropic (Claude)
35
+ export ANTHROPIC_API_KEY=sk-ant-...
36
+ # Or use OAuth token (retrieved via: claude setup-token)
37
+ export ANTHROPIC_OAUTH_TOKEN=...
38
+
39
+ # OpenAI (GPT)
40
+ export OPENAI_API_KEY=sk-...
41
+
42
+ # Google (Gemini)
43
+ export GEMINI_API_KEY=...
44
+
45
+ # Groq
46
+ export GROQ_API_KEY=gsk_...
47
+
48
+ # Cerebras
49
+ export CEREBRAS_API_KEY=csk-...
50
+
51
+ # xAI (Grok)
52
+ export XAI_API_KEY=xai-...
53
+
54
+ # OpenRouter
55
+ export OPENROUTER_API_KEY=sk-or-...
56
+
57
+ # ZAI
58
+ export ZAI_API_KEY=...
59
+ ```
60
+
61
+ If no API key is set, the CLI will prompt you to configure one on first run.
62
+
63
+ ## Slash Commands
64
+
65
+ The CLI supports several commands to control its behavior:
66
+
67
+ ### /model
68
+
69
+ Switch models mid-session. Opens an interactive selector where you can type to search (by provider or model name), use arrow keys to navigate, Enter to select, or Escape to cancel.
70
+
71
+ ### /thinking
72
+
73
+ Adjust thinking/reasoning level for supported models (Claude Sonnet 4, GPT-5, Gemini 2.5). Opens an interactive selector where you can use arrow keys to navigate, Enter to select, or Escape to cancel.
74
+
75
+ ### /export [filename]
76
+
77
+ Export the current session to a self-contained HTML file:
78
+
79
+ ```
80
+ /export # Auto-generates filename
81
+ /export my-session.html # Custom filename
82
+ ```
83
+
84
+ The HTML file includes the full conversation with syntax highlighting and is viewable in any browser.
85
+
86
+ ### /session
87
+
88
+ Show session information and statistics:
89
+
90
+ ```
91
+ /session
92
+ ```
93
+
94
+ Displays:
95
+ - Session file path and ID
96
+ - Message counts (user, assistant, total)
97
+ - Token usage (input, output, cache read/write, total)
98
+ - Total cost (if available)
99
+
100
+ ## Editor Features
101
+
102
+ The interactive input editor includes several productivity features:
103
+
104
+ ### Path Completion
105
+
106
+ Press **Tab** to autocomplete file and directory paths:
107
+ - Works with relative paths: `./src/` + Tab → complete files in src/
108
+ - Works with parent directories: `../../` + Tab → navigate up and complete
109
+ - Works with home directory: `~/Des` + Tab → `~/Desktop/`
110
+ - Use **Up/Down arrows** to navigate completion suggestions
111
+ - Press **Enter** to select a completion
112
+ - Shows matching files and directories as you type
113
+
114
+ ### File Drag & Drop
115
+
116
+ Drag files from your OS file explorer (Finder on macOS, Explorer on Windows) directly onto the terminal. The file path will be automatically inserted into the editor. Works great with screenshots from macOS screenshot tool.
117
+
118
+ ### Multi-line Paste
119
+
120
+ Paste multiple lines of text (e.g., code snippets, logs) and they'll be automatically coalesced into a compact `[paste #123 <N> lines]` reference in the editor. The full content is still sent to the model.
121
+
122
+ ### Keyboard Shortcuts
123
+
124
+ - **Ctrl+K**: Delete current line
125
+ - **Ctrl+C**: Clear editor (first press) / Exit pi (second press)
126
+ - **Tab**: Path completion
127
+ - **Enter**: Send message
128
+ - **Shift+Enter**: Insert new line (multi-line input)
129
+ - **Arrow keys**: Move cursor
130
+ - **Ctrl+A** / **Home** / **Cmd+Left** (macOS): Jump to start of line
131
+ - **Ctrl+E** / **End** / **Cmd+Right** (macOS): Jump to end of line
132
+
133
+ ## Project Context Files
134
+
135
+ The agent automatically loads context from `AGENT.md` or `CLAUDE.md` files at the start of new sessions (not when continuing/resuming). These files are loaded in hierarchical order to support both global preferences and monorepo structures.
136
+
137
+ ### File Locations
138
+
139
+ Context files are loaded in this order:
140
+
141
+ 1. **Global context**: `~/.pi/agent/AGENT.md` or `CLAUDE.md`
142
+ - Applies to all your coding sessions
143
+ - Great for personal coding preferences and workflows
144
+
145
+ 2. **Parent directories** (top-most first down to current directory)
146
+ - Walks up from current directory to filesystem root
147
+ - Each directory can have its own `AGENT.md` or `CLAUDE.md`
148
+ - Perfect for monorepos with shared context at higher levels
149
+
150
+ 3. **Current directory**: Your project's `AGENT.md` or `CLAUDE.md`
151
+ - Most specific context, loaded last
152
+ - Overwrites or extends parent/global context
153
+
154
+ **File preference**: In each directory, `AGENT.md` is preferred over `CLAUDE.md` if both exist.
155
+
156
+ ### What to Include
157
+
158
+ Context files are useful for:
159
+ - Project-specific instructions and guidelines
160
+ - Common bash commands and workflows
161
+ - Architecture documentation
162
+ - Coding conventions and style guides
163
+ - Dependencies and setup information
164
+ - Testing instructions
165
+ - Repository etiquette (branch naming, merge vs. rebase, etc.)
166
+
167
+ ### Example
168
+
169
+ ```markdown
170
+ # Common Commands
171
+ - npm run build: Build the project
172
+ - npm test: Run tests
173
+
174
+ # Code Style
175
+ - Use TypeScript strict mode
176
+ - Prefer async/await over promises
177
+
178
+ # Workflow
179
+ - Always run tests before committing
180
+ - Update CHANGELOG.md for user-facing changes
181
+ ```
182
+
183
+ All context files are automatically included in the system prompt at session start, along with the current date/time and working directory. This ensures the AI has complete project context from the very first message.
184
+
185
+ ## Image Support
186
+
187
+ Send images to vision-capable models by providing file paths:
188
+
189
+ ```
190
+ You: What is in this screenshot? /path/to/image.png
191
+ ```
192
+
193
+ Supported formats: `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`
194
+
195
+ The image will be automatically encoded and sent with your message. JPEG and PNG are supported across all vision models. Other formats may only be supported by some models.
196
+
197
+ ## Session Management
198
+
199
+ Sessions are automatically saved in `~/.pi/agent/sessions/` organized by working directory. Each session is stored as a JSONL file with a unique timestamp-based ID.
200
+
201
+ To continue the most recent session:
202
+
203
+ ```bash
204
+ pi --continue
205
+ # or
206
+ pi -c
207
+ ```
208
+
209
+ To browse and select from past sessions:
210
+
211
+ ```bash
212
+ pi --resume
213
+ # or
214
+ pi -r
215
+ ```
216
+
217
+ This opens an interactive session selector where you can:
218
+ - Type to search through session messages
219
+ - Use arrow keys to navigate the list
220
+ - Press Enter to resume a session
221
+ - Press Escape to cancel
222
+
223
+ Sessions include all conversation messages, tool calls and results, model switches, and thinking level changes.
224
+
225
+ To run without saving a session (ephemeral mode):
226
+
227
+ ```bash
228
+ pi --no-session
229
+ ```
230
+
231
+ To use a specific session file instead of auto-generating one:
232
+
233
+ ```bash
234
+ pi --session /path/to/my-session.jsonl
235
+ ```
236
+
237
+ ## CLI Options
238
+
239
+ ```bash
240
+ pi [options] [messages...]
241
+ ```
242
+
243
+ ### Options
244
+
245
+ **--provider <name>**
246
+ Provider name. Available: `anthropic`, `openai`, `google`, `xai`, `groq`, `cerebras`, `openrouter`, `zai`. Default: `anthropic`
247
+
248
+ **--model <id>**
249
+ Model ID. Default: `claude-sonnet-4-5`
250
+
251
+ **--api-key <key>**
252
+ API key (overrides environment variables)
253
+
254
+ **--system-prompt <text|file>**
255
+ Custom system prompt. Can be:
256
+ - Inline text: `--system-prompt "You are a helpful assistant"`
257
+ - File path: `--system-prompt ./my-prompt.txt`
258
+
259
+ If the argument is a valid file path, the file contents will be used as the system prompt. Otherwise, the text is used directly. Project context files and datetime are automatically appended.
260
+
261
+ **--mode <mode>**
262
+ Output mode for non-interactive usage. Options:
263
+ - `text` (default): Output only the final assistant message text
264
+ - `json`: Stream all agent events as JSON (one event per line). Events are emitted by `@mariozechner/pi-agent` and include message updates, tool executions, and completions
265
+ - `rpc`: JSON mode plus stdin listener for headless operation. Send JSON commands on stdin: `{"type":"prompt","message":"..."}` or `{"type":"abort"}`. See [test/rpc-example.ts](test/rpc-example.ts) for a complete example
266
+
267
+ **--no-session**
268
+ Don't save session (ephemeral mode)
269
+
270
+ **--session <path>**
271
+ Use specific session file path instead of auto-generating one
272
+
273
+ **--continue, -c**
274
+ Continue the most recent session
275
+
276
+ **--resume, -r**
277
+ Select a session to resume (opens interactive selector)
278
+
279
+ **--help, -h**
280
+ Show help message
281
+
282
+ ### Examples
283
+
284
+ ```bash
285
+ # Start interactive mode
286
+ pi
287
+
288
+ # Single message mode (text output)
289
+ pi "List all .ts files in src/"
290
+
291
+ # JSON mode - stream all agent events
292
+ pi --mode json "List all .ts files in src/"
293
+
294
+ # RPC mode - headless operation (see test/rpc-example.ts)
295
+ pi --mode rpc --no-session
296
+ # Then send JSON on stdin:
297
+ # {"type":"prompt","message":"List all .ts files"}
298
+ # {"type":"abort"}
299
+
300
+ # Continue previous session
301
+ pi -c "What did we discuss?"
302
+
303
+ # Use different model
304
+ pi --provider openai --model gpt-4o "Help me refactor this code"
305
+ ```
306
+
307
+ ## Tools
308
+
309
+ ### Built-in Tools
310
+
311
+ The agent has access to four core tools for working with your codebase:
312
+
313
+ **read**
314
+ Read file contents. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, defaults to first 2000 lines. Use offset/limit parameters for large files. Lines longer than 2000 characters are truncated.
315
+
316
+ **write**
317
+ Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.
318
+
319
+ **edit**
320
+ Edit a file by replacing exact text. The oldText must match exactly (including whitespace). Use this for precise, surgical edits. Returns an error if the text appears multiple times or isn't found.
321
+
322
+ **bash**
323
+ Execute a bash command in the current working directory. Returns stdout and stderr. Commands run with a 30 second timeout.
324
+
325
+ ### MCP & Adding Your Own Tools
326
+
327
+ **pi does and will not support MCP.** Instead, it relies on the four built-in tools above and assumes the agent can invoke pre-existing CLI tools or write them on the fly as needed.
328
+
329
+ **Here's the gist:**
330
+
331
+ 1. Create a simple CLI tool (any language, any executable)
332
+ 2. Write a concise README.md describing what it does and how to use it
333
+ 3. Tell the agent to read that README
334
+
335
+ **Minimal example:**
336
+
337
+ `~/agent-tools/screenshot/README.md`:
338
+ ```markdown
339
+ # Screenshot Tool
340
+
341
+ Takes a screenshot of your main display.
342
+
343
+ ## Usage
344
+ ```bash
345
+ screenshot.sh
346
+ ```
347
+
348
+ Returns the path to the saved PNG file.
349
+ ```
350
+
351
+ `~/agent-tools/screenshot/screenshot.sh`:
352
+ ```bash
353
+ #!/bin/bash
354
+ screencapture -x /tmp/screenshot-$(date +%s).png
355
+ ls -t /tmp/screenshot-*.png | head -1
356
+ ```
357
+
358
+ **In your session:**
359
+ ```
360
+ You: Read ~/agent-tools/screenshot/README.md and use that tool to take a screenshot
361
+ ```
362
+
363
+ The agent will read the README, understand the tool, and invoke it via bash as needed. If you need a new tool, ask the agent to write it for you.
364
+
365
+ You can also reference tool READMEs in your `AGENT.md` files to make them automatically available:
366
+ - Global: `~/.pi/agent/AGENT.md` - available in all sessions
367
+ - Project-specific: `./AGENT.md` - available in this project
368
+
369
+ **Real-world example:**
370
+
371
+ The [exa-search](https://github.com/badlogic/exa-search) tools provide web search capabilities via the Exa API. Built by the agent itself in ~2 minutes. Far from perfect, but functional. Just tell your agent: "Read ~/agent-tools/exa-search/README.md and search for X".
372
+
373
+ For a detailed walkthrough with more examples, and the reasons for and benefits of this decision, see: https://mariozechner.at/posts/2025-11-02-what-if-you-dont-need-mcp/
374
+
375
+ ## Security (YOLO by default)
376
+
377
+ This agent runs in full YOLO mode and assumes you know what you're doing. It has unrestricted access to your filesystem and can execute any command without permission checks or safety rails.
378
+
379
+ **What this means:**
380
+ - No permission prompts for file operations or commands
381
+ - No pre-checking of bash commands for malicious content
382
+ - Full filesystem access - can read, write, or delete anything
383
+ - Can execute any command with your user privileges
384
+
385
+ **Why:**
386
+ - Permission systems add massive friction while being easily circumvented
387
+ - Pre-checking tools for "dangerous" patterns introduces latency, false positives, and is ineffective
388
+
389
+ **Prompt injection risks:**
390
+ - By default, pi has no web search or fetch tool
391
+ - However, it can use `curl` or read files from disk
392
+ - Both provide ample surface area for prompt injection attacks
393
+ - Malicious content in files or command outputs can influence behavior
394
+
395
+ **Mitigations:**
396
+ - Run pi inside a container if you're uncomfortable with full access
397
+ - Use a different tool if you need guardrails
398
+ - Don't use pi on systems with sensitive data you can't afford to lose
399
+ - Fork pi and add all of the above
400
+
401
+ This is how I want it to work and I'm not likely to change my stance on this.
402
+
403
+ Use at your own risk.
404
+
405
+ ## Sub-Agents
406
+
407
+ **pi does not and will not support sub-agents as a built-in feature.** If the agent needs to delegate work, it can:
408
+
409
+ 1. Spawn another instance of itself via the `pi` CLI command
410
+ 2. Write a custom tool with a README.md that describes how to invoke pi for specific tasks
411
+
412
+ **Why no built-in sub-agents:**
413
+
414
+ Context transfer between agents is generally poor. Information gets lost, compressed, or misrepresented when passed through agent boundaries. Direct execution with full context is more effective than delegation with summarized context.
415
+
416
+ If you need parallel work on independent tasks, manually run multiple `pi` sessions in different terminal tabs. You're the orchestrator.
417
+
418
+ ## To-Dos
419
+
420
+ **pi does not and will not support built-in to-dos.** In my experience, to-do lists generally confuse models more than they help.
421
+
422
+ If you need task tracking, make it stateful by writing to a file:
423
+
424
+ ```markdown
425
+ # TODO.md
426
+
427
+ - [x] Implement user authentication
428
+ - [x] Add database migrations
429
+ - [ ] Write API documentation
430
+ - [ ] Add rate limiting
431
+ ```
432
+
433
+ The agent can read and update this file as needed. Using checkboxes keeps track of what's done and what remains. Simple, visible, and under your control.
434
+
435
+ ## Planning
436
+
437
+ **pi does not and will not have a built-in planning mode.** Telling the agent to think through a problem together with you, without modifying files or executing commands, is generally sufficient.
438
+
439
+ If you need persistent planning across sessions, write it to a file:
440
+
441
+ ```markdown
442
+ # PLAN.md
443
+
444
+ ## Goal
445
+ Refactor authentication system to support OAuth
446
+
447
+ ## Approach
448
+ 1. Research OAuth 2.0 flows
449
+ 2. Design token storage schema
450
+ 3. Implement authorization server endpoints
451
+ 4. Update client-side login flow
452
+ 5. Add tests
453
+
454
+ ## Current Step
455
+ Working on step 3 - authorization endpoints
456
+ ```
457
+
458
+ The agent can read, update, and reference the plan as it works. Unlike ephemeral planning modes that only exist within a session, file-based plans persist and can be versioned with your code.
459
+
460
+ ## Background Bash
461
+
462
+ **pi does not and will not implement background bash execution.** Instead, tell the agent to use `tmux` or something like [tterminal-cp](https://mariozechner.at/posts/2025-08-15-mcp-vs-cli/). Bonus points: you can watch the agent interact with a CLI like a debugger and even intervene if necessary.
463
+
464
+ ## Planned Features
465
+
466
+ Things that might happen eventually:
467
+
468
+ - **Custom/local models**: Support for Ollama, llama.cpp, vLLM, SGLang, LM Studio via JSON config file
469
+ - **Auto-compaction**: Currently, watch the context percentage at the bottom. When it approaches 80%, either:
470
+ - Ask the agent to write a summary .md file you can load in a new session
471
+ - Switch to a model with bigger context (e.g., Gemini) using `/model` and either continue with that model, or let it summarize the session to a .md file to be loaded in a new session
472
+ - **Message queuing**: Core engine supports it, just needs UI wiring
473
+ - **Better RPC mode docs**: It works, you'll figure it out (see `test/rpc-example.ts`)
474
+ - **Beter Markdown and tool call/result rendering**
475
+ - **Full details mode**: use `/export out.html` for now
476
+ - **More flicker than Claude Code**: One day...
477
+
478
+ ## License
479
+
480
+ MIT
481
+
482
+ ## See Also
483
+
484
+ - [@mariozechner/pi-ai](https://www.npmjs.com/package/@mariozechner/pi-ai): Core LLM toolkit with multi-provider support
485
+ - [@mariozechner/pi-agent](https://www.npmjs.com/package/@mariozechner/pi-agent): Agent framework with tool execution
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n\n// Suppress punycode deprecation warning from dependencies\n// This warning comes from old dependencies still using the deprecated punycode module\nconst originalEmit = process.emit;\n// @ts-expect-error - Monkey-patch emit to filter warnings\nprocess.emit = (event, ...args) => {\n\tif (event === \"warning\") {\n\t\tconst warning = args[0] as any;\n\t\tif (warning?.name === \"DeprecationWarning\" && warning?.code === \"DEP0040\") {\n\t\t\treturn false; // Suppress punycode deprecation\n\t\t}\n\t}\n\t// @ts-expect-error - Call original with event and args\n\treturn originalEmit.apply(process, [event, ...args]);\n};\n\nimport { main } from \"./main.js\";\n\nmain(process.argv.slice(2)).catch((err) => {\n\tconsole.error(err);\n\tprocess.exit(1);\n});\n"]}
package/dist/cli.js ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ // Suppress punycode deprecation warning from dependencies
3
+ // This warning comes from old dependencies still using the deprecated punycode module
4
+ const originalEmit = process.emit;
5
+ // @ts-expect-error - Monkey-patch emit to filter warnings
6
+ process.emit = (event, ...args) => {
7
+ if (event === "warning") {
8
+ const warning = args[0];
9
+ if (warning?.name === "DeprecationWarning" && warning?.code === "DEP0040") {
10
+ return false; // Suppress punycode deprecation
11
+ }
12
+ }
13
+ // @ts-expect-error - Call original with event and args
14
+ return originalEmit.apply(process, [event, ...args]);
15
+ };
16
+ import { main } from "./main.js";
17
+ main(process.argv.slice(2)).catch((err) => {
18
+ console.error(err);
19
+ process.exit(1);
20
+ });
21
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,0DAA0D;AAC1D,sFAAsF;AACtF,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;AAClC,0DAA0D;AAC1D,OAAO,CAAC,IAAI,GAAG,CAAC,KAAK,EAAE,GAAG,IAAI,EAAE,EAAE,CAAC;IAClC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,IAAI,CAAC,CAAC,CAAQ,CAAC;QAC/B,IAAI,OAAO,EAAE,IAAI,KAAK,oBAAoB,IAAI,OAAO,EAAE,IAAI,KAAK,SAAS,EAAE,CAAC;YAC3E,OAAO,KAAK,CAAC,CAAC,gCAAgC;QAC/C,CAAC;IACF,CAAC;IACD,uDAAuD;IACvD,OAAO,YAAY,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC;AAAA,CACrD,CAAC;AAEF,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC;IAC1C,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAAA,CAChB,CAAC,CAAC","sourcesContent":["#!/usr/bin/env node\n\n// Suppress punycode deprecation warning from dependencies\n// This warning comes from old dependencies still using the deprecated punycode module\nconst originalEmit = process.emit;\n// @ts-expect-error - Monkey-patch emit to filter warnings\nprocess.emit = (event, ...args) => {\n\tif (event === \"warning\") {\n\t\tconst warning = args[0] as any;\n\t\tif (warning?.name === \"DeprecationWarning\" && warning?.code === \"DEP0040\") {\n\t\t\treturn false; // Suppress punycode deprecation\n\t\t}\n\t}\n\t// @ts-expect-error - Call original with event and args\n\treturn originalEmit.apply(process, [event, ...args]);\n};\n\nimport { main } from \"./main.js\";\n\nmain(process.argv.slice(2)).catch((err) => {\n\tconsole.error(err);\n\tprocess.exit(1);\n});\n"]}
@@ -0,0 +1,7 @@
1
+ import type { AgentState } from "@mariozechner/pi-agent";
2
+ import type { SessionManager } from "./session-manager.js";
3
+ /**
4
+ * Export session to a self-contained HTML file matching TUI visual style
5
+ */
6
+ export declare function exportSessionToHtml(sessionManager: SessionManager, state: AgentState, outputPath?: string): string;
7
+ //# sourceMappingURL=export-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"export-html.d.ts","sourceRoot":"","sources":["../src/export-html.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAMzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA0S3D;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,cAAc,EAAE,KAAK,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAqXlH","sourcesContent":["import type { AgentState } from \"@mariozechner/pi-agent\";\nimport type { AssistantMessage, Message, ToolResultMessage, UserMessage } from \"@mariozechner/pi-ai\";\nimport { readFileSync, writeFileSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { basename, dirname, join } from \"path\";\nimport { fileURLToPath } from \"url\";\nimport type { SessionManager } from \"./session-manager.js\";\n\n// Get version from package.json\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\nconst packageJson = JSON.parse(readFileSync(join(__dirname, \"../package.json\"), \"utf-8\"));\nconst VERSION = packageJson.version;\n\n/**\n * TUI Color scheme (matching exact RGB values from TUI components)\n */\nconst COLORS = {\n\t// Backgrounds\n\tuserMessageBg: \"rgb(52, 53, 65)\", // Dark slate\n\ttoolPendingBg: \"rgb(40, 40, 50)\", // Dark blue-gray\n\ttoolSuccessBg: \"rgb(40, 50, 40)\", // Dark green\n\ttoolErrorBg: \"rgb(60, 40, 40)\", // Dark red\n\tbodyBg: \"rgb(24, 24, 30)\", // Very dark background\n\tcontainerBg: \"rgb(30, 30, 36)\", // Slightly lighter container\n\n\t// Text colors (matching chalk colors)\n\ttext: \"rgb(229, 229, 231)\", // Light gray (close to white)\n\ttextDim: \"rgb(161, 161, 170)\", // Dimmed gray\n\tcyan: \"rgb(103, 232, 249)\", // Cyan for paths\n\tgreen: \"rgb(34, 197, 94)\", // Green for success\n\tred: \"rgb(239, 68, 68)\", // Red for errors\n\tyellow: \"rgb(234, 179, 8)\", // Yellow for warnings\n\titalic: \"rgb(161, 161, 170)\", // Gray italic for thinking\n};\n\n/**\n * Escape HTML special characters\n */\nfunction escapeHtml(text: string): string {\n\treturn text\n\t\t.replace(/&/g, \"&amp;\")\n\t\t.replace(/</g, \"&lt;\")\n\t\t.replace(/>/g, \"&gt;\")\n\t\t.replace(/\"/g, \"&quot;\")\n\t\t.replace(/'/g, \"&#039;\");\n}\n\n/**\n * Shorten path with tilde notation\n */\nfunction shortenPath(path: string): string {\n\tconst home = homedir();\n\tif (path.startsWith(home)) {\n\t\treturn \"~\" + path.slice(home.length);\n\t}\n\treturn path;\n}\n\n/**\n * Replace tabs with 3 spaces\n */\nfunction replaceTabs(text: string): string {\n\treturn text.replace(/\\t/g, \" \");\n}\n\n/**\n * Format tool execution matching TUI ToolExecutionComponent\n */\nfunction formatToolExecution(\n\ttoolName: string,\n\targs: any,\n\tresult?: ToolResultMessage,\n): { html: string; bgColor: string } {\n\tlet html = \"\";\n\tconst isError = result?.isError || false;\n\tconst bgColor = result ? (isError ? COLORS.toolErrorBg : COLORS.toolSuccessBg) : COLORS.toolPendingBg;\n\n\t// Get text output from result\n\tconst getTextOutput = (): string => {\n\t\tif (!result) return \"\";\n\t\tconst textBlocks = result.content.filter((c) => c.type === \"text\");\n\t\treturn textBlocks.map((c: any) => c.text).join(\"\\n\");\n\t};\n\n\t// Format based on tool type (matching TUI logic exactly)\n\tif (toolName === \"bash\") {\n\t\tconst command = args?.command || \"\";\n\t\thtml = `<div class=\"tool-command\">$ ${escapeHtml(command || \"...\")}</div>`;\n\n\t\tif (result) {\n\t\t\tconst output = getTextOutput().trim();\n\t\t\tif (output) {\n\t\t\t\tconst lines = output.split(\"\\n\");\n\t\t\t\tconst maxLines = 5;\n\t\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\t\tif (remaining > 0) {\n\t\t\t\t\t// Truncated output - make it expandable\n\t\t\t\t\thtml += '<div class=\"tool-output expandable\" onclick=\"this.classList.toggle(\\'expanded\\')\">';\n\t\t\t\t\thtml += '<div class=\"output-preview\">';\n\t\t\t\t\tfor (const line of displayLines) {\n\t\t\t\t\t\thtml += `<div>${escapeHtml(line)}</div>`;\n\t\t\t\t\t}\n\t\t\t\t\thtml += `<div class=\"expand-hint\">... (${remaining} more lines) - click to expand</div>`;\n\t\t\t\t\thtml += \"</div>\";\n\t\t\t\t\thtml += '<div class=\"output-full\">';\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\thtml += `<div>${escapeHtml(line)}</div>`;\n\t\t\t\t\t}\n\t\t\t\t\thtml += \"</div>\";\n\t\t\t\t\thtml += \"</div>\";\n\t\t\t\t} else {\n\t\t\t\t\t// Short output - show all\n\t\t\t\t\thtml += '<div class=\"tool-output\">';\n\t\t\t\t\tfor (const line of displayLines) {\n\t\t\t\t\t\thtml += `<div>${escapeHtml(line)}</div>`;\n\t\t\t\t\t}\n\t\t\t\t\thtml += \"</div>\";\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else if (toolName === \"read\") {\n\t\tconst path = shortenPath(args?.file_path || args?.path || \"\");\n\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">read</span> <span class=\"tool-path\">${escapeHtml(path || \"...\")}</span></div>`;\n\n\t\tif (result) {\n\t\t\tconst output = getTextOutput();\n\t\t\tconst lines = output.split(\"\\n\");\n\t\t\tconst maxLines = 10;\n\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\tif (remaining > 0) {\n\t\t\t\t// Truncated output - make it expandable\n\t\t\t\thtml += '<div class=\"tool-output expandable\" onclick=\"this.classList.toggle(\\'expanded\\')\">';\n\t\t\t\thtml += '<div class=\"output-preview\">';\n\t\t\t\tfor (const line of displayLines) {\n\t\t\t\t\thtml += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t\t\t\t}\n\t\t\t\thtml += `<div class=\"expand-hint\">... (${remaining} more lines) - click to expand</div>`;\n\t\t\t\thtml += \"</div>\";\n\t\t\t\thtml += '<div class=\"output-full\">';\n\t\t\t\tfor (const line of lines) {\n\t\t\t\t\thtml += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t\t\t\t}\n\t\t\t\thtml += \"</div>\";\n\t\t\t\thtml += \"</div>\";\n\t\t\t} else {\n\t\t\t\t// Short output - show all\n\t\t\t\thtml += '<div class=\"tool-output\">';\n\t\t\t\tfor (const line of displayLines) {\n\t\t\t\t\thtml += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t\t\t\t}\n\t\t\t\thtml += \"</div>\";\n\t\t\t}\n\t\t}\n\t} else if (toolName === \"write\") {\n\t\tconst path = shortenPath(args?.file_path || args?.path || \"\");\n\t\tconst fileContent = args?.content || \"\";\n\t\tconst lines = fileContent ? fileContent.split(\"\\n\") : [];\n\t\tconst totalLines = lines.length;\n\n\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">write</span> <span class=\"tool-path\">${escapeHtml(path || \"...\")}</span>`;\n\t\tif (totalLines > 10) {\n\t\t\thtml += ` <span class=\"line-count\">(${totalLines} lines)</span>`;\n\t\t}\n\t\thtml += \"</div>\";\n\n\t\tif (fileContent) {\n\t\t\tconst maxLines = 10;\n\t\t\tconst displayLines = lines.slice(0, maxLines);\n\t\t\tconst remaining = lines.length - maxLines;\n\n\t\t\tif (remaining > 0) {\n\t\t\t\t// Truncated output - make it expandable\n\t\t\t\thtml += '<div class=\"tool-output expandable\" onclick=\"this.classList.toggle(\\'expanded\\')\">';\n\t\t\t\thtml += '<div class=\"output-preview\">';\n\t\t\t\tfor (const line of displayLines) {\n\t\t\t\t\thtml += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t\t\t\t}\n\t\t\t\thtml += `<div class=\"expand-hint\">... (${remaining} more lines) - click to expand</div>`;\n\t\t\t\thtml += \"</div>\";\n\t\t\t\thtml += '<div class=\"output-full\">';\n\t\t\t\tfor (const line of lines) {\n\t\t\t\t\thtml += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t\t\t\t}\n\t\t\t\thtml += \"</div>\";\n\t\t\t\thtml += \"</div>\";\n\t\t\t} else {\n\t\t\t\t// Short output - show all\n\t\t\t\thtml += '<div class=\"tool-output\">';\n\t\t\t\tfor (const line of displayLines) {\n\t\t\t\t\thtml += `<div>${escapeHtml(replaceTabs(line))}</div>`;\n\t\t\t\t}\n\t\t\t\thtml += \"</div>\";\n\t\t\t}\n\t\t}\n\n\t\tif (result) {\n\t\t\tconst output = getTextOutput().trim();\n\t\t\tif (output) {\n\t\t\t\thtml += `<div class=\"tool-output\"><div>${escapeHtml(output)}</div></div>`;\n\t\t\t}\n\t\t}\n\t} else if (toolName === \"edit\") {\n\t\tconst path = shortenPath(args?.file_path || args?.path || \"\");\n\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">edit</span> <span class=\"tool-path\">${escapeHtml(path || \"...\")}</span></div>`;\n\n\t\t// Show diff if available from result.details.diff\n\t\tif (result?.details?.diff) {\n\t\t\tconst diffLines = result.details.diff.split(\"\\n\");\n\t\t\thtml += '<div class=\"tool-diff\">';\n\t\t\tfor (const line of diffLines) {\n\t\t\t\tif (line.startsWith(\"+\")) {\n\t\t\t\t\thtml += `<div class=\"diff-line-new\">${escapeHtml(line)}</div>`;\n\t\t\t\t} else if (line.startsWith(\"-\")) {\n\t\t\t\t\thtml += `<div class=\"diff-line-old\">${escapeHtml(line)}</div>`;\n\t\t\t\t} else {\n\t\t\t\t\thtml += `<div class=\"diff-line-context\">${escapeHtml(line)}</div>`;\n\t\t\t\t}\n\t\t\t}\n\t\t\thtml += \"</div>\";\n\t\t}\n\n\t\tif (result) {\n\t\t\tconst output = getTextOutput().trim();\n\t\t\tif (output) {\n\t\t\t\thtml += `<div class=\"tool-output\"><div>${escapeHtml(output)}</div></div>`;\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// Generic tool\n\t\thtml = `<div class=\"tool-header\"><span class=\"tool-name\">${escapeHtml(toolName)}</span></div>`;\n\t\thtml += `<div class=\"tool-output\"><pre>${escapeHtml(JSON.stringify(args, null, 2))}</pre></div>`;\n\n\t\tif (result) {\n\t\t\tconst output = getTextOutput();\n\t\t\tif (output) {\n\t\t\t\thtml += `<div class=\"tool-output\"><div>${escapeHtml(output)}</div></div>`;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { html, bgColor };\n}\n\n/**\n * Format a message as HTML (matching TUI component styling)\n */\nfunction formatMessage(message: Message, toolResultsMap: Map<string, ToolResultMessage>): string {\n\tlet html = \"\";\n\n\tif (message.role === \"user\") {\n\t\tconst userMsg = message as UserMessage;\n\t\tlet textContent = \"\";\n\n\t\tif (typeof userMsg.content === \"string\") {\n\t\t\ttextContent = userMsg.content;\n\t\t} else {\n\t\t\tconst textBlocks = userMsg.content.filter((c) => c.type === \"text\");\n\t\t\ttextContent = textBlocks.map((c: any) => c.text).join(\"\");\n\t\t}\n\n\t\tif (textContent.trim()) {\n\t\t\thtml += `<div class=\"user-message\">${escapeHtml(textContent).replace(/\\n/g, \"<br>\")}</div>`;\n\t\t}\n\t} else if (message.role === \"assistant\") {\n\t\tconst assistantMsg = message as AssistantMessage;\n\n\t\t// Render text and thinking content\n\t\tfor (const content of assistantMsg.content) {\n\t\t\tif (content.type === \"text\" && content.text.trim()) {\n\t\t\t\thtml += `<div class=\"assistant-text\">${escapeHtml(content.text.trim()).replace(/\\n/g, \"<br>\")}</div>`;\n\t\t\t} else if (content.type === \"thinking\" && content.thinking.trim()) {\n\t\t\t\thtml += `<div class=\"thinking-text\">${escapeHtml(content.thinking.trim()).replace(/\\n/g, \"<br>\")}</div>`;\n\t\t\t}\n\t\t}\n\n\t\t// Render tool calls with their results\n\t\tfor (const content of assistantMsg.content) {\n\t\t\tif (content.type === \"toolCall\") {\n\t\t\t\tconst toolResult = toolResultsMap.get(content.id);\n\t\t\t\tconst { html: toolHtml, bgColor } = formatToolExecution(content.name, content.arguments, toolResult);\n\t\t\t\thtml += `<div class=\"tool-execution\" style=\"background-color: ${bgColor}\">${toolHtml}</div>`;\n\t\t\t}\n\t\t}\n\n\t\t// Show error/abort status if no tool calls\n\t\tconst hasToolCalls = assistantMsg.content.some((c) => c.type === \"toolCall\");\n\t\tif (!hasToolCalls) {\n\t\t\tif (assistantMsg.stopReason === \"aborted\") {\n\t\t\t\thtml += '<div class=\"error-text\">Aborted</div>';\n\t\t\t} else if (assistantMsg.stopReason === \"error\") {\n\t\t\t\tconst errorMsg = assistantMsg.errorMessage || \"Unknown error\";\n\t\t\t\thtml += `<div class=\"error-text\">Error: ${escapeHtml(errorMsg)}</div>`;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn html;\n}\n\n/**\n * Export session to a self-contained HTML file matching TUI visual style\n */\nexport function exportSessionToHtml(sessionManager: SessionManager, state: AgentState, outputPath?: string): string {\n\tconst sessionFile = sessionManager.getSessionFile();\n\tconst timestamp = new Date().toISOString();\n\n\t// Use session filename + .html if no output path provided\n\tif (!outputPath) {\n\t\tconst sessionBasename = basename(sessionFile, \".jsonl\");\n\t\toutputPath = `${sessionBasename}.html`;\n\t}\n\n\t// Read and parse session data\n\tconst sessionContent = readFileSync(sessionFile, \"utf8\");\n\tconst lines = sessionContent.trim().split(\"\\n\");\n\n\tlet sessionHeader: any = null;\n\tconst messages: Message[] = [];\n\tconst toolResultsMap = new Map<string, ToolResultMessage>();\n\n\tfor (const line of lines) {\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line);\n\t\t\tif (entry.type === \"session\") {\n\t\t\t\tsessionHeader = entry;\n\t\t\t} else if (entry.type === \"message\") {\n\t\t\t\tmessages.push(entry.message);\n\t\t\t\t// Build map of tool call ID to result\n\t\t\t\tif (entry.message.role === \"toolResult\") {\n\t\t\t\t\ttoolResultsMap.set(entry.message.toolCallId, entry.message);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Skip malformed lines\n\t\t}\n\t}\n\n\t// Generate messages HTML\n\tlet messagesHtml = \"\";\n\tfor (const message of messages) {\n\t\tif (message.role !== \"toolResult\") {\n\t\t\t// Skip toolResult messages as they're rendered with their tool calls\n\t\t\tmessagesHtml += formatMessage(message, toolResultsMap);\n\t\t}\n\t}\n\n\t// Generate HTML (matching TUI aesthetic)\n\tconst html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Session Export - ${basename(sessionFile)}</title>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n font-size: 14px;\n line-height: 1.6;\n color: ${COLORS.text};\n background: ${COLORS.bodyBg};\n padding: 24px;\n }\n\n .container {\n max-width: 1200px;\n margin: 0 auto;\n }\n\n .header {\n margin-bottom: 24px;\n padding: 16px;\n background: ${COLORS.containerBg};\n border-radius: 4px;\n }\n\n .header h1 {\n font-size: 16px;\n font-weight: bold;\n margin-bottom: 12px;\n color: ${COLORS.cyan};\n }\n\n .header-info {\n display: flex;\n flex-direction: column;\n gap: 6px;\n font-size: 13px;\n }\n\n .info-item {\n color: ${COLORS.textDim};\n display: flex;\n align-items: baseline;\n }\n\n .info-label {\n font-weight: 600;\n margin-right: 8px;\n min-width: 80px;\n }\n\n .info-value {\n color: ${COLORS.text};\n flex: 1;\n }\n\n .messages {\n display: flex;\n flex-direction: column;\n gap: 16px;\n }\n\n /* User message - matching TUI UserMessageComponent */\n .user-message {\n background: ${COLORS.userMessageBg};\n padding: 12px 16px;\n border-radius: 4px;\n white-space: pre-wrap;\n word-wrap: break-word;\n }\n\n /* Assistant text - matching TUI AssistantMessageComponent */\n .assistant-text {\n padding: 12px 16px;\n white-space: pre-wrap;\n word-wrap: break-word;\n }\n\n /* Thinking text - gray italic */\n .thinking-text {\n padding: 12px 16px;\n color: ${COLORS.italic};\n font-style: italic;\n white-space: pre-wrap;\n word-wrap: break-word;\n }\n\n /* Tool execution - matching TUI ToolExecutionComponent */\n .tool-execution {\n padding: 12px 16px;\n border-radius: 4px;\n margin-top: 8px;\n }\n\n .tool-header {\n font-weight: bold;\n }\n\n .tool-name {\n font-weight: bold;\n }\n\n .tool-path {\n color: ${COLORS.cyan};\n }\n\n .line-count {\n color: ${COLORS.textDim};\n }\n\n .tool-command {\n font-weight: bold;\n }\n\n .tool-output {\n margin-top: 12px;\n color: ${COLORS.textDim};\n white-space: pre-wrap;\n font-family: inherit;\n }\n\n .tool-output > div {\n line-height: 1.4;\n }\n\n .tool-output pre {\n margin: 0;\n font-family: inherit;\n color: inherit;\n }\n\n /* Expandable tool output */\n .tool-output.expandable {\n cursor: pointer;\n }\n\n .tool-output.expandable:hover {\n opacity: 0.9;\n }\n\n .tool-output.expandable .output-full {\n display: none;\n }\n\n .tool-output.expandable.expanded .output-preview {\n display: none;\n }\n\n .tool-output.expandable.expanded .output-full {\n display: block;\n }\n\n .expand-hint {\n color: ${COLORS.cyan};\n font-style: italic;\n margin-top: 4px;\n }\n\n /* System prompt section */\n .system-prompt {\n background: rgb(60, 55, 40);\n padding: 12px 16px;\n border-radius: 4px;\n margin-bottom: 16px;\n }\n\n .system-prompt-header {\n font-weight: bold;\n color: ${COLORS.yellow};\n margin-bottom: 8px;\n }\n\n .system-prompt-content {\n color: ${COLORS.textDim};\n white-space: pre-wrap;\n word-wrap: break-word;\n font-size: 13px;\n }\n\n .tools-list {\n background: rgb(60, 55, 40);\n padding: 12px 16px;\n border-radius: 4px;\n margin-bottom: 16px;\n }\n\n .tools-header {\n font-weight: bold;\n color: ${COLORS.yellow};\n margin-bottom: 8px;\n }\n\n .tools-content {\n color: ${COLORS.textDim};\n font-size: 13px;\n }\n\n .tool-item {\n margin: 4px 0;\n }\n\n .tool-item-name {\n font-weight: bold;\n color: ${COLORS.text};\n }\n\n /* Diff styling */\n .tool-diff {\n margin-top: 12px;\n font-size: 13px;\n font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;\n overflow-x: auto;\n max-width: 100%;\n }\n\n .diff-line-old {\n color: ${COLORS.red};\n white-space: pre;\n }\n\n .diff-line-new {\n color: ${COLORS.green};\n white-space: pre;\n }\n\n .diff-line-context {\n color: ${COLORS.textDim};\n white-space: pre;\n }\n\n /* Error text */\n .error-text {\n color: ${COLORS.red};\n padding: 12px 16px;\n }\n\n .footer {\n margin-top: 48px;\n padding: 20px;\n text-align: center;\n color: ${COLORS.textDim};\n font-size: 12px;\n }\n\n @media print {\n body {\n background: white;\n color: black;\n }\n .tool-execution {\n border: 1px solid #ddd;\n }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h1>pi v${VERSION}</h1>\n <div class=\"header-info\">\n <div class=\"info-item\">\n <span class=\"info-label\">Session:</span>\n <span class=\"info-value\">${escapeHtml(sessionHeader?.id || \"unknown\")}</span>\n </div>\n <div class=\"info-item\">\n <span class=\"info-label\">Date:</span>\n <span class=\"info-value\">${sessionHeader?.timestamp ? new Date(sessionHeader.timestamp).toLocaleString() : timestamp}</span>\n </div>\n <div class=\"info-item\">\n <span class=\"info-label\">Model:</span>\n <span class=\"info-value\">${escapeHtml(sessionHeader?.model || state.model.id)}</span>\n </div>\n <div class=\"info-item\">\n <span class=\"info-label\">Messages:</span>\n <span class=\"info-value\">${messages.filter((m) => m.role !== \"toolResult\").length}</span>\n </div>\n <div class=\"info-item\">\n <span class=\"info-label\">Directory:</span>\n <span class=\"info-value\">${escapeHtml(shortenPath(sessionHeader?.cwd || process.cwd()))}</span>\n </div>\n <div class=\"info-item\">\n <span class=\"info-label\">Thinking:</span>\n <span class=\"info-value\">${escapeHtml(sessionHeader?.thinkingLevel || state.thinkingLevel)}</span>\n </div>\n </div>\n </div>\n\n <div class=\"system-prompt\">\n <div class=\"system-prompt-header\">System Prompt</div>\n <div class=\"system-prompt-content\">${escapeHtml(sessionHeader?.systemPrompt || state.systemPrompt)}</div>\n </div>\n\n <div class=\"tools-list\">\n <div class=\"tools-header\">Available Tools</div>\n <div class=\"tools-content\">\n ${state.tools\n\t\t\t\t\t\t\t.map(\n\t\t\t\t\t\t\t\t(tool) =>\n\t\t\t\t\t\t\t\t\t`<div class=\"tool-item\"><span class=\"tool-item-name\">${escapeHtml(tool.name)}</span> - ${escapeHtml(tool.description)}</div>`,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.join(\"\")}\n </div>\n </div>\n\n <div class=\"messages\">\n ${messagesHtml}\n </div>\n\n <div class=\"footer\">\n Generated by pi coding-agent on ${new Date().toLocaleString()}\n </div>\n </div>\n</body>\n</html>`;\n\n\t// Write HTML file\n\twriteFileSync(outputPath, html, \"utf8\");\n\n\treturn outputPath;\n}\n"]}