@oh-my-pi/pi-coding-agent 16.1.1 → 16.1.2

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 (69) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/dist/cli.js +3314 -3338
  3. package/dist/types/cli/bench-cli.d.ts +2 -1
  4. package/dist/types/config/settings-schema.d.ts +1 -1
  5. package/dist/types/main.d.ts +2 -0
  6. package/dist/types/modes/components/assistant-message.d.ts +12 -0
  7. package/dist/types/modes/components/welcome.d.ts +1 -1
  8. package/dist/types/sdk.d.ts +19 -2
  9. package/dist/types/session/auth-broker-config.d.ts +33 -6
  10. package/dist/types/system-prompt.d.ts +5 -1
  11. package/dist/types/task/executor.d.ts +10 -0
  12. package/dist/types/tools/find.d.ts +0 -2
  13. package/dist/types/tools/search.d.ts +3 -3
  14. package/package.json +12 -12
  15. package/scripts/measure-prompt-tokens.ts +63 -0
  16. package/src/cli/bench-cli.ts +64 -3
  17. package/src/cli/startup-cwd.ts +3 -13
  18. package/src/config/settings-schema.ts +1 -1
  19. package/src/cursor.ts +1 -1
  20. package/src/debug/raw-sse-buffer.ts +31 -10
  21. package/src/eval/py/prelude.py +1 -1
  22. package/src/export/html/tool-views.generated.js +1 -1
  23. package/src/extensibility/extensions/runner.ts +8 -2
  24. package/src/internal-urls/docs-index.generated.txt +1 -1
  25. package/src/main.ts +29 -9
  26. package/src/modes/components/assistant-message.ts +86 -0
  27. package/src/modes/components/tips.txt +2 -1
  28. package/src/modes/components/welcome.ts +86 -8
  29. package/src/modes/controllers/event-controller.ts +1 -1
  30. package/src/prompts/system/personalities/default.md +8 -16
  31. package/src/prompts/system/system-prompt.md +101 -115
  32. package/src/prompts/tools/ast-edit.md +10 -12
  33. package/src/prompts/tools/ast-grep.md +14 -18
  34. package/src/prompts/tools/bash.md +19 -21
  35. package/src/prompts/tools/browser.md +24 -24
  36. package/src/prompts/tools/checkpoint.md +0 -1
  37. package/src/prompts/tools/debug.md +11 -15
  38. package/src/prompts/tools/eval.md +27 -27
  39. package/src/prompts/tools/find.md +6 -10
  40. package/src/prompts/tools/github.md +11 -15
  41. package/src/prompts/tools/goal.md +0 -7
  42. package/src/prompts/tools/inspect-image.md +0 -1
  43. package/src/prompts/tools/irc.md +15 -24
  44. package/src/prompts/tools/job.md +5 -8
  45. package/src/prompts/tools/learn.md +2 -2
  46. package/src/prompts/tools/lsp.md +27 -30
  47. package/src/prompts/tools/manage-skill.md +4 -4
  48. package/src/prompts/tools/read.md +21 -23
  49. package/src/prompts/tools/replace.md +0 -1
  50. package/src/prompts/tools/resolve.md +4 -9
  51. package/src/prompts/tools/rewind.md +1 -1
  52. package/src/prompts/tools/search.md +8 -10
  53. package/src/prompts/tools/task.md +33 -38
  54. package/src/prompts/tools/todo.md +14 -18
  55. package/src/prompts/tools/web-search.md +0 -4
  56. package/src/prompts/tools/write.md +1 -1
  57. package/src/sdk.ts +49 -102
  58. package/src/session/agent-session.ts +17 -2
  59. package/src/session/auth-broker-config.ts +36 -76
  60. package/src/session/session-history-format.ts +1 -1
  61. package/src/session/session-manager.ts +33 -6
  62. package/src/system-prompt.ts +28 -8
  63. package/src/task/executor.ts +57 -0
  64. package/src/task/index.ts +15 -1
  65. package/src/tools/browser.ts +1 -1
  66. package/src/tools/eval.ts +1 -1
  67. package/src/tools/find.ts +4 -17
  68. package/src/tools/memory-edit.ts +1 -1
  69. package/src/tools/search.ts +5 -5
@@ -1,6 +1,6 @@
1
1
  import type { ResolvedThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, ApiKeyResolver, AssistantMessageEventStream, Context, Model, SimpleStreamOptions } from "@oh-my-pi/pi-ai";
3
- import type { CanonicalModelVariant } from "@oh-my-pi/pi-catalog/identity";
3
+ import { type CanonicalModelVariant } from "@oh-my-pi/pi-catalog/identity";
4
4
  import type { ApiKeyResolverModel } from "../config/api-key-resolver";
5
5
  import { type CanonicalModelQueryOptions } from "../config/model-registry";
6
6
  import { Settings } from "../config/settings";
@@ -20,6 +20,7 @@ export interface BenchModelRegistry {
20
20
  resolveCanonicalModel?(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined;
21
21
  getCanonicalVariants?(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[];
22
22
  getCanonicalId?(model: Model<Api>): string | undefined;
23
+ hasConfiguredAuth?(model: Model<Api>): boolean;
23
24
  }
24
25
  export interface BenchRuntime {
25
26
  modelRegistry: BenchModelRegistry;
@@ -874,7 +874,7 @@ export declare const SETTINGS_SCHEMA: {
874
874
  };
875
875
  readonly inlineToolDescriptors: {
876
876
  readonly type: "boolean";
877
- readonly default: true;
877
+ readonly default: false;
878
878
  readonly ui: {
879
879
  readonly tab: "model";
880
880
  readonly group: "Prompt";
@@ -54,6 +54,8 @@ export declare class SessionResolutionError extends Error {
54
54
  }
55
55
  /** Resolves CLI session flags into an existing, forked, in-memory, or cancelled session manager. */
56
56
  export declare function createSessionManager(parsed: Args, cwd: string, activeSettings?: Settings, askToForkSession?: SessionPrompt, askToMoveSession?: SessionPrompt): Promise<SessionManager | undefined>;
57
+ /** Apply resolved CLI/discovered prompt files without bypassing system prompt templates. */
58
+ export declare function applyResolvedSystemPromptInputs(options: CreateAgentSessionOptions, resolvedSystemPrompt: string | undefined, resolvedAppendPrompt: string | undefined): void;
57
59
  interface RunRootCommandDependencies {
58
60
  createAgentSession?: typeof createAgentSession;
59
61
  discoverAuthStorage?: typeof discoverAuthStorage;
@@ -28,6 +28,18 @@ export declare class AssistantMessageComponent extends Container {
28
28
  */
29
29
  setErrorPinned(pinned: boolean): void;
30
30
  isTranscriptBlockFinalized(): boolean;
31
+ /**
32
+ * Whether this still-live block's scrolled-off rows may be committed to
33
+ * immutable native scrollback (the {@link TranscriptContainer} durable-
34
+ * snapshot path). Reflowing Markdown — a streaming mermaid diagram or a GFM
35
+ * table — re-lays-out its body as source arrives (the diagram reshapes, the
36
+ * table re-aligns its columns), so committing an intermediate layout strands
37
+ * a stale fragment in native scrollback that only a full repaint (Ctrl+L) can
38
+ * clear. While such content is still streaming the block therefore stays
39
+ * wholly in the repaintable live region and commits once, at its final
40
+ * layout, when the turn finalizes.
41
+ */
42
+ isTranscriptBlockCommitStable(): boolean;
31
43
  getTranscriptBlockVersion(): number;
32
44
  markTranscriptBlockFinalized(): void;
33
45
  setToolResultImages(toolCallId: string, images: ImageContent[]): void;
@@ -9,7 +9,7 @@ export declare const WELCOME_SESSION_SLOTS = 4;
9
9
  * the box height is constant regardless of how many servers a project has.
10
10
  */
11
11
  export declare const WELCOME_LSP_SLOTS = 4;
12
- export declare function renderWelcomeTip(tip: string, boxWidth: number): string[];
12
+ export declare function renderWelcomeTip(tip: string, boxWidth: number, phase?: number): string[];
13
13
  export interface RecentSession {
14
14
  name: string;
15
15
  timeAgo: string;
@@ -18,7 +18,7 @@ import { MCPManager, type MCPToolsLoadResult } from "./mcp";
18
18
  import type { MnemopiSessionState } from "./mnemopi/state";
19
19
  import { AgentRegistry } from "./registry/agent-registry";
20
20
  import { AgentSession } from "./session/agent-session";
21
- import { AuthStorage } from "./session/auth-storage";
21
+ import type { AuthStorage } from "./session/auth-storage";
22
22
  import { SessionManager } from "./session/session-manager";
23
23
  import { type BuildSystemPromptResult } from "./system-prompt";
24
24
  import { type ConfiguredThinkingLevel } from "./thinking";
@@ -48,8 +48,12 @@ export interface CreateAgentSessionOptions {
48
48
  model: Model;
49
49
  thinkingLevel?: ThinkingLevel;
50
50
  }>;
51
- /** System prompt blocks. Array replaces default, function receives default blocks and returns final blocks. */
51
+ /** Provider-facing system prompt override. Replaces the fully rendered default blocks. */
52
52
  systemPrompt?: string | string[] | ((defaultPrompt: string[]) => string | string[]);
53
+ /** Already-loaded custom prompt text rendered through the bundled custom system prompt template. */
54
+ customSystemPrompt?: string;
55
+ /** Already-loaded text appended through the bundled system prompt templates. */
56
+ appendSystemPrompt?: string;
53
57
  /** Optional provider-facing session identifier for prompt caches and sticky auth selection.
54
58
  * Keeps persisted session files isolated while reusing provider-side caches. */
55
59
  providerSessionId?: string;
@@ -179,6 +183,15 @@ export interface CreateAgentSessionOptions {
179
183
  * `@opentelemetry/api` package returns a no-op tracer in that case.
180
184
  */
181
185
  telemetry?: AgentTelemetryConfig;
186
+ /**
187
+ * Fired once, when the agent loop hands its first request to the provider
188
+ * transport (i.e. the `streamFn` wrapper is first invoked). Used to measure
189
+ * subagent launch latency — the boundary between "session built" and "model
190
+ * call dispatched". This is the loop's dispatch point, slightly before the
191
+ * actual provider HTTP call (per-request prep, identical across all
192
+ * requests, follows it), which is the right granularity for launch timing.
193
+ */
194
+ onFirstChatDispatch?: () => void;
182
195
  /** Whether to auto-approve all tool calls (--auto-approve CLI flag). Default: false */
183
196
  autoApprove?: boolean;
184
197
  }
@@ -222,6 +235,9 @@ export { BashTool, BUILTIN_TOOLS, createTools, EditTool, EvalTool, FindTool, HID
222
235
  * the client receives access tokens with `refresh = "__remote__"` and calls
223
236
  * back into the broker through the {@link AuthStorageOptions.refreshOAuthCredential}
224
237
  * override to re-mint access tokens when needed.
238
+ *
239
+ * Delegates to {@link ./session/auth-broker-config} so the TUI and the catalog
240
+ * generator share the same credential-discovery logic.
225
241
  */
226
242
  export declare function discoverAuthStorage(agentDir?: string): Promise<AuthStorage>;
227
243
  /**
@@ -287,6 +303,7 @@ export interface BuildSystemPromptOptions {
287
303
  content: string;
288
304
  }>;
289
305
  cwd?: string;
306
+ customPrompt?: string;
290
307
  appendPrompt?: string;
291
308
  inlineToolDescriptors?: boolean;
292
309
  }
@@ -1,9 +1,27 @@
1
- export interface AuthBrokerClientConfig {
2
- url: string;
3
- token: string;
4
- }
5
- /** Path to the local bearer token file. Created on the broker host by `omp auth-broker token`. */
6
- export declare function getAuthBrokerTokenFilePath(): string;
1
+ /**
2
+ * Resolve auth-broker connection configuration for the local omp client.
3
+ *
4
+ * This is a thin coding-agent wrapper around the shared resolver in
5
+ * `@oh-my-pi/pi-ai/auth-broker/discover` that preserves the process-lifetime
6
+ * memoization expected by the CLI and injects the full `resolveConfigValue`
7
+ * (including `!command` config indirection) from coding-agent's config layer.
8
+ *
9
+ * Precedence (highest first):
10
+ * 1. `OMP_AUTH_BROKER_URL` / `OMP_AUTH_BROKER_TOKEN` env vars.
11
+ * 2. `auth.broker.url` / `auth.broker.token` in `~/.omp/agent/config.yml`
12
+ * (hidden from the settings UI; `!command` resolution supported).
13
+ * 3. Token file `~/.omp/auth-broker.token` (paired with URL from env or config).
14
+ *
15
+ * Returns null when no broker URL is configured — caller falls back to the
16
+ * local SQLite store.
17
+ *
18
+ * Reads config.yml directly (instead of going through `Settings.init`) because
19
+ * `discoverAuthStorage` runs before the settings singleton is initialized in
20
+ * `runRootCommand`, and we want hand-edited config entries to be honoured at
21
+ * boot without forcing a startup reorder.
22
+ */
23
+ import { type AuthBrokerClientConfig, type DiscoverAuthStorageOptions, discoverAuthStorage as discoverAuthStorageShared, getAuthBrokerTokenFilePath } from "@oh-my-pi/pi-ai/auth-broker/discover";
24
+ export { type AuthBrokerClientConfig, getAuthBrokerTokenFilePath };
7
25
  /**
8
26
  * Read broker configuration. Returns null when the URL is missing
9
27
  * (broker disabled — local store is used). Throws when URL is set but no
@@ -15,3 +33,12 @@ export declare function getAuthBrokerTokenFilePath(): string;
15
33
  * retried. Concurrent callers share one in-flight resolution.
16
34
  */
17
35
  export declare function resolveAuthBrokerConfig(): Promise<AuthBrokerClientConfig | null>;
36
+ /**
37
+ * Create an AuthStorage instance, using the broker when configured and falling
38
+ * back to the local SQLite store otherwise. Delegates to the shared resolver in
39
+ * pi-ai so the CLI, subagents, and the catalog generator all see the same
40
+ * credentials.
41
+ *
42
+ * Default `agentDir` is the current configured agent directory.
43
+ */
44
+ export declare function discoverAuthStorage(agentDir?: string, options?: Omit<DiscoverAuthStorageOptions, "agentDir" | "configValueResolver">): ReturnType<typeof discoverAuthStorageShared>;
@@ -48,13 +48,17 @@ export declare function buildSystemPromptToolMetadata(tools: Map<string, AgentTo
48
48
  export interface BuildSystemPromptOptions {
49
49
  /** Custom system prompt (replaces default). */
50
50
  customPrompt?: string;
51
+ /** Already-loaded custom system prompt text; bypasses path resolution. */
52
+ resolvedCustomPrompt?: string;
51
53
  /** Tools to include in prompt. */
52
54
  tools?: Map<string, SystemPromptToolMetadata>;
53
55
  /** Tool names to include in prompt. */
54
56
  toolNames?: string[];
55
57
  /** Text to append to system prompt. */
56
58
  appendSystemPrompt?: string;
57
- /** Inline full tool descriptors in the system prompt. Default: true */
59
+ /** Already-loaded append prompt text; bypasses path resolution. */
60
+ resolvedAppendSystemPrompt?: string;
61
+ /** Inline full tool descriptors in the system prompt. Default: false */
58
62
  inlineToolDescriptors?: boolean;
59
63
  /**
60
64
  * Whether provider-native tool calling is active (no owned/in-band syntax).
@@ -83,6 +83,16 @@ export interface ExecutorOptions {
83
83
  enableLsp?: boolean;
84
84
  signal?: AbortSignal;
85
85
  onProgress?: (progress: AgentProgress) => void;
86
+ /**
87
+ * Epochs (ms, `Date.now()`) bracketing the concurrency-semaphore wait:
88
+ * `invokedAt` is stamped at the spawn boundary before `acquire()`,
89
+ * `acquiredAt` immediately after. {@link runSubprocess} reports true queue
90
+ * wait (`acquiredAt - invokedAt`) and pre-run setup (`startTime - acquiredAt`)
91
+ * separately in the launch-timing debug log. Undefined for callers that
92
+ * bypass the semaphore path.
93
+ */
94
+ invokedAt?: number;
95
+ acquiredAt?: number;
86
96
  sessionFile?: string | null;
87
97
  persistArtifacts?: boolean;
88
98
  artifactsDir?: string;
@@ -11,7 +11,6 @@ declare const findSchema: import("arktype/internal/variants/object.ts").ObjectTy
11
11
  hidden?: boolean | undefined;
12
12
  gitignore?: boolean | undefined;
13
13
  limit?: number | undefined;
14
- timeout?: number | undefined;
15
14
  }, {}>;
16
15
  export type FindToolInput = typeof findSchema.infer;
17
16
  export interface FindToolDetails {
@@ -70,7 +69,6 @@ export declare class FindTool implements AgentTool<typeof findSchema, FindToolDe
70
69
  hidden?: boolean | undefined;
71
70
  gitignore?: boolean | undefined;
72
71
  limit?: number | undefined;
73
- timeout?: number | undefined;
74
72
  }, {}>;
75
73
  readonly examples: readonly ToolExample<typeof findSchema.infer>[];
76
74
  readonly strict = true;
@@ -8,7 +8,7 @@ import type { OutputMeta } from "./output-meta";
8
8
  declare const searchSchema: import("arktype/internal/variants/object.ts").ObjectType<{
9
9
  pattern: string;
10
10
  paths?: string | string[] | undefined;
11
- i?: boolean | undefined;
11
+ case?: boolean | undefined;
12
12
  gitignore?: boolean | undefined;
13
13
  skip?: number | null | undefined;
14
14
  }, {}>;
@@ -67,7 +67,7 @@ export declare class SearchTool implements AgentTool<typeof searchSchema, Search
67
67
  readonly parameters: import("arktype/internal/variants/object.ts").ObjectType<{
68
68
  pattern: string;
69
69
  paths?: string | string[] | undefined;
70
- i?: boolean | undefined;
70
+ case?: boolean | undefined;
71
71
  gitignore?: boolean | undefined;
72
72
  skip?: number | null | undefined;
73
73
  }, {}>;
@@ -78,7 +78,7 @@ export declare class SearchTool implements AgentTool<typeof searchSchema, Search
78
78
  interface SearchRenderArgs {
79
79
  pattern: string;
80
80
  paths?: string | string[];
81
- i?: boolean;
81
+ case?: boolean;
82
82
  gitignore?: boolean;
83
83
  skip?: number;
84
84
  }
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.1",
4
+ "version": "16.1.2",
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.1",
52
- "@oh-my-pi/omp-stats": "16.1.1",
53
- "@oh-my-pi/pi-agent-core": "16.1.1",
54
- "@oh-my-pi/pi-ai": "16.1.1",
55
- "@oh-my-pi/pi-catalog": "16.1.1",
56
- "@oh-my-pi/pi-mnemopi": "16.1.1",
57
- "@oh-my-pi/pi-natives": "16.1.1",
58
- "@oh-my-pi/pi-tui": "16.1.1",
59
- "@oh-my-pi/pi-utils": "16.1.1",
60
- "@oh-my-pi/pi-wire": "16.1.1",
61
- "@oh-my-pi/snapcompact": "16.1.1",
51
+ "@oh-my-pi/hashline": "16.1.2",
52
+ "@oh-my-pi/omp-stats": "16.1.2",
53
+ "@oh-my-pi/pi-agent-core": "16.1.2",
54
+ "@oh-my-pi/pi-ai": "16.1.2",
55
+ "@oh-my-pi/pi-catalog": "16.1.2",
56
+ "@oh-my-pi/pi-mnemopi": "16.1.2",
57
+ "@oh-my-pi/pi-natives": "16.1.2",
58
+ "@oh-my-pi/pi-tui": "16.1.2",
59
+ "@oh-my-pi/pi-utils": "16.1.2",
60
+ "@oh-my-pi/pi-wire": "16.1.2",
61
+ "@oh-my-pi/snapcompact": "16.1.2",
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",
@@ -0,0 +1,63 @@
1
+ import { countTokens } from "@oh-my-pi/pi-agent-core";
2
+ import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
3
+ import { estimateToolSchemaTokens } from "@oh-my-pi/pi-coding-agent/modes/utils/context-usage";
4
+ import { buildSystemPrompt } from "@oh-my-pi/pi-coding-agent/system-prompt";
5
+ import { createTools, type Tool, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools";
6
+
7
+ function bytes(s: string): number {
8
+ return Buffer.byteLength(s, "utf-8");
9
+ }
10
+ function est(s: string): number {
11
+ return (bytes(s) + 3) >> 2;
12
+ }
13
+
14
+ await Settings.init({ inMemory: true, cwd: process.cwd() });
15
+ const settings = Settings.isolated({});
16
+
17
+ const session: ToolSession = {
18
+ cwd: process.cwd(),
19
+ hasUI: false,
20
+ getSessionFile: () => null,
21
+ getSessionSpawns: () => "*",
22
+ settings,
23
+ } as ToolSession;
24
+
25
+ const tools = await createTools(session);
26
+ const toolsMap = new Map<string, Tool>(tools.map(t => [t.name, t]));
27
+
28
+ console.log(`active tools (${tools.length}): ${tools.map(t => t.name).join(", ")}\n`);
29
+
30
+ const rows: Array<{ name: string; descBytes: number; tok: number; schemaTok: number }> = [];
31
+ for (const t of tools) {
32
+ const tok = estimateToolSchemaTokens([t as never]);
33
+ const descBytes = bytes(t.description ?? "");
34
+ const descTok = est(t.description ?? "");
35
+ rows.push({ name: t.name, descBytes, tok, schemaTok: tok - descTok });
36
+ }
37
+ rows.sort((a, b) => b.tok - a.tok);
38
+
39
+ const totalTok = estimateToolSchemaTokens(tools as never);
40
+ console.log("per-tool tokens (sorted): name | total tok | desc bytes | ~schema tok");
41
+ for (const r of rows) {
42
+ console.log(
43
+ ` ${r.name.padEnd(20)} ${String(r.tok).padStart(6)} ${String(r.descBytes).padStart(7)} ${String(r.schemaTok).padStart(6)}`,
44
+ );
45
+ }
46
+ console.log(`\nTOOLS TOTAL tokens: ${totalTok}\n`);
47
+
48
+ const built = await buildSystemPrompt({
49
+ tools: toolsMap as never,
50
+ toolNames: tools.map(t => t.name),
51
+ inlineToolDescriptors: false,
52
+ nativeTools: true,
53
+ cwd: process.cwd(),
54
+ skills: [],
55
+ contextFiles: [],
56
+ workspaceTree: { rootPath: process.cwd(), rendered: "", truncated: false, totalLines: 0, agentsMdFiles: [] },
57
+ });
58
+ const parts = built.systemPrompt;
59
+ const part0 = parts[0] ?? "";
60
+ const rest = parts.slice(1).join("\n");
61
+ console.log(`system prompt parts: ${parts.length}`);
62
+ console.log(`SYSTEM PROMPT tokens (part0, no skills): ${countTokens(part0)} (bytes=${bytes(part0)})`);
63
+ console.log(`SYSTEM CONTEXT tokens (parts[1..]): ${countTokens(rest)} (bytes=${bytes(rest)})`);
@@ -11,7 +11,7 @@ import type {
11
11
  SimpleStreamOptions,
12
12
  } from "@oh-my-pi/pi-ai";
13
13
  import { streamSimple } from "@oh-my-pi/pi-ai";
14
- import type { CanonicalModelVariant } from "@oh-my-pi/pi-catalog/identity";
14
+ import { buildModelProviderPriorityRank, type CanonicalModelVariant } from "@oh-my-pi/pi-catalog/identity";
15
15
  import { replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
16
16
  import { formatDuration, getProjectDir } from "@oh-my-pi/pi-utils";
17
17
  import chalk from "chalk";
@@ -50,6 +50,7 @@ export interface BenchModelRegistry {
50
50
  resolveCanonicalModel?(canonicalId: string, options?: CanonicalModelQueryOptions): Model<Api> | undefined;
51
51
  getCanonicalVariants?(canonicalId: string, options?: CanonicalModelQueryOptions): CanonicalModelVariant[];
52
52
  getCanonicalId?(model: Model<Api>): string | undefined;
53
+ hasConfiguredAuth?(model: Model<Api>): boolean;
53
54
  }
54
55
 
55
56
  export interface BenchRuntime {
@@ -346,6 +347,56 @@ interface BenchTarget {
346
347
  thinking: ResolvedThinkingLevel | undefined;
347
348
  }
348
349
 
350
+ /** Highest-priority provider variant: native/OAuth transports outrank mirrors. */
351
+ function pickHighestPriorityProvider(models: Model<Api>[], providerOrder?: readonly string[]): Model<Api> | undefined {
352
+ if (models.length <= 1) return models[0];
353
+ const priority = buildModelProviderPriorityRank(providerOrder);
354
+ return [...models].sort((a, b) => {
355
+ const aRank = priority.get(a.provider.toLowerCase()) ?? Number.POSITIVE_INFINITY;
356
+ const bRank = priority.get(b.provider.toLowerCase()) ?? Number.POSITIVE_INFINITY;
357
+ return aRank - bRank;
358
+ })[0];
359
+ }
360
+
361
+ /**
362
+ * Bench resolves selectors against the entire catalog (credentials are ignored),
363
+ * so an ambiguous id shared by several providers can land on one the user never
364
+ * authenticated. For non-pinned selectors, redirect to an equivalent model under
365
+ * a provider with configured auth. An explicit `provider/id` selector is honored
366
+ * verbatim — even unauthenticated — so forced benchmarking keeps working.
367
+ */
368
+ function resolveAuthenticatedAlternative(
369
+ selector: string,
370
+ model: Model<Api>,
371
+ modelRegistry: BenchModelRegistry,
372
+ providerOrder?: readonly string[],
373
+ ): Model<Api> | undefined {
374
+ if (!modelRegistry.hasConfiguredAuth) return undefined;
375
+ // A pinned `provider/...` selector is authoritative; never redirect off it.
376
+ if (selector.trim().toLowerCase().startsWith(`${model.provider.toLowerCase()}/`)) return undefined;
377
+ if (modelRegistry.hasConfiguredAuth(model)) return undefined;
378
+
379
+ const seen = new Set<string>();
380
+ const authenticated: Model<Api>[] = [];
381
+ const consider = (candidate: Model<Api>): void => {
382
+ const key = `${candidate.provider}/${candidate.id}`;
383
+ if (seen.has(key)) return;
384
+ seen.add(key);
385
+ if (modelRegistry.hasConfiguredAuth?.(candidate)) authenticated.push(candidate);
386
+ };
387
+ // Canonical variants link the same logical model across providers even when
388
+ // ids differ (e.g. fireworks `gpt-oss-20b` <-> openrouter `openai/gpt-oss-20b`).
389
+ const canonicalId = modelRegistry.getCanonicalId?.(model);
390
+ if (canonicalId) {
391
+ for (const variant of modelRegistry.getCanonicalVariants?.(canonicalId) ?? []) consider(variant.model);
392
+ }
393
+ // Same-id fallback for entries outside the canonical index.
394
+ for (const candidate of modelRegistry.getAll()) {
395
+ if (candidate.id === model.id) consider(candidate);
396
+ }
397
+ return pickHighestPriorityProvider(authenticated, providerOrder);
398
+ }
399
+
349
400
  function resolveBenchModels(
350
401
  selectors: string[],
351
402
  modelRegistry: BenchModelRegistry,
@@ -366,10 +417,20 @@ function resolveBenchModels(
366
417
  continue;
367
418
  }
368
419
  if (result.warning) writeStderr(`${chalk.yellow(`Warning: ${result.warning}`)}\n`);
420
+ let model = result.model;
421
+ const authenticated = resolveAuthenticatedAlternative(selector, model, modelRegistry, preferences.providerOrder);
422
+ if (authenticated) {
423
+ writeStderr(
424
+ `${chalk.yellow(
425
+ `Warning: no credentials for "${model.provider}"; benchmarking ${formatModelString(authenticated)} instead. Pin "${formatModelString(model)}" to force it.`,
426
+ )}\n`,
427
+ );
428
+ model = authenticated;
429
+ }
369
430
  resolved.push({
370
431
  selector,
371
- model: result.model,
372
- thinking: resolveThinkingLevelForModel(result.model, result.thinkingLevel),
432
+ model,
433
+ thinking: resolveThinkingLevelForModel(model, result.thinkingLevel),
373
434
  });
374
435
  }
375
436
  if (errors.length > 0) {
@@ -1,7 +1,6 @@
1
- import * as fs from "node:fs/promises";
2
1
  import * as os from "node:os";
3
2
  import * as path from "node:path";
4
- import { getProjectDir, normalizePathForComparison, setProjectDir } from "@oh-my-pi/pi-utils";
3
+ import { directoryExists, getProjectDir, normalizePathForComparison, setProjectDir } from "@oh-my-pi/pi-utils";
5
4
  import type { Args } from "./args";
6
5
 
7
6
  async function maybeAutoChdir(parsed: Args): Promise<void> {
@@ -22,19 +21,10 @@ async function maybeAutoChdir(parsed: Args): Promise<void> {
22
21
  return;
23
22
  }
24
23
 
25
- const isDirectory = async (p: string) => {
26
- try {
27
- const s = await fs.stat(p);
28
- return s.isDirectory();
29
- } catch {
30
- return false;
31
- }
32
- };
33
-
34
24
  const candidates = [path.join(home, "tmp"), "/tmp", "/var/tmp"];
35
25
  for (const candidate of candidates) {
36
26
  try {
37
- if (!(await isDirectory(candidate))) {
27
+ if (!(await directoryExists(candidate))) {
38
28
  continue;
39
29
  }
40
30
  setProjectDir(candidate);
@@ -46,7 +36,7 @@ async function maybeAutoChdir(parsed: Args): Promise<void> {
46
36
 
47
37
  try {
48
38
  const fallback = os.tmpdir();
49
- if (fallback && normalizePath(fallback) !== cwd && (await isDirectory(fallback))) {
39
+ if (fallback && normalizePath(fallback) !== cwd && (await directoryExists(fallback))) {
50
40
  setProjectDir(fallback);
51
41
  }
52
42
  } catch {
@@ -924,7 +924,7 @@ export const SETTINGS_SCHEMA = {
924
924
 
925
925
  inlineToolDescriptors: {
926
926
  type: "boolean",
927
- default: true,
927
+ default: false,
928
928
  ui: {
929
929
  tab: "model",
930
930
  group: "Prompt",
package/src/cursor.ts CHANGED
@@ -181,7 +181,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
181
181
  const toolResultMessage = await executeTool(this.options, "search", toolCallId, {
182
182
  pattern: args.pattern,
183
183
  paths: [searchPath],
184
- i: args.caseInsensitive || undefined,
184
+ case: args.caseInsensitive === true ? false : undefined,
185
185
  });
186
186
  return toolResultMessage;
187
187
  }
@@ -119,9 +119,16 @@ export class RawSseDebugBuffer {
119
119
  #records: RawSseDebugRecord[] = [];
120
120
  // Parallel to `#records`: `#recordChars[i]` is the precomputed char count
121
121
  // for `#records[i]`. Kept in lockstep by `#append` (push both) and
122
- // `#enforceLimits` (shift both). See the comment above the class for why
123
- // this is a sidecar array instead of a per-record property.
122
+ // `#enforceLimits` (advance `#head` to evict, then `slice` both together
123
+ // when compacting). See the comment above the class for why this is a
124
+ // sidecar array instead of a per-record property.
124
125
  #recordChars: number[] = [];
126
+ // Head-index ring over `#records`/`#recordChars`: index of the oldest live
127
+ // record. Eviction advances `#head` (amortized O(1)) rather than an O(n)
128
+ // front `shift()`; the dead `[0, #head)` prefix is reclaimed lazily by
129
+ // `#enforceLimits`. Live count is `#records.length - #head`; the live
130
+ // records are `#records[#head ..]`.
131
+ #head = 0;
125
132
  #totalChars = 0;
126
133
  #droppedRecords = 0;
127
134
  #droppedChars = 0;
@@ -181,7 +188,7 @@ export class RawSseDebugBuffer {
181
188
 
182
189
  snapshot(): RawSseDebugSnapshot {
183
190
  return {
184
- records: [...this.#records],
191
+ records: this.#records.slice(this.#head),
185
192
  droppedRecords: this.#droppedRecords,
186
193
  droppedChars: this.#droppedChars,
187
194
  totalEvents: this.#totalEvents,
@@ -190,9 +197,12 @@ export class RawSseDebugBuffer {
190
197
  }
191
198
 
192
199
  toRawText(): string {
193
- // Reads the live array directly: `rawRecordText` only computes a string
194
- // from each record, so no caller-visible mutation is possible.
195
- const body = this.#records.map(rawRecordText).join("\n");
200
+ // Reads the live window directly: `rawRecordText` only computes a string
201
+ // from each record, so no caller-visible mutation is possible. With a
202
+ // non-empty dead prefix we map a slice past `#head`; `#head === 0` (the
203
+ // common case) maps `#records` in place with no extra copy.
204
+ const live = this.#head === 0 ? this.#records : this.#records.slice(this.#head);
205
+ const body = live.map(rawRecordText).join("\n");
196
206
  if (this.#droppedRecords === 0) return body;
197
207
  const dropped = `: omp-debug-dropped records=${this.#droppedRecords} chars=${this.#droppedChars}\n\n`;
198
208
  return body.length > 0 ? `${dropped}${body}` : dropped;
@@ -208,14 +218,25 @@ export class RawSseDebugBuffer {
208
218
  }
209
219
 
210
220
  #enforceLimits(): void {
211
- while (this.#records.length > MAX_RAW_SSE_EVENTS || this.#totalChars > MAX_RAW_SSE_CHARS) {
212
- if (this.#records.length === 0) return;
213
- this.#records.shift();
214
- const chars = this.#recordChars.shift() ?? 0;
221
+ while (this.#records.length - this.#head > MAX_RAW_SSE_EVENTS || this.#totalChars > MAX_RAW_SSE_CHARS) {
222
+ if (this.#records.length - this.#head === 0) break;
223
+ const chars = this.#recordChars[this.#head] ?? 0;
224
+ this.#head += 1;
215
225
  this.#totalChars = Math.max(0, this.#totalChars - chars);
216
226
  this.#droppedRecords += 1;
217
227
  this.#droppedChars += chars;
218
228
  }
229
+ // Reclaim the consumed `[0, #head)` prefix once it grows large: one O(n)
230
+ // memmove amortized over many O(1) evictions, bounding the backing arrays
231
+ // to ~2x the live window. `#head >= MAX_RAW_SSE_EVENTS` covers the
232
+ // full-record-count steady state; `#head > liveCount` covers a small live
233
+ // window held by a few large records under the char budget.
234
+ const liveCount = this.#records.length - this.#head;
235
+ if (this.#head >= MAX_RAW_SSE_EVENTS || this.#head > liveCount) {
236
+ this.#records = this.#records.slice(this.#head);
237
+ this.#recordChars = this.#recordChars.slice(this.#head);
238
+ this.#head = 0;
239
+ }
219
240
  }
220
241
 
221
242
  #emit(): void {
@@ -5,7 +5,7 @@ if "__omp_prelude_loaded__" not in globals():
5
5
  from pathlib import Path
6
6
  import os, json, math, re
7
7
  from urllib.parse import unquote
8
- INTENT_FIELD = "_i"
8
+ INTENT_FIELD = "i"
9
9
 
10
10
  # __omp_display is injected by runner.py before the prelude executes; it
11
11
  # mirrors IPython's display() semantics with the same MIME bundle output.