@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.
- package/CHANGELOG.md +13 -0
- package/dist/{CHANGELOG-d9xenpn9.md → CHANGELOG-k9ghy5sn.md} +13 -0
- package/dist/cli.js +10795 -10791
- package/dist/types/advisor/runtime.d.ts +1 -1
- package/dist/types/config/custom-models.d.ts +31 -0
- package/dist/types/config/model-config-values.d.ts +19 -0
- package/dist/types/config/model-patch.d.ts +93 -0
- package/dist/types/config/model-provider-discovery.d.ts +51 -0
- package/dist/types/config/model-registry.d.ts +5 -52
- package/dist/types/config/settings.d.ts +5 -3
- package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
- package/dist/types/discovery/agent-plugin-format.d.ts +6 -13
- package/dist/types/discovery/contained-path.d.ts +2 -3
- package/dist/types/eval/executor-base.d.ts +8 -2
- package/dist/types/eval/kernel-session-registry.d.ts +60 -0
- package/dist/types/export/share.d.ts +1 -1
- package/dist/types/lsp/diagnostics.d.ts +96 -0
- package/dist/types/lsp/index.d.ts +7 -128
- package/dist/types/lsp/servers.d.ts +72 -0
- package/dist/types/lsp/tool.d.ts +42 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
- package/dist/types/lsp/writethrough.d.ts +33 -0
- package/dist/types/mcp/transports/header-policy.d.ts +2 -1
- package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
- package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
- package/dist/types/modes/theme/color.d.ts +19 -0
- package/dist/types/modes/theme/loader.d.ts +19 -0
- package/dist/types/modes/theme/schema.d.ts +175 -0
- package/dist/types/modes/theme/symbols.d.ts +28 -0
- package/dist/types/modes/theme/theme-class.d.ts +244 -0
- package/dist/types/modes/theme/theme.d.ts +9 -280
- package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
- package/dist/types/registry/agent-registry.d.ts +1 -3
- package/dist/types/secrets/index.d.ts +3 -1
- package/dist/types/secrets/message-transform.d.ts +40 -0
- package/dist/types/secrets/obfuscator.d.ts +0 -108
- package/dist/types/secrets/placeholder-scan.d.ts +95 -0
- package/dist/types/secrets/placeholder.d.ts +94 -0
- package/dist/types/secrets/replacement.d.ts +82 -0
- package/dist/types/session/agent-session-types.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/session-handoff.d.ts +3 -3
- package/dist/types/session/session-manager.d.ts +32 -0
- package/dist/types/session/session-provider-boundary.d.ts +1 -1
- package/dist/types/session/turn-recovery.d.ts +2 -2
- package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
- package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
- package/dist/types/slash-commands/builtin-control.d.ts +2 -0
- package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
- package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
- package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/slash-commands/builtin-session.d.ts +2 -0
- package/dist/types/tools/gh-common.d.ts +69 -0
- package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
- package/dist/types/tools/gh-pr-diff.d.ts +102 -0
- package/dist/types/tools/gh-run-watch.d.ts +78 -0
- package/dist/types/tools/gh-search.d.ts +61 -0
- package/dist/types/tools/gh-types.d.ts +340 -0
- package/dist/types/tools/gh-view.d.ts +88 -0
- package/dist/types/tools/gh.d.ts +4 -201
- package/dist/types/tools/read-archive.d.ts +16 -0
- package/dist/types/tools/read-format.d.ts +104 -0
- package/dist/types/tools/read-path-resolution.d.ts +19 -0
- package/dist/types/tools/read-pdf-images.d.ts +12 -0
- package/dist/types/tools/read-renderer.d.ts +24 -0
- package/dist/types/tools/read-selector.d.ts +27 -0
- package/dist/types/tools/read-sqlite.d.ts +16 -0
- package/dist/types/tools/read-summary.d.ts +19 -0
- package/dist/types/tools/read.d.ts +1 -23
- package/dist/types/tools/shell-tokenize.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +0 -5
- package/package.json +13 -13
- package/src/advisor/runtime.ts +12 -7
- package/src/config/custom-models.ts +188 -0
- package/src/config/model-config-values.ts +128 -0
- package/src/config/model-patch.ts +252 -0
- package/src/config/model-provider-discovery.ts +132 -0
- package/src/config/model-registry.ts +64 -704
- package/src/config/settings.ts +7 -3
- package/src/debug/raw-sse-buffer.ts +20 -0
- package/src/discovery/agent-plugin-format.ts +7 -7
- package/src/discovery/contained-path.ts +2 -5
- package/src/edit/hashline/diff.ts +5 -1
- package/src/eval/executor-base.ts +39 -6
- package/src/eval/jl/executor.ts +82 -371
- package/src/eval/kernel-session-registry.ts +398 -0
- package/src/eval/py/executor.ts +53 -261
- package/src/eval/rb/executor.ts +36 -279
- package/src/export/share.ts +2 -1
- package/src/lsp/diagnostics.ts +516 -0
- package/src/lsp/index.ts +20 -2819
- package/src/lsp/servers.ts +296 -0
- package/src/lsp/tool.ts +1352 -0
- package/src/lsp/workspace-diagnostics.ts +170 -0
- package/src/lsp/writethrough.ts +561 -0
- package/src/mcp/transports/header-policy.ts +1 -1
- package/src/modes/components/agent-hub-projection.ts +2 -2
- package/src/modes/components/agent-hub-renderer.ts +3 -7
- package/src/modes/controllers/command-controller.ts +4 -1
- package/src/modes/theme/color.ts +133 -0
- package/src/modes/theme/loader.ts +178 -0
- package/src/modes/theme/schema.ts +263 -0
- package/src/modes/theme/symbols.ts +1000 -0
- package/src/modes/theme/theme-class.ts +611 -0
- package/src/modes/theme/theme.ts +27 -2453
- package/src/modes/theme/tui-adapters.ts +279 -0
- package/src/registry/agent-registry.ts +2 -2
- package/src/secrets/index.ts +6 -12
- package/src/secrets/message-transform.ts +287 -0
- package/src/secrets/obfuscator.ts +39 -1303
- package/src/secrets/placeholder-scan.ts +506 -0
- package/src/secrets/placeholder.ts +309 -0
- package/src/secrets/replacement.ts +216 -0
- package/src/session/agent-session-types.ts +6 -0
- package/src/session/agent-session.ts +113 -7
- package/src/session/indexed-session-storage.ts +7 -2
- package/src/session/session-advisors.ts +32 -34
- package/src/session/session-handoff.ts +39 -20
- package/src/session/session-manager.ts +67 -3
- package/src/session/session-provider-boundary.ts +3 -6
- package/src/session/turn-recovery.ts +21 -12
- package/src/slash-commands/builtin-collaboration.ts +504 -0
- package/src/slash-commands/builtin-completions.ts +291 -0
- package/src/slash-commands/builtin-control.ts +78 -0
- package/src/slash-commands/builtin-lifecycle.ts +506 -0
- package/src/slash-commands/builtin-marketplace.ts +567 -0
- package/src/slash-commands/builtin-modes.ts +518 -0
- package/src/slash-commands/builtin-registry.ts +21 -3000
- package/src/slash-commands/builtin-session.ts +592 -0
- package/src/task/executor.ts +17 -14
- package/src/tools/gh-common.ts +288 -0
- package/src/tools/gh-pr-checkout.ts +679 -0
- package/src/tools/gh-pr-diff.ts +473 -0
- package/src/tools/gh-run-watch.ts +1015 -0
- package/src/tools/gh-search.ts +500 -0
- package/src/tools/gh-types.ts +379 -0
- package/src/tools/gh-view.ts +612 -0
- package/src/tools/gh.ts +109 -3821
- package/src/tools/read-archive.ts +209 -0
- package/src/tools/read-format.ts +593 -0
- package/src/tools/read-path-resolution.ts +36 -0
- package/src/tools/read-pdf-images.ts +250 -0
- package/src/tools/read-renderer.ts +289 -0
- package/src/tools/read-selector.ts +84 -0
- package/src/tools/read-sqlite.ts +215 -0
- package/src/tools/read-summary.ts +199 -0
- package/src/tools/read.ts +77 -1820
- package/src/tools/shell-tokenize.ts +1 -1
- package/src/utils/changelog.ts +1 -1
- package/src/utils/title-generator.ts +3 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
package/src/secrets/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
}
|