pi-auto-save-to-markdown 0.8.2 → 0.9.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 +18 -12
- package/README.zh.md +15 -10
- package/index.ts +57 -40
- package/markdown.ts +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,11 +92,11 @@ original creation timestamp — and rewrites the frontmatter title and the
|
|
|
92
92
|
document heading to match. The rename happens at most once: later `/name`
|
|
93
93
|
changes never touch the filename, and manually renamed files are left alone.
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
````markdown
|
|
96
96
|
---
|
|
97
97
|
title: "Fix login redirect loop"
|
|
98
98
|
agent: "pi"
|
|
99
|
-
format_version: "1.
|
|
99
|
+
format_version: "1.6"
|
|
100
100
|
session_id: "d0a4f541-976d-4d1b-8e1c-30a1f2b3c4d5"
|
|
101
101
|
session_key: "c2088d77"
|
|
102
102
|
branch_last_entry_id: "019be3a2-1f4d-7c8a-9b01-d23e45f6a7b8"
|
|
@@ -141,21 +141,27 @@ I'll trace the middleware order first.
|
|
|
141
141
|
> [!quote]- Tool Calls · 1 (read)
|
|
142
142
|
> **`read`** `{"filePath":"/Users/me/project/src/auth/middleware.ts"}`
|
|
143
143
|
>
|
|
144
|
-
>
|
|
144
|
+
> ```
|
|
145
|
+
> import { NextResponse } from "next/server";
|
|
146
|
+
> export function middleware(…) …
|
|
147
|
+
> ```
|
|
145
148
|
|
|
146
149
|
---
|
|
147
|
-
|
|
150
|
+
````
|
|
148
151
|
|
|
149
152
|
The body renders user and assistant messages in full (assistant thinking and
|
|
150
153
|
per-turn tool calls are each folded into a collapsed Obsidian callout —
|
|
151
|
-
`> [!tldr]- Thinking` and `> [!quote]- Tool Calls · …`)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
`> [!tldr]- Thinking` and `> [!quote]- Tool Calls · …`), recording every tool
|
|
155
|
+
call with its full raw result: the file is a documentary record that may be
|
|
156
|
+
@-referenced back into a conversation, and a truncated half-result would be
|
|
157
|
+
wasted when the tool is called again and misleading when it is not, while
|
|
158
|
+
local reading (grep, ranged reads) makes size a non-issue. Callouts are used
|
|
159
|
+
instead of HTML `<details>` because Obsidian renders markdown inside HTML
|
|
160
|
+
blocks unreliably; outside Obsidian the callouts degrade to plain blockquotes.
|
|
161
|
+
Arguments render as full JSON in inline code spans and results verbatim —
|
|
162
|
+
whitespace intact, nothing capped — in fenced code blocks (with a delimiter
|
|
163
|
+
sized to survive backticks inside the content), so raw tool output renders
|
|
164
|
+
literally instead of being parsed as markdown.
|
|
159
165
|
|
|
160
166
|
Prompt blocks the client or the agent injects into a user message — the
|
|
161
167
|
editor's active selection, attached or referenced notes, loaded skills — are
|
package/README.zh.md
CHANGED
|
@@ -66,11 +66,11 @@ PI_SAVE_CONVERSATION_DIR=notes/ai pi
|
|
|
66
66
|
|
|
67
67
|
会话的真实名称在建文件之后才到达时(如 Claudian 在首轮回复后才生成标题),下一次保存会把文件一次性改名为 `<名称>-<key>-<原时间戳>.md`(保留原创建时间戳),并同步改写 frontmatter 标题与正文标题。改名至多发生一次:之后的 `/name` 改名不再影响文件名,手动整理过的文件名也不会被动。
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
````markdown
|
|
70
70
|
---
|
|
71
71
|
title: "修复登录重定向死循环"
|
|
72
72
|
agent: "pi"
|
|
73
|
-
format_version: "1.
|
|
73
|
+
format_version: "1.6"
|
|
74
74
|
session_id: "d0a4f541-976d-4d1b-8e1c-30a1f2b3c4d5"
|
|
75
75
|
session_key: "c2088d77"
|
|
76
76
|
branch_last_entry_id: "019be3a2-1f4d-7c8a-9b01-d23e45f6a7b8"
|
|
@@ -115,19 +115,24 @@ Assistant <span style="font-size: 0.5em; color: var(--text-faint);">2026-08-29 1
|
|
|
115
115
|
> [!quote]- Tool Calls · 1 (read)
|
|
116
116
|
> **`read`** `{"filePath":"/Users/me/project/src/auth/middleware.ts"}`
|
|
117
117
|
>
|
|
118
|
-
>
|
|
118
|
+
> ```
|
|
119
|
+
> import { NextResponse } from "next/server";
|
|
120
|
+
> export function middleware(…) …
|
|
121
|
+
> ```
|
|
119
122
|
|
|
120
123
|
---
|
|
121
|
-
|
|
124
|
+
````
|
|
122
125
|
|
|
123
126
|
正文完整渲染 user / assistant 消息(assistant 的 thinking 与每轮工具调用
|
|
124
127
|
分别折叠在可折叠的 Obsidian callout 中——`> [!tldr]- Thinking` 和
|
|
125
|
-
`> [!quote]- Tool Calls ·
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
`> [!quote]- Tool Calls · …`),每次工具调用连同其完整原始结果一起记录:
|
|
129
|
+
归档文件是可能被 @ 引回对话的史料,截断的半个结果在工具重调时是浪费、在
|
|
130
|
+
不再调用时是误导,而局部阅读(grep、按行段读取)让体积不成问题。之所以用
|
|
131
|
+
callout 而不是 HTML `<details>`,是因为 Obsidian 对 HTML 块内嵌 Markdown 的
|
|
132
|
+
渲染不可靠;在非 Obsidian 环境下 callout 退化为普通引用块。参数以完整 JSON
|
|
133
|
+
包在 inline code 里,结果逐字保真——空白原样、不截断——放在 fenced code
|
|
134
|
+
block 中(分隔符长度会自动压过内容中的反引号序列),工具的原始输出因此按
|
|
135
|
+
字面渲染,不会被当作 Markdown 解析。
|
|
131
136
|
|
|
132
137
|
客户端或 agent 注入到用户消息中的提示块——编辑器当前选区、附加或引用的
|
|
133
138
|
笔记、加载的 skill——会从原始 XML(Obsidian 无法渲染,只会显示成裸露的
|
package/index.ts
CHANGED
|
@@ -51,18 +51,22 @@
|
|
|
51
51
|
* wrapped in single blank lines.
|
|
52
52
|
* - Tool call/result folding: calls live in the assistant entry while their
|
|
53
53
|
* results are separate toolResult entries; saves pair them by toolCall id
|
|
54
|
-
* and fold each assistant block's calls, with
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
54
|
+
* and fold each assistant block's calls, with their FULL results, into one
|
|
55
|
+
* collapsed Obsidian callout (`> [!quote]- Tool Calls · …`). Thinking folds
|
|
56
|
+
* the same way into `> [!tldr]- Thinking`. Callouts are used instead of
|
|
57
|
+
* HTML `<details>` because Obsidian's views render embedded markdown
|
|
58
|
+
* inside HTML blocks unreliably, while callouts fold and render markdown
|
|
59
|
+
* in both Live Preview and Reading view. Outside Obsidian the callouts
|
|
60
|
+
* degrade to plain blockquotes. Arguments render as full JSON in inline
|
|
61
|
+
* code spans and results verbatim — whitespace intact, nothing capped —
|
|
62
|
+
* in fenced code blocks (delimiters sized to survive backticks inside the
|
|
63
|
+
* content), so raw output renders literally instead of being parsed as
|
|
64
|
+
* markdown. Nothing is truncated because the file is a documentary record
|
|
65
|
+
* that may be @-referenced back into a conversation: a half result is
|
|
66
|
+
* wasted when the tool is called again and misleading when it is not,
|
|
67
|
+
* while local reading (grep, ranged reads) makes size a non-issue. A
|
|
68
|
+
* result whose call was saved in an earlier file (mid-turn manual save)
|
|
69
|
+
* renders as a standalone block with the same full content.
|
|
66
70
|
* - Injected prompt blocks: the host client and the agent runtime append
|
|
67
71
|
* machine-readable XML to user messages — the editor's active selection
|
|
68
72
|
* (CDATA content), note references and attachments (linked_note /
|
|
@@ -194,7 +198,13 @@ import * as fsSync from "node:fs";
|
|
|
194
198
|
import * as os from "node:os";
|
|
195
199
|
import * as path from "node:path";
|
|
196
200
|
import { debug } from "./debug.js";
|
|
197
|
-
import {
|
|
201
|
+
import {
|
|
202
|
+
callout,
|
|
203
|
+
fencedCode,
|
|
204
|
+
inlineCode,
|
|
205
|
+
renderUserMessageText,
|
|
206
|
+
stripInjectedBlocks,
|
|
207
|
+
} from "./markdown.js";
|
|
198
208
|
|
|
199
209
|
const CUSTOM_TYPE = "pi-claudian-auto-save-markdown";
|
|
200
210
|
const ENV_SUBDIR = "PI_SAVE_CONVERSATION_DIR";
|
|
@@ -234,7 +244,7 @@ const SAVE_STATE_SCHEMA = "1.2";
|
|
|
234
244
|
* the frontmatter and the document heading); additive frontmatter fields do
|
|
235
245
|
* NOT bump it — they are invisible to any within-major parser.
|
|
236
246
|
*/
|
|
237
|
-
const FORMAT_VERSION = "1.
|
|
247
|
+
const FORMAT_VERSION = "1.6";
|
|
238
248
|
|
|
239
249
|
/**
|
|
240
250
|
* Package version of this extension, read best-effort from the adjacent
|
|
@@ -255,9 +265,6 @@ const EXTENSION_VERSION: string | null = (() => {
|
|
|
255
265
|
|
|
256
266
|
const MAX_TITLE_LENGTH = 60;
|
|
257
267
|
const TITLE_FALLBACK_LENGTH = 40;
|
|
258
|
-
const TOOL_RESULT_PREVIEW = 500;
|
|
259
|
-
const TOOL_ARGS_PREVIEW = 160;
|
|
260
|
-
|
|
261
268
|
type AgentMessage = SessionMessageEntry["message"];
|
|
262
269
|
type UserMessage = Extract<AgentMessage, { role: "user" }>;
|
|
263
270
|
type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
|
|
@@ -959,15 +966,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
959
966
|
return "untitled";
|
|
960
967
|
}
|
|
961
968
|
|
|
962
|
-
|
|
963
|
-
|
|
969
|
+
/** Full argument JSON on one line (JSON.stringify escapes newlines), never truncated. */
|
|
970
|
+
function renderArgs(args: unknown): string {
|
|
964
971
|
try {
|
|
965
|
-
|
|
972
|
+
return JSON.stringify(args) ?? "";
|
|
966
973
|
} catch {
|
|
967
|
-
|
|
974
|
+
return String(args);
|
|
968
975
|
}
|
|
969
|
-
s = s.replace(/\s+/g, " ").trim();
|
|
970
|
-
return s.length > TOOL_ARGS_PREVIEW ? s.slice(0, TOOL_ARGS_PREVIEW) + " …" : s;
|
|
971
976
|
}
|
|
972
977
|
|
|
973
978
|
// ---------- markdown rendering ----------
|
|
@@ -981,30 +986,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
981
986
|
return s.replace(/^(?:[ \t]*\n)+/, "").replace(/\s+$/, "");
|
|
982
987
|
}
|
|
983
988
|
|
|
984
|
-
/** One tool call with its paired result
|
|
989
|
+
/** One tool call with its paired full result (null when no result entry exists). */
|
|
985
990
|
interface RenderedToolCall {
|
|
986
991
|
name: string;
|
|
987
992
|
args: string;
|
|
988
993
|
result: string | null;
|
|
994
|
+
error: boolean;
|
|
989
995
|
}
|
|
990
996
|
|
|
991
|
-
/**
|
|
992
|
-
|
|
997
|
+
/**
|
|
998
|
+
* Full raw result text: text blocks joined with blank lines, non-text
|
|
999
|
+
* blocks as placeholders. Error status stays OUT of the content (it rides
|
|
1000
|
+
* the call's head line) so the saved text is exactly what the tool
|
|
1001
|
+
* returned.
|
|
1002
|
+
*/
|
|
1003
|
+
function resultText(m: ToolResultMessage): string {
|
|
993
1004
|
const texts: string[] = [];
|
|
994
1005
|
for (const b of m.content) {
|
|
995
1006
|
if (b.type === "text") texts.push(b.text);
|
|
996
1007
|
else texts.push(`_[image: ${b.mimeType}]_`);
|
|
997
1008
|
}
|
|
998
|
-
|
|
999
|
-
const capped =
|
|
1000
|
-
flat.length > TOOL_RESULT_PREVIEW ? flat.slice(0, TOOL_RESULT_PREVIEW) + " …" : flat;
|
|
1001
|
-
const status = m.isError ? " (error)" : "";
|
|
1002
|
-
return `${capped}${status}`.trim();
|
|
1009
|
+
return texts.join("\n\n").trim();
|
|
1003
1010
|
}
|
|
1004
1011
|
|
|
1005
|
-
/**
|
|
1012
|
+
/**
|
|
1013
|
+
* Standalone block for a result whose call is not in this file — same full
|
|
1014
|
+
* content as folded results.
|
|
1015
|
+
*/
|
|
1006
1016
|
function renderToolResult(m: ToolResultMessage): string {
|
|
1007
|
-
|
|
1017
|
+
const text = resultText(m);
|
|
1018
|
+
const err = m.isError ? " (error)" : "";
|
|
1019
|
+
return `**Tool · ${m.toolName}**${err}\n\n${text ? fencedCode(text) : "_(empty result)_"}`;
|
|
1008
1020
|
}
|
|
1009
1021
|
|
|
1010
1022
|
/** "read, web_search ×2" — tool names with repeat counts, first-seen order. */
|
|
@@ -1016,16 +1028,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
1016
1028
|
|
|
1017
1029
|
/**
|
|
1018
1030
|
* Fold tool calls and their paired results into one collapsed callout.
|
|
1019
|
-
*
|
|
1020
|
-
* raw output renders literally instead of being
|
|
1031
|
+
* Arguments render as full JSON in inline code spans and results verbatim
|
|
1032
|
+
* in fenced code blocks, so raw output renders literally instead of being
|
|
1033
|
+
* parsed as markdown.
|
|
1021
1034
|
*/
|
|
1022
1035
|
function renderToolCallsCallout(calls: RenderedToolCall[]): string {
|
|
1023
1036
|
const summary = summarizeToolNames(calls.map((c) => c.name));
|
|
1024
1037
|
const items = calls.map((c) => {
|
|
1025
|
-
const
|
|
1038
|
+
const err = c.error ? " (error)" : "";
|
|
1039
|
+
const head = c.args
|
|
1040
|
+
? `**\`${c.name}\`**${err} ${inlineCode(c.args)}`
|
|
1041
|
+
: `**\`${c.name}\`**${err}`;
|
|
1026
1042
|
const result =
|
|
1027
|
-
c.result === null ? "_(no result)_" :
|
|
1028
|
-
return `${head}\n\n
|
|
1043
|
+
c.result === null ? "_(no result)_" : c.result ? fencedCode(c.result) : "_(empty result)_";
|
|
1044
|
+
return `${head}\n\n${result}`;
|
|
1029
1045
|
});
|
|
1030
1046
|
return callout("quote", `Tool Calls · ${calls.length} (${summary})`, items.join("\n\n"));
|
|
1031
1047
|
}
|
|
@@ -1061,8 +1077,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1061
1077
|
results.delete(b.id);
|
|
1062
1078
|
calls.push({
|
|
1063
1079
|
name: b.name,
|
|
1064
|
-
args:
|
|
1065
|
-
result: r ?
|
|
1080
|
+
args: renderArgs(b.arguments),
|
|
1081
|
+
result: r ? resultText(r) : null,
|
|
1082
|
+
error: r ? r.isError : false,
|
|
1066
1083
|
});
|
|
1067
1084
|
}
|
|
1068
1085
|
}
|
package/markdown.ts
CHANGED
|
@@ -27,6 +27,19 @@ export function inlineCode(text: string): string {
|
|
|
27
27
|
return `${fence}${text}${fence}`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Fenced code block for arbitrary raw output (full tool results): the fence
|
|
32
|
+
* is always one backtick longer than the longest backtick run inside the
|
|
33
|
+
* text (and at least three), so content that itself contains backticks
|
|
34
|
+
* cannot break out of the block. Tool output renders literally, whitespace
|
|
35
|
+
* intact, instead of being parsed as markdown.
|
|
36
|
+
*/
|
|
37
|
+
export function fencedCode(text: string): string {
|
|
38
|
+
const longest = (text.match(/`+/g) ?? []).reduce((a, r) => Math.max(a, r.length), 0);
|
|
39
|
+
const fence = "`".repeat(Math.max(3, longest + 1));
|
|
40
|
+
return `${fence}\n${text}\n${fence}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
/**
|
|
31
44
|
* Obsidian callout (`> [!type]- title`) wrapping a markdown body: every body
|
|
32
45
|
* line is prefixed with `>` (empty lines become bare `>`), so the body keeps
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-auto-save-to-markdown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|