@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.
- package/CHANGELOG.md +69 -0
- package/dist/cli.js +3705 -4015
- package/dist/types/advisor/runtime.d.ts +3 -0
- package/dist/types/cli/args.d.ts +2 -5
- package/dist/types/cli/flag-tables.d.ts +2 -2
- package/dist/types/cli/session-picker.d.ts +4 -2
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +32 -1
- package/dist/types/eval/agent-bridge.d.ts +19 -0
- package/dist/types/eval/js/shared/helpers.d.ts +1 -13
- package/dist/types/eval/js/shared/types.d.ts +1 -1
- package/dist/types/eval/js/worker-protocol.d.ts +1 -1
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/export/share.d.ts +8 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/transports/stdio.d.ts +12 -1
- package/dist/types/modes/components/plugin-settings.d.ts +5 -0
- package/dist/types/modes/components/session-selector.d.ts +25 -0
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +4 -3
- package/dist/types/modes/components/status-line/types.d.ts +1 -0
- package/dist/types/secrets/obfuscator.d.ts +3 -3
- package/dist/types/task/isolation-runner.d.ts +128 -0
- package/dist/types/task/worktree.d.ts +14 -1
- package/dist/types/thinking.d.ts +15 -0
- package/dist/types/tools/eval-render.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +11 -17
- package/dist/types/tools/todo.d.ts +26 -28
- package/dist/types/tui/output-block.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/shell-snapshot.d.ts +10 -0
- package/dist/types/web/search/providers/exa.d.ts +2 -0
- package/dist/types/web/search/providers/perplexity.d.ts +17 -3
- package/package.json +12 -12
- package/scripts/build-binary.ts +18 -4
- package/src/advisor/__tests__/advisor.test.ts +114 -0
- package/src/advisor/runtime.ts +129 -1
- package/src/cli/args.ts +4 -5
- package/src/cli/flag-tables.ts +3 -3
- package/src/cli/gallery-fixtures/interaction.ts +6 -9
- package/src/cli/gallery-fixtures/shell.ts +15 -23
- package/src/cli/session-picker.ts +17 -3
- package/src/commands/launch.ts +3 -3
- package/src/config/model-registry.ts +12 -4
- package/src/config/settings-schema.ts +37 -1
- package/src/edit/renderer.ts +34 -12
- package/src/eval/__tests__/agent-bridge.test.ts +462 -3
- package/src/eval/__tests__/helpers-local-roots.test.ts +2 -5
- package/src/eval/__tests__/julia-prelude.test.ts +1 -30
- package/src/eval/__tests__/prelude-agent.test.ts +42 -8
- package/src/eval/agent-bridge.ts +301 -71
- package/src/eval/jl/prelude.jl +32 -227
- package/src/eval/jl/runner.jl +38 -12
- package/src/eval/js/shared/helpers.ts +1 -114
- package/src/eval/js/shared/prelude.txt +13 -27
- package/src/eval/js/shared/runtime.ts +0 -6
- package/src/eval/js/shared/types.ts +1 -1
- package/src/eval/js/worker-protocol.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +13 -0
- package/src/eval/py/executor.ts +1 -1
- package/src/eval/py/prelude.py +47 -105
- package/src/eval/py/runner.py +0 -6
- package/src/eval/rb/prelude.rb +21 -189
- package/src/eval/rb/runner.rb +116 -9
- package/src/exec/bash-executor.ts +44 -0
- package/src/export/html/tool-views.generated.js +29 -29
- package/src/export/share.ts +51 -28
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/local-protocol.ts +100 -53
- package/src/main.ts +15 -4
- package/src/mcp/transports/stdio.ts +20 -4
- package/src/modes/acp/acp-event-mapper.ts +7 -2
- package/src/modes/components/custom-editor.test.ts +22 -0
- package/src/modes/components/custom-editor.ts +10 -1
- package/src/modes/components/footer.ts +4 -3
- package/src/modes/components/plugin-settings.ts +7 -1
- package/src/modes/components/session-selector.ts +143 -29
- package/src/modes/components/status-line/component.ts +5 -1
- package/src/modes/components/status-line/context-thresholds.ts +11 -3
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/controllers/command-controller.ts +6 -0
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/utils/copy-targets.ts +7 -2
- package/src/prompts/system/system-prompt.md +6 -6
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/bash.md +18 -2
- package/src/prompts/tools/eval.md +19 -19
- package/src/prompts/tools/search.md +1 -0
- package/src/prompts/tools/todo.md +1 -1
- package/src/secrets/obfuscator.ts +3 -9
- package/src/session/agent-session.ts +264 -52
- package/src/slash-commands/builtin-registry.ts +2 -1
- package/src/task/index.ts +61 -207
- package/src/task/isolation-runner.ts +354 -0
- package/src/task/worktree.ts +46 -9
- package/src/thinking.ts +20 -0
- package/src/tools/ask.ts +44 -38
- package/src/tools/bash.ts +9 -2
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/eval-render.ts +34 -27
- package/src/tools/eval.ts +100 -103
- package/src/tools/index.ts +8 -1
- package/src/tools/read.ts +136 -60
- package/src/tools/todo.ts +60 -64
- package/src/tui/code-cell.ts +1 -1
- package/src/tui/output-block.ts +11 -0
- package/src/utils/image-resize.ts +30 -0
- package/src/utils/shell-snapshot.ts +63 -1
- package/src/web/search/providers/exa.ts +85 -1
- 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-
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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 = "
|
|
57
|
+
readonly loadMode = "essential";
|
|
66
58
|
readonly label = "Eval";
|
|
67
59
|
get description(): string;
|
|
68
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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[
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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,
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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.
|
|
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.
|
|
52
|
-
"@oh-my-pi/omp-stats": "16.1.
|
|
53
|
-
"@oh-my-pi/pi-agent-core": "16.1.
|
|
54
|
-
"@oh-my-pi/pi-ai": "16.1.
|
|
55
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
56
|
-
"@oh-my-pi/pi-mnemopi": "16.1.
|
|
57
|
-
"@oh-my-pi/pi-natives": "16.1.
|
|
58
|
-
"@oh-my-pi/pi-tui": "16.1.
|
|
59
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
60
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
61
|
-
"@oh-my-pi/snapcompact": "16.1.
|
|
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",
|
package/scripts/build-binary.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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&SECRET<TOKEN>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);
|
package/src/advisor/runtime.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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?:
|
|
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
|
-
|
|
91
|
+
parseThinking: parseCliThinkingLevel,
|
|
93
92
|
builtinToolNames: BUILTIN_TOOL_NAMES,
|
|
94
|
-
thinkingEfforts:
|
|
93
|
+
thinkingEfforts: CLI_THINKING_LEVELS,
|
|
95
94
|
};
|
|
96
95
|
|
|
97
96
|
export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { type: "boolean" | "string" }>): Args {
|
package/src/cli/flag-tables.ts
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* real implementations at the dispatch site.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import type {
|
|
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
|
-
|
|
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.
|
|
168
|
+
const thinking = deps.parseThinking(value);
|
|
169
169
|
if (thinking !== undefined) {
|
|
170
170
|
result.thinking = thinking;
|
|
171
171
|
} else {
|