pi-session-exporter 1.0.0
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 +89 -0
- package/index.ts +248 -0
- package/package.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# pi-session-exporter
|
|
2
|
+
|
|
3
|
+
Export your Pi session as clean Markdown for sharing in PRs, issues, docs, and Slack.
|
|
4
|
+
|
|
5
|
+
## What it produces
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
# Pi Session Export
|
|
9
|
+
|
|
10
|
+
**Exported:** 2026-05-22 14:30:00
|
|
11
|
+
**Directory:** `~/my-project`
|
|
12
|
+
**Session:** refactoring-auth
|
|
13
|
+
**Entries:** 24
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
### 👤 You
|
|
18
|
+
|
|
19
|
+
Refactor the auth middleware to use JWT instead of sessions
|
|
20
|
+
|
|
21
|
+
### 🤖 Assistant
|
|
22
|
+
|
|
23
|
+
I'll refactor the auth middleware. Let me start by reading the current implementation.
|
|
24
|
+
|
|
25
|
+
### 🔧 Tool: `read`
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"path": "src/middleware/auth.ts"
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
<details>
|
|
34
|
+
<summary>📋 Result: `read`</summary>
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
export function authMiddleware(req, res, next) {
|
|
38
|
+
...
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
</details>
|
|
43
|
+
...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- **Clean Markdown** with roles (👤 You, 🤖 Assistant, 🔧 Tool, 📋 Result)
|
|
49
|
+
- **Tool calls shown as code blocks** with JSON args
|
|
50
|
+
- **Tool results collapsed** in `<details>` for readability
|
|
51
|
+
- **Thinking sections** collapsed when present
|
|
52
|
+
- **Plaintext export** — one-line summary per entry
|
|
53
|
+
- **JSON export** — structured data for automation
|
|
54
|
+
- **Image indicators** — shows 🖼️ when user messages have attachments
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# npm
|
|
60
|
+
pi install npm:pi-session-exporter
|
|
61
|
+
|
|
62
|
+
# GitHub
|
|
63
|
+
pi install git:github.com/Jaraxxxx/pi-session-exporter
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
| Method | Action |
|
|
69
|
+
|--------|--------|
|
|
70
|
+
| `/export` | Export as Markdown |
|
|
71
|
+
| `/export json` | Export as JSON |
|
|
72
|
+
| `/export plaintext` | Export as plaintext |
|
|
73
|
+
| `Ctrl+E` | Quick export as Markdown |
|
|
74
|
+
|
|
75
|
+
The LLM can also call the `export_session` tool.
|
|
76
|
+
|
|
77
|
+
Files are saved to `.pi-exports/` in your working directory.
|
|
78
|
+
|
|
79
|
+
## Output location
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
your-project/
|
|
83
|
+
└── .pi-exports/
|
|
84
|
+
└── pi-session-20260522-143000.md
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- Pi coding agent
|
package/index.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
|
|
5
|
+
type ExportFormat = "markdown" | "plaintext" | "json";
|
|
6
|
+
|
|
7
|
+
// ---- Helpers ----
|
|
8
|
+
|
|
9
|
+
function ensureDir(dir: string) {
|
|
10
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function timestamp(): string {
|
|
14
|
+
return new Date().toISOString().replace(/T/, " ").replace(/\..+/, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function trunc(s: string, max = 120): string {
|
|
18
|
+
return s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function safeStr(v: any): string {
|
|
22
|
+
if (v == null) return "";
|
|
23
|
+
if (typeof v === "string") return v;
|
|
24
|
+
try { return JSON.stringify(v, null, 2); } catch { return String(v); }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function entryToMarkdown(e: any, idx: number): string {
|
|
28
|
+
// Use only safe properties from Pi entries — avoid e.content, e.message (internal AgentMessage)
|
|
29
|
+
const type = e.type || "";
|
|
30
|
+
const role = e.role || "";
|
|
31
|
+
const toolName = e.toolName || e.name || "";
|
|
32
|
+
const input = e.input || e.arguments || null;
|
|
33
|
+
const result = e.result != null ? safeStr(e.result) : "";
|
|
34
|
+
const customType = e.customType || "";
|
|
35
|
+
const metaStr = customType ? ` [${customType}]` : "";
|
|
36
|
+
|
|
37
|
+
// Tool call entry
|
|
38
|
+
if (type === "tool_call") {
|
|
39
|
+
const argsStr = typeof input === "string" ? input : input ? JSON.stringify(input, null, 2) : "{}";
|
|
40
|
+
return `\n### 🔧 Tool: \`${toolName || "unknown"}\`${metaStr}\n\n\`\`\`json\n${trunc(argsStr, 500)}\n\`\`\`\n`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Tool result entry
|
|
44
|
+
if (type === "tool_result") {
|
|
45
|
+
const label = toolName || e.toolCallId || "";
|
|
46
|
+
if (result) {
|
|
47
|
+
return `\n<details>\n<summary>📋 Result: \`${label}\`${metaStr}</summary>\n\n\`\`\`\n${trunc(result, 2000)}\n\`\`\`\n\n</details>\n`;
|
|
48
|
+
}
|
|
49
|
+
return `\n<details>\n<summary>📋 Result: \`${label}\`${metaStr}</summary>\n\n_(empty result)_\n\n</details>\n`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// System entry
|
|
53
|
+
if (type === "system" || role === "system") {
|
|
54
|
+
return `\n### ⚙️ System\n\n\`\`\`\n${trunc(result || "(system)", 2000)}\n\`\`\`\n`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// User message entry
|
|
58
|
+
if (type === "user" || role === "user") {
|
|
59
|
+
return `\n### 👤 You${metaStr}\n\n${result || "(user message)"}\n`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Assistant message entry
|
|
63
|
+
if (type === "assistant" || role === "assistant") {
|
|
64
|
+
return `\n### 🤖 Assistant${metaStr}\n\n${result || "(response)"}\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Default: use result or show type
|
|
68
|
+
return `\n### ${type || role || "unknown"}${metaStr}\n\n${trunc(result, 2000)}\n`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---- Export logic ----
|
|
72
|
+
|
|
73
|
+
function safeEntries(ctx: any): any[] {
|
|
74
|
+
try {
|
|
75
|
+
const entries = ctx.sessionManager.getEntries?.() || [];
|
|
76
|
+
// Filter out entries that trigger the internal Pi bug with message.content iteration
|
|
77
|
+
return entries.filter((e: any) => {
|
|
78
|
+
try {
|
|
79
|
+
// Touch key properties to detect broken entries
|
|
80
|
+
void (e.type); void (e.role); void (e.result); void (e.input);
|
|
81
|
+
return true;
|
|
82
|
+
} catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function safeEntryForExport(e: any): any {
|
|
92
|
+
try {
|
|
93
|
+
return {
|
|
94
|
+
type: e.type,
|
|
95
|
+
role: e.role,
|
|
96
|
+
customType: e.customType,
|
|
97
|
+
timestamp: e.timestamp,
|
|
98
|
+
toolName: e.toolName,
|
|
99
|
+
input: e.input,
|
|
100
|
+
result: e.result != null ? safeStr(e.result) : null,
|
|
101
|
+
};
|
|
102
|
+
} catch {
|
|
103
|
+
return { type: "error", role: "unknown" };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function exportSession(ctx: any, format: ExportFormat): { path: string; size: number } {
|
|
108
|
+
try {
|
|
109
|
+
return exportSessionImpl(ctx, format);
|
|
110
|
+
} catch (e: any) {
|
|
111
|
+
// Pi has a known bug where session entries with message.content can throw
|
|
112
|
+
// "message.content is not iterable" during serialization. Gracefully degrade.
|
|
113
|
+
const cwd = ctx.sessionManager.getCwd?.() || process.cwd();
|
|
114
|
+
const outDir = join(cwd, ".pi-exports");
|
|
115
|
+
ensureDir(outDir);
|
|
116
|
+
const now = new Date();
|
|
117
|
+
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
118
|
+
const timeStr = now.toISOString().slice(11, 19).replace(/:/g, "");
|
|
119
|
+
const body = `# Pi Session Export\n\n> ⚠️ Export partially failed: ${e.message}\n\nPi's internal session entries could not be fully serialized due to an internal bug. Please report this.`;
|
|
120
|
+
const ext = format === "json" ? ".json" : format === "plaintext" ? ".txt" : ".md";
|
|
121
|
+
const filepath = join(outDir, `pi-session-${dateStr}-${timeStr}${ext}`);
|
|
122
|
+
writeFileSync(filepath, body, "utf-8");
|
|
123
|
+
return { path: filepath, size: Buffer.byteLength(body) };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function exportSessionImpl(ctx: any, format: ExportFormat): { path: string; size: number } {
|
|
128
|
+
const entries = safeEntries(ctx);
|
|
129
|
+
const cwd = ctx.sessionManager.getCwd();
|
|
130
|
+
|
|
131
|
+
const now = new Date();
|
|
132
|
+
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
133
|
+
const timeStr = now.toISOString().slice(11, 19).replace(/:/g, "");
|
|
134
|
+
const outDir = join(cwd, ".pi-exports");
|
|
135
|
+
ensureDir(outDir);
|
|
136
|
+
|
|
137
|
+
let ext = ".md";
|
|
138
|
+
let body = "";
|
|
139
|
+
|
|
140
|
+
if (format === "json") {
|
|
141
|
+
ext = ".json";
|
|
142
|
+
const exportData = {
|
|
143
|
+
exportedAt: now.toISOString(),
|
|
144
|
+
cwd,
|
|
145
|
+
sessionName: ctx.sessionManager.getSessionName?.() || null,
|
|
146
|
+
entryCount: entries.length,
|
|
147
|
+
entries: entries.map(safeEntryForExport),
|
|
148
|
+
};
|
|
149
|
+
body = JSON.stringify(exportData, null, 2);
|
|
150
|
+
} else if (format === "plaintext") {
|
|
151
|
+
ext = ".txt";
|
|
152
|
+
const lines: string[] = [];
|
|
153
|
+
lines.push(`# Pi Session Export`);
|
|
154
|
+
lines.push(`# Exported: ${timestamp()}`);
|
|
155
|
+
lines.push(`# CWD: ${cwd}`);
|
|
156
|
+
lines.push("#");
|
|
157
|
+
for (const e of entries) {
|
|
158
|
+
const type = e.type || "unknown";
|
|
159
|
+
if (type === "system") continue;
|
|
160
|
+
const info = safeEntryForExport(e);
|
|
161
|
+
const line = `[${type.toUpperCase()}] ${trunc(String(info.result || info.input || type), 100)}`;
|
|
162
|
+
lines.push(line);
|
|
163
|
+
}
|
|
164
|
+
body = lines.join("\n");
|
|
165
|
+
} else {
|
|
166
|
+
// Markdown
|
|
167
|
+
const lines: string[] = [];
|
|
168
|
+
lines.push(`# Pi Session Export`);
|
|
169
|
+
lines.push("");
|
|
170
|
+
lines.push(`**Exported:** ${timestamp()} `);
|
|
171
|
+
lines.push(`**Directory:** \`${cwd}\` `);
|
|
172
|
+
lines.push(`**Session:** ${ctx.sessionManager.getSessionName?.() || "(unnamed)"} `);
|
|
173
|
+
lines.push(`**Entries:** ${entries.length}`);
|
|
174
|
+
lines.push("");
|
|
175
|
+
lines.push("---");
|
|
176
|
+
lines.push("");
|
|
177
|
+
|
|
178
|
+
for (let i = 0; i < entries.length; i++) {
|
|
179
|
+
const e = entries[i];
|
|
180
|
+
const type = e.type || "";
|
|
181
|
+
|
|
182
|
+
if (type === "tool_call") {
|
|
183
|
+
lines.push(entryToMarkdown(e, i));
|
|
184
|
+
// Include the next entry as the tool result
|
|
185
|
+
if (i + 1 < entries.length && entries[i + 1].type === "tool_result") {
|
|
186
|
+
lines.push(entryToMarkdown(entries[i + 1], i + 1));
|
|
187
|
+
i++;
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (type === "tool_result") {
|
|
193
|
+
lines.push(entryToMarkdown(e, i));
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
lines.push(entryToMarkdown(e, i));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
lines.push("");
|
|
201
|
+
lines.push("---");
|
|
202
|
+
lines.push(`_Generated by [pi-session-exporter](https://github.com/Jaraxxxx/pi-session-exporter)_`);
|
|
203
|
+
body = lines.join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const filename = `pi-session-${dateStr}-${timeStr}${ext}`;
|
|
207
|
+
const filepath = join(outDir, filename);
|
|
208
|
+
writeFileSync(filepath, body, "utf-8");
|
|
209
|
+
const size = statSync(filepath).size;
|
|
210
|
+
|
|
211
|
+
return { path: filepath, size };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---- Extension ----
|
|
215
|
+
|
|
216
|
+
export default function (pi: ExtensionAPI) {
|
|
217
|
+
pi.registerCommand({
|
|
218
|
+
name: "export",
|
|
219
|
+
description: "Export session as Markdown (default), plaintext, or JSON",
|
|
220
|
+
async handler(_args: string[], ctx: any) {
|
|
221
|
+
const format = _args[0] === "json" ? "json" : _args[0] === "plaintext" ? "plaintext" : "markdown";
|
|
222
|
+
const result = exportSession(ctx, format);
|
|
223
|
+
ctx.ui.notify(`Exported to ${relative(ctx.cwd, result.path)} (${(result.size / 1024).toFixed(1)}KB)`, "success");
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
pi.registerShortcut("e", { ctrl: true }, (_event: any, ctx: any) => {
|
|
228
|
+
if (!ctx.hasUI) return;
|
|
229
|
+
const result = exportSession(ctx, "markdown");
|
|
230
|
+
ctx.ui.notify(`Exported to ${relative(ctx.cwd, result.path)} (${(result.size / 1024).toFixed(1)}KB)`, "success");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
pi.registerTool({
|
|
234
|
+
name: "export_session",
|
|
235
|
+
description: "Export the current session to a Markdown file in .pi-exports/ directory. Use when user asks to save/share the conversation.",
|
|
236
|
+
parameters: {
|
|
237
|
+
type: "object",
|
|
238
|
+
properties: {
|
|
239
|
+
format: { type: "string", enum: ["markdown", "plaintext", "json"], description: "Export format. Default is markdown." },
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
async execute(_toolCallId: any, args: any, _signal: any, _onUpdate: any, ctx: any) {
|
|
243
|
+
const format = args.format || "markdown";
|
|
244
|
+
const result = exportSession(ctx, format);
|
|
245
|
+
return `Session exported to \`.pi-exports/${relative(ctx.cwd, result.path)}\` (${(result.size / 1024).toFixed(1)}KB, ${safeEntries(ctx).length} entries).`;
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-session-exporter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Export Pi session history as clean Markdown for sharing in PRs, issues, docs, and Slack",
|
|
5
|
+
"keywords": ["pi-package", "pi-extension", "pi-command", "session", "export", "markdown", "sharing"],
|
|
6
|
+
"author": { "name": "Jay Rathod", "url": "https://github.com/Jaraxxxx" },
|
|
7
|
+
"repository": { "type": "git", "url": "https://github.com/Jaraxxxx/pi-session-exporter" },
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"pi": { "extensions": ["./index.ts"] },
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"@earendil-works/pi-ai": "*",
|
|
13
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
14
|
+
"@earendil-works/pi-tui": "*"
|
|
15
|
+
}
|
|
16
|
+
}
|