mitsupi 1.1.0 → 1.1.1
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 +5 -0
- package/README.md +1 -3
- package/intercepted-commands/pip +7 -0
- package/intercepted-commands/pip3 +7 -0
- package/intercepted-commands/poetry +3 -0
- package/intercepted-commands/python +50 -0
- package/intercepted-commands/python3 +50 -0
- package/package.json +1 -1
- package/pi-extensions/answer.ts +1 -1
- package/pi-extensions/uv.ts +42 -0
- package/skills/uv/SKILL.md +36 -0
- package/skills/uv/build.md +65 -0
- package/skills/uv/scripts.md +98 -0
- package/pi-extensions/qna.ts +0 -167
- package/skills/improve-skill/SKILL.md +0 -155
- package/skills/improve-skill/scripts/extract-session.js +0 -349
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to agent-stuff are documented here.
|
|
4
4
|
|
|
5
|
+
## 1.1.1
|
|
6
|
+
|
|
7
|
+
* Removed the deprecated `qna` extension.
|
|
8
|
+
* Added `uv` extension and skill for uv integration.
|
|
9
|
+
|
|
5
10
|
## 1.1.0
|
|
6
11
|
|
|
7
12
|
* Added project review guidelines and preserved review state across navigation.
|
package/README.md
CHANGED
|
@@ -16,7 +16,6 @@ All skill files are in the [`skills`](skills) folder:
|
|
|
16
16
|
* [`/web-browser`](skills/web-browser) - Claude Skill for using Puppeteer in a Node environment to browse the web
|
|
17
17
|
* [`/tmux`](skills/tmux) - Claude Skill for driving tmux directly with keystrokes and pane output scraping
|
|
18
18
|
* [`/sentry`](skills/sentry) - Alternative way to access Sentry as a Claude Skill for reading issues
|
|
19
|
-
* [`/improve-skill`](skills/improve-skill) - Claude Skill for analyzing coding agent sessions to improve or create new skills
|
|
20
19
|
* [`/pi-share`](skills/pi-share) - Claude Skill for loading and parsing session transcripts from shittycodingagent.ai
|
|
21
20
|
* [`/anachb`](skills/anachb) - Claude Skill for querying Austrian public transport (VOR AnachB) for departures, routes, and disruptions
|
|
22
21
|
* [`/oebb-scotty`](skills/oebb-scotty) - Claude Skill for Austrian rail travel planning via ÖBB Scotty API
|
|
@@ -26,8 +25,7 @@ All skill files are in the [`skills`](skills) folder:
|
|
|
26
25
|
|
|
27
26
|
Custom extensions for the PI Coding Agent can be found in the [`pi-extensions`](pi-extensions) folder:
|
|
28
27
|
|
|
29
|
-
* [`
|
|
30
|
-
* [`answer.ts`](pi-extensions/answer.ts) - Alternative to `qna.ts` with a custom interactive TUI for answering questions one by one.
|
|
28
|
+
* [`answer.ts`](pi-extensions/answer.ts) - Interactive TUI for answering questions one by one.
|
|
31
29
|
* [`review.ts`](pi-extensions/review.ts) - Code review command inspired by Codex. Supports reviewing uncommitted changes, against a base branch (PR style), specific commits, or with custom instructions. Includes Ctrl+R shortcut.
|
|
32
30
|
* [`loop.ts`](pi-extensions/loop.ts) - Runs a prompt loop for rapid iterative coding with optional auto-continue control.
|
|
33
31
|
* [`files.ts`](pi-extensions/files.ts) - Unified file browser that merges git status (dirty first) with session references, plus reveal/open/edit and diff actions.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Check for disallowed module invocations
|
|
4
|
+
for arg in "$@"; do
|
|
5
|
+
case "$arg" in
|
|
6
|
+
-mpip|-m\ pip|pip)
|
|
7
|
+
echo "Error: 'python -m pip' is disabled. Use uv instead:" >&2
|
|
8
|
+
echo "" >&2
|
|
9
|
+
echo " To install a package for a script: uv run --with PACKAGE python script.py" >&2
|
|
10
|
+
echo " To add a dependency to the project: uv add PACKAGE" >&2
|
|
11
|
+
echo "" >&2
|
|
12
|
+
exit 1
|
|
13
|
+
;;
|
|
14
|
+
-mvenv|-m\ venv|venv)
|
|
15
|
+
echo "Error: 'python -m venv' is disabled. Use uv instead:" >&2
|
|
16
|
+
echo "" >&2
|
|
17
|
+
echo " To create a virtual environment: uv venv" >&2
|
|
18
|
+
echo "" >&2
|
|
19
|
+
exit 1
|
|
20
|
+
;;
|
|
21
|
+
esac
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
# Check for -m flag followed by pip or venv
|
|
25
|
+
prev=""
|
|
26
|
+
for arg in "$@"; do
|
|
27
|
+
if [ "$prev" = "-m" ]; then
|
|
28
|
+
case "$arg" in
|
|
29
|
+
pip)
|
|
30
|
+
echo "Error: 'python -m pip' is disabled. Use uv instead:" >&2
|
|
31
|
+
echo "" >&2
|
|
32
|
+
echo " To install a package for a script: uv run --with PACKAGE python script.py" >&2
|
|
33
|
+
echo " To add a dependency to the project: uv add PACKAGE" >&2
|
|
34
|
+
echo "" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
;;
|
|
37
|
+
venv)
|
|
38
|
+
echo "Error: 'python -m venv' is disabled. Use uv instead:" >&2
|
|
39
|
+
echo "" >&2
|
|
40
|
+
echo " To create a virtual environment: uv venv" >&2
|
|
41
|
+
echo "" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
fi
|
|
46
|
+
prev="$arg"
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
# Dispatch to uv run python
|
|
50
|
+
exec uv run python "$@"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Check for disallowed module invocations
|
|
4
|
+
for arg in "$@"; do
|
|
5
|
+
case "$arg" in
|
|
6
|
+
-mpip|-m\ pip|pip)
|
|
7
|
+
echo "Error: 'python3 -m pip' is disabled. Use uv instead:" >&2
|
|
8
|
+
echo "" >&2
|
|
9
|
+
echo " To install a package for a script: uv run --with PACKAGE python script.py" >&2
|
|
10
|
+
echo " To add a dependency to the project: uv add PACKAGE" >&2
|
|
11
|
+
echo "" >&2
|
|
12
|
+
exit 1
|
|
13
|
+
;;
|
|
14
|
+
-mvenv|-m\ venv|venv)
|
|
15
|
+
echo "Error: 'python3 -m venv' is disabled. Use uv instead:" >&2
|
|
16
|
+
echo "" >&2
|
|
17
|
+
echo " To create a virtual environment: uv venv" >&2
|
|
18
|
+
echo "" >&2
|
|
19
|
+
exit 1
|
|
20
|
+
;;
|
|
21
|
+
esac
|
|
22
|
+
done
|
|
23
|
+
|
|
24
|
+
# Check for -m flag followed by pip or venv
|
|
25
|
+
prev=""
|
|
26
|
+
for arg in "$@"; do
|
|
27
|
+
if [ "$prev" = "-m" ]; then
|
|
28
|
+
case "$arg" in
|
|
29
|
+
pip)
|
|
30
|
+
echo "Error: 'python3 -m pip' is disabled. Use uv instead:" >&2
|
|
31
|
+
echo "" >&2
|
|
32
|
+
echo " To install a package for a script: uv run --with PACKAGE python script.py" >&2
|
|
33
|
+
echo " To add a dependency to the project: uv add PACKAGE" >&2
|
|
34
|
+
echo "" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
;;
|
|
37
|
+
venv)
|
|
38
|
+
echo "Error: 'python3 -m venv' is disabled. Use uv instead:" >&2
|
|
39
|
+
echo "" >&2
|
|
40
|
+
echo " To create a virtual environment: uv venv" >&2
|
|
41
|
+
echo "" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
fi
|
|
46
|
+
prev="$arg"
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
# Dispatch to uv run python
|
|
50
|
+
exec uv run python "$@"
|
package/package.json
CHANGED
package/pi-extensions/answer.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Q&A extraction hook - extracts questions from assistant responses
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Custom interactive TUI for answering questions.
|
|
5
5
|
*
|
|
6
6
|
* Demonstrates the "prompt generator" pattern with custom TUI:
|
|
7
7
|
* 1. /answer command gets the last assistant message
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UV Extension - Redirects Python tooling to uv equivalents
|
|
3
|
+
*
|
|
4
|
+
* This extension wraps the bash tool to prepend intercepted-commands to PATH,
|
|
5
|
+
* which contains shim scripts that intercept common Python tooling commands
|
|
6
|
+
* and redirect agents to use uv instead.
|
|
7
|
+
*
|
|
8
|
+
* Intercepted commands:
|
|
9
|
+
* - pip/pip3: Blocked with suggestions to use `uv add` or `uv run --with`
|
|
10
|
+
* - poetry: Blocked with uv equivalents (uv init, uv add, uv sync, uv run)
|
|
11
|
+
* - python/python3: Redirected to `uv run python`, with special handling to
|
|
12
|
+
* block `python -m pip` and `python -m venv`
|
|
13
|
+
*
|
|
14
|
+
* The shim scripts are located in the intercepted-commands directory and
|
|
15
|
+
* provide helpful error messages with the equivalent uv commands.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
19
|
+
import { createBashTool } from "@mariozechner/pi-coding-agent";
|
|
20
|
+
import { dirname, join } from "path";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
|
+
|
|
23
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const interceptedCommandsPath = join(__dirname, "..", "intercepted-commands");
|
|
25
|
+
|
|
26
|
+
export default function (pi: ExtensionAPI) {
|
|
27
|
+
const cwd = process.cwd();
|
|
28
|
+
const bashTool = createBashTool(cwd, {
|
|
29
|
+
commandPrefix: `export PATH="${interceptedCommandsPath}:$PATH"`,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
pi.on("session_start", (_event, ctx) => {
|
|
33
|
+
ctx.ui.notify("UV interceptor loaded", "info");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
pi.registerTool({
|
|
37
|
+
...bashTool,
|
|
38
|
+
async execute(id, params, onUpdate, _ctx, signal) {
|
|
39
|
+
return bashTool.execute(id, params, signal, onUpdate);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: uv
|
|
3
|
+
description: "Use `uv` instead of pip/python/venv. Run scripts with `uv run script.py`, add deps with `uv add`, use inline script metadata for standalone scripts."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Quick Reference
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
uv run script.py # Run a script
|
|
10
|
+
uv run --with requests script.py # Run with ad-hoc dependency
|
|
11
|
+
uv add requests # Add dependency to project
|
|
12
|
+
uv init --script foo.py # Create script with inline metadata
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Inline Script Dependencies
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
# /// script
|
|
19
|
+
# requires-python = ">=3.12"
|
|
20
|
+
# dependencies = ["requests"]
|
|
21
|
+
# ///
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
See [scripts.md](scripts.md) for full details on running scripts, locking, and reproducibility.
|
|
25
|
+
|
|
26
|
+
## Build Backend
|
|
27
|
+
|
|
28
|
+
Use `uv_build` for pure Python packages:
|
|
29
|
+
|
|
30
|
+
```toml
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["uv_build>=0.9.28,<0.10.0"]
|
|
33
|
+
build-backend = "uv_build"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
See [build.md](build.md) for project structure, namespaces, and file inclusion.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# uv Build Backend
|
|
2
|
+
|
|
3
|
+
Use `uv_build` for pure Python packages. For extension modules, use `hatchling` instead.
|
|
4
|
+
|
|
5
|
+
## pyproject.toml
|
|
6
|
+
|
|
7
|
+
```toml
|
|
8
|
+
[project]
|
|
9
|
+
name = "my-package"
|
|
10
|
+
version = "0.1.0"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
dependencies = []
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["uv_build>=0.9.28,<0.10.0"]
|
|
16
|
+
build-backend = "uv_build"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Project Structure
|
|
20
|
+
|
|
21
|
+
Default layout uses `src/<package_name>/__init__.py`:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
pyproject.toml
|
|
25
|
+
src/
|
|
26
|
+
└── my_package/
|
|
27
|
+
└── __init__.py
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Package name is normalized: `Foo-Bar` → `foo_bar`.
|
|
31
|
+
|
|
32
|
+
### Custom Module Location
|
|
33
|
+
|
|
34
|
+
```toml
|
|
35
|
+
[tool.uv.build-backend]
|
|
36
|
+
module-name = "mymodule"
|
|
37
|
+
module-root = "" # Use project root instead of src/
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Namespace Packages
|
|
41
|
+
|
|
42
|
+
For `foo.bar` namespace:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
src/foo/bar/__init__.py # No __init__.py in foo/
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```toml
|
|
49
|
+
[tool.uv.build-backend]
|
|
50
|
+
module-name = "foo.bar"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## File Inclusion/Exclusion
|
|
54
|
+
|
|
55
|
+
Excludes `__pycache__`, `*.pyc`, `*.pyo` by default.
|
|
56
|
+
|
|
57
|
+
```toml
|
|
58
|
+
[tool.uv.build-backend]
|
|
59
|
+
source-include = ["assets/**"]
|
|
60
|
+
source-exclude = ["/dist", "tests/**"]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- Includes are anchored (`pyproject.toml` = only root)
|
|
64
|
+
- Excludes are not anchored (`__pycache__` = all dirs named that)
|
|
65
|
+
- Use `/prefix` to anchor excludes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Running Scripts with uv
|
|
2
|
+
|
|
3
|
+
## Basic Usage
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
uv run script.py # Run a script
|
|
7
|
+
uv run script.py arg1 arg2 # With arguments
|
|
8
|
+
uv run --python 3.10 script.py # Specific Python version
|
|
9
|
+
echo 'print("hi")' | uv run - # From stdin
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
In a project directory, use `--no-project` to skip installing the project:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv run --no-project script.py
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Ad-hoc Dependencies
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv run --with requests script.py
|
|
22
|
+
uv run --with 'requests>2,<3' script.py
|
|
23
|
+
uv run --with requests --with rich script.py
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Inline Script Metadata (Recommended)
|
|
27
|
+
|
|
28
|
+
Declare dependencies directly in the script:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# /// script
|
|
32
|
+
# requires-python = ">=3.12"
|
|
33
|
+
# dependencies = [
|
|
34
|
+
# "requests<3",
|
|
35
|
+
# "rich",
|
|
36
|
+
# ]
|
|
37
|
+
# ///
|
|
38
|
+
|
|
39
|
+
import requests
|
|
40
|
+
from rich import print
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then just: `uv run script.py`
|
|
44
|
+
|
|
45
|
+
### Managing Dependencies
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv init --script example.py --python 3.12 # Create script with metadata
|
|
49
|
+
uv add --script example.py requests rich # Add dependencies
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Alternative Index
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv add --index "https://example.com/simple" --script example.py requests
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Adds to metadata:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
# [[tool.uv.index]]
|
|
62
|
+
# url = "https://example.com/simple"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Locking Dependencies
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uv lock --script example.py # Creates example.py.lock
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Reproducibility
|
|
72
|
+
|
|
73
|
+
Pin resolution date:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# /// script
|
|
77
|
+
# dependencies = ["requests"]
|
|
78
|
+
# [tool.uv]
|
|
79
|
+
# exclude-newer = "2023-10-16T00:00:00Z"
|
|
80
|
+
# ///
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Executable Scripts (Shebang)
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
#!/usr/bin/env -S uv run --script
|
|
87
|
+
# /// script
|
|
88
|
+
# dependencies = ["httpx"]
|
|
89
|
+
# ///
|
|
90
|
+
|
|
91
|
+
import httpx
|
|
92
|
+
print(httpx.get("https://example.com"))
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
chmod +x myscript
|
|
97
|
+
./myscript
|
|
98
|
+
```
|
package/pi-extensions/qna.ts
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Q&A extraction hook - extracts questions from assistant responses
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates the "prompt generator" pattern:
|
|
5
|
-
* 1. /qna command gets the last assistant message
|
|
6
|
-
* 2. Shows a spinner while extracting (hides editor)
|
|
7
|
-
* 3. Loads the result into the editor for user to fill in answers
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { complete, type Model, type Api, type UserMessage } from "@mariozechner/pi-ai";
|
|
11
|
-
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
12
|
-
import { BorderedLoader } from "@mariozechner/pi-coding-agent";
|
|
13
|
-
|
|
14
|
-
const SYSTEM_PROMPT = `You are a question extractor. Given text from a conversation, extract any questions that need answering and format them for the user to fill in.
|
|
15
|
-
|
|
16
|
-
Output format:
|
|
17
|
-
- List each question on its own line, prefixed with "Q: "
|
|
18
|
-
- After each question, add a blank line for the answer prefixed with "A: "
|
|
19
|
-
- If no questions are found, output "No questions found in the last message."
|
|
20
|
-
- When there is important context needed to answer a question, use quote characters (">") and add it into the question block as additional lines.
|
|
21
|
-
|
|
22
|
-
Example output:
|
|
23
|
-
Q: What is your preferred database?
|
|
24
|
-
> we can only configure MySQL and PostgreSQL because of what is implemented.
|
|
25
|
-
A:
|
|
26
|
-
|
|
27
|
-
Q: Should we use TypeScript or JavaScript?
|
|
28
|
-
A:
|
|
29
|
-
|
|
30
|
-
Keep questions in the order they appeared. Be concise.`;
|
|
31
|
-
|
|
32
|
-
const HAIKU_MODEL_ID = "claude-haiku-4-5";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Check if the current model is a Claude Opus or Sonnet model from Anthropic provider.
|
|
36
|
-
* If so, try to use claude-haiku-4-5 instead for cost efficiency.
|
|
37
|
-
*/
|
|
38
|
-
async function selectExtractionModel(
|
|
39
|
-
currentModel: Model<Api>,
|
|
40
|
-
modelRegistry: { find: (provider: string, modelId: string) => Model<Api> | undefined; getApiKey: (model: Model<Api>) => Promise<string | undefined> },
|
|
41
|
-
): Promise<Model<Api>> {
|
|
42
|
-
// Only consider switching if the provider is anthropic and the model is opus or sonnet
|
|
43
|
-
if (currentModel.provider !== "anthropic") {
|
|
44
|
-
return currentModel;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const modelId = currentModel.id.toLowerCase();
|
|
48
|
-
const isOpusOrSonnet = modelId.includes("opus") || modelId.includes("sonnet");
|
|
49
|
-
if (!isOpusOrSonnet) {
|
|
50
|
-
return currentModel;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Try to find and use claude-haiku-4-5
|
|
54
|
-
const haikuModel = modelRegistry.find("anthropic", HAIKU_MODEL_ID);
|
|
55
|
-
if (!haikuModel) {
|
|
56
|
-
return currentModel;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Check if we have an API key for the haiku model
|
|
60
|
-
const apiKey = await modelRegistry.getApiKey(haikuModel);
|
|
61
|
-
if (!apiKey) {
|
|
62
|
-
return currentModel;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return haikuModel;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export default function (pi: ExtensionAPI) {
|
|
69
|
-
const qnaHandler = async (ctx: ExtensionContext) => {
|
|
70
|
-
if (!ctx.hasUI) {
|
|
71
|
-
ctx.ui.notify("qna requires interactive mode", "error");
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (!ctx.model) {
|
|
76
|
-
ctx.ui.notify("No model selected", "error");
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Find the last assistant message on the current branch
|
|
81
|
-
const branch = ctx.sessionManager.getBranch();
|
|
82
|
-
let lastAssistantText: string | undefined;
|
|
83
|
-
|
|
84
|
-
for (let i = branch.length - 1; i >= 0; i--) {
|
|
85
|
-
const entry = branch[i];
|
|
86
|
-
if (entry.type === "message") {
|
|
87
|
-
const msg = entry.message;
|
|
88
|
-
if ("role" in msg && msg.role === "assistant") {
|
|
89
|
-
if (msg.stopReason !== "stop") {
|
|
90
|
-
ctx.ui.notify(`Last assistant message incomplete (${msg.stopReason})`, "error");
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
const textParts = msg.content
|
|
94
|
-
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
95
|
-
.map((c) => c.text);
|
|
96
|
-
if (textParts.length > 0) {
|
|
97
|
-
lastAssistantText = textParts.join("\n");
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (!lastAssistantText) {
|
|
105
|
-
ctx.ui.notify("No assistant messages found", "error");
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Select the best model for extraction (prefer haiku for cost efficiency)
|
|
110
|
-
const extractionModel = await selectExtractionModel(ctx.model, ctx.modelRegistry);
|
|
111
|
-
|
|
112
|
-
// Run extraction with loader UI
|
|
113
|
-
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
|
114
|
-
const loader = new BorderedLoader(tui, theme, `Extracting questions using ${extractionModel.id}...`);
|
|
115
|
-
loader.onAbort = () => done(null);
|
|
116
|
-
|
|
117
|
-
// Do the work
|
|
118
|
-
const doExtract = async () => {
|
|
119
|
-
const apiKey = await ctx.modelRegistry.getApiKey(extractionModel);
|
|
120
|
-
const userMessage: UserMessage = {
|
|
121
|
-
role: "user",
|
|
122
|
-
content: [{ type: "text", text: lastAssistantText! }],
|
|
123
|
-
timestamp: Date.now(),
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const response = await complete(
|
|
127
|
-
extractionModel,
|
|
128
|
-
{ systemPrompt: SYSTEM_PROMPT, messages: [userMessage] },
|
|
129
|
-
{ apiKey, signal: loader.signal },
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
if (response.stopReason === "aborted") {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return response.content
|
|
137
|
-
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
138
|
-
.map((c) => c.text)
|
|
139
|
-
.join("\n");
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
doExtract()
|
|
143
|
-
.then(done)
|
|
144
|
-
.catch(() => done(null));
|
|
145
|
-
|
|
146
|
-
return loader;
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
if (result === null) {
|
|
150
|
-
ctx.ui.notify("Cancelled", "info");
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
ctx.ui.setEditorText(result);
|
|
155
|
-
ctx.ui.notify("Questions loaded. Edit and submit when ready.", "info");
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
pi.registerCommand("qna", {
|
|
159
|
-
description: "Extract questions from last assistant message into editor",
|
|
160
|
-
handler: (_args, ctx) => qnaHandler(ctx),
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
pi.registerShortcut("ctrl+,", {
|
|
164
|
-
description: "Extract questions into editor",
|
|
165
|
-
handler: qnaHandler,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: improve-skill
|
|
3
|
-
description: "Analyze coding agent session transcripts to improve existing skills or create new ones. Use when asked to improve a skill based on a session, or extract a new skill from session history."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Improve Skill
|
|
7
|
-
|
|
8
|
-
This skill helps analyze coding agent sessions to improve or create skills. It works with Claude Code, Pi, and Codex session files.
|
|
9
|
-
|
|
10
|
-
## Quick Start
|
|
11
|
-
|
|
12
|
-
Extract the current session and generate an improvement prompt:
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
# Auto-detect agent and extract current session
|
|
16
|
-
./scripts/extract-session.js
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Session Extraction
|
|
20
|
-
|
|
21
|
-
The `extract-session.js` script finds and parses session files from any of the three agents:
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
# Auto-detect (uses most recent session for current working directory)
|
|
25
|
-
./scripts/extract-session.js
|
|
26
|
-
|
|
27
|
-
# Specify agent type
|
|
28
|
-
./scripts/extract-session.js --agent claude
|
|
29
|
-
./scripts/extract-session.js --agent pi
|
|
30
|
-
./scripts/extract-session.js --agent codex
|
|
31
|
-
|
|
32
|
-
# Specify a different working directory
|
|
33
|
-
./scripts/extract-session.js --cwd /path/to/project
|
|
34
|
-
|
|
35
|
-
# Use a specific session file
|
|
36
|
-
./scripts/extract-session.js /path/to/session.jsonl
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
**Session file locations:**
|
|
40
|
-
- **Claude Code**: `~/.claude/projects/<encoded-cwd>/*.jsonl`
|
|
41
|
-
- **Pi**: `~/.pi/agent/sessions/<encoded-cwd>/*.jsonl`
|
|
42
|
-
- **Codex**: `~/.codex/sessions/YYYY/MM/DD/*.jsonl`
|
|
43
|
-
|
|
44
|
-
## Workflow: Improve an Existing Skill
|
|
45
|
-
|
|
46
|
-
When asked to improve a skill based on a session:
|
|
47
|
-
|
|
48
|
-
1. **Extract the session transcript:**
|
|
49
|
-
```bash
|
|
50
|
-
./scripts/extract-session.js > /tmp/session-transcript.txt
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
2. **Find the existing skill** in one of these locations:
|
|
54
|
-
- `~/.codex/skills/<skill-name>/SKILL.md`
|
|
55
|
-
- `~/.claude/skills/<skill-name>/SKILL.md`
|
|
56
|
-
- `~/.pi/agent/skills/<skill-name>/SKILL.md`
|
|
57
|
-
|
|
58
|
-
3. **Generate an improvement prompt** for a new session:
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
62
|
-
COPY THE FOLLOWING PROMPT INTO A NEW AGENT SESSION:
|
|
63
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
64
|
-
|
|
65
|
-
I need to improve the "<skill-name>" skill based on a session where I used it.
|
|
66
|
-
|
|
67
|
-
First, read the current skill at: <path-to-skill>
|
|
68
|
-
|
|
69
|
-
Then analyze this session transcript to understand:
|
|
70
|
-
- Where I struggled to use the skill correctly
|
|
71
|
-
- What information was missing from the skill
|
|
72
|
-
- What examples would have helped
|
|
73
|
-
- What I had to figure out on my own
|
|
74
|
-
|
|
75
|
-
<session_transcript>
|
|
76
|
-
<paste transcript here>
|
|
77
|
-
</session_transcript>
|
|
78
|
-
|
|
79
|
-
Based on this analysis, improve the skill by:
|
|
80
|
-
1. Adding missing instructions or clarifications
|
|
81
|
-
2. Adding examples for common use cases discovered
|
|
82
|
-
3. Fixing any incorrect guidance
|
|
83
|
-
4. Making the skill more concise where possible
|
|
84
|
-
|
|
85
|
-
Write the improved skill back to the same location.
|
|
86
|
-
|
|
87
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## Workflow: Create a New Skill
|
|
91
|
-
|
|
92
|
-
When asked to create a new skill from a session:
|
|
93
|
-
|
|
94
|
-
1. **Extract the session transcript:**
|
|
95
|
-
```bash
|
|
96
|
-
./scripts/extract-session.js > /tmp/session-transcript.txt
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
2. **Generate a creation prompt** for a new session:
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
103
|
-
COPY THE FOLLOWING PROMPT INTO A NEW AGENT SESSION:
|
|
104
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
105
|
-
|
|
106
|
-
Analyze this session transcript to extract a reusable skill called "<skill-name>":
|
|
107
|
-
|
|
108
|
-
<session_transcript>
|
|
109
|
-
<paste transcript here>
|
|
110
|
-
</session_transcript>
|
|
111
|
-
|
|
112
|
-
Create a new skill that captures:
|
|
113
|
-
1. The core capability or workflow demonstrated
|
|
114
|
-
2. Key commands, APIs, or patterns used
|
|
115
|
-
3. Common pitfalls and how to avoid them
|
|
116
|
-
4. Example usage for typical scenarios
|
|
117
|
-
|
|
118
|
-
Write the skill to: ~/.codex/skills/<skill-name>/SKILL.md
|
|
119
|
-
|
|
120
|
-
Use this format:
|
|
121
|
-
---
|
|
122
|
-
name: <skill-name>
|
|
123
|
-
description: "<one-line description>"
|
|
124
|
-
---
|
|
125
|
-
|
|
126
|
-
# <Skill Name> Skill
|
|
127
|
-
|
|
128
|
-
<overview and quick reference>
|
|
129
|
-
|
|
130
|
-
## <Section for each major capability>
|
|
131
|
-
|
|
132
|
-
<instructions and examples>
|
|
133
|
-
|
|
134
|
-
═══════════════════════════════════════════════════════════════════════════════
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## Why a Separate Session?
|
|
138
|
-
|
|
139
|
-
The improvement prompt is meant to be copied into a **fresh agent session** because:
|
|
140
|
-
|
|
141
|
-
1. **Token efficiency** - The current session already has a lot of context; starting fresh means only the transcript and skill are loaded
|
|
142
|
-
2. **Clean analysis** - The new session can focus purely on improvement without being influenced by the current task
|
|
143
|
-
3. **Reproducibility** - The prompt is self-contained and can be shared or reused
|
|
144
|
-
|
|
145
|
-
## Tips for Good Skill Improvements
|
|
146
|
-
|
|
147
|
-
When analyzing a transcript, look for:
|
|
148
|
-
|
|
149
|
-
- **Confusion patterns** - Where did the agent retry or change approach?
|
|
150
|
-
- **Missing examples** - What specific commands or code patterns were discovered?
|
|
151
|
-
- **Workarounds** - What did the agent have to figure out that wasn't documented?
|
|
152
|
-
- **Errors** - What failed and how was it resolved?
|
|
153
|
-
- **Successful patterns** - What worked well and should be highlighted?
|
|
154
|
-
|
|
155
|
-
Keep skills concise - focus on the most important information and examples.
|
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Extract session transcript from Claude Code, Pi, or Codex session files.
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* ./extract-session.js [session-path]
|
|
8
|
-
* ./extract-session.js --agent claude|pi|codex [--cwd /path/to/dir]
|
|
9
|
-
*
|
|
10
|
-
* If no arguments, auto-detects based on current working directory.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const fs = require('fs');
|
|
14
|
-
const path = require('path');
|
|
15
|
-
const os = require('os');
|
|
16
|
-
|
|
17
|
-
// Parse arguments
|
|
18
|
-
const args = process.argv.slice(2);
|
|
19
|
-
let sessionPath = null;
|
|
20
|
-
let agent = null;
|
|
21
|
-
let cwd = process.cwd();
|
|
22
|
-
|
|
23
|
-
for (let i = 0; i < args.length; i++) {
|
|
24
|
-
if (args[i] === '--agent' && args[i + 1]) {
|
|
25
|
-
agent = args[++i];
|
|
26
|
-
} else if (args[i] === '--cwd' && args[i + 1]) {
|
|
27
|
-
cwd = args[++i];
|
|
28
|
-
} else if (!args[i].startsWith('-')) {
|
|
29
|
-
sessionPath = args[i];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Encode CWD for session path lookup
|
|
35
|
-
*/
|
|
36
|
-
function encodeCwd(cwd, style) {
|
|
37
|
-
if (style === 'pi') {
|
|
38
|
-
// Pi uses: --<cwd-without-leading-slash-with-slashes-as-dashes>--
|
|
39
|
-
// e.g., /Users/mitsuhiko/Development/myproject -> --Users-mitsuhiko-Development-myproject--
|
|
40
|
-
const safePath = `--${cwd.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`;
|
|
41
|
-
return safePath;
|
|
42
|
-
}
|
|
43
|
-
// Claude Code: just replace / with -
|
|
44
|
-
return cwd.replace(/\//g, '-');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Find the most recent session file in a directory
|
|
49
|
-
*/
|
|
50
|
-
function findMostRecentSession(dir) {
|
|
51
|
-
if (!fs.existsSync(dir)) return null;
|
|
52
|
-
|
|
53
|
-
const files = fs.readdirSync(dir)
|
|
54
|
-
.filter(f => f.endsWith('.jsonl'))
|
|
55
|
-
.map(f => ({
|
|
56
|
-
name: f,
|
|
57
|
-
path: path.join(dir, f),
|
|
58
|
-
mtime: fs.statSync(path.join(dir, f)).mtime
|
|
59
|
-
}))
|
|
60
|
-
.sort((a, b) => b.mtime - a.mtime);
|
|
61
|
-
|
|
62
|
-
return files.length > 0 ? files[0].path : null;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Find Codex session matching CWD
|
|
67
|
-
*/
|
|
68
|
-
function findCodexSession(targetCwd) {
|
|
69
|
-
const baseDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
70
|
-
if (!fs.existsSync(baseDir)) return null;
|
|
71
|
-
|
|
72
|
-
// Find all session files, sorted by mtime
|
|
73
|
-
const allSessions = [];
|
|
74
|
-
|
|
75
|
-
function walkDir(dir) {
|
|
76
|
-
if (!fs.existsSync(dir)) return;
|
|
77
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
78
|
-
const fullPath = path.join(dir, entry.name);
|
|
79
|
-
if (entry.isDirectory()) {
|
|
80
|
-
walkDir(fullPath);
|
|
81
|
-
} else if (entry.name.endsWith('.jsonl')) {
|
|
82
|
-
allSessions.push({
|
|
83
|
-
path: fullPath,
|
|
84
|
-
mtime: fs.statSync(fullPath).mtime
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
walkDir(baseDir);
|
|
91
|
-
allSessions.sort((a, b) => b.mtime - a.mtime);
|
|
92
|
-
|
|
93
|
-
// Find most recent matching CWD
|
|
94
|
-
for (const session of allSessions.slice(0, 50)) { // Check last 50
|
|
95
|
-
try {
|
|
96
|
-
const firstLine = fs.readFileSync(session.path, 'utf8').split('\n')[0];
|
|
97
|
-
const data = JSON.parse(firstLine);
|
|
98
|
-
if (data.payload?.cwd === targetCwd) {
|
|
99
|
-
return session.path;
|
|
100
|
-
}
|
|
101
|
-
} catch (e) {
|
|
102
|
-
// Skip invalid files
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Auto-detect session based on CWD
|
|
111
|
-
*/
|
|
112
|
-
function autoDetectSession(cwd) {
|
|
113
|
-
// Try Claude Code first
|
|
114
|
-
const claudePath = path.join(os.homedir(), '.claude', 'projects', encodeCwd(cwd, 'claude'));
|
|
115
|
-
let session = findMostRecentSession(claudePath);
|
|
116
|
-
if (session) return { agent: 'claude', path: session };
|
|
117
|
-
|
|
118
|
-
// Try Pi
|
|
119
|
-
const piPath = path.join(os.homedir(), '.pi', 'agent', 'sessions', encodeCwd(cwd, 'pi'));
|
|
120
|
-
session = findMostRecentSession(piPath);
|
|
121
|
-
if (session) return { agent: 'pi', path: session };
|
|
122
|
-
|
|
123
|
-
// Try Codex
|
|
124
|
-
session = findCodexSession(cwd);
|
|
125
|
-
if (session) return { agent: 'codex', path: session };
|
|
126
|
-
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Parse Claude Code session format
|
|
132
|
-
*/
|
|
133
|
-
function parseClaudeSession(content) {
|
|
134
|
-
const messages = [];
|
|
135
|
-
const lines = content.trim().split('\n');
|
|
136
|
-
|
|
137
|
-
for (const line of lines) {
|
|
138
|
-
try {
|
|
139
|
-
const entry = JSON.parse(line);
|
|
140
|
-
if (entry.message?.role && entry.message?.content) {
|
|
141
|
-
const msg = entry.message;
|
|
142
|
-
messages.push({
|
|
143
|
-
role: msg.role,
|
|
144
|
-
content: extractContent(msg.content),
|
|
145
|
-
timestamp: entry.timestamp
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
} catch (e) {
|
|
149
|
-
// Skip invalid lines
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return messages;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Parse Pi session format
|
|
158
|
-
*/
|
|
159
|
-
function parsePiSession(content) {
|
|
160
|
-
const messages = [];
|
|
161
|
-
const lines = content.trim().split('\n');
|
|
162
|
-
|
|
163
|
-
for (const line of lines) {
|
|
164
|
-
try {
|
|
165
|
-
const entry = JSON.parse(line);
|
|
166
|
-
if (entry.type === 'message' && entry.message?.role) {
|
|
167
|
-
messages.push({
|
|
168
|
-
role: entry.message.role,
|
|
169
|
-
content: extractContent(entry.message.content),
|
|
170
|
-
timestamp: entry.timestamp
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
} catch (e) {
|
|
174
|
-
// Skip invalid lines
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return messages;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Parse Codex session format
|
|
183
|
-
*/
|
|
184
|
-
function parseCodexSession(content) {
|
|
185
|
-
const messages = [];
|
|
186
|
-
const lines = content.trim().split('\n');
|
|
187
|
-
|
|
188
|
-
for (const line of lines) {
|
|
189
|
-
try {
|
|
190
|
-
const entry = JSON.parse(line);
|
|
191
|
-
if (entry.type === 'response_item' && entry.payload?.role) {
|
|
192
|
-
const payload = entry.payload;
|
|
193
|
-
messages.push({
|
|
194
|
-
role: payload.role,
|
|
195
|
-
content: extractContent(payload.content),
|
|
196
|
-
timestamp: entry.timestamp
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
} catch (e) {
|
|
200
|
-
// Skip invalid lines
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return messages;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Extract text content from various content formats
|
|
209
|
-
*/
|
|
210
|
-
function extractContent(content) {
|
|
211
|
-
if (typeof content === 'string') return content;
|
|
212
|
-
if (!Array.isArray(content)) return JSON.stringify(content);
|
|
213
|
-
|
|
214
|
-
const parts = [];
|
|
215
|
-
for (const item of content) {
|
|
216
|
-
if (typeof item === 'string') {
|
|
217
|
-
parts.push(item);
|
|
218
|
-
} else if (item.type === 'text') {
|
|
219
|
-
parts.push(item.text);
|
|
220
|
-
} else if (item.type === 'input_text') {
|
|
221
|
-
parts.push(item.text);
|
|
222
|
-
} else if (item.type === 'tool_use') {
|
|
223
|
-
parts.push(`[Tool: ${item.name}]\n${JSON.stringify(item.input, null, 2)}`);
|
|
224
|
-
} else if (item.type === 'tool_result') {
|
|
225
|
-
const result = typeof item.content === 'string'
|
|
226
|
-
? item.content
|
|
227
|
-
: JSON.stringify(item.content);
|
|
228
|
-
// Truncate long tool results
|
|
229
|
-
const truncated = result.length > 500
|
|
230
|
-
? result.slice(0, 500) + '\n[... truncated ...]'
|
|
231
|
-
: result;
|
|
232
|
-
parts.push(`[Tool Result]\n${truncated}`);
|
|
233
|
-
} else {
|
|
234
|
-
parts.push(`[${item.type}]`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return parts.join('\n');
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Format messages as readable transcript
|
|
243
|
-
*/
|
|
244
|
-
function formatTranscript(messages, maxMessages = 100) {
|
|
245
|
-
const recent = messages.slice(-maxMessages);
|
|
246
|
-
const lines = [];
|
|
247
|
-
|
|
248
|
-
for (const msg of recent) {
|
|
249
|
-
const role = msg.role.toUpperCase();
|
|
250
|
-
lines.push(`\n### ${role}:\n`);
|
|
251
|
-
lines.push(msg.content);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (messages.length > maxMessages) {
|
|
255
|
-
lines.unshift(`\n[... ${messages.length - maxMessages} earlier messages omitted ...]\n`);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return lines.join('\n');
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Main
|
|
262
|
-
async function main() {
|
|
263
|
-
let result;
|
|
264
|
-
|
|
265
|
-
if (sessionPath) {
|
|
266
|
-
// Explicit path provided
|
|
267
|
-
if (!fs.existsSync(sessionPath)) {
|
|
268
|
-
console.error(`Session file not found: ${sessionPath}`);
|
|
269
|
-
process.exit(1);
|
|
270
|
-
}
|
|
271
|
-
// Guess agent from path
|
|
272
|
-
if (sessionPath.includes('.claude')) {
|
|
273
|
-
result = { agent: 'claude', path: sessionPath };
|
|
274
|
-
} else if (sessionPath.includes('.pi')) {
|
|
275
|
-
result = { agent: 'pi', path: sessionPath };
|
|
276
|
-
} else if (sessionPath.includes('.codex')) {
|
|
277
|
-
result = { agent: 'codex', path: sessionPath };
|
|
278
|
-
} else {
|
|
279
|
-
// Default to Claude format
|
|
280
|
-
result = { agent: 'claude', path: sessionPath };
|
|
281
|
-
}
|
|
282
|
-
} else if (agent) {
|
|
283
|
-
// Agent specified, find session for that agent
|
|
284
|
-
if (agent === 'claude') {
|
|
285
|
-
const dir = path.join(os.homedir(), '.claude', 'projects', encodeCwd(cwd, 'claude'));
|
|
286
|
-
const session = findMostRecentSession(dir);
|
|
287
|
-
if (!session) {
|
|
288
|
-
console.error(`No Claude Code session found for: ${cwd}`);
|
|
289
|
-
process.exit(1);
|
|
290
|
-
}
|
|
291
|
-
result = { agent: 'claude', path: session };
|
|
292
|
-
} else if (agent === 'pi') {
|
|
293
|
-
const dir = path.join(os.homedir(), '.pi', 'agent', 'sessions', encodeCwd(cwd, 'pi'));
|
|
294
|
-
const session = findMostRecentSession(dir);
|
|
295
|
-
if (!session) {
|
|
296
|
-
console.error(`No Pi session found for: ${cwd}`);
|
|
297
|
-
process.exit(1);
|
|
298
|
-
}
|
|
299
|
-
result = { agent: 'pi', path: session };
|
|
300
|
-
} else if (agent === 'codex') {
|
|
301
|
-
const session = findCodexSession(cwd);
|
|
302
|
-
if (!session) {
|
|
303
|
-
console.error(`No Codex session found for: ${cwd}`);
|
|
304
|
-
process.exit(1);
|
|
305
|
-
}
|
|
306
|
-
result = { agent: 'codex', path: session };
|
|
307
|
-
} else {
|
|
308
|
-
console.error(`Unknown agent: ${agent}`);
|
|
309
|
-
process.exit(1);
|
|
310
|
-
}
|
|
311
|
-
} else {
|
|
312
|
-
// Auto-detect
|
|
313
|
-
result = autoDetectSession(cwd);
|
|
314
|
-
if (!result) {
|
|
315
|
-
console.error(`No session found for: ${cwd}`);
|
|
316
|
-
console.error('Try specifying --agent claude|pi|codex or provide a session path directly.');
|
|
317
|
-
process.exit(1);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Read and parse session
|
|
322
|
-
const content = fs.readFileSync(result.path, 'utf8');
|
|
323
|
-
|
|
324
|
-
let messages;
|
|
325
|
-
switch (result.agent) {
|
|
326
|
-
case 'claude':
|
|
327
|
-
messages = parseClaudeSession(content);
|
|
328
|
-
break;
|
|
329
|
-
case 'pi':
|
|
330
|
-
messages = parsePiSession(content);
|
|
331
|
-
break;
|
|
332
|
-
case 'codex':
|
|
333
|
-
messages = parseCodexSession(content);
|
|
334
|
-
break;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Output metadata and transcript
|
|
338
|
-
console.log(`# Session Transcript`);
|
|
339
|
-
console.log(`Agent: ${result.agent}`);
|
|
340
|
-
console.log(`File: ${result.path}`);
|
|
341
|
-
console.log(`Messages: ${messages.length}`);
|
|
342
|
-
console.log('');
|
|
343
|
-
console.log(formatTranscript(messages));
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
main().catch(e => {
|
|
347
|
-
console.error(e.message);
|
|
348
|
-
process.exit(1);
|
|
349
|
-
});
|