pi-auto-save-to-markdown 0.8.1 → 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 +22 -12
- package/README.zh.md +19 -10
- package/index.ts +102 -59
- package/markdown.ts +13 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,10 @@ pi install npm:pi-auto-save-to-markdown
|
|
|
28
28
|
Automatic: after every settled agent turn (`agent_settled`), the current
|
|
29
29
|
conversation branch is written to `<cwd>/<folder>/<title>-<key>-<time>.md`.
|
|
30
30
|
|
|
31
|
+
Sessions Pi does not persist (in-memory, `--no-session` — including the
|
|
32
|
+
ephemeral auxiliary agents host clients spawn next to the real conversation,
|
|
33
|
+
such as Claudian's title generation) are skipped by every automatic save; only
|
|
34
|
+
the explicit `/save-conversation` command archives such a session on demand.
|
|
31
35
|
Manual: run `/save-conversation` to save the current branch immediately and
|
|
32
36
|
report the file path.
|
|
33
37
|
|
|
@@ -88,11 +92,11 @@ original creation timestamp — and rewrites the frontmatter title and the
|
|
|
88
92
|
document heading to match. The rename happens at most once: later `/name`
|
|
89
93
|
changes never touch the filename, and manually renamed files are left alone.
|
|
90
94
|
|
|
91
|
-
|
|
95
|
+
````markdown
|
|
92
96
|
---
|
|
93
97
|
title: "Fix login redirect loop"
|
|
94
98
|
agent: "pi"
|
|
95
|
-
format_version: "1.
|
|
99
|
+
format_version: "1.6"
|
|
96
100
|
session_id: "d0a4f541-976d-4d1b-8e1c-30a1f2b3c4d5"
|
|
97
101
|
session_key: "c2088d77"
|
|
98
102
|
branch_last_entry_id: "019be3a2-1f4d-7c8a-9b01-d23e45f6a7b8"
|
|
@@ -137,21 +141,27 @@ I'll trace the middleware order first.
|
|
|
137
141
|
> [!quote]- Tool Calls · 1 (read)
|
|
138
142
|
> **`read`** `{"filePath":"/Users/me/project/src/auth/middleware.ts"}`
|
|
139
143
|
>
|
|
140
|
-
>
|
|
144
|
+
> ```
|
|
145
|
+
> import { NextResponse } from "next/server";
|
|
146
|
+
> export function middleware(…) …
|
|
147
|
+
> ```
|
|
141
148
|
|
|
142
149
|
---
|
|
143
|
-
|
|
150
|
+
````
|
|
144
151
|
|
|
145
152
|
The body renders user and assistant messages in full (assistant thinking and
|
|
146
153
|
per-turn tool calls are each folded into a collapsed Obsidian callout —
|
|
147
|
-
`> [!tldr]- Thinking` and `> [!quote]- Tool Calls · …`)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
155
165
|
|
|
156
166
|
Prompt blocks the client or the agent injects into a user message — the
|
|
157
167
|
editor's active selection, attached or referenced notes, loaded skills — are
|
package/README.zh.md
CHANGED
|
@@ -21,6 +21,10 @@ pi install npm:pi-auto-save-to-markdown
|
|
|
21
21
|
|
|
22
22
|
自动:每个 agent 轮次完全结束(`agent_settled`,含自动重试与压缩全部完成)后,当前对话分支写入 `<cwd>/<文件夹>/<标题>-<key>-<时间>.md`。
|
|
23
23
|
|
|
24
|
+
Pi 未持久化的会话(内存态、`--no-session`——包括宿主客户端在真实对话旁派生的临时辅助
|
|
25
|
+
agent,例如 Claudian 的标题生成)会被所有自动保存路径跳过;只有显式的
|
|
26
|
+
`/save-conversation` 命令才会按需归档此类会话。
|
|
27
|
+
|
|
24
28
|
手动:运行 `/save-conversation` 立即保存当前分支并显示文件路径。
|
|
25
29
|
|
|
26
30
|
批量:运行 `/save-conversation-all` 保存**当前项目的全部 session**——即项目的
|
|
@@ -62,11 +66,11 @@ PI_SAVE_CONVERSATION_DIR=notes/ai pi
|
|
|
62
66
|
|
|
63
67
|
会话的真实名称在建文件之后才到达时(如 Claudian 在首轮回复后才生成标题),下一次保存会把文件一次性改名为 `<名称>-<key>-<原时间戳>.md`(保留原创建时间戳),并同步改写 frontmatter 标题与正文标题。改名至多发生一次:之后的 `/name` 改名不再影响文件名,手动整理过的文件名也不会被动。
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
````markdown
|
|
66
70
|
---
|
|
67
71
|
title: "修复登录重定向死循环"
|
|
68
72
|
agent: "pi"
|
|
69
|
-
format_version: "1.
|
|
73
|
+
format_version: "1.6"
|
|
70
74
|
session_id: "d0a4f541-976d-4d1b-8e1c-30a1f2b3c4d5"
|
|
71
75
|
session_key: "c2088d77"
|
|
72
76
|
branch_last_entry_id: "019be3a2-1f4d-7c8a-9b01-d23e45f6a7b8"
|
|
@@ -111,19 +115,24 @@ Assistant <span style="font-size: 0.5em; color: var(--text-faint);">2026-08-29 1
|
|
|
111
115
|
> [!quote]- Tool Calls · 1 (read)
|
|
112
116
|
> **`read`** `{"filePath":"/Users/me/project/src/auth/middleware.ts"}`
|
|
113
117
|
>
|
|
114
|
-
>
|
|
118
|
+
> ```
|
|
119
|
+
> import { NextResponse } from "next/server";
|
|
120
|
+
> export function middleware(…) …
|
|
121
|
+
> ```
|
|
115
122
|
|
|
116
123
|
---
|
|
117
|
-
|
|
124
|
+
````
|
|
118
125
|
|
|
119
126
|
正文完整渲染 user / assistant 消息(assistant 的 thinking 与每轮工具调用
|
|
120
127
|
分别折叠在可折叠的 Obsidian callout 中——`> [!tldr]- Thinking` 和
|
|
121
|
-
`> [!quote]- Tool Calls ·
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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 解析。
|
|
127
136
|
|
|
128
137
|
客户端或 agent 注入到用户消息中的提示块——编辑器当前选区、附加或引用的
|
|
129
138
|
笔记、加载的 skill——会从原始 XML(Obsidian 无法渲染,只会显示成裸露的
|
package/index.ts
CHANGED
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
* - Trigger: `agent_settled` — fires once per user prompt, after the turn is
|
|
10
10
|
* fully done (including automatic retries and compaction), so each save
|
|
11
11
|
* captures a settled state of the conversation.
|
|
12
|
+
* - Skip rule: every automatic path (the `agent_settled` save and the batch
|
|
13
|
+
* command's live save) skips sessions Pi does not persist — no session
|
|
14
|
+
* file, i.e. an in-memory SessionManager (`--no-session`): those are
|
|
15
|
+
* ephemeral auxiliary agents host clients spawn next to the real
|
|
16
|
+
* conversation (Claudian's title generation, instruction refinement,
|
|
17
|
+
* inline edits) or one-shot `pi --no-session` runs, not project
|
|
18
|
+
* conversations, and archiving them would mint junk "User's request …"
|
|
19
|
+
* files. The manual /save-conversation command still saves such a session
|
|
20
|
+
* on explicit demand; the batch never sees them (no jsonl on disk).
|
|
12
21
|
* - Location: a subfolder of the session's working directory (`ctx.cwd`, the
|
|
13
22
|
* directory the session was started in), defaulting to `ai-conversations`.
|
|
14
23
|
* Override with the PI_SAVE_CONVERSATION_DIR environment variable; set it to
|
|
@@ -42,18 +51,22 @@
|
|
|
42
51
|
* wrapped in single blank lines.
|
|
43
52
|
* - Tool call/result folding: calls live in the assistant entry while their
|
|
44
53
|
* results are separate toolResult entries; saves pair them by toolCall id
|
|
45
|
-
* and fold each assistant block's calls, with
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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.
|
|
57
70
|
* - Injected prompt blocks: the host client and the agent runtime append
|
|
58
71
|
* machine-readable XML to user messages — the editor's active selection
|
|
59
72
|
* (CDATA content), note references and attachments (linked_note /
|
|
@@ -185,7 +198,13 @@ import * as fsSync from "node:fs";
|
|
|
185
198
|
import * as os from "node:os";
|
|
186
199
|
import * as path from "node:path";
|
|
187
200
|
import { debug } from "./debug.js";
|
|
188
|
-
import {
|
|
201
|
+
import {
|
|
202
|
+
callout,
|
|
203
|
+
fencedCode,
|
|
204
|
+
inlineCode,
|
|
205
|
+
renderUserMessageText,
|
|
206
|
+
stripInjectedBlocks,
|
|
207
|
+
} from "./markdown.js";
|
|
189
208
|
|
|
190
209
|
const CUSTOM_TYPE = "pi-claudian-auto-save-markdown";
|
|
191
210
|
const ENV_SUBDIR = "PI_SAVE_CONVERSATION_DIR";
|
|
@@ -225,7 +244,7 @@ const SAVE_STATE_SCHEMA = "1.2";
|
|
|
225
244
|
* the frontmatter and the document heading); additive frontmatter fields do
|
|
226
245
|
* NOT bump it — they are invisible to any within-major parser.
|
|
227
246
|
*/
|
|
228
|
-
const FORMAT_VERSION = "1.
|
|
247
|
+
const FORMAT_VERSION = "1.6";
|
|
229
248
|
|
|
230
249
|
/**
|
|
231
250
|
* Package version of this extension, read best-effort from the adjacent
|
|
@@ -246,9 +265,6 @@ const EXTENSION_VERSION: string | null = (() => {
|
|
|
246
265
|
|
|
247
266
|
const MAX_TITLE_LENGTH = 60;
|
|
248
267
|
const TITLE_FALLBACK_LENGTH = 40;
|
|
249
|
-
const TOOL_RESULT_PREVIEW = 500;
|
|
250
|
-
const TOOL_ARGS_PREVIEW = 160;
|
|
251
|
-
|
|
252
268
|
type AgentMessage = SessionMessageEntry["message"];
|
|
253
269
|
type UserMessage = Extract<AgentMessage, { role: "user" }>;
|
|
254
270
|
type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
|
|
@@ -950,15 +966,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
950
966
|
return "untitled";
|
|
951
967
|
}
|
|
952
968
|
|
|
953
|
-
|
|
954
|
-
|
|
969
|
+
/** Full argument JSON on one line (JSON.stringify escapes newlines), never truncated. */
|
|
970
|
+
function renderArgs(args: unknown): string {
|
|
955
971
|
try {
|
|
956
|
-
|
|
972
|
+
return JSON.stringify(args) ?? "";
|
|
957
973
|
} catch {
|
|
958
|
-
|
|
974
|
+
return String(args);
|
|
959
975
|
}
|
|
960
|
-
s = s.replace(/\s+/g, " ").trim();
|
|
961
|
-
return s.length > TOOL_ARGS_PREVIEW ? s.slice(0, TOOL_ARGS_PREVIEW) + " …" : s;
|
|
962
976
|
}
|
|
963
977
|
|
|
964
978
|
// ---------- markdown rendering ----------
|
|
@@ -972,30 +986,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
972
986
|
return s.replace(/^(?:[ \t]*\n)+/, "").replace(/\s+$/, "");
|
|
973
987
|
}
|
|
974
988
|
|
|
975
|
-
/** One tool call with its paired result
|
|
989
|
+
/** One tool call with its paired full result (null when no result entry exists). */
|
|
976
990
|
interface RenderedToolCall {
|
|
977
991
|
name: string;
|
|
978
992
|
args: string;
|
|
979
993
|
result: string | null;
|
|
994
|
+
error: boolean;
|
|
980
995
|
}
|
|
981
996
|
|
|
982
|
-
/**
|
|
983
|
-
|
|
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 {
|
|
984
1004
|
const texts: string[] = [];
|
|
985
1005
|
for (const b of m.content) {
|
|
986
1006
|
if (b.type === "text") texts.push(b.text);
|
|
987
1007
|
else texts.push(`_[image: ${b.mimeType}]_`);
|
|
988
1008
|
}
|
|
989
|
-
|
|
990
|
-
const capped =
|
|
991
|
-
flat.length > TOOL_RESULT_PREVIEW ? flat.slice(0, TOOL_RESULT_PREVIEW) + " …" : flat;
|
|
992
|
-
const status = m.isError ? " (error)" : "";
|
|
993
|
-
return `${capped}${status}`.trim();
|
|
1009
|
+
return texts.join("\n\n").trim();
|
|
994
1010
|
}
|
|
995
1011
|
|
|
996
|
-
/**
|
|
1012
|
+
/**
|
|
1013
|
+
* Standalone block for a result whose call is not in this file — same full
|
|
1014
|
+
* content as folded results.
|
|
1015
|
+
*/
|
|
997
1016
|
function renderToolResult(m: ToolResultMessage): string {
|
|
998
|
-
|
|
1017
|
+
const text = resultText(m);
|
|
1018
|
+
const err = m.isError ? " (error)" : "";
|
|
1019
|
+
return `**Tool · ${m.toolName}**${err}\n\n${text ? fencedCode(text) : "_(empty result)_"}`;
|
|
999
1020
|
}
|
|
1000
1021
|
|
|
1001
1022
|
/** "read, web_search ×2" — tool names with repeat counts, first-seen order. */
|
|
@@ -1007,16 +1028,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
1007
1028
|
|
|
1008
1029
|
/**
|
|
1009
1030
|
* Fold tool calls and their paired results into one collapsed callout.
|
|
1010
|
-
*
|
|
1011
|
-
* 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.
|
|
1012
1034
|
*/
|
|
1013
1035
|
function renderToolCallsCallout(calls: RenderedToolCall[]): string {
|
|
1014
1036
|
const summary = summarizeToolNames(calls.map((c) => c.name));
|
|
1015
1037
|
const items = calls.map((c) => {
|
|
1016
|
-
const
|
|
1038
|
+
const err = c.error ? " (error)" : "";
|
|
1039
|
+
const head = c.args
|
|
1040
|
+
? `**\`${c.name}\`**${err} ${inlineCode(c.args)}`
|
|
1041
|
+
: `**\`${c.name}\`**${err}`;
|
|
1017
1042
|
const result =
|
|
1018
|
-
c.result === null ? "_(no result)_" :
|
|
1019
|
-
return `${head}\n\n
|
|
1043
|
+
c.result === null ? "_(no result)_" : c.result ? fencedCode(c.result) : "_(empty result)_";
|
|
1044
|
+
return `${head}\n\n${result}`;
|
|
1020
1045
|
});
|
|
1021
1046
|
return callout("quote", `Tool Calls · ${calls.length} (${summary})`, items.join("\n\n"));
|
|
1022
1047
|
}
|
|
@@ -1052,8 +1077,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1052
1077
|
results.delete(b.id);
|
|
1053
1078
|
calls.push({
|
|
1054
1079
|
name: b.name,
|
|
1055
|
-
args:
|
|
1056
|
-
result: r ?
|
|
1080
|
+
args: renderArgs(b.arguments),
|
|
1081
|
+
result: r ? resultText(r) : null,
|
|
1082
|
+
error: r ? r.isError : false,
|
|
1057
1083
|
});
|
|
1058
1084
|
}
|
|
1059
1085
|
}
|
|
@@ -1925,6 +1951,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1925
1951
|
|
|
1926
1952
|
// 1. Automatic: save after every settled agent turn.
|
|
1927
1953
|
pi.on("agent_settled", async (_event: AgentSettledEvent, ctx: ExtensionContext) => {
|
|
1954
|
+
// Non-persisted sessions (in-memory, `--no-session`) are ephemeral
|
|
1955
|
+
// auxiliary agents — see the skip rule in the header. The manual
|
|
1956
|
+
// /save-conversation command below is the explicit-demand escape hatch.
|
|
1957
|
+
if (!ctx.sessionManager.getSessionFile()) {
|
|
1958
|
+
debug(
|
|
1959
|
+
"agent_settled — session has no session file (in-memory / --no-session); skipping auto-save",
|
|
1960
|
+
);
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1928
1963
|
debug("agent_settled — saving conversation");
|
|
1929
1964
|
try {
|
|
1930
1965
|
const live = liveContext(ctx);
|
|
@@ -2005,28 +2040,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
2005
2040
|
|
|
2006
2041
|
// The live session first, through the normal path: its in-memory tree may
|
|
2007
2042
|
// be fresher than disk, and its state entry goes through pi.appendEntry.
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2043
|
+
// A non-persisted current session (in-memory / --no-session) is skipped —
|
|
2044
|
+
// see the skip rule in the header.
|
|
2045
|
+
if (!ctx.sessionManager.getSessionFile()) {
|
|
2046
|
+
debug(
|
|
2047
|
+
"/" + COMMAND_ALL + " — current session not persisted (in-memory / --no-session); skipped",
|
|
2048
|
+
);
|
|
2049
|
+
} else {
|
|
2050
|
+
try {
|
|
2051
|
+
const r = await runSave(live);
|
|
2052
|
+
count(r);
|
|
2053
|
+
if (ctx.hasUI) {
|
|
2054
|
+
const target = r.file ? relativeForUser(live, r.file) : "";
|
|
2055
|
+
ctx.ui.notify(`${NOTIFY_TAG} ${r.message}${target ? ` → ${target}` : ""}`, "info");
|
|
2056
|
+
if (r.recovered && r.file) {
|
|
2057
|
+
ctx.ui.notify(`${NOTIFY_TAG} ${r.recovered} → ${target}`, "warning");
|
|
2058
|
+
}
|
|
2059
|
+
if (r.switchedFrom && r.file) {
|
|
2060
|
+
ctx.ui.notify(
|
|
2061
|
+
`${NOTIFY_TAG} branch changed — new branch file ${target}; the earlier branch file ${r.switchedFrom} is kept`,
|
|
2062
|
+
"info",
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2016
2065
|
}
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
);
|
|
2066
|
+
} catch (e) {
|
|
2067
|
+
failed.push("<current session>");
|
|
2068
|
+
debug("/" + COMMAND_ALL + " live save failed:", String(e));
|
|
2069
|
+
if (ctx.hasUI) {
|
|
2070
|
+
ctx.ui.notify(`${NOTIFY_TAG} save failed: ${String(e)}`, "error");
|
|
2022
2071
|
}
|
|
2023
2072
|
}
|
|
2024
|
-
} catch (e) {
|
|
2025
|
-
failed.push("<current session>");
|
|
2026
|
-
debug("/" + COMMAND_ALL + " live save failed:", String(e));
|
|
2027
|
-
if (ctx.hasUI) {
|
|
2028
|
-
ctx.ui.notify(`${NOTIFY_TAG} save failed: ${String(e)}`, "error");
|
|
2029
|
-
}
|
|
2030
2073
|
}
|
|
2031
2074
|
|
|
2032
2075
|
// The project's sessions directory — every session jsonl lives there.
|
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",
|