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

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 (65) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/dist/cli.js +2990 -2991
  3. package/dist/types/config/model-resolver.d.ts +3 -3
  4. package/dist/types/mnemopi/embed-client.d.ts +70 -0
  5. package/dist/types/mnemopi/embed-protocol.d.ts +52 -0
  6. package/dist/types/mnemopi/embed-worker.d.ts +12 -0
  7. package/dist/types/mnemopi/state.d.ts +9 -1
  8. package/dist/types/modes/components/cache-invalidation-marker.d.ts +23 -10
  9. package/dist/types/modes/components/status-line/component.d.ts +2 -3
  10. package/dist/types/sdk.d.ts +12 -0
  11. package/dist/types/session/agent-session.d.ts +2 -0
  12. package/dist/types/session/agent-storage.d.ts +2 -0
  13. package/dist/types/session/auth-broker-config.d.ts +3 -2
  14. package/dist/types/session/history-storage.d.ts +1 -1
  15. package/dist/types/session/tool-choice-queue.d.ts +2 -0
  16. package/dist/types/tools/image-gen.d.ts +2 -2
  17. package/dist/types/tools/index.d.ts +2 -0
  18. package/dist/types/tui/hyperlink.d.ts +3 -2
  19. package/dist/types/utils/image-loading.d.ts +1 -1
  20. package/dist/types/utils/ipc.d.ts +22 -0
  21. package/dist/types/web/search/providers/perplexity-auth.d.ts +37 -0
  22. package/package.json +12 -12
  23. package/src/cli/bench-cli.ts +33 -2
  24. package/src/cli/dry-balance-cli.ts +4 -2
  25. package/src/cli.ts +8 -0
  26. package/src/commands/token.ts +52 -33
  27. package/src/config/append-only-context-mode.ts +45 -0
  28. package/src/config/model-discovery.ts +3 -0
  29. package/src/config/model-registry.ts +21 -3
  30. package/src/config/model-resolver.ts +31 -8
  31. package/src/discovery/builtin-rules/ts-no-return-type.md +0 -1
  32. package/src/extensibility/plugins/manager.ts +82 -22
  33. package/src/lsp/client.ts +24 -0
  34. package/src/mnemopi/backend.ts +49 -3
  35. package/src/mnemopi/embed-client.ts +401 -0
  36. package/src/mnemopi/embed-protocol.ts +35 -0
  37. package/src/mnemopi/embed-worker.ts +113 -0
  38. package/src/mnemopi/state.ts +29 -1
  39. package/src/modes/components/cache-invalidation-marker.ts +31 -15
  40. package/src/modes/components/custom-editor.test.ts +4 -3
  41. package/src/modes/components/custom-editor.ts +1 -1
  42. package/src/modes/components/model-selector.ts +2 -2
  43. package/src/modes/components/status-line/component.ts +64 -18
  44. package/src/modes/components/welcome.ts +1 -1
  45. package/src/modes/controllers/event-controller.ts +8 -0
  46. package/src/modes/controllers/selector-controller.ts +2 -2
  47. package/src/modes/theme/theme.ts +69 -0
  48. package/src/sdk.ts +37 -0
  49. package/src/session/agent-session.ts +13 -0
  50. package/src/session/agent-storage.ts +14 -0
  51. package/src/session/auth-broker-config.ts +2 -1
  52. package/src/session/history-storage.ts +13 -1
  53. package/src/session/tool-choice-queue.ts +6 -0
  54. package/src/stt/asr-client.ts +2 -7
  55. package/src/tiny/title-client.ts +2 -7
  56. package/src/tools/image-gen.ts +4 -8
  57. package/src/tools/index.ts +2 -0
  58. package/src/tools/render-utils.ts +4 -1
  59. package/src/tools/resolve.ts +1 -0
  60. package/src/tts/tts-client.ts +2 -7
  61. package/src/tui/hyperlink.ts +6 -3
  62. package/src/utils/image-loading.ts +12 -2
  63. package/src/utils/ipc.ts +38 -0
  64. package/src/web/search/providers/perplexity-auth.ts +133 -0
  65. package/src/web/search/providers/perplexity.ts +2 -125
@@ -201,9 +201,9 @@ export declare function resolveModelScope(patterns: string[], modelRegistry: Pic
201
201
  * the result to models matching those patterns.
202
202
  *
203
203
  * Returns the unfiltered available list when `enabledModels` is empty.
204
- * Returns an empty list when `enabledModels` is configured but no available
205
- * model matches any pattern — callers MUST treat this as "no usable model"
206
- * rather than falling back to the global default (see issue #1022).
204
+ * Returns an empty list when `enabledModels` is configured but no model matches
205
+ * any pattern — callers MUST treat this as "no usable model" rather than
206
+ * falling back to the global default (see issue #1022).
207
207
  */
208
208
  export declare function resolveAllowedModels(modelRegistry: Pick<ModelRegistry, "getAvailable" | "getCanonicalVariants">, settings: Settings | undefined, preferences?: ModelMatchPreferences): Promise<Model<Api>[]>;
209
209
  /**
@@ -0,0 +1,70 @@
1
+ import type { Subprocess } from "bun";
2
+ import type { MnemopiEmbedModelId, MnemopiEmbedWorkerInbound, MnemopiEmbedWorkerOutbound } from "./embed-protocol";
3
+ /**
4
+ * Abstraction over the mnemopi embeddings subprocess. The runtime
5
+ * implementation is a Bun child process so `onnxruntime-node`'s NAPI
6
+ * constructor + finalizer never run inside the main agent address space —
7
+ * those destructors segfault Bun on Windows when mnemopi's local embedding
8
+ * provider loads fastembed in the main process (issue #3031; the mnemopi
9
+ * sibling of the tiny-model fix from #1606 / #1607).
10
+ */
11
+ export interface MnemopiEmbedWorkerHandle {
12
+ send(message: MnemopiEmbedWorkerInbound): void;
13
+ onMessage(handler: (message: MnemopiEmbedWorkerOutbound) => void): () => void;
14
+ onError(handler: (error: Error) => void): () => void;
15
+ terminate(): Promise<void>;
16
+ }
17
+ /**
18
+ * Hidden subcommand on the main CLI that boots the mnemopi embeddings worker
19
+ * in the spawned subprocess. Kept in sync with the dispatch in `cli.ts`.
20
+ */
21
+ export declare const MNEMOPI_EMBED_WORKER_ARG = "__omp_worker_mnemopi_embed";
22
+ interface SpawnedSubprocess {
23
+ proc: Subprocess<"ignore", "ignore", "ignore">;
24
+ inbound: Set<(message: MnemopiEmbedWorkerOutbound) => void>;
25
+ errors: Set<(error: Error) => void>;
26
+ /**
27
+ * Flipped to `true` right before the deliberate SIGKILL so `onExit` can
28
+ * distinguish the expected hard-kill from a crash (SIGSEGV from a native
29
+ * fault, OOM SIGKILL, operator `kill -9`). Only the latter surfaces as a
30
+ * worker error so callers don't await forever.
31
+ */
32
+ intentionalExit: {
33
+ value: boolean;
34
+ };
35
+ }
36
+ /**
37
+ * Spawn the mnemopi embeddings worker as a subprocess. Exported for tests and
38
+ * the smoke probe; production callers go through {@link spawnMnemopiEmbedWorker}.
39
+ */
40
+ export declare function createMnemopiEmbedSubprocess(): SpawnedSubprocess;
41
+ /**
42
+ * Per-model wrapper produced by {@link MnemopiEmbedClient.initialize}.
43
+ * `embed()` round-trips one batch of texts through the worker subprocess and
44
+ * yields the resulting vectors in a single asynchronous batch — fastembed's
45
+ * own iterator was emitting batches that we collect on the child side anyway,
46
+ * and serializing per-batch over IPC would not improve throughput.
47
+ */
48
+ export interface MnemopiSubprocessEmbeddingModel {
49
+ embed(texts: string[], batchSize?: number): AsyncIterable<number[][]>;
50
+ }
51
+ export declare class MnemopiEmbedClient {
52
+ #private;
53
+ constructor(spawnWorker?: () => MnemopiEmbedWorkerHandle);
54
+ /**
55
+ * Load the named fastembed model inside the subprocess. Resolves to a
56
+ * thin wrapper whose `embed()` round-trips through the same worker, or
57
+ * `null` when the worker cannot init the model (missing peer, native
58
+ * load failure, etc.). Multiple calls with the same model reuse the
59
+ * single in-flight worker; calling with a different model loads it on
60
+ * the child without restarting the process.
61
+ */
62
+ initialize(model: MnemopiEmbedModelId, cacheDir: string | undefined): Promise<MnemopiSubprocessEmbeddingModel | null>;
63
+ terminate(): Promise<void>;
64
+ }
65
+ export declare const mnemopiEmbedClient: MnemopiEmbedClient;
66
+ export declare function shutdownMnemopiEmbedClient(): Promise<void>;
67
+ export declare function smokeTestMnemopiEmbedWorker({ timeoutMs, }?: {
68
+ timeoutMs?: number;
69
+ }): Promise<void>;
70
+ export {};
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Wire types between the parent (`MnemopiEmbedClient`) and the local
3
+ * embeddings subprocess. The parent owns the subprocess lifecycle (graceful
4
+ * work, hard `SIGKILL` on shutdown); the protocol carries no explicit close
5
+ * handshake — once the parent decides to terminate, it signals the OS to reap
6
+ * the child so `onnxruntime-node`'s NAPI finalizer never runs in the main
7
+ * agent address space (it crashes Bun on Windows shutdown — issue #3031, the
8
+ * mnemopi sibling of the tiny-model fix from #1606/#1607). See
9
+ * `embed-client.ts` for the spawn/kill glue.
10
+ */
11
+ /** Identifier of the fastembed model the worker should load (e.g. `fast-bge-base-en-v1.5`). */
12
+ export type MnemopiEmbedModelId = string;
13
+ export type MnemopiEmbedWorkerInbound = {
14
+ type: "ping";
15
+ id: string;
16
+ } | {
17
+ type: "init";
18
+ id: string;
19
+ model: MnemopiEmbedModelId;
20
+ cacheDir?: string;
21
+ } | {
22
+ type: "embed";
23
+ id: string;
24
+ model: MnemopiEmbedModelId;
25
+ cacheDir?: string;
26
+ texts: string[];
27
+ batchSize?: number;
28
+ };
29
+ export type MnemopiEmbedWorkerOutbound = {
30
+ type: "pong";
31
+ id: string;
32
+ } | {
33
+ type: "ready";
34
+ id: string;
35
+ } | {
36
+ type: "vectors";
37
+ id: string;
38
+ vectors: number[][];
39
+ } | {
40
+ type: "error";
41
+ id: string;
42
+ error: string;
43
+ } | {
44
+ type: "log";
45
+ level: "debug" | "warn" | "error";
46
+ msg: string;
47
+ meta?: Record<string, unknown>;
48
+ };
49
+ export interface MnemopiEmbedTransport {
50
+ send(message: MnemopiEmbedWorkerOutbound): void;
51
+ onMessage(handler: (message: MnemopiEmbedWorkerInbound) => void): () => void;
52
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Mnemopi local-embeddings worker. Loaded inside the dedicated subprocess
3
+ * spawned by `embed-client.ts` (re-entered through the agent CLI's hidden
4
+ * `__omp_worker_mnemopi_embed` selector). The whole point of this module is
5
+ * that `loadFastembed()` — and therefore `onnxruntime-node`'s NAPI
6
+ * constructor + finalizer — only ever runs in this child address space. The
7
+ * parent `SIGKILL`s us on shutdown so the destructor that crashes Bun on
8
+ * Windows shutdown (issue #3031, mnemopi sibling of #1606/#1607) never runs
9
+ * in either process.
10
+ */
11
+ import type { MnemopiEmbedTransport } from "./embed-protocol";
12
+ export declare function startMnemopiEmbedWorker(transport: MnemopiEmbedTransport): void;
@@ -4,7 +4,15 @@ import type { Mnemopi, RecallResult } from "@oh-my-pi/pi-mnemopi";
4
4
  import type * as MnemopiCoreNs from "@oh-my-pi/pi-mnemopi/core";
5
5
  import type { AgentSession } from "../session/agent-session";
6
6
  import type { MnemopiBackendConfig } from "./config";
7
- /** Lazily load `@oh-my-pi/pi-mnemopi` (memoized). */
7
+ /**
8
+ * Lazily load `@oh-my-pi/pi-mnemopi` (memoized) and route fastembed loads
9
+ * through the dedicated embeddings subprocess. The override is installed once
10
+ * — before any consumer gets the chance to call `embed()` — so
11
+ * `onnxruntime-node`'s NAPI constructor + finalizer never run inside the
12
+ * agent's address space (issue #3031). Test seams that swap the initializer
13
+ * with `setLocalModelInitializerForTests` still win because both go through
14
+ * the same module-level slot.
15
+ */
8
16
  export declare function loadMnemopi(): Promise<typeof MnemopiNs>;
9
17
  /** Lazily load `@oh-my-pi/pi-mnemopi/core` (memoized). */
10
18
  export declare function loadMnemopiCore(): Promise<typeof MnemopiCoreNs>;
@@ -6,20 +6,33 @@ export interface CacheInvalidation {
6
6
  reprocessedTokens: number;
7
7
  }
8
8
  /**
9
- * Decide whether `current` turn lost the prompt cache that `prev` established.
9
+ * Decide whether `current` turn lost a *working* prompt cache that `prev` was
10
+ * reusing.
10
11
  *
11
12
  * The provider reports a warm prefix as `cacheRead`; a model/thinking/tool/
12
13
  * system-prompt change (or a history rewrite) breaks the prefix, so the next
13
- * request reads nothing from cache and re-pays for the whole prompt. We detect
14
- * that as: the previous turn cached a meaningful prefix, yet this turn's
14
+ * request reads nothing from cache and re-pays for the whole prompt. We flag
15
+ * only the transition where a demonstrably warm cache goes cold: the previous
16
+ * turn must have actually READ a meaningful prefix back, and this turn's
15
17
  * `cacheRead` collapsed to zero while it still reprocessed a non-trivial prompt.
16
- * Returns `undefined` (no marker) for the first turn, tiny contexts, turns
17
- * that reused any cache, and crucially turns on providers with *implicit*
18
- * best-effort caching. Only an explicit, prefix-controlled cache (Anthropic /
19
- * Bedrock `cache_control`) re-creates the prefix on a cold turn (`cacheWrite >
20
- * 0`); implicit caches (Google / OpenAI / Fireworks) report `cacheWrite: 0` and
21
- * drop `cacheRead` to zero intermittently as routine propagation noise that
22
- * self-heals the next turn, so flagging it would be a false positive.
18
+ *
19
+ * Requiring a prior warm read is deliberate. A turn that merely WROTE the prefix
20
+ * (`cacheRead` 0) has not proven the cache is live — that is the session's first
21
+ * request, or a re-write after expiry so a following cold turn there is
22
+ * expected, not an invalidation the user caused (e.g. a long-running first tool
23
+ * call outliving the provider's 5-minute cache TTL surfaced a spurious "cache
24
+ * miss" right under the opening message). It also collapses a run of consecutive
25
+ * cold turns to the single marker at the moment the cache actually broke, instead
26
+ * of repeating the banner on every turn while it re-warms.
27
+ *
28
+ * Returns `undefined` (no marker) for the first turn, turns whose predecessor
29
+ * never read a warm prefix, tiny contexts, turns that reused any cache, and —
30
+ * crucially — turns on providers with *implicit* best-effort caching. Only an
31
+ * explicit, prefix-controlled cache (Anthropic / Bedrock `cache_control`)
32
+ * re-creates the prefix on a cold turn (`cacheWrite > 0`); implicit caches
33
+ * (Google / OpenAI / Fireworks) report `cacheWrite: 0` and drop `cacheRead` to
34
+ * zero intermittently as routine propagation noise that self-heals the next
35
+ * turn, so flagging it would be a false positive.
23
36
  */
24
37
  export declare function detectCacheInvalidation(prev: Usage | undefined, current: Usage): CacheInvalidation | undefined;
25
38
  /**
@@ -34,9 +34,8 @@ export declare class StatusLineComponent implements Component {
34
34
  dispose(): void;
35
35
  invalidate(): void;
36
36
  /**
37
- * Background-refresh the Anthropic OAuth quota report. Guarded by a 5-min
38
- * TTL on both success (cache lifetime) and error (backoff). Exposed
39
- * (non-private) so unit tests can verify the backoff invariant.
37
+ * Startup redraws only arm a short-delayed task; timeout releases the render
38
+ * cadence while a late successful fetch can still refresh the cached segment.
40
39
  */
41
40
  refreshUsageInBackground(): void;
42
41
  /**
@@ -262,6 +262,18 @@ export declare function discoverSessionExtensionPaths(options: Pick<CreateAgentS
262
262
  * repeated. Keep this the single source of the discovery branch logic.
263
263
  */
264
264
  export declare function loadSessionExtensions(options: Pick<CreateAgentSessionOptions, "disableExtensionDiscovery" | "additionalExtensionPaths">, cwd: string, settings: Settings, eventBus: EventBus): Promise<LoadExtensionsResult>;
265
+ /**
266
+ * Load discovered/configured extensions and register their providers into
267
+ * `modelRegistry`, then discover the dynamic provider catalogs. One-shot CLIs
268
+ * (`omp bench`, dry-balance) build a bare {@link ModelRegistry} that only knows
269
+ * built-in catalog providers; without this, providers contributed by an
270
+ * extension (e.g. a custom OpenAI-compatible provider under
271
+ * `~/.omp/agent/extensions/`) never reach model resolution. Mirrors the
272
+ * session / `omp models` path: drain the queued provider registrations, then
273
+ * `refreshRuntimeProviders` so dynamically-discovered models exist before
274
+ * selectors are resolved.
275
+ */
276
+ export declare function loadCliExtensionProviders(modelRegistry: ModelRegistry, settings: Settings, cwd: string, options?: Pick<CreateAgentSessionOptions, "disableExtensionDiscovery" | "additionalExtensionPaths">): Promise<void>;
265
277
  /**
266
278
  * Discover skills from cwd and agentDir.
267
279
  */
@@ -399,6 +399,8 @@ export declare class AgentSession {
399
399
  nextToolChoiceDirective(): ToolChoiceDirective | undefined;
400
400
  /** Peek the head non-forcing pending preview invoker, for the `resolve` tool's dispatch. */
401
401
  peekPendingInvoker(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
402
+ /** Clear stale non-forcing pending preview invokers after `resolve` proves none can run. */
403
+ clearPendingInvokers(): void;
402
404
  /**
403
405
  * Force the next model call to target a specific active tool, then terminate
404
406
  * the agent loop. Pushes a two-step sequence [forced, "none"] so the model
@@ -16,6 +16,8 @@ export declare class AgentStorage {
16
16
  * @returns AgentStorage instance for the given path
17
17
  */
18
18
  static open(dbPath?: string): Promise<AgentStorage>;
19
+ /** @internal Reset all singletons and close their databases — test-only. */
20
+ static resetInstance(): void;
19
21
  /**
20
22
  * Reads legacy settings persisted in the agent.db `settings` table.
21
23
  * The canonical settings store is `config.yml`; this accessor only
@@ -20,7 +20,8 @@
20
20
  * `runRootCommand`, and we want hand-edited config entries to be honoured at
21
21
  * boot without forcing a startup reorder.
22
22
  */
23
- import { type AuthBrokerClientConfig, type DiscoverAuthStorageOptions, discoverAuthStorage as discoverAuthStorageShared, getAuthBrokerTokenFilePath } from "@oh-my-pi/pi-ai/auth-broker/discover";
23
+ import { type AuthBrokerClientConfig, type DiscoverAuthStorageOptions, getAuthBrokerTokenFilePath } from "@oh-my-pi/pi-ai/auth-broker/discover";
24
+ import type { AuthStorage } from "./auth-storage";
24
25
  export { type AuthBrokerClientConfig, getAuthBrokerTokenFilePath };
25
26
  /**
26
27
  * Read broker configuration. Returns null when the URL is missing
@@ -41,4 +42,4 @@ export declare function resolveAuthBrokerConfig(): Promise<AuthBrokerClientConfi
41
42
  *
42
43
  * Default `agentDir` is the current configured agent directory.
43
44
  */
44
- export declare function discoverAuthStorage(agentDir?: string, options?: Omit<DiscoverAuthStorageOptions, "agentDir" | "configValueResolver">): ReturnType<typeof discoverAuthStorageShared>;
45
+ export declare function discoverAuthStorage(agentDir?: string, options?: Omit<DiscoverAuthStorageOptions, "agentDir" | "configValueResolver">): Promise<AuthStorage>;
@@ -10,7 +10,7 @@ export declare class HistoryStorage {
10
10
  #private;
11
11
  private constructor();
12
12
  static open(dbPath?: string): HistoryStorage;
13
- /** @internal Reset the singleton — test-only. */
13
+ /** @internal Reset the singleton and close its database — test-only. */
14
14
  static resetInstance(): void;
15
15
  /**
16
16
  * Register a resolver that supplies the current session ID for prompts added
@@ -71,6 +71,8 @@ export declare class ToolChoiceQueue {
71
71
  registerPendingInvoker(id: string, sourceToolName: string, onInvoked: (input: unknown) => Promise<unknown> | unknown): void;
72
72
  /** Drop the pending invoker with this id (e.g. after it resolves). */
73
73
  removePendingInvoker(id: string): void;
74
+ /** Drop every pending preview invoker without touching hard tool-choice directives. */
75
+ clearPendingInvokers(): void;
74
76
  /** True when at least one non-forcing pending preview is registered. */
75
77
  get hasPendingInvoker(): boolean;
76
78
  /** The head (most-recently registered) pending invoker's handler, for resolve dispatch. */
@@ -61,6 +61,6 @@ export declare function isImageProviderPreference(value: unknown): value is Imag
61
61
  /** Set the preferred image provider from settings */
62
62
  export declare function setPreferredImageProvider(provider: ImageProviderPreference): void;
63
63
  export declare const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails>;
64
- export declare function getImageGenTools(modelRegistry?: ModelRegistry, activeModel?: Model): Promise<Array<CustomTool<typeof imageGenSchema, ImageGenToolDetails>>>;
65
- export declare function getImageGenToolsWithRegistry(modelRegistry: ModelRegistry, activeModel?: Model): Promise<Array<CustomTool<typeof imageGenSchema, ImageGenToolDetails>>>;
64
+ export declare function getImageGenTools(_modelRegistry?: ModelRegistry, _activeModel?: Model): Promise<Array<CustomTool<typeof imageGenSchema, ImageGenToolDetails>>>;
65
+ export declare function getImageGenToolsWithRegistry(_modelRegistry: ModelRegistry, _activeModel?: Model): Promise<Array<CustomTool<typeof imageGenSchema, ImageGenToolDetails>>>;
66
66
  export {};
@@ -276,6 +276,8 @@ export interface ToolSession {
276
276
  * tool dispatches to it so a staged preview resolves WITHOUT forcing tool_choice — the
277
277
  * agent-loop's SoftToolRequirement lifecycle owns reminder injection and escalation. */
278
278
  peekPendingInvoker?(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
279
+ /** Clear stale pending preview markers when `resolve` cannot dispatch them. */
280
+ clearPendingInvokers?(): void;
279
281
  /** Peek the long-lived "standing" resolve handler registered by a mode (e.g. plan mode).
280
282
  * Consulted by the `resolve` tool as a fallback when no queue invoker is in flight,
281
283
  * letting modes accept `resolve` invocations without forcing the tool choice every turn. */
@@ -5,6 +5,7 @@
5
5
  * - `"off"`: never
6
6
  * - `"auto"`: when `process.stdout.isTTY`, `NO_COLOR` is unset, and the detected terminal reports hyperlink support
7
7
  * - `"always"`: unconditionally (useful for viewers that support OSC 8 without advertising it)
8
+ * Before settings initialization, returns false so early render paths stay plain text.
8
9
  */
9
10
  export declare function isHyperlinkEnabled(): boolean;
10
11
  /**
@@ -23,8 +24,8 @@ export declare function urlHyperlink(url: string, displayText: string): string;
23
24
  * Wrap `displayText` in an OSC 8 hyperlink pointing at an HTTP(S) URL,
24
25
  * bypassing terminal capability auto-detection. Used for auth prompts where
25
26
  * an inert "click" label blocks login on terminals whose capabilities are
26
- * not advertised. Still returns plain text when the user has explicitly
27
- * opted out via `tui.hyperlinks=off`.
27
+ * not advertised. Still returns plain text before settings initialization or
28
+ * when the user has explicitly opted out via `tui.hyperlinks=off`.
28
29
  */
29
30
  export declare function urlHyperlinkAlways(url: string, displayText: string): string;
30
31
  /**
@@ -8,7 +8,7 @@ export declare const SUPPORTED_INPUT_IMAGE_MIME_TYPES: Set<string>;
8
8
  * with an opaque HTTP 400. Detect those models so the resize pipeline encodes
9
9
  * to PNG/JPEG instead — the automatic equivalent of `OMP_NO_WEBP=1`.
10
10
  */
11
- export declare function modelLacksWebpSupport(model: Pick<Model, "provider" | "api"> | undefined): boolean;
11
+ export declare function modelLacksWebpSupport(model: Pick<Model, "provider" | "api" | "imageInputDecoder"> | undefined): boolean;
12
12
  /**
13
13
  * `true` when `model` cannot decode WebP, otherwise `undefined` so the
14
14
  * `OMP_NO_WEBP` env fallback in {@link resizeImage} still applies. Feed straight
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Narrow a value to a thenable so a rejection handler can be attached.
3
+ *
4
+ * Mirrors the local helper in `mcp/transports/stdio.ts` (kept separate because
5
+ * that copy serves the FileSink stdin-write path and is battle-tested there).
6
+ * This shared copy is the home for the IPC `send()` sites.
7
+ */
8
+ export declare function isThenable(value: unknown): value is PromiseLike<unknown>;
9
+ /**
10
+ * Send a message to a Bun subprocess over IPC, neutralizing both the
11
+ * synchronous throw ("cannot be used after the process has exited") and any
12
+ * asynchronous rejection (EPIPE from a pipe that broke between exit being
13
+ * observed and the next `send()`). The dead worker is detected separately via
14
+ * `onExit`/`onError` and respawned or disabled by the owning client; an
15
+ * un-awaited EPIPE rejection must not escape as a fatal unhandled rejection
16
+ * that takes down the whole session. See issue #2997.
17
+ *
18
+ * `label` prefixes the debug log on synchronous failure (e.g. "tts").
19
+ */
20
+ export declare function safeSend(proc: {
21
+ send(message: unknown): unknown;
22
+ }, message: unknown, label: string): void;
@@ -0,0 +1,37 @@
1
+ import type { AuthStorage, OAuthAccess } from "@oh-my-pi/pi-ai";
2
+ export declare const PERPLEXITY_CHAT_BASE_URL = "https://api.perplexity.ai";
3
+ export declare const PERPLEXITY_RESPONSES_BASE_URL = "https://api.perplexity.ai/v1";
4
+ export declare const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
5
+ export declare const OAUTH_EXPIRY_BUFFER_MS: number;
6
+ export interface ApiConfig {
7
+ type: "api_key";
8
+ apiKey: string;
9
+ provider: "perplexity" | "openrouter";
10
+ chatBaseUrl: string;
11
+ responsesBaseUrl: string;
12
+ modelPrefix: string;
13
+ useResponses: boolean;
14
+ }
15
+ export type PerplexityAuth = ApiConfig | {
16
+ type: "oauth";
17
+ access: OAuthAccess;
18
+ } | {
19
+ type: "cookies";
20
+ cookies: string;
21
+ } | {
22
+ type: "anonymous";
23
+ };
24
+ export interface PerplexityAuthOptions {
25
+ signal?: AbortSignal;
26
+ forceRefresh?: boolean;
27
+ }
28
+ /** Detect API-key endpoints to try in priority order (Perplexity direct, then OpenRouter). */
29
+ export declare function getApiConfigs(authStorage: AuthStorage, sessionId: string | undefined, options?: PerplexityAuthOptions): Promise<ApiConfig[]>;
30
+ /**
31
+ * Decode a Perplexity JWT's `exp` claim, in ms. Returns `undefined` when the
32
+ * token has no `exp` (which is the common case — Perplexity sessions are
33
+ * server-side and effectively non-expiring from the client's POV).
34
+ */
35
+ export declare function jwtExpiryMs(token: string): number | undefined;
36
+ /** Collect all available auth methods to try in priority order */
37
+ export declare function getAvailableAuthMethods(authStorage: AuthStorage, sessionId: string | undefined, options?: PerplexityAuthOptions): Promise<PerplexityAuth[]>;
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.2",
4
+ "version": "16.1.4",
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.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",
51
+ "@oh-my-pi/hashline": "16.1.4",
52
+ "@oh-my-pi/omp-stats": "16.1.4",
53
+ "@oh-my-pi/pi-agent-core": "16.1.4",
54
+ "@oh-my-pi/pi-ai": "16.1.4",
55
+ "@oh-my-pi/pi-catalog": "16.1.4",
56
+ "@oh-my-pi/pi-mnemopi": "16.1.4",
57
+ "@oh-my-pi/pi-natives": "16.1.4",
58
+ "@oh-my-pi/pi-tui": "16.1.4",
59
+ "@oh-my-pi/pi-utils": "16.1.4",
60
+ "@oh-my-pi/pi-wire": "16.1.4",
61
+ "@oh-my-pi/snapcompact": "16.1.4",
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",
@@ -25,7 +25,7 @@ import {
25
25
  } from "../config/model-resolver";
26
26
  import { Settings } from "../config/settings";
27
27
  import benchPrompt from "../prompts/bench.md" with { type: "text" };
28
- import { discoverAuthStorage } from "../sdk";
28
+ import { discoverAuthStorage, loadCliExtensionProviders } from "../sdk";
29
29
  import { resolveThinkingLevelForModel, shouldDisableReasoning, toReasoningEffort } from "../thinking";
30
30
 
31
31
  const DEFAULT_RUNS = 1;
@@ -145,6 +145,23 @@ function isFirstTokenEvent(event: AssistantMessageEvent): boolean {
145
145
  }
146
146
  }
147
147
 
148
+ /** Final message carries visible output — non-empty text/thinking or a tool call. */
149
+ function hasVisibleFinalContent(message: AssistantMessage): boolean {
150
+ return message.content.some(block => {
151
+ switch (block.type) {
152
+ case "text":
153
+ return block.text.length > 0;
154
+ case "thinking":
155
+ return block.thinking.length > 0;
156
+ case "redactedThinking":
157
+ case "toolCall":
158
+ return true;
159
+ default:
160
+ return false;
161
+ }
162
+ });
163
+ }
164
+
148
165
  /**
149
166
  * Tokens/s over the generation window (duration minus TTFT) so queue/prefill
150
167
  * latency does not dilute throughput. Falls back to total duration when the
@@ -232,6 +249,18 @@ async function runBenchRequest(
232
249
  const rawTtft = message.ttft ?? (firstTokenAt === undefined ? durationMs : firstTokenAt - startedAt);
233
250
  const ttftMs = Number.isFinite(rawTtft) && rawTtft > 0 ? rawTtft : 0;
234
251
  const outputTokens = Number.isFinite(message.usage.output) && message.usage.output > 0 ? message.usage.output : 0;
252
+ // A run that streamed no content (no delta/end event set firstTokenAt),
253
+ // carries no visible final content, and measured no output tokens
254
+ // benchmarked nothing — a genuinely empty stream (e.g. a gateway that 200s
255
+ // with an empty body). Surface it as a failure instead of a misleading
256
+ // 0-token "✓". Streaming and buffered providers that produce content keep
257
+ // passing even when usage is omitted.
258
+ if (firstTokenAt === undefined && outputTokens === 0 && !hasVisibleFinalContent(message)) {
259
+ return {
260
+ ok: false,
261
+ error: `provider returned no output (0 tokens, empty stream; stop reason: ${message.stopReason ?? "unknown"})`,
262
+ };
263
+ }
235
264
  return {
236
265
  ok: true,
237
266
  ttftMs,
@@ -328,8 +357,10 @@ export function formatBenchTable(summary: BenchSummary): string {
328
357
  async function createDefaultRuntime(): Promise<BenchRuntime> {
329
358
  const authStorage = await discoverAuthStorage();
330
359
  try {
331
- const settings = await Settings.init({ cwd: getProjectDir() });
360
+ const cwd = getProjectDir();
361
+ const settings = await Settings.init({ cwd });
332
362
  const modelRegistry = new ModelRegistry(authStorage);
363
+ await loadCliExtensionProviders(modelRegistry, settings, cwd);
333
364
  return {
334
365
  modelRegistry,
335
366
  settings,
@@ -26,7 +26,7 @@ import {
26
26
  } from "../config/model-resolver";
27
27
  import { Settings } from "../config/settings";
28
28
  import dryBalanceBenchPrompt from "../prompts/dry-balance-bench.md" with { type: "text" };
29
- import { discoverAuthStorage } from "../sdk";
29
+ import { discoverAuthStorage, loadCliExtensionProviders } from "../sdk";
30
30
 
31
31
  const DEFAULT_SAMPLE_COUNT = 100;
32
32
  const DEFAULT_CONCURRENCY = 32;
@@ -523,8 +523,10 @@ async function runBenchTargets(
523
523
  async function createDefaultRuntime(): Promise<DryBalanceRuntime> {
524
524
  const authStorage = await discoverAuthStorage();
525
525
  try {
526
- const settings = await Settings.init({ cwd: getProjectDir() });
526
+ const cwd = getProjectDir();
527
+ const settings = await Settings.init({ cwd });
527
528
  const modelRegistry = new ModelRegistry(authStorage);
529
+ await loadCliExtensionProviders(modelRegistry, settings, cwd);
528
530
  return {
529
531
  modelRegistry,
530
532
  settings,
package/src/cli.ts CHANGED
@@ -68,6 +68,7 @@ async function runSmokeTest(): Promise<void> {
68
68
  const { smokeTestTinyTitleWorker } = await import("./tiny/title-client");
69
69
  const { smokeTestSttWorker } = await import("./stt/asr-client");
70
70
  const { smokeTestTtsWorker } = await import("./tts/tts-client");
71
+ const { smokeTestMnemopiEmbedWorker } = await import("./mnemopi/embed-client");
71
72
  const { smokeTestJsEvalWorker } = await import("./eval/js/context-manager");
72
73
  await smokeTestSyncWorker();
73
74
 
@@ -87,6 +88,7 @@ async function runSmokeTest(): Promise<void> {
87
88
  await smokeTestSttWorker();
88
89
  await smokeTestJsEvalWorker();
89
90
  await smokeTestTtsWorker();
91
+ await smokeTestMnemopiEmbedWorker();
90
92
  process.stdout.write("smoke-test: ok\n");
91
93
  }
92
94
 
@@ -96,6 +98,7 @@ const TAB_WORKER_ARG = "__omp_worker_tab";
96
98
  const JS_EVAL_WORKER_ARG = "__omp_worker_js_eval";
97
99
  const STT_WORKER_ARG = "__omp_worker_stt";
98
100
  const TTS_WORKER_ARG = "__omp_worker_tts";
101
+ const MNEMOPI_EMBED_WORKER_ARG = "__omp_worker_mnemopi_embed";
99
102
 
100
103
  async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
101
104
  if (arg === TINY_WORKER_ARG) {
@@ -151,6 +154,11 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
151
154
  await runIpcSubprocessWorker(startTtsWorker);
152
155
  return true;
153
156
  }
157
+ if (arg === MNEMOPI_EMBED_WORKER_ARG) {
158
+ const { startMnemopiEmbedWorker } = await import("./mnemopi/embed-worker");
159
+ await runIpcSubprocessWorker(startMnemopiEmbedWorker);
160
+ return true;
161
+ }
154
162
  return false;
155
163
  }
156
164