@oh-my-pi/pi-coding-agent 16.1.14 → 16.1.16

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 (111) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/cli.js +3705 -4015
  3. package/dist/types/advisor/runtime.d.ts +3 -0
  4. package/dist/types/cli/args.d.ts +2 -5
  5. package/dist/types/cli/flag-tables.d.ts +2 -2
  6. package/dist/types/cli/session-picker.d.ts +4 -2
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/settings-schema.d.ts +32 -1
  9. package/dist/types/eval/agent-bridge.d.ts +19 -0
  10. package/dist/types/eval/js/shared/helpers.d.ts +1 -13
  11. package/dist/types/eval/js/shared/types.d.ts +1 -1
  12. package/dist/types/eval/js/worker-protocol.d.ts +1 -1
  13. package/dist/types/eval/py/executor.d.ts +1 -1
  14. package/dist/types/export/share.d.ts +8 -1
  15. package/dist/types/internal-urls/local-protocol.d.ts +18 -1
  16. package/dist/types/main.d.ts +2 -0
  17. package/dist/types/mcp/transports/stdio.d.ts +12 -1
  18. package/dist/types/modes/components/plugin-settings.d.ts +5 -0
  19. package/dist/types/modes/components/session-selector.d.ts +25 -0
  20. package/dist/types/modes/components/status-line/context-thresholds.d.ts +4 -3
  21. package/dist/types/modes/components/status-line/types.d.ts +1 -0
  22. package/dist/types/secrets/obfuscator.d.ts +3 -3
  23. package/dist/types/task/isolation-runner.d.ts +128 -0
  24. package/dist/types/task/worktree.d.ts +14 -1
  25. package/dist/types/thinking.d.ts +15 -0
  26. package/dist/types/tools/eval-render.d.ts +3 -0
  27. package/dist/types/tools/eval.d.ts +11 -17
  28. package/dist/types/tools/todo.d.ts +26 -28
  29. package/dist/types/tui/output-block.d.ts +8 -0
  30. package/dist/types/utils/image-resize.d.ts +2 -0
  31. package/dist/types/utils/shell-snapshot.d.ts +10 -0
  32. package/dist/types/web/search/providers/exa.d.ts +2 -0
  33. package/dist/types/web/search/providers/perplexity.d.ts +17 -3
  34. package/package.json +12 -12
  35. package/scripts/build-binary.ts +18 -4
  36. package/src/advisor/__tests__/advisor.test.ts +114 -0
  37. package/src/advisor/runtime.ts +129 -1
  38. package/src/cli/args.ts +4 -5
  39. package/src/cli/flag-tables.ts +3 -3
  40. package/src/cli/gallery-fixtures/interaction.ts +6 -9
  41. package/src/cli/gallery-fixtures/shell.ts +15 -23
  42. package/src/cli/session-picker.ts +17 -3
  43. package/src/commands/launch.ts +3 -3
  44. package/src/config/model-registry.ts +12 -4
  45. package/src/config/settings-schema.ts +37 -1
  46. package/src/edit/renderer.ts +34 -12
  47. package/src/eval/__tests__/agent-bridge.test.ts +462 -3
  48. package/src/eval/__tests__/helpers-local-roots.test.ts +2 -5
  49. package/src/eval/__tests__/julia-prelude.test.ts +1 -30
  50. package/src/eval/__tests__/prelude-agent.test.ts +42 -8
  51. package/src/eval/agent-bridge.ts +301 -71
  52. package/src/eval/jl/prelude.jl +32 -227
  53. package/src/eval/jl/runner.jl +38 -12
  54. package/src/eval/js/shared/helpers.ts +1 -114
  55. package/src/eval/js/shared/prelude.txt +13 -27
  56. package/src/eval/js/shared/runtime.ts +0 -6
  57. package/src/eval/js/shared/types.ts +1 -1
  58. package/src/eval/js/worker-protocol.ts +1 -1
  59. package/src/eval/py/__tests__/prelude.test.ts +13 -0
  60. package/src/eval/py/executor.ts +1 -1
  61. package/src/eval/py/prelude.py +47 -105
  62. package/src/eval/py/runner.py +0 -6
  63. package/src/eval/rb/prelude.rb +21 -189
  64. package/src/eval/rb/runner.rb +116 -9
  65. package/src/exec/bash-executor.ts +44 -0
  66. package/src/export/html/tool-views.generated.js +29 -29
  67. package/src/export/share.ts +51 -28
  68. package/src/internal-urls/docs-index.generated.txt +1 -1
  69. package/src/internal-urls/local-protocol.ts +100 -53
  70. package/src/main.ts +15 -4
  71. package/src/mcp/transports/stdio.ts +20 -4
  72. package/src/modes/acp/acp-event-mapper.ts +7 -2
  73. package/src/modes/components/custom-editor.test.ts +22 -0
  74. package/src/modes/components/custom-editor.ts +10 -1
  75. package/src/modes/components/footer.ts +4 -3
  76. package/src/modes/components/plugin-settings.ts +7 -1
  77. package/src/modes/components/session-selector.ts +143 -29
  78. package/src/modes/components/status-line/component.ts +5 -1
  79. package/src/modes/components/status-line/context-thresholds.ts +11 -3
  80. package/src/modes/components/status-line/segments.ts +1 -1
  81. package/src/modes/components/status-line/types.ts +1 -0
  82. package/src/modes/controllers/command-controller.ts +6 -0
  83. package/src/modes/rpc/rpc-mode.ts +6 -0
  84. package/src/modes/utils/copy-targets.ts +7 -2
  85. package/src/prompts/system/system-prompt.md +6 -6
  86. package/src/prompts/system/workflow-notice.md +3 -3
  87. package/src/prompts/tools/bash.md +18 -2
  88. package/src/prompts/tools/eval.md +19 -19
  89. package/src/prompts/tools/search.md +1 -0
  90. package/src/prompts/tools/todo.md +1 -1
  91. package/src/secrets/obfuscator.ts +3 -9
  92. package/src/session/agent-session.ts +264 -52
  93. package/src/slash-commands/builtin-registry.ts +2 -1
  94. package/src/task/index.ts +61 -207
  95. package/src/task/isolation-runner.ts +354 -0
  96. package/src/task/worktree.ts +46 -9
  97. package/src/thinking.ts +20 -0
  98. package/src/tools/ask.ts +44 -38
  99. package/src/tools/bash.ts +9 -2
  100. package/src/tools/browser/tab-worker.ts +1 -1
  101. package/src/tools/eval-render.ts +34 -27
  102. package/src/tools/eval.ts +100 -103
  103. package/src/tools/index.ts +8 -1
  104. package/src/tools/read.ts +136 -60
  105. package/src/tools/todo.ts +60 -64
  106. package/src/tui/code-cell.ts +1 -1
  107. package/src/tui/output-block.ts +11 -0
  108. package/src/utils/image-resize.ts +30 -0
  109. package/src/utils/shell-snapshot.ts +63 -1
  110. package/src/web/search/providers/exa.ts +85 -1
  111. package/src/web/search/providers/perplexity.ts +18 -6
@@ -6,29 +6,21 @@ export { EVAL_DEFAULT_PREVIEW_LINES, evalToolRenderer } from "./eval-render";
6
6
  /** Language tokens the eval tool accepts, in stable display order. */
7
7
  export type EvalLanguageToken = "py" | "js" | "rb" | "jl";
8
8
  /**
9
- * Per-cell input. Each cell runs in order; state persists within a language
10
- * across cells and across tool calls. This static schema carries the full
11
- * language union for typing; {@link buildEvalSchema} narrows the wire copy per
12
- * session so disabled backends are never advertised to the model.
9
+ * Per-call input: a single cell. State persists within a language across
10
+ * separate eval calls and across tool calls, so each call is one logical step
11
+ * and later calls reuse what earlier ones defined. This static schema carries
12
+ * the full language union for typing; {@link buildEvalSchema} narrows the wire
13
+ * copy per session so disabled backends are never advertised to the model.
13
14
  */
14
- declare const evalCellSchema: import("arktype/internal/variants/object.ts").ObjectType<{
15
+ export declare const evalSchema: import("arktype/internal/variants/object.ts").ObjectType<{
15
16
  title?: string | undefined;
16
17
  timeout?: number | undefined;
17
18
  reset?: boolean | undefined;
18
19
  language: "jl" | "js" | "py" | "rb";
19
20
  code: string;
20
21
  }, {}>;
21
- export type EvalCellInput = typeof evalCellSchema.infer;
22
- export declare const evalSchema: import("arktype/internal/variants/object.ts").ObjectType<{
23
- cells: {
24
- title?: string | undefined;
25
- timeout?: number | undefined;
26
- reset?: boolean | undefined;
27
- language: "jl" | "js" | "py" | "rb";
28
- code: string;
29
- }[];
30
- }, {}>;
31
22
  export type EvalToolParams = typeof evalSchema.infer;
23
+ export type EvalCellInput = EvalToolParams;
32
24
  export type EvalToolResult = {
33
25
  content: Array<{
34
26
  type: "text";
@@ -62,10 +54,12 @@ export declare class EvalTool implements AgentTool<typeof evalSchema> {
62
54
  readonly approval: "exec";
63
55
  readonly formatApprovalDetails: (args: unknown) => string[];
64
56
  get summary(): string;
65
- readonly loadMode = "discoverable";
57
+ readonly loadMode = "essential";
66
58
  readonly label = "Eval";
67
59
  get description(): string;
68
- readonly examples: readonly ToolExample<typeof evalSchema.infer>[];
60
+ /** All reuse-chain examples; the `examples` getter filters by enabled languages. */
61
+ private static readonly ALL_EXAMPLES;
62
+ get examples(): readonly ToolExample<typeof evalSchema.infer>[];
69
63
  get parameters(): typeof evalSchema;
70
64
  readonly concurrency = "exclusive";
71
65
  readonly strict = true;
@@ -24,16 +24,14 @@ export interface TodoToolDetails {
24
24
  completedTasks?: TodoCompletionTransition[];
25
25
  }
26
26
  declare const todoSchema: import("arktype/internal/variants/object.ts").ObjectType<{
27
- ops: {
28
- op: "append" | "done" | "drop" | "init" | "rm" | "start" | "view";
29
- list?: {
30
- phase: string;
31
- items: string[];
32
- }[] | undefined;
33
- task?: string | undefined;
34
- phase?: string | undefined;
35
- items?: string[] | undefined;
36
- }[];
27
+ op: "append" | "done" | "drop" | "init" | "rm" | "start" | "view";
28
+ list?: {
29
+ phase: string;
30
+ items: string[];
31
+ }[] | undefined;
32
+ task?: string | undefined;
33
+ phase?: string | undefined;
34
+ items?: string[] | undefined;
37
35
  }, {}>;
38
36
  type TodoParams = TodoSchema;
39
37
  type TodoSchema = typeof todoSchema.infer;
@@ -70,7 +68,7 @@ export declare function selectStickyTodoWindow(tasks: TodoItem[], maxVisible?: n
70
68
  */
71
69
  export declare function todoMatchesAnyDescription(content: string, descriptions: readonly string[]): boolean;
72
70
  /** Apply an array of `todo`-style ops to existing phases. Used by /todo slash command. */
73
- export declare function applyOpsToPhases(currentPhases: TodoPhase[], ops: TodoParams["ops"]): {
71
+ export declare function applyOpsToPhases(currentPhases: TodoPhase[], ops: TodoParams[]): {
74
72
  phases: TodoPhase[];
75
73
  errors: string[];
76
74
  };
@@ -90,16 +88,14 @@ export declare class TodoTool implements AgentTool<typeof todoSchema, TodoToolDe
90
88
  readonly summary = "Write a structured todo list to track progress within a session";
91
89
  readonly description: string;
92
90
  readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
93
- ops: {
94
- op: "append" | "done" | "drop" | "init" | "rm" | "start" | "view";
95
- list?: {
96
- phase: string;
97
- items: string[];
98
- }[] | undefined;
99
- task?: string | undefined;
100
- phase?: string | undefined;
101
- items?: string[] | undefined;
102
- }[];
91
+ op: "append" | "done" | "drop" | "init" | "rm" | "start" | "view";
92
+ list?: {
93
+ phase: string;
94
+ items: string[];
95
+ }[] | undefined;
96
+ task?: string | undefined;
97
+ phase?: string | undefined;
98
+ items?: string[] | undefined;
103
99
  }, {}>;
104
100
  readonly concurrency = "exclusive";
105
101
  readonly strict = true;
@@ -108,13 +104,15 @@ export declare class TodoTool implements AgentTool<typeof todoSchema, TodoToolDe
108
104
  constructor(session: ToolSession);
109
105
  execute(_toolCallId: string, params: TodoParams, _signal?: AbortSignal, _onUpdate?: AgentToolUpdateCallback<TodoToolDetails>, _context?: AgentToolContext): Promise<AgentToolResult<TodoToolDetails>>;
110
106
  }
111
- type TodoRenderArgs = {
112
- ops?: Array<{
113
- op?: string;
114
- task?: string;
115
- phase?: string;
116
- items?: string[];
117
- }>;
107
+ type TodoRenderOp = {
108
+ op?: string;
109
+ task?: string;
110
+ phase?: string;
111
+ items?: string[];
112
+ };
113
+ /** New single-op shape `{op,...}`; legacy `{ops:[...]}` still seen in old transcripts. */
114
+ type TodoRenderArgs = TodoRenderOp & {
115
+ ops?: TodoRenderOp[];
118
116
  };
119
117
  /** One-based ASCII roman numeral for display (I, II, III, IV, …). */
120
118
  export declare function phaseRomanNumeral(oneBasedIndex: number): string;
@@ -26,6 +26,14 @@ export type FramedBlockComponent = Component & {
26
26
  };
27
27
  export declare function markFramedBlockComponent<T extends Component>(component: T): T & FramedBlockComponent;
28
28
  export declare function isFramedBlockComponent(component: Component): boolean;
29
+ /**
30
+ * Inner content width that {@link renderOutputBlock} wraps its body to, for a
31
+ * given outer `width`: both vertical borders (1 cell each) plus the left
32
+ * content padding. Renderers that size a tail window MUST budget visual rows
33
+ * against this, not the outer width — otherwise the block re-wraps their lines
34
+ * into more rows than they counted and the box overflows its intended height.
35
+ */
36
+ export declare function outputBlockContentWidth(width: number, contentPaddingLeft?: number): number;
29
37
  export declare function renderOutputBlock(options: OutputBlockOptions, theme: Theme): string[];
30
38
  /**
31
39
  * Cached wrapper around `renderOutputBlock`.
@@ -2,6 +2,8 @@ import type { ImageContent } from "@oh-my-pi/pi-ai";
2
2
  export interface ImageResizeOptions {
3
3
  maxWidth?: number;
4
4
  maxHeight?: number;
5
+ /** Smallest allowed edge length (px). Inputs below this are scaled up. */
6
+ minDimension?: number;
5
7
  maxBytes?: number;
6
8
  jpegQuality?: number;
7
9
  excludeWebP?: boolean;
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Strip alias definitions brush's whitespace-only expander cannot execute.
3
+ *
4
+ * Returns the rewritten snapshot plus the list of dropped alias names so the
5
+ * caller can surface them in the debug log.
6
+ */
7
+ export declare function sanitizeSnapshotForBrush(content: string): {
8
+ content: string;
9
+ dropped: string[];
10
+ };
1
11
  /**
2
12
  * Create a shell snapshot, caching the result.
3
13
  * Returns the path to the snapshot file, or null if creation failed.
@@ -10,6 +10,8 @@ import { type AuthStorage, type FetchImpl } from "@oh-my-pi/pi-ai";
10
10
  import type { SearchResponse } from "../../../web/search/types";
11
11
  import type { SearchParams } from "./base";
12
12
  import { SearchProvider } from "./base";
13
+ /** Reset Exa request pacing state for isolated provider tests. */
14
+ export declare function resetExaSearchThrottleForTest(): void;
13
15
  type ExaSearchType = "neural" | "fast" | "auto" | "deep";
14
16
  type ExaSearchParamType = ExaSearchType | "keyword";
15
17
  export interface ExaSearchParams {
@@ -33,11 +33,25 @@ export declare function searchPerplexity(params: PerplexitySearchParams): Promis
33
33
  export declare class PerplexityProvider extends SearchProvider {
34
34
  readonly id = "perplexity";
35
35
  readonly label = "Perplexity";
36
+ /**
37
+ * Auto-chain admission. Requires a direct Perplexity credential
38
+ * (`PERPLEXITY_COOKIES`, OAuth session, or `PERPLEXITY_API_KEY`).
39
+ *
40
+ * OpenRouter auth is intentionally NOT accepted here: silently using
41
+ * OpenRouter's `perplexity/sonar-pro` whenever any OpenRouter key is
42
+ * configured surprises users (and bills them) for a path they never
43
+ * asked for. The auto chain skips Perplexity in that case and falls
44
+ * through to the next configured provider. Users who DO want the
45
+ * OpenRouter-backed Perplexity path can still opt in by setting
46
+ * `webSearch: perplexity` explicitly — see {@link isExplicitlyAvailable}.
47
+ */
36
48
  isAvailable(authStorage: AuthStorage): boolean;
37
49
  /**
38
- * Perplexity accepts anonymous browser-style ask requests, but keep auto
39
- * provider selection credential-gated so a configured provider keeps priority
40
- * over the anonymous fallback.
50
+ * Perplexity accepts anonymous browser-style ask requests, and the
51
+ * OpenRouter-backed `perplexity/sonar-pro` path is opt-in through
52
+ * explicit selection. Keep auto-chain admission credential-gated so a
53
+ * configured provider keeps priority over the anonymous/OpenRouter
54
+ * fallbacks.
41
55
  */
42
56
  isExplicitlyAvailable(_authStorage: AuthStorage): boolean;
43
57
  search(params: SearchParams): Promise<SearchResponse>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.1.14",
4
+ "version": "16.1.16",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -48,17 +48,17 @@
48
48
  "@agentclientprotocol/sdk": "0.25.0",
49
49
  "@babel/parser": "^7.29.7",
50
50
  "@mozilla/readability": "^0.6.0",
51
- "@oh-my-pi/hashline": "16.1.14",
52
- "@oh-my-pi/omp-stats": "16.1.14",
53
- "@oh-my-pi/pi-agent-core": "16.1.14",
54
- "@oh-my-pi/pi-ai": "16.1.14",
55
- "@oh-my-pi/pi-catalog": "16.1.14",
56
- "@oh-my-pi/pi-mnemopi": "16.1.14",
57
- "@oh-my-pi/pi-natives": "16.1.14",
58
- "@oh-my-pi/pi-tui": "16.1.14",
59
- "@oh-my-pi/pi-utils": "16.1.14",
60
- "@oh-my-pi/pi-wire": "16.1.14",
61
- "@oh-my-pi/snapcompact": "16.1.14",
51
+ "@oh-my-pi/hashline": "16.1.16",
52
+ "@oh-my-pi/omp-stats": "16.1.16",
53
+ "@oh-my-pi/pi-agent-core": "16.1.16",
54
+ "@oh-my-pi/pi-ai": "16.1.16",
55
+ "@oh-my-pi/pi-catalog": "16.1.16",
56
+ "@oh-my-pi/pi-mnemopi": "16.1.16",
57
+ "@oh-my-pi/pi-natives": "16.1.16",
58
+ "@oh-my-pi/pi-tui": "16.1.16",
59
+ "@oh-my-pi/pi-utils": "16.1.16",
60
+ "@oh-my-pi/pi-wire": "16.1.16",
61
+ "@oh-my-pi/snapcompact": "16.1.16",
62
62
  "@opentelemetry/api": "^1.9.1",
63
63
  "@opentelemetry/context-async-hooks": "^2.7.1",
64
64
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -5,7 +5,15 @@ import * as path from "node:path";
5
5
 
6
6
  const packageDir = path.join(import.meta.dir, "..");
7
7
  const repoRoot = path.join(packageDir, "..", "..");
8
- const outputPath = path.join(packageDir, "dist", "omp");
8
+ // Optional cross-compile target, e.g. CROSS_TARGET=linux-arm64 → bun build
9
+ // --target=bun-linux-arm64, embeds the matching native, outputs dist/omp-<target>.
10
+ const crossTarget = Bun.env.CROSS_TARGET || null;
11
+ const [crossPlatform, crossArch] = crossTarget ? crossTarget.split("-") : [null, null];
12
+ // x64 uses the baseline bun runtime so it runs under Rosetta / pre-AVX2 CPUs
13
+ // (the modern bun-linux-x64 target SIGILLs under Apple-Silicon Rosetta).
14
+ const bunTarget = crossTarget ? (crossTarget === "linux-x64" ? "bun-linux-x64-baseline" : `bun-${crossTarget}`) : null;
15
+ const outName = crossTarget ? `omp-${crossTarget}` : "omp";
16
+ const outputPath = path.join(packageDir, "dist", outName);
9
17
 
10
18
  // Transformers.js is an optional, native-heavy dependency that is never bundled
11
19
  // into the binary; the tiny-model worker `bun install`s it into a runtime cache
@@ -17,7 +25,7 @@ const transformersVersion = (
17
25
  ).version;
18
26
 
19
27
  function shouldAdhocSignDarwinBinary(): boolean {
20
- return process.platform === "darwin";
28
+ return process.platform === "darwin" && !crossTarget;
21
29
  }
22
30
 
23
31
  async function runCommand(
@@ -43,7 +51,12 @@ async function main(): Promise<void> {
43
51
  try {
44
52
  await runCommand(["bun", "--cwd=../stats", "scripts/generate-client-bundle.ts", "--generate"]);
45
53
  await runCommand(["bun", "scripts/generate-docs-index.ts", "--generate"]);
46
- await runCommand(["bun", "--cwd=../natives", "run", "embed:native"]);
54
+ await runCommand(
55
+ ["bun", "--cwd=../natives", "run", "embed:native"],
56
+ crossTarget
57
+ ? { ...Bun.env, TARGET_PLATFORM: crossPlatform as string, TARGET_ARCH: crossArch as string }
58
+ : Bun.env,
59
+ );
47
60
  await runCommand(["bun", "scripts/embed-mupdf-wasm.ts", "--generate"]);
48
61
  try {
49
62
  const buildEnv = shouldAdhocSignDarwinBinary() ? { ...Bun.env, BUN_NO_CODESIGN_MACHO_BINARY: "1" } : Bun.env;
@@ -52,6 +65,7 @@ async function main(): Promise<void> {
52
65
  "bun",
53
66
  "build",
54
67
  "--compile",
68
+ ...(bunTarget ? ["--target", bunTarget] : []),
55
69
  "--no-compile-autoload-bunfig",
56
70
  "--no-compile-autoload-dotenv",
57
71
  "--no-compile-autoload-tsconfig",
@@ -85,7 +99,7 @@ async function main(): Promise<void> {
85
99
  "./packages/coding-agent/src/extensibility/legacy-pi-ai-shim.ts",
86
100
  "./packages/coding-agent/src/extensibility/legacy-pi-coding-agent-shim.ts",
87
101
  "--outfile",
88
- "packages/coding-agent/dist/omp",
102
+ `packages/coding-agent/dist/${outName}`,
89
103
  ],
90
104
  buildEnv,
91
105
  repoRoot,
@@ -3,6 +3,7 @@ import type { AgentMessage, AgentTelemetryConfig } from "@oh-my-pi/pi-agent-core
3
3
  import { type } from "arktype";
4
4
  import { createAdvisorMessageCard } from "../../modes/components/advisor-message";
5
5
  import { getThemeByName } from "../../modes/theme/theme";
6
+ import { SecretObfuscator } from "../../secrets/obfuscator";
6
7
  import { formatSessionHistoryMarkdown } from "../../session/session-history-format";
7
8
  import { YieldQueue } from "../../session/yield-queue";
8
9
  import {
@@ -447,6 +448,119 @@ describe("advisor", () => {
447
448
  expect(promptInputs[0]).not.toContain("note");
448
449
  });
449
450
 
451
+ it("obfuscates session updates before prompting the advisor", async () => {
452
+ const secret = "ADVISOR_SECRET_TOKEN_123";
453
+ const obfuscator = new SecretObfuscator([{ type: "plain", content: secret }]);
454
+ const placeholder = obfuscator.obfuscate(secret);
455
+ const promptInputs: string[] = [];
456
+ const agent = makeAgent(promptInputs);
457
+ const messages: AgentMessage[] = [{ role: "user", content: `token ${secret}`, timestamp: 1 } as AgentMessage];
458
+ const host: AdvisorRuntimeHost = {
459
+ snapshotMessages: () => messages,
460
+ enqueueAdvice: () => {},
461
+ obfuscator,
462
+ };
463
+ const runtime = new AdvisorRuntime(agent, host);
464
+
465
+ runtime.onTurnEnd();
466
+ await Promise.resolve();
467
+
468
+ expect(promptInputs).toHaveLength(1);
469
+ expect(promptInputs[0]).toContain(placeholder);
470
+ expect(promptInputs[0]).not.toContain(secret);
471
+ });
472
+
473
+ it("redacts expanded primary context before XML escaping", async () => {
474
+ const secret = "ADVISOR&SECRET<TOKEN>123";
475
+ const obfuscator = new SecretObfuscator([{ type: "plain", content: secret }]);
476
+ const placeholder = obfuscator.obfuscate(secret);
477
+ const promptInputs: string[] = [];
478
+ const agent = makeAgent(promptInputs);
479
+ const messages: AgentMessage[] = [
480
+ {
481
+ role: "custom",
482
+ customType: "plan-mode-context",
483
+ content: `Plan mode carries ${secret}`,
484
+ display: false,
485
+ timestamp: 1,
486
+ } as AgentMessage,
487
+ ];
488
+ const host: AdvisorRuntimeHost = {
489
+ snapshotMessages: () => messages,
490
+ enqueueAdvice: () => {},
491
+ obfuscator,
492
+ };
493
+ const runtime = new AdvisorRuntime(agent, host);
494
+
495
+ runtime.onTurnEnd();
496
+ await Promise.resolve();
497
+
498
+ expect(promptInputs).toHaveLength(1);
499
+ expect(promptInputs[0]).toContain(placeholder);
500
+ expect(promptInputs[0]).not.toContain(secret);
501
+ expect(promptInputs[0]).not.toContain("ADVISOR&amp;SECRET&lt;TOKEN&gt;123");
502
+ });
503
+
504
+ it("redacts file-mention paths before formatting", async () => {
505
+ const secret = "MENTION_SECRET_TOKEN_123";
506
+ const obfuscator = new SecretObfuscator([{ type: "plain", content: secret }]);
507
+ const placeholder = obfuscator.obfuscate(secret);
508
+ const promptInputs: string[] = [];
509
+ const agent = makeAgent(promptInputs);
510
+ const messages: AgentMessage[] = [
511
+ {
512
+ role: "fileMention",
513
+ files: [{ path: `notes/${secret}.txt`, content: "ignored" }],
514
+ timestamp: 1,
515
+ } as unknown as AgentMessage,
516
+ ];
517
+ const host: AdvisorRuntimeHost = {
518
+ snapshotMessages: () => messages,
519
+ enqueueAdvice: () => {},
520
+ obfuscator,
521
+ };
522
+ const runtime = new AdvisorRuntime(agent, host);
523
+
524
+ runtime.onTurnEnd();
525
+ await Promise.resolve();
526
+
527
+ expect(promptInputs).toHaveLength(1);
528
+ expect(promptInputs[0]).toContain(placeholder);
529
+ expect(promptInputs[0]).not.toContain(secret);
530
+ });
531
+
532
+ it("redacts nested async-result job labels before formatting", async () => {
533
+ const secret = "JOB_LABEL_SECRET_TOKEN_123";
534
+ const obfuscator = new SecretObfuscator([{ type: "plain", content: secret }]);
535
+ const placeholder = obfuscator.obfuscate(secret);
536
+ const promptInputs: string[] = [];
537
+ const agent = makeAgent(promptInputs);
538
+ const messages: AgentMessage[] = [
539
+ {
540
+ role: "custom",
541
+ customType: "async-result",
542
+ content: "",
543
+ details: { jobs: [{ label: `bash: echo ${secret}`, jobId: "j1" }] },
544
+ display: true,
545
+ attribution: "agent",
546
+ timestamp: 1,
547
+ } as unknown as AgentMessage,
548
+ ];
549
+ const host: AdvisorRuntimeHost = {
550
+ snapshotMessages: () => messages,
551
+ enqueueAdvice: () => {},
552
+ obfuscator,
553
+ };
554
+ const runtime = new AdvisorRuntime(agent, host);
555
+
556
+ runtime.onTurnEnd();
557
+ await Promise.resolve();
558
+
559
+ expect(promptInputs).toHaveLength(1);
560
+ expect(promptInputs[0]).toContain(placeholder);
561
+ expect(promptInputs[0]).not.toContain(secret);
562
+ });
563
+
450
564
  it("expands plan-mode context once, then collapses an unchanged re-injection", async () => {
451
565
  const promptInputs: string[] = [];
452
566
  const agent = makeAgent(promptInputs);
@@ -1,6 +1,8 @@
1
1
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
3
+ import type { AssistantMessage, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
3
4
  import { logger } from "@oh-my-pi/pi-utils";
5
+ import { obfuscateToolArguments, type SecretObfuscator } from "../secrets/obfuscator";
4
6
  import { formatSessionHistoryMarkdown, PRIMARY_CONTEXT_CUSTOM_TYPES } from "../session/session-history-format";
5
7
 
6
8
  /** Minimal slice of `Agent` the runtime drives — satisfied by pi-agent-core `Agent`. */
@@ -16,6 +18,8 @@ export interface AdvisorRuntimeHost {
16
18
  snapshotMessages(): AgentMessage[];
17
19
  /** Surface one advice note to the primary (enqueues into the session YieldQueue). */
18
20
  enqueueAdvice(note: string, severity?: "nit" | "concern" | "blocker"): void;
21
+ /** Redact primary transcript bytes before they reach the advisor model. */
22
+ obfuscator?: SecretObfuscator;
19
23
  /**
20
24
  * Pre-prompt context maintenance for the advisor's own append-only context.
21
25
  * Promotes the advisor model to a larger sibling when its context nears the
@@ -174,7 +178,9 @@ export class AdvisorRuntime {
174
178
  .map(m => this.#dedupContextMessage(m));
175
179
  this.#lastCount = all.length;
176
180
  if (delta.length === 0) return null;
177
- const md = formatSessionHistoryMarkdown(delta, {
181
+ const obfuscator = this.host.obfuscator;
182
+ const formattedDelta = obfuscator?.hasSecrets() ? obfuscateAdvisorDelta(obfuscator, delta) : delta;
183
+ const md = formatSessionHistoryMarkdown(formattedDelta, {
178
184
  includeThinking: true,
179
185
  includeToolIntent: true,
180
186
  watchedRoles: true,
@@ -304,3 +310,125 @@ export class AdvisorRuntime {
304
310
  }
305
311
  }
306
312
  }
313
+
314
+ type TextualContent = string | readonly (TextContent | ImageContent)[];
315
+
316
+ function obfuscateTextualContent(obfuscator: SecretObfuscator, content: TextualContent): TextualContent {
317
+ if (typeof content === "string") return obfuscator.obfuscate(content);
318
+ let changed = false;
319
+ const result = content.map((block): TextContent | ImageContent => {
320
+ if (block.type !== "text") return block;
321
+ const text = obfuscator.obfuscate(block.text);
322
+ if (text === block.text) return block;
323
+ changed = true;
324
+ return { ...block, text };
325
+ });
326
+ return changed ? result : content;
327
+ }
328
+
329
+ function obfuscateAssistantMessage(obfuscator: SecretObfuscator, message: AssistantMessage): AssistantMessage {
330
+ let changed = false;
331
+ const content = message.content.map((block): AssistantMessage["content"][number] => {
332
+ if (block.type === "text") {
333
+ const text = obfuscator.obfuscate(block.text);
334
+ if (text === block.text) return block;
335
+ changed = true;
336
+ return { ...block, text };
337
+ }
338
+ if (block.type === "toolCall") {
339
+ const args = obfuscateToolArguments(obfuscator, block.arguments);
340
+ if (args === block.arguments) return block;
341
+ changed = true;
342
+ return { ...block, arguments: args };
343
+ }
344
+ return block;
345
+ });
346
+ return changed ? { ...message, content } : message;
347
+ }
348
+
349
+ function obfuscateDetails(
350
+ obfuscator: SecretObfuscator,
351
+ details: Record<string, unknown> | undefined,
352
+ ): Record<string, unknown> | undefined {
353
+ if (!details) return details;
354
+ // Walk strings at every depth: `customOneLiner` renders nested fields
355
+ // (e.g. `async-result` reads `details.jobs[].label`/`jobId`), so a shallow
356
+ // pass leaks any secret a background job's label happens to contain.
357
+ return obfuscateToolArguments(obfuscator, details);
358
+ }
359
+
360
+ function obfuscateAdvisorMessage(obfuscator: SecretObfuscator, message: AgentMessage): AgentMessage {
361
+ switch (message.role) {
362
+ case "user":
363
+ case "developer":
364
+ case "toolResult": {
365
+ const content = obfuscateTextualContent(obfuscator, message.content as TextualContent);
366
+ return content === message.content ? message : ({ ...(message as object), content } as AgentMessage);
367
+ }
368
+ case "assistant":
369
+ return obfuscateAssistantMessage(obfuscator, message as AssistantMessage) as AgentMessage;
370
+ case "custom":
371
+ case "hookMessage": {
372
+ const msg = message as AgentMessage & {
373
+ content: TextualContent;
374
+ details?: Record<string, unknown>;
375
+ };
376
+ const content = obfuscateTextualContent(obfuscator, msg.content);
377
+ const details = obfuscateDetails(obfuscator, msg.details);
378
+ if (content === msg.content && details === msg.details) return message;
379
+ return { ...(message as object), content, details } as AgentMessage;
380
+ }
381
+ case "bashExecution": {
382
+ const msg = message as AgentMessage & { command: string; output: string };
383
+ const command = obfuscator.obfuscate(msg.command);
384
+ const output = obfuscator.obfuscate(msg.output);
385
+ return command === msg.command && output === msg.output
386
+ ? message
387
+ : ({ ...(message as object), command, output } as AgentMessage);
388
+ }
389
+ case "pythonExecution": {
390
+ const msg = message as AgentMessage & { code: string; output: string };
391
+ const code = obfuscator.obfuscate(msg.code);
392
+ const output = obfuscator.obfuscate(msg.output);
393
+ return code === msg.code && output === msg.output
394
+ ? message
395
+ : ({ ...(message as object), code, output } as AgentMessage);
396
+ }
397
+ case "branchSummary": {
398
+ const msg = message as AgentMessage & { summary: string };
399
+ const summary = obfuscator.obfuscate(msg.summary);
400
+ return summary === msg.summary ? message : ({ ...(message as object), summary } as AgentMessage);
401
+ }
402
+ case "compactionSummary": {
403
+ const msg = message as AgentMessage & { summary: string };
404
+ const summary = obfuscator.obfuscate(msg.summary);
405
+ return summary === msg.summary ? message : ({ ...(message as object), summary } as AgentMessage);
406
+ }
407
+ case "fileMention": {
408
+ const msg = message as AgentMessage & {
409
+ files: Array<{ path: string; content: string; image?: unknown }>;
410
+ };
411
+ let changed = false;
412
+ const files = msg.files.map(file => {
413
+ const path = obfuscator.obfuscate(file.path);
414
+ const content = obfuscator.obfuscate(file.content);
415
+ if (path === file.path && content === file.content) return file;
416
+ changed = true;
417
+ return { ...file, path, content };
418
+ });
419
+ return changed ? ({ ...(message as object), files } as AgentMessage) : message;
420
+ }
421
+ default:
422
+ return message;
423
+ }
424
+ }
425
+
426
+ function obfuscateAdvisorDelta(obfuscator: SecretObfuscator, messages: AgentMessage[]): AgentMessage[] {
427
+ let changed = false;
428
+ const result = messages.map(message => {
429
+ const next = obfuscateAdvisorMessage(obfuscator, message);
430
+ if (next !== message) changed = true;
431
+ return next;
432
+ });
433
+ return changed ? result : messages;
434
+ }
package/src/cli/args.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * CLI argument parsing and help display
3
3
  */
4
- import { type Effort, THINKING_EFFORTS } from "@oh-my-pi/pi-catalog/effort";
5
4
  import { APP_NAME, CONFIG_DIR_NAME, logger } from "@oh-my-pi/pi-utils";
6
5
  import chalk from "chalk";
7
- import { parseEffort } from "../thinking";
6
+ import { CLI_THINKING_LEVELS, type ConfiguredThinkingLevel, parseCliThinkingLevel } from "../thinking";
8
7
  import { BUILTIN_TOOL_NAMES } from "../tools/builtin-names";
9
8
  import {
10
9
  OPTIONAL_FLAGS,
@@ -32,7 +31,7 @@ export interface Args {
32
31
  apiKey?: string;
33
32
  systemPrompt?: string;
34
33
  appendSystemPrompt?: string;
35
- thinking?: Effort;
34
+ thinking?: ConfiguredThinkingLevel;
36
35
  hideThinking?: boolean;
37
36
  advisor?: boolean;
38
37
  continue?: boolean;
@@ -89,9 +88,9 @@ export interface Args {
89
88
  */
90
89
  const PARSE_DEPS: ParseDeps = {
91
90
  logger,
92
- parseEffort,
91
+ parseThinking: parseCliThinkingLevel,
93
92
  builtinToolNames: BUILTIN_TOOL_NAMES,
94
- thinkingEfforts: THINKING_EFFORTS,
93
+ thinkingEfforts: CLI_THINKING_LEVELS,
95
94
  };
96
95
 
97
96
  export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { type: "boolean" | "string" }>): Args {
@@ -30,7 +30,7 @@
30
30
  * real implementations at the dispatch site.
31
31
  */
32
32
 
33
- import type { Effort } from "@oh-my-pi/pi-ai";
33
+ import type { ConfiguredThinkingLevel } from "../thinking";
34
34
  import type { Args } from "./args";
35
35
 
36
36
  /**
@@ -44,7 +44,7 @@ import type { Args } from "./args";
44
44
  */
45
45
  export interface ParseDeps {
46
46
  logger: { warn: (message: string, meta?: Record<string, unknown>) => void };
47
- parseEffort: (value: string | null | undefined) => Effort | undefined;
47
+ parseThinking: (value: string | null | undefined) => ConfiguredThinkingLevel | undefined;
48
48
  builtinToolNames: readonly string[];
49
49
  thinkingEfforts: readonly string[];
50
50
  }
@@ -165,7 +165,7 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
165
165
  result.tools = valid;
166
166
  },
167
167
  "--thinking": (result, value, deps) => {
168
- const thinking = deps.parseEffort(value);
168
+ const thinking = deps.parseThinking(value);
169
169
  if (thinking !== undefined) {
170
170
  result.thinking = thinking;
171
171
  } else {