@pi-claudian/auto-save-to-markdown 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/README.zh.md +13 -2
- package/index.ts +33 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,11 +78,15 @@ session_file: "~/.pi/agent/sessions/--Users-me-project-20260829-050500_ab12.json
|
|
|
78
78
|
|
|
79
79
|
# Fix login redirect loop
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
User · 13:05:12
|
|
82
|
+
===
|
|
82
83
|
|
|
83
84
|
The login page redirects in a loop after the auth refactor...
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
Assistant · 13:05:40 · claude-sonnet-4-5
|
|
89
|
+
===
|
|
86
90
|
|
|
87
91
|
<details>
|
|
88
92
|
<summary>Thinking</summary>
|
|
@@ -98,6 +102,8 @@ I'll trace the middleware order first.
|
|
|
98
102
|
- `read` — {"filePath":"/Users/me/project/src/auth/middleware.ts"}
|
|
99
103
|
|
|
100
104
|
> **Tool · read** /Users/me/project/src/auth/middleware.ts — 120 lines …
|
|
105
|
+
|
|
106
|
+
---
|
|
101
107
|
```
|
|
102
108
|
|
|
103
109
|
The body renders user and assistant messages in full (assistant thinking is
|
|
@@ -105,6 +111,13 @@ kept in a collapsible `<details>` block) and summarizes each tool call and
|
|
|
105
111
|
result in one line, so the file stays readable while still showing what the
|
|
106
112
|
agent did.
|
|
107
113
|
|
|
114
|
+
Each message block opens with a setext level-1 info header (`User · …`,
|
|
115
|
+
underlined with `===`) — one level above the `##` headings AI content
|
|
116
|
+
typically starts with, and distinguishable from content `#` headings when
|
|
117
|
+
parsing — and ends with a `---` separator wrapped in single blank lines
|
|
118
|
+
(extra blank lines are trimmed), so blocks are easy to tell apart both when
|
|
119
|
+
reading and when splitting the file programmatically.
|
|
120
|
+
|
|
108
121
|
### Fragmented thinking repair
|
|
109
122
|
|
|
110
123
|
Some upstream reasoning streams (observed with z-ai/GLM via OpenRouter) store
|
package/README.zh.md
CHANGED
|
@@ -68,11 +68,15 @@ session_file: "~/.pi/agent/sessions/--Users-me-project-20260829-050500_ab12.json
|
|
|
68
68
|
|
|
69
69
|
# 修复登录重定向死循环
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
User · 13:05:12
|
|
72
|
+
===
|
|
72
73
|
|
|
73
74
|
auth 重构之后登录页一直重定向死循环……
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
Assistant · 13:05:40 · claude-sonnet-4-5
|
|
79
|
+
===
|
|
76
80
|
|
|
77
81
|
<details>
|
|
78
82
|
<summary>Thinking</summary>
|
|
@@ -88,12 +92,19 @@ auth 重构之后登录页一直重定向死循环……
|
|
|
88
92
|
- `read` — {"filePath":"/Users/me/project/src/auth/middleware.ts"}
|
|
89
93
|
|
|
90
94
|
> **Tool · read** /Users/me/project/src/auth/middleware.ts — 120 lines …
|
|
95
|
+
|
|
96
|
+
---
|
|
91
97
|
```
|
|
92
98
|
|
|
93
99
|
正文完整渲染 user / assistant 消息(assistant 的 thinking 放在可折叠的
|
|
94
100
|
`<details>` 块中),每个工具调用和结果各压缩成一行摘要,既可读又能看出
|
|
95
101
|
agent 做了什么。
|
|
96
102
|
|
|
103
|
+
每个消息块以 setext 一级信息头(`User · …`,下一行以 `===` 下划)开头——
|
|
104
|
+
高于 AI 内容常见的 `##` 二级标题,解析时也能与内容中的 `#` 一级标题区分
|
|
105
|
+
开——并以"上下各一个空行"包裹的 `---` 分隔线结尾(多余空行会被裁剪),
|
|
106
|
+
无论是阅读还是程序化切分,都能清楚地区分每个消息块。
|
|
107
|
+
|
|
97
108
|
### 碎片化 thinking 修复
|
|
98
109
|
|
|
99
110
|
部分上游推理流(在 z-ai/GLM 经 OpenRouter 的场景中观察到)会把 thinking
|
package/index.ts
CHANGED
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
* - Frontmatter: title, session id, tree (branch key), model, provider,
|
|
21
21
|
* cumulative cost and tokens (input, output, cache read/write), message
|
|
22
22
|
* count, created/updated timestamps, project root and session file.
|
|
23
|
+
* - Body format: every message block opens with a setext-H1 info header
|
|
24
|
+
* (`User · HH:MM:SS` / `Assistant · HH:MM:SS · model`, underlined with
|
|
25
|
+
* `===`, distinct from the `#`/`##` ATX headings AI content uses) and
|
|
26
|
+
* ends with a `---` separator wrapped in single blank lines.
|
|
23
27
|
* - Branching: each file records exactly ONE branch (the root→leaf path
|
|
24
28
|
* returned by sessionManager.getBranch()). State is persisted via
|
|
25
29
|
* `pi.appendEntry()` custom entries, which are part of the session tree
|
|
@@ -278,10 +282,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
278
282
|
|
|
279
283
|
// ---------- markdown rendering ----------
|
|
280
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Strip leading blank lines and trailing whitespace from a rendered block,
|
|
287
|
+
* so joins and separators always keep exactly one blank line around them
|
|
288
|
+
* no matter what blank lines the content itself starts or ends with.
|
|
289
|
+
*/
|
|
290
|
+
function tighten(s: string): string {
|
|
291
|
+
return s.replace(/^(?:[ \t]*\n)+/, "").replace(/\s+$/, "");
|
|
292
|
+
}
|
|
293
|
+
|
|
281
294
|
function renderAssistant(m: AssistantMessage, t: string): string {
|
|
282
295
|
// Render blocks in their original chronological order: thinking always
|
|
283
296
|
// precedes the text it produced, instead of being grouped after the fact.
|
|
284
|
-
|
|
297
|
+
// Setext H1 (`===` underline): one level above the `##` headings AI
|
|
298
|
+
// content typically starts with, and distinct from content `#` headings.
|
|
299
|
+
const header = `Assistant · ${t}${m.model ? ` · ${m.model}` : ""}\n===`;
|
|
285
300
|
const parts: string[] = [];
|
|
286
301
|
const thinkings: string[] = [];
|
|
287
302
|
const flushThinking = () => {
|
|
@@ -332,7 +347,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
332
347
|
const m = e.message;
|
|
333
348
|
const t = clock(e.timestamp);
|
|
334
349
|
if (m.role === "user") {
|
|
335
|
-
blocks.push(
|
|
350
|
+
blocks.push(`User · ${t}\n===\n\n${userText(m.content)}`);
|
|
336
351
|
} else if (m.role === "assistant") {
|
|
337
352
|
blocks.push(renderAssistant(m, t));
|
|
338
353
|
} else if (m.role === "toolResult") {
|
|
@@ -341,7 +356,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
341
356
|
// Other roles (custom, bashExecution, branchSummary, compactionSummary)
|
|
342
357
|
// are not part of the rendered conversation record.
|
|
343
358
|
}
|
|
344
|
-
|
|
359
|
+
if (blocks.length === 0) return "";
|
|
360
|
+
// Every block ends with a `---` separator wrapped in single blank lines
|
|
361
|
+
// (the blank line above also keeps `---` from turning the last content
|
|
362
|
+
// line into a setext H2). The trailing separator after the final block
|
|
363
|
+
// makes later appends uniform: new blocks simply continue after it.
|
|
364
|
+
return `${blocks.map(tighten).join("\n\n---\n\n")}\n\n---\n`;
|
|
345
365
|
}
|
|
346
366
|
|
|
347
367
|
// ---------- frontmatter ----------
|
|
@@ -560,8 +580,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
560
580
|
|
|
561
581
|
if (fullCreate) {
|
|
562
582
|
const meta = computeMeta(ctx, plan.pathMessages, plan.branchKey, undefined);
|
|
563
|
-
const body = renderEntries(plan.pathMessages);
|
|
564
|
-
const content = `${frontmatter(meta)}\n\n# ${meta.title}\n\n${body}
|
|
583
|
+
const body = renderEntries(plan.pathMessages); // ends with the trailing separator
|
|
584
|
+
const content = `${frontmatter(meta)}\n\n# ${meta.title}\n\n${body}`;
|
|
565
585
|
await atomicWrite(filePath, content);
|
|
566
586
|
debug("created conversation file:", filePath);
|
|
567
587
|
return {
|
|
@@ -584,10 +604,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
584
604
|
|
|
585
605
|
const existing = await fs.readFile(filePath, "utf-8");
|
|
586
606
|
const meta = computeMeta(ctx, plan.pathMessages, plan.branchKey, parseCreated(existing));
|
|
587
|
-
const appended = renderEntries(plan.appendEntries);
|
|
607
|
+
const appended = renderEntries(plan.appendEntries); // ends with the trailing separator
|
|
588
608
|
let updated = replaceFrontmatter(existing, frontmatter(meta));
|
|
589
|
-
|
|
590
|
-
|
|
609
|
+
// Collapse trailing blank lines to a single newline so the separator
|
|
610
|
+
// always has exactly one blank line above it, whatever earlier saves
|
|
611
|
+
// (or a manual edit) left behind.
|
|
612
|
+
updated = updated.replace(/\s*$/, "\n");
|
|
613
|
+
// Files written by the old format end without a `---` separator; add one
|
|
614
|
+
// at the boundary so old and new content stay delimited.
|
|
615
|
+
updated += updated.endsWith("---\n") ? `\n${appended}` : `\n---\n\n${appended}`;
|
|
591
616
|
await atomicWrite(filePath, updated);
|
|
592
617
|
debug("appended", plan.appendEntries.length, "entries to:", filePath);
|
|
593
618
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-claudian/auto-save-to-markdown",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Pi extension that automatically saves each completed conversation turn as a markdown file with YAML frontmatter, one file per session-tree branch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|