@oh-my-pi/pi-coding-agent 17.2.11 → 17.2.12

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.
Files changed (152) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/{CHANGELOG-d9xenpn9.md → CHANGELOG-k9ghy5sn.md} +13 -0
  3. package/dist/cli.js +10795 -10791
  4. package/dist/types/advisor/runtime.d.ts +1 -1
  5. package/dist/types/config/custom-models.d.ts +31 -0
  6. package/dist/types/config/model-config-values.d.ts +19 -0
  7. package/dist/types/config/model-patch.d.ts +93 -0
  8. package/dist/types/config/model-provider-discovery.d.ts +51 -0
  9. package/dist/types/config/model-registry.d.ts +5 -52
  10. package/dist/types/config/settings.d.ts +5 -3
  11. package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
  12. package/dist/types/discovery/agent-plugin-format.d.ts +6 -13
  13. package/dist/types/discovery/contained-path.d.ts +2 -3
  14. package/dist/types/eval/executor-base.d.ts +8 -2
  15. package/dist/types/eval/kernel-session-registry.d.ts +60 -0
  16. package/dist/types/export/share.d.ts +1 -1
  17. package/dist/types/lsp/diagnostics.d.ts +96 -0
  18. package/dist/types/lsp/index.d.ts +7 -128
  19. package/dist/types/lsp/servers.d.ts +72 -0
  20. package/dist/types/lsp/tool.d.ts +42 -0
  21. package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
  22. package/dist/types/lsp/writethrough.d.ts +33 -0
  23. package/dist/types/mcp/transports/header-policy.d.ts +2 -1
  24. package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
  25. package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
  26. package/dist/types/modes/theme/color.d.ts +19 -0
  27. package/dist/types/modes/theme/loader.d.ts +19 -0
  28. package/dist/types/modes/theme/schema.d.ts +175 -0
  29. package/dist/types/modes/theme/symbols.d.ts +28 -0
  30. package/dist/types/modes/theme/theme-class.d.ts +244 -0
  31. package/dist/types/modes/theme/theme.d.ts +9 -280
  32. package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
  33. package/dist/types/registry/agent-registry.d.ts +1 -3
  34. package/dist/types/secrets/index.d.ts +3 -1
  35. package/dist/types/secrets/message-transform.d.ts +40 -0
  36. package/dist/types/secrets/obfuscator.d.ts +0 -108
  37. package/dist/types/secrets/placeholder-scan.d.ts +95 -0
  38. package/dist/types/secrets/placeholder.d.ts +94 -0
  39. package/dist/types/secrets/replacement.d.ts +82 -0
  40. package/dist/types/session/agent-session-types.d.ts +6 -0
  41. package/dist/types/session/agent-session.d.ts +1 -1
  42. package/dist/types/session/session-handoff.d.ts +3 -3
  43. package/dist/types/session/session-manager.d.ts +32 -0
  44. package/dist/types/session/session-provider-boundary.d.ts +1 -1
  45. package/dist/types/session/turn-recovery.d.ts +2 -2
  46. package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
  47. package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
  48. package/dist/types/slash-commands/builtin-control.d.ts +2 -0
  49. package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
  50. package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
  51. package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
  52. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  53. package/dist/types/slash-commands/builtin-session.d.ts +2 -0
  54. package/dist/types/tools/gh-common.d.ts +69 -0
  55. package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
  56. package/dist/types/tools/gh-pr-diff.d.ts +102 -0
  57. package/dist/types/tools/gh-run-watch.d.ts +78 -0
  58. package/dist/types/tools/gh-search.d.ts +61 -0
  59. package/dist/types/tools/gh-types.d.ts +340 -0
  60. package/dist/types/tools/gh-view.d.ts +88 -0
  61. package/dist/types/tools/gh.d.ts +4 -201
  62. package/dist/types/tools/read-archive.d.ts +16 -0
  63. package/dist/types/tools/read-format.d.ts +104 -0
  64. package/dist/types/tools/read-path-resolution.d.ts +19 -0
  65. package/dist/types/tools/read-pdf-images.d.ts +12 -0
  66. package/dist/types/tools/read-renderer.d.ts +24 -0
  67. package/dist/types/tools/read-selector.d.ts +27 -0
  68. package/dist/types/tools/read-sqlite.d.ts +16 -0
  69. package/dist/types/tools/read-summary.d.ts +19 -0
  70. package/dist/types/tools/read.d.ts +1 -23
  71. package/dist/types/tools/shell-tokenize.d.ts +2 -1
  72. package/dist/types/utils/changelog.d.ts +0 -5
  73. package/package.json +13 -13
  74. package/src/advisor/runtime.ts +12 -7
  75. package/src/config/custom-models.ts +188 -0
  76. package/src/config/model-config-values.ts +128 -0
  77. package/src/config/model-patch.ts +252 -0
  78. package/src/config/model-provider-discovery.ts +132 -0
  79. package/src/config/model-registry.ts +64 -704
  80. package/src/config/settings.ts +7 -3
  81. package/src/debug/raw-sse-buffer.ts +20 -0
  82. package/src/discovery/agent-plugin-format.ts +7 -7
  83. package/src/discovery/contained-path.ts +2 -5
  84. package/src/edit/hashline/diff.ts +5 -1
  85. package/src/eval/executor-base.ts +39 -6
  86. package/src/eval/jl/executor.ts +82 -371
  87. package/src/eval/kernel-session-registry.ts +398 -0
  88. package/src/eval/py/executor.ts +53 -261
  89. package/src/eval/rb/executor.ts +36 -279
  90. package/src/export/share.ts +2 -1
  91. package/src/lsp/diagnostics.ts +516 -0
  92. package/src/lsp/index.ts +20 -2819
  93. package/src/lsp/servers.ts +296 -0
  94. package/src/lsp/tool.ts +1352 -0
  95. package/src/lsp/workspace-diagnostics.ts +170 -0
  96. package/src/lsp/writethrough.ts +561 -0
  97. package/src/mcp/transports/header-policy.ts +1 -1
  98. package/src/modes/components/agent-hub-projection.ts +2 -2
  99. package/src/modes/components/agent-hub-renderer.ts +3 -7
  100. package/src/modes/controllers/command-controller.ts +4 -1
  101. package/src/modes/theme/color.ts +133 -0
  102. package/src/modes/theme/loader.ts +178 -0
  103. package/src/modes/theme/schema.ts +263 -0
  104. package/src/modes/theme/symbols.ts +1000 -0
  105. package/src/modes/theme/theme-class.ts +611 -0
  106. package/src/modes/theme/theme.ts +27 -2453
  107. package/src/modes/theme/tui-adapters.ts +279 -0
  108. package/src/registry/agent-registry.ts +2 -2
  109. package/src/secrets/index.ts +6 -12
  110. package/src/secrets/message-transform.ts +287 -0
  111. package/src/secrets/obfuscator.ts +39 -1303
  112. package/src/secrets/placeholder-scan.ts +506 -0
  113. package/src/secrets/placeholder.ts +309 -0
  114. package/src/secrets/replacement.ts +216 -0
  115. package/src/session/agent-session-types.ts +6 -0
  116. package/src/session/agent-session.ts +113 -7
  117. package/src/session/indexed-session-storage.ts +7 -2
  118. package/src/session/session-advisors.ts +32 -34
  119. package/src/session/session-handoff.ts +39 -20
  120. package/src/session/session-manager.ts +67 -3
  121. package/src/session/session-provider-boundary.ts +3 -6
  122. package/src/session/turn-recovery.ts +21 -12
  123. package/src/slash-commands/builtin-collaboration.ts +504 -0
  124. package/src/slash-commands/builtin-completions.ts +291 -0
  125. package/src/slash-commands/builtin-control.ts +78 -0
  126. package/src/slash-commands/builtin-lifecycle.ts +506 -0
  127. package/src/slash-commands/builtin-marketplace.ts +567 -0
  128. package/src/slash-commands/builtin-modes.ts +518 -0
  129. package/src/slash-commands/builtin-registry.ts +21 -3000
  130. package/src/slash-commands/builtin-session.ts +592 -0
  131. package/src/task/executor.ts +17 -14
  132. package/src/tools/gh-common.ts +288 -0
  133. package/src/tools/gh-pr-checkout.ts +679 -0
  134. package/src/tools/gh-pr-diff.ts +473 -0
  135. package/src/tools/gh-run-watch.ts +1015 -0
  136. package/src/tools/gh-search.ts +500 -0
  137. package/src/tools/gh-types.ts +379 -0
  138. package/src/tools/gh-view.ts +612 -0
  139. package/src/tools/gh.ts +109 -3821
  140. package/src/tools/read-archive.ts +209 -0
  141. package/src/tools/read-format.ts +593 -0
  142. package/src/tools/read-path-resolution.ts +36 -0
  143. package/src/tools/read-pdf-images.ts +250 -0
  144. package/src/tools/read-renderer.ts +289 -0
  145. package/src/tools/read-selector.ts +84 -0
  146. package/src/tools/read-sqlite.ts +215 -0
  147. package/src/tools/read-summary.ts +199 -0
  148. package/src/tools/read.ts +77 -1820
  149. package/src/tools/shell-tokenize.ts +1 -1
  150. package/src/utils/changelog.ts +1 -1
  151. package/src/utils/title-generator.ts +3 -1
  152. package/src/web/search/providers/zai.ts +12 -3
@@ -0,0 +1,279 @@
1
+ import {
2
+ type HighlightColors as NativeHighlightColors,
3
+ highlightCode as nativeHighlightCode,
4
+ supportsLanguage as nativeSupportsLanguage,
5
+ } from "@oh-my-pi/pi-natives";
6
+ import type { EditorTheme, MarkdownTheme, SelectListTheme, SettingsListTheme, SymbolTheme } from "@oh-my-pi/pi-tui";
7
+ import chalk from "@oh-my-pi/pi-utils/chalk";
8
+ import { LRUCache } from "@oh-my-pi/pi-utils/lru";
9
+ import { resolveMermaidAscii } from "./mermaid-cache";
10
+ import { theme } from "./theme";
11
+ import type { Theme } from "./theme-class";
12
+
13
+ // ============================================================================
14
+ // TUI Helpers
15
+ // ============================================================================
16
+
17
+ let cachedHighlightColorsFor: Theme | undefined;
18
+ let cachedHighlightColors: NativeHighlightColors | undefined;
19
+
20
+ function getHighlightColors(t: Theme): NativeHighlightColors {
21
+ if (cachedHighlightColorsFor !== t || !cachedHighlightColors) {
22
+ cachedHighlightColorsFor = t;
23
+ cachedHighlightColors = {
24
+ comment: t.getFgAnsi("syntaxComment"),
25
+ keyword: t.getFgAnsi("syntaxKeyword"),
26
+ function: t.getFgAnsi("syntaxFunction"),
27
+ variable: t.getFgAnsi("syntaxVariable"),
28
+ string: t.getFgAnsi("syntaxString"),
29
+ number: t.getFgAnsi("syntaxNumber"),
30
+ type: t.getFgAnsi("syntaxType"),
31
+ operator: t.getFgAnsi("syntaxOperator"),
32
+ punctuation: t.getFgAnsi("syntaxPunctuation"),
33
+ inserted: t.getFgAnsi("toolDiffAdded"),
34
+ deleted: t.getFgAnsi("toolDiffRemoved"),
35
+ };
36
+ }
37
+ return cachedHighlightColors;
38
+ }
39
+
40
+ /**
41
+ * Memoized native syntax highlight. Returns the joined ANSI string, or `null`
42
+ * when the native tokenizer throws so callers can apply their own fallback.
43
+ *
44
+ * Keyed on `(lang, code)` and reset whenever the active `theme` instance
45
+ * changes — the ANSI colors are baked into the highlighted output, so a theme
46
+ * switch (which always reassigns `theme`) must invalidate every entry.
47
+ *
48
+ * Why this exists: animated tool blocks (eval/bash) repaint their box on every
49
+ * ~33ms border-shimmer frame, and markdown re-lexes on every streamed delta.
50
+ * Without memoization each frame can re-tokenize an unchanged code body through
51
+ * the Rust FFI — ~26ms for 100 lines, ~40ms for 150 — consuming or overrunning
52
+ * the 33ms frame budget and starving the spinner/render timers (the "TUI freeze").
53
+ */
54
+ const HIGHLIGHT_CACHE_MAX = 256;
55
+ const highlightCache = new LRUCache<string, string>({ max: HIGHLIGHT_CACHE_MAX });
56
+ let highlightCacheTheme: Theme | undefined;
57
+
58
+ function highlightCached(code: string, validLang: string | undefined, highlightTheme: Theme): string | null {
59
+ if (highlightCacheTheme !== highlightTheme) {
60
+ highlightCache.clear();
61
+ highlightCacheTheme = highlightTheme;
62
+ }
63
+ const key = `${validLang ?? ""}\x00${code}`;
64
+ const hit = highlightCache.get(key);
65
+ if (hit !== undefined) {
66
+ return hit;
67
+ }
68
+ let highlighted: string;
69
+ try {
70
+ highlighted = nativeHighlightCode(code, validLang, getHighlightColors(highlightTheme));
71
+ } catch {
72
+ return null;
73
+ }
74
+ highlightCache.set(key, highlighted);
75
+ return highlighted;
76
+ }
77
+
78
+ /**
79
+ * Highlight code with syntax coloring based on file extension or language.
80
+ * Returns array of highlighted lines.
81
+ */
82
+ export function highlightCode(code: string, lang?: string, highlightTheme: Theme = theme): string[] {
83
+ const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
84
+ const highlighted = highlightCached(code, validLang, highlightTheme);
85
+ // Always return a fresh array: callers (e.g. renderCodeCell) push extra lines
86
+ // onto the result, which would corrupt the cached string otherwise.
87
+ return (highlighted ?? code).split("\n");
88
+ }
89
+
90
+ export function getSymbolTheme(): SymbolTheme {
91
+ // Guard against `theme` being undefined (pre-init or cross-module-instance
92
+ // plugin calls). Fall back to the ASCII preset so the returned symbols are
93
+ // usable instead of crashing. See #2998.
94
+ if (typeof theme === "undefined") {
95
+ const box = {
96
+ topLeft: "+",
97
+ topRight: "+",
98
+ bottomLeft: "+",
99
+ bottomRight: "+",
100
+ horizontal: "-",
101
+ vertical: "|",
102
+ cross: "+",
103
+ teeDown: "+",
104
+ teeUp: "+",
105
+ teeLeft: "+",
106
+ teeRight: "+",
107
+ };
108
+ return {
109
+ cursor: ">",
110
+ inputCursor: "|",
111
+ boxRound: box,
112
+ boxSharp: box,
113
+ table: box,
114
+ quoteBorder: "|",
115
+ hrChar: "-",
116
+ colorSwatch: "[]",
117
+ spinnerFrames: ["-", "\\", "|", "/"],
118
+ };
119
+ }
120
+ const preset = theme.getSymbolPreset();
121
+
122
+ return {
123
+ cursor: theme.nav.cursor,
124
+ inputCursor: preset === "ascii" ? "|" : "▏",
125
+ boxRound: theme.boxRound,
126
+ boxSharp: theme.boxSharp,
127
+ table: theme.boxSharp,
128
+ quoteBorder: theme.md.quoteBorder,
129
+ hrChar: theme.md.hrChar,
130
+ colorSwatch: theme.md.colorSwatch,
131
+ spinnerFrames: theme.getSpinnerFrames("activity"),
132
+ };
133
+ }
134
+
135
+ let cachedMarkdownTheme: MarkdownTheme | undefined;
136
+ let cachedMarkdownThemeRef: Theme | undefined;
137
+ let markdownMermaidRendering = true;
138
+
139
+ export function setMarkdownMermaidRendering(enabled: boolean): void {
140
+ if (markdownMermaidRendering === enabled) return;
141
+ markdownMermaidRendering = enabled;
142
+ cachedMarkdownTheme = undefined;
143
+ }
144
+
145
+ export function getMarkdownTheme(): MarkdownTheme {
146
+ if (cachedMarkdownTheme !== undefined && cachedMarkdownThemeRef === theme) {
147
+ return cachedMarkdownTheme;
148
+ }
149
+ const mermaid = markdownMermaidRendering
150
+ ? (() => {
151
+ // Mermaid ASCII diagrams render with the active palette so they read as
152
+ // content rather than raw monochrome. Roles mirror the SVG renderer's
153
+ // mapping; `text`/`muted`/`border`/`borderMuted`/`accent` exist in every theme.
154
+ const mermaidColorMode =
155
+ theme.getColorMode() === "truecolor" ? ("truecolor" as const) : ("ansi256" as const);
156
+ const mermaidTheme = {
157
+ fg: theme.getColorHex("text"),
158
+ border: theme.getColorHex("border"),
159
+ line: theme.getColorHex("muted"),
160
+ arrow: theme.getColorHex("accent"),
161
+ corner: theme.getColorHex("muted"),
162
+ junction: theme.getColorHex("borderMuted"),
163
+ };
164
+ return { mermaidColorMode, mermaidTheme };
165
+ })()
166
+ : undefined;
167
+ const markdownTheme: MarkdownTheme = {
168
+ heading: (text: string) => theme.fg("mdHeading", text),
169
+ link: (text: string) => theme.fg("mdLink", text),
170
+ linkUrl: (text: string) => theme.fg("mdLinkUrl", text),
171
+ code: (text: string) => theme.fg("mdCode", text),
172
+ codeBlock: (text: string) => theme.fg("mdCodeBlock", text),
173
+ codeBlockBorder: (text: string) => theme.fg("mdCodeBlockBorder", text),
174
+ quote: (text: string) => theme.fg("mdQuote", text),
175
+ quoteBorder: (text: string) => theme.fg("mdQuoteBorder", text),
176
+ hr: (text: string) => theme.fg("mdHr", text),
177
+ listBullet: (text: string) => theme.fg("mdListBullet", text),
178
+ bold: (text: string) => theme.bold(text),
179
+ italic: (text: string) => theme.italic(text),
180
+ underline: (text: string) => theme.underline(text),
181
+ strikethrough: (text: string) => chalk.strikethrough(text),
182
+ symbols: getSymbolTheme(),
183
+ resolveMermaidAscii: mermaid
184
+ ? (source, maxWidth) =>
185
+ resolveMermaidAscii(source, {
186
+ maxWidth,
187
+ theme: mermaid.mermaidTheme,
188
+ colorMode: mermaid.mermaidColorMode,
189
+ })
190
+ : undefined,
191
+ highlightCode: (code: string, lang?: string): string[] => {
192
+ const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
193
+ const highlighted = highlightCached(code, validLang, theme);
194
+ if (highlighted !== null) return highlighted.split("\n");
195
+ return code.split("\n").map(line => theme.fg("mdCodeBlock", line));
196
+ },
197
+ };
198
+ cachedMarkdownTheme = markdownTheme;
199
+ cachedMarkdownThemeRef = theme;
200
+ return markdownTheme;
201
+ }
202
+
203
+ export function getSelectListTheme(): SelectListTheme {
204
+ // Guard against `theme` being undefined (pre-init or cross-module-instance
205
+ // plugin calls). See #2998.
206
+ if (typeof theme === "undefined") {
207
+ return {
208
+ selectedPrefix: (text: string) => text,
209
+ selectedText: (text: string) => text,
210
+ description: (text: string) => text,
211
+ scrollInfo: (text: string) => text,
212
+ noMatch: (text: string) => text,
213
+ symbols: getSymbolTheme(),
214
+ hovered: (text: string) => text,
215
+ };
216
+ }
217
+ return {
218
+ selectedPrefix: (text: string) => theme.fg("accent", text),
219
+ selectedText: (text: string) => theme.fg("accent", text),
220
+ description: (text: string) => theme.fg("muted", text),
221
+ scrollInfo: (text: string) => theme.fg("muted", text),
222
+ noMatch: (text: string) => theme.fg("muted", text),
223
+ symbols: getSymbolTheme(),
224
+ hovered: (text: string) => theme.bg("selectedBg", text),
225
+ };
226
+ }
227
+
228
+ export function getEditorTheme(): EditorTheme {
229
+ // Guard against `theme` being undefined (pre-init or cross-module-instance
230
+ // plugin calls). See #2998.
231
+ if (typeof theme === "undefined") {
232
+ return {
233
+ borderColor: (text: string) => text,
234
+ selectList: getSelectListTheme(),
235
+ symbols: getSymbolTheme(),
236
+ hintStyle: (text: string) => text,
237
+ };
238
+ }
239
+ return {
240
+ borderColor: (text: string) => theme.fg("borderMuted", text),
241
+ selectList: getSelectListTheme(),
242
+ symbols: getSymbolTheme(),
243
+ hintStyle: (text: string) => theme.fg("dim", text),
244
+ };
245
+ }
246
+
247
+ export function getSettingsListTheme(): SettingsListTheme {
248
+ // Plugins (e.g. pi-rtk-optimizer) may call this before `initTheme()` assigns
249
+ // the global `theme`, or from a separate module instance under npm-global
250
+ // installs where the live binding was never initialized. Fall back to plain
251
+ // text so the call returns a usable (unstyled) theme instead of crashing with
252
+ // "undefined is not an object (evaluating 'theme.fg')". See #2998.
253
+ if (typeof theme === "undefined") {
254
+ return {
255
+ label: (text: string) => text,
256
+ value: (text: string) => text,
257
+ description: (text: string) => text,
258
+ cursor: "> ",
259
+ hint: (text: string) => text,
260
+ heading: (text: string) => text,
261
+ section: (text: string) => text,
262
+ hovered: (text: string) => text,
263
+ };
264
+ }
265
+ return {
266
+ label: (text: string, selected: boolean, changed: boolean) =>
267
+ changed ? theme.fg("statusLineGitDirty", text) : selected ? theme.fg("accent", text) : text,
268
+ value: (text: string, selected: boolean, changed: boolean) =>
269
+ changed ? theme.fg("statusLineGitDirty", text) : selected ? theme.fg("accent", text) : theme.fg("muted", text),
270
+ description: (text: string) => theme.fg("dim", text),
271
+ cursor: theme.fg("accent", `${theme.nav.cursor} `),
272
+ hint: (text: string) => theme.fg("dim", text),
273
+ heading: (text: string, dimmed: boolean) =>
274
+ dimmed ? theme.fg("dim", theme.underline(text)) : theme.fg("muted", theme.bold(theme.underline(text))),
275
+ section: (text: string, active: boolean) =>
276
+ active ? theme.fg("accent", theme.bold(text)) : theme.fg("muted", text),
277
+ hovered: (text: string) => theme.bg("selectedBg", text),
278
+ };
279
+ }
@@ -15,7 +15,7 @@ import { oneLineLabel } from "../task/types";
15
15
  export const MAIN_AGENT_ID = "Main";
16
16
 
17
17
  /** Sidecar marker retained beside a child transcript after an explicit kill. */
18
- export const AGENT_TOMBSTONE_SUFFIX = ".tombstone";
18
+ const AGENT_TOMBSTONE_SUFFIX = ".tombstone";
19
19
 
20
20
  export function getAgentTombstonePath(sessionFile: string): string {
21
21
  return `${sessionFile}${AGENT_TOMBSTONE_SUFFIX}`;
@@ -30,7 +30,7 @@ export function getAgentTombstonePath(sessionFile: string): string {
30
30
  */
31
31
  export type AgentStatus = "running" | "idle" | "parked" | "aborted";
32
32
  /** Provenance of a displayed duration: active runtime, transcript span, or unavailable. */
33
- export type AgentDurationKind = "active" | "span" | "unknown";
33
+ type AgentDurationKind = "active" | "span" | "unknown";
34
34
  /**
35
35
  * - `main`/`sub`: the user-facing agent tree (driving agent + task subagents).
36
36
  * - `advisor`: a passive review transcript persisted like a subagent for usage
@@ -4,14 +4,10 @@ import * as path from "node:path";
4
4
  import { SENSITIVE_TOKEN_RE } from "@oh-my-pi/pi-ai/providers/transform-messages";
5
5
  import { getSecretPlaceholderKeyPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
6
6
  import { YAML } from "bun";
7
- import {
8
- regexHasUnresolvableShortMatchFallback,
9
- type SecretEntry,
10
- SecretObfuscator,
11
- sanitizeSecretFriendlyName,
12
- secretEntriesNeedPlaceholderKey,
13
- } from "./obfuscator";
7
+ import { type SecretEntry, SecretObfuscator } from "./obfuscator";
8
+ import { sanitizeSecretFriendlyName, secretEntriesNeedPlaceholderKey } from "./placeholder";
14
9
  import { compileSecretRegex } from "./regex";
10
+ import { regexHasUnresolvableShortMatchFallback } from "./replacement";
15
11
 
16
12
  const PLACEHOLDER_KEY_RE = /^[A-Za-z0-9_-]{43}$/;
17
13
  const cachedPlaceholderKeys = new Map<string, string>();
@@ -158,11 +154,9 @@ export {
158
154
  deobfuscateToolArguments,
159
155
  obfuscateMessages,
160
156
  obfuscateProviderContext,
161
- type SecretEntry,
162
- SecretObfuscator,
163
- secretEntriesNeedPlaceholderKey,
164
- secretEntryNeedsPlaceholderKey,
165
- } from "./obfuscator";
157
+ } from "./message-transform";
158
+ export { type SecretEntry, SecretObfuscator } from "./obfuscator";
159
+ export { secretEntriesNeedPlaceholderKey, secretEntryNeedsPlaceholderKey } from "./placeholder";
166
160
 
167
161
  /**
168
162
  * Load secrets from project-local and global secrets.yml files.
@@ -0,0 +1,287 @@
1
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
+ import type { AssistantMessage, Context, ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
3
+ import type { SessionContext } from "../session/session-context";
4
+ import type { JsonValue, SecretObfuscator } from "./obfuscator";
5
+ import { collectJsonRegexSecretValues, mapJsonStrings } from "./placeholder-scan";
6
+
7
+ // ═══════════════════════════════════════════════════════════════════════════
8
+ // Display restore (inbound, persisted/provider → local display)
9
+ // ═══════════════════════════════════════════════════════════════════════════
10
+
11
+ /**
12
+ * Restore secret placeholders for local display. Only message kinds the model
13
+ * itself authored from obfuscated context carry placeholders — assistant
14
+ * content and the LLM-written branch/compaction summaries. User, developer, and
15
+ * tool-result messages are persisted with their literal text, so operator-authored
16
+ * placeholder-shaped text must survive untouched; those roles are never walked.
17
+ */
18
+ export function deobfuscateSessionContext(
19
+ sessionContext: SessionContext,
20
+ obfuscator: SecretObfuscator | undefined,
21
+ ): SessionContext {
22
+ if (!obfuscator?.hasSecrets()) return sessionContext;
23
+ const messages = deobfuscateAgentMessages(obfuscator, sessionContext.messages);
24
+ return messages === sessionContext.messages ? sessionContext : { ...sessionContext, messages };
25
+ }
26
+
27
+ export function deobfuscateAgentMessages(obfuscator: SecretObfuscator, messages: AgentMessage[]): AgentMessage[] {
28
+ const deob = (text: string): string => obfuscator.deobfuscate(text);
29
+ let changed = false;
30
+ const result = messages.map((message): AgentMessage => {
31
+ switch (message.role) {
32
+ case "assistant": {
33
+ const content = deobfuscateAssistantContent(obfuscator, message.content);
34
+ if (content === message.content) return message;
35
+ changed = true;
36
+ return { ...message, content };
37
+ }
38
+ case "branchSummary": {
39
+ const summary = deob(message.summary);
40
+ if (summary === message.summary) return message;
41
+ changed = true;
42
+ return { ...message, summary };
43
+ }
44
+ case "compactionSummary": {
45
+ const summary = deob(message.summary);
46
+ const shortSummary = message.shortSummary === undefined ? undefined : deob(message.shortSummary);
47
+ const blocks = message.blocks === undefined ? undefined : deobfuscateTextBlocks(obfuscator, message.blocks);
48
+ if (summary === message.summary && shortSummary === message.shortSummary && blocks === message.blocks) {
49
+ return message;
50
+ }
51
+ changed = true;
52
+ return { ...message, summary, shortSummary, blocks };
53
+ }
54
+ default:
55
+ return message;
56
+ }
57
+ });
58
+ return changed ? result : messages;
59
+ }
60
+
61
+ /**
62
+ * Restore placeholders in assistant content: visible text and tool-call
63
+ * arguments/intent/rawBlock. Thinking and signatures are opaque
64
+ * provider-replay/hidden-reasoning data and pass through byte-identical.
65
+ */
66
+ export function deobfuscateAssistantContent(
67
+ obfuscator: SecretObfuscator,
68
+ content: AssistantMessage["content"],
69
+ ): AssistantMessage["content"] {
70
+ if (!obfuscator.hasSecrets()) return content;
71
+ const deob = (text: string): string => obfuscator.deobfuscate(text);
72
+ let changed = false;
73
+ const result = content.map((block): AssistantMessage["content"][number] => {
74
+ if (block.type === "text") {
75
+ const text = deob(block.text);
76
+ if (text === block.text) return block;
77
+ changed = true;
78
+ return { ...block, text };
79
+ }
80
+
81
+ if (block.type === "toolCall") {
82
+ const args = deobfuscateToolArguments(obfuscator, block.arguments);
83
+ const intent = block.intent === undefined ? undefined : deob(block.intent);
84
+ const rawBlock = block.rawBlock === undefined ? undefined : deob(block.rawBlock);
85
+ if (args === block.arguments && intent === block.intent && rawBlock === block.rawBlock) return block;
86
+ changed = true;
87
+ return { ...block, arguments: args, intent, rawBlock };
88
+ }
89
+ return block;
90
+ });
91
+ return changed ? result : content;
92
+ }
93
+
94
+ /**
95
+ * Restore placeholders inside a tool call's arguments. Arguments are arbitrary
96
+ * model-authored JSON, so tool-call arguments are the ONLY place a recursive
97
+ * JSON walk runs.
98
+ */
99
+ export function deobfuscateToolArguments(
100
+ obfuscator: SecretObfuscator,
101
+ args: Record<string, unknown>,
102
+ ): Record<string, unknown> {
103
+ if (!obfuscator.hasSecrets()) return args;
104
+ return mapJsonStrings(args as JsonValue, s => obfuscator.deobfuscate(s)) as Record<string, unknown>;
105
+ }
106
+
107
+ /** Redact secrets inside a tool call's arguments (same JSON-walk exception as {@link deobfuscateToolArguments}). */
108
+ export function obfuscateToolArguments(
109
+ obfuscator: SecretObfuscator,
110
+ args: Record<string, unknown>,
111
+ sharedRegexSecretValues?: ReadonlySet<string>,
112
+ ): Record<string, unknown> {
113
+ if (!obfuscator.hasSecrets()) return args;
114
+ const regexSecretValues = sharedRegexSecretValues ?? collectJsonRegexSecretValues(obfuscator, args as JsonValue);
115
+ return mapJsonStrings(args as JsonValue, s => obfuscator.obfuscate(s, regexSecretValues)) as Record<string, unknown>;
116
+ }
117
+
118
+ // ═══════════════════════════════════════════════════════════════════════════
119
+ // Outbound obfuscation (local → provider)
120
+ // ═══════════════════════════════════════════════════════════════════════════
121
+
122
+ type UserFacingMessage = Extract<Message, { role: "user" | "developer" | "toolResult" }>;
123
+
124
+ /** Obfuscate `text` blocks of a content array; image and other blocks pass through. */
125
+ function obfuscateTextBlocks(
126
+ obfuscator: SecretObfuscator,
127
+ content: (TextContent | ImageContent)[],
128
+ sharedRegexSecretValues?: ReadonlySet<string>,
129
+ ): (TextContent | ImageContent)[] {
130
+ let changed = false;
131
+ const result = content.map((block): TextContent | ImageContent => {
132
+ if (block.type !== "text") return block;
133
+ const text = obfuscator.obfuscate(block.text, sharedRegexSecretValues);
134
+ if (text === block.text) return block;
135
+ changed = true;
136
+ return { ...block, text };
137
+ });
138
+ return changed ? result : content;
139
+ }
140
+
141
+ /** Restore placeholders in `text` blocks of a content array; image and other blocks pass through. */
142
+ function deobfuscateTextBlocks(
143
+ obfuscator: SecretObfuscator,
144
+ content: (TextContent | ImageContent)[],
145
+ ): (TextContent | ImageContent)[] {
146
+ let changed = false;
147
+ const result = content.map((block): TextContent | ImageContent => {
148
+ if (block.type !== "text") return block;
149
+ const text = obfuscator.deobfuscate(block.text);
150
+ if (text === block.text) return block;
151
+ changed = true;
152
+ return { ...block, text };
153
+ });
154
+ return changed ? result : content;
155
+ }
156
+
157
+ /**
158
+ * Re-obfuscate assistant content before it returns to a provider after session
159
+ * restoration, removing friendly prefixes made unsafe by this batch. A changed
160
+ * thinking block loses its byte-bound replay signature.
161
+ */
162
+ function obfuscateAssistantContentForReplay(
163
+ obfuscator: SecretObfuscator,
164
+ content: AssistantMessage["content"],
165
+ sharedRegexSecretValues: ReadonlySet<string>,
166
+ ): AssistantMessage["content"] {
167
+ const obfuscate = (text: string): string =>
168
+ obfuscator.stripUnsafeFriendlyPlaceholderPrefixes(
169
+ obfuscator.obfuscate(text, sharedRegexSecretValues),
170
+ sharedRegexSecretValues,
171
+ );
172
+ let changed = false;
173
+ const result = content.map((block): AssistantMessage["content"][number] => {
174
+ if (block.type === "text") {
175
+ const text = obfuscate(block.text);
176
+ if (text === block.text) return block;
177
+ changed = true;
178
+ return { ...block, text };
179
+ }
180
+ if (block.type === "thinking") {
181
+ const thinking = obfuscate(block.thinking);
182
+ if (thinking === block.thinking) return block;
183
+ changed = true;
184
+ return { ...block, thinking, thinkingSignature: undefined };
185
+ }
186
+ if (block.type === "toolCall") {
187
+ const args = mapJsonStrings(block.arguments as JsonValue, obfuscate) as Record<string, unknown>;
188
+ const intent = block.intent === undefined ? undefined : obfuscate(block.intent);
189
+ const rawBlock = block.rawBlock === undefined ? undefined : obfuscate(block.rawBlock);
190
+ if (args === block.arguments && intent === block.intent && rawBlock === block.rawBlock) return block;
191
+ changed = true;
192
+ return { ...block, arguments: args, intent, rawBlock };
193
+ }
194
+ return block;
195
+ });
196
+ return changed ? result : content;
197
+ }
198
+
199
+ function collectMessageRegexSecretValues(obfuscator: SecretObfuscator, messages: Message[]): Set<string> {
200
+ const values = new Set<string>();
201
+ const addText = (text: string | undefined): void => {
202
+ if (text === undefined) return;
203
+ for (const value of obfuscator.collectRegexSecretValuesForObfuscation(text)) {
204
+ values.add(value);
205
+ }
206
+ };
207
+ for (const message of messages) {
208
+ if (message.role === "assistant") {
209
+ for (const block of message.content) {
210
+ if (block.type === "text") addText(block.text);
211
+ else if (block.type === "thinking") addText(block.thinking);
212
+ else if (block.type === "toolCall") {
213
+ for (const value of collectJsonRegexSecretValues(obfuscator, block.arguments as JsonValue)) {
214
+ values.add(value);
215
+ }
216
+ addText(block.intent);
217
+ addText(block.rawBlock);
218
+ }
219
+ }
220
+ continue;
221
+ }
222
+ if (
223
+ message.role !== "user" &&
224
+ message.role !== "toolResult" &&
225
+ !(message.role === "developer" && message.attribution === "user")
226
+ ) {
227
+ continue;
228
+ }
229
+ const target = message as UserFacingMessage;
230
+ if (typeof target.content === "string") {
231
+ addText(target.content);
232
+ continue;
233
+ }
234
+ for (const block of target.content) {
235
+ if (block.type === "text") addText(block.text);
236
+ }
237
+ }
238
+ return values;
239
+ }
240
+
241
+ /**
242
+ * Redact secrets from outbound messages. User messages, tool results, and
243
+ * user-authored developer messages (e.g. `@file` mentions) are obfuscated.
244
+ * Assistant replay content is re-obfuscated too, because session restoration
245
+ * expands keyed placeholders locally before the next provider request. Inline
246
+ * image bytes are never walked.
247
+ */
248
+ export function obfuscateMessages(obfuscator: SecretObfuscator, messages: Message[]): Message[] {
249
+ if (!obfuscator.hasSecrets()) return messages;
250
+ const sharedRegexSecretValues = collectMessageRegexSecretValues(obfuscator, messages);
251
+ let changed = false;
252
+ const result = messages.map((message): Message => {
253
+ if (
254
+ message.role !== "user" &&
255
+ message.role !== "toolResult" &&
256
+ !(message.role === "developer" && message.attribution === "user")
257
+ ) {
258
+ if (message.role !== "assistant") return message;
259
+ const content = obfuscateAssistantContentForReplay(obfuscator, message.content, sharedRegexSecretValues);
260
+ if (content === message.content) return message;
261
+ changed = true;
262
+ return { ...message, content };
263
+ }
264
+ const target = message as UserFacingMessage;
265
+ if (typeof target.content === "string") {
266
+ const content = obfuscator.obfuscate(target.content, sharedRegexSecretValues);
267
+ if (content === target.content) return message;
268
+ changed = true;
269
+ return { ...target, content } as Message;
270
+ }
271
+ const content = obfuscateTextBlocks(obfuscator, target.content, sharedRegexSecretValues);
272
+ if (content === target.content) return message;
273
+ changed = true;
274
+ return { ...target, content } as Message;
275
+ });
276
+ return changed ? result : messages;
277
+ }
278
+
279
+ /**
280
+ * Redact outbound provider context. Only conversation messages are rewritten;
281
+ * the static system prompt and tool schemas pass through unchanged.
282
+ */
283
+ export function obfuscateProviderContext(obfuscator: SecretObfuscator | undefined, context: Context): Context {
284
+ if (!obfuscator?.hasSecrets()) return context;
285
+ const messages = obfuscateMessages(obfuscator, context.messages);
286
+ return messages === context.messages ? context : { ...context, messages };
287
+ }