@oh-my-pi/pi-coding-agent 17.3.0 → 17.3.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 (70) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/{CHANGELOG-66nakf5b.md → CHANGELOG-fr2awajz.md} +20 -0
  3. package/dist/cli.js +2823 -2823
  4. package/dist/docs-index.generated.txt +1 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/extension-flags.d.ts +3 -3
  7. package/dist/types/cli/flag-tables.d.ts +0 -1
  8. package/dist/types/cli/setup-cli.d.ts +10 -0
  9. package/dist/types/cli/update-cli.d.ts +48 -9
  10. package/dist/types/commands/completions.d.ts +3 -0
  11. package/dist/types/config/claude-paths.d.ts +7 -0
  12. package/dist/types/discovery/agents.d.ts +6 -6
  13. package/dist/types/discovery/helpers.d.ts +3 -4
  14. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  15. package/dist/types/extensibility/extensions/types.d.ts +4 -0
  16. package/dist/types/launch/broker.d.ts +5 -1
  17. package/dist/types/main.d.ts +1 -1
  18. package/dist/types/mcp/transports/stdio.d.ts +6 -3
  19. package/dist/types/modes/components/footer.d.ts +3 -2
  20. package/dist/types/modes/interactive-mode.d.ts +2 -1
  21. package/dist/types/modes/rpc/rpc-client.d.ts +2 -0
  22. package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
  23. package/dist/types/modes/runtime-init.d.ts +3 -1
  24. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  25. package/dist/types/task/executor.d.ts +2 -0
  26. package/dist/types/utils/git.d.ts +19 -0
  27. package/dist/types/utils/shell-snapshot.d.ts +4 -1
  28. package/package.json +13 -13
  29. package/src/async/job-manager.ts +33 -4
  30. package/src/cli/args.ts +14 -3
  31. package/src/cli/extension-flags.ts +6 -10
  32. package/src/cli/flag-tables.ts +2 -10
  33. package/src/cli/gc-cli.ts +13 -3
  34. package/src/cli/setup-cli.ts +2 -2
  35. package/src/cli/update-cli.ts +246 -107
  36. package/src/commands/completions.ts +16 -14
  37. package/src/config/claude-paths.ts +18 -0
  38. package/src/config/model-registry.ts +2 -2
  39. package/src/config.ts +4 -3
  40. package/src/discovery/agents.ts +7 -7
  41. package/src/discovery/claude.ts +5 -6
  42. package/src/discovery/helpers.ts +12 -11
  43. package/src/extensibility/extensions/runner.ts +5 -0
  44. package/src/extensibility/extensions/types.ts +5 -0
  45. package/src/extensibility/legacy-typebox.ts +45 -4
  46. package/src/launch/broker.ts +26 -4
  47. package/src/lsp/mux/server.ts +7 -1
  48. package/src/main.ts +30 -3
  49. package/src/mcp/transports/stdio.ts +7 -3
  50. package/src/modes/acp/acp-agent.ts +1 -0
  51. package/src/modes/components/footer.ts +17 -35
  52. package/src/modes/components/status-line/component.ts +14 -27
  53. package/src/modes/controllers/extension-ui-controller.ts +2 -2
  54. package/src/modes/interactive-mode.ts +16 -9
  55. package/src/modes/print-mode.ts +1 -0
  56. package/src/modes/rpc/rpc-client.ts +4 -2
  57. package/src/modes/rpc/rpc-input.ts +27 -0
  58. package/src/modes/rpc/rpc-mode.ts +11 -19
  59. package/src/modes/runtime-init.ts +5 -1
  60. package/src/modes/utils/ui-helpers.ts +74 -53
  61. package/src/session/agent-session.ts +24 -9
  62. package/src/session/claude-session-store.ts +4 -3
  63. package/src/task/executor.ts +8 -4
  64. package/src/tools/browser/launch.ts +9 -0
  65. package/src/tools/read-format.ts +11 -10
  66. package/src/tools/run-scope.ts +4 -2
  67. package/src/utils/external-editor.ts +10 -11
  68. package/src/utils/git.ts +27 -0
  69. package/src/utils/shell-snapshot.ts +5 -1
  70. package/src/web/search/providers/gemini.ts +4 -9
@@ -82,6 +82,8 @@ export interface Args {
82
82
  export declare function parseArgs(inputArgs: string[], extensionFlags?: Map<string, {
83
83
  type: "boolean" | "string";
84
84
  }>): Args;
85
+ /** Reject requested tool names absent from the fully discovered session registry. */
86
+ export declare function validateToolNames(requested: readonly string[] | undefined, known: readonly string[]): void;
85
87
  /**
86
88
  * Emit a stderr error listing the unrecognized flags and return `true` when
87
89
  * there were any. Caller is expected to exit with a non-zero status. Splitting
@@ -29,8 +29,8 @@ export interface ExtensionFlagSink {
29
29
  * semantics and surfaces in `unknownFlags` — without consuming the following
30
30
  * message or overwriting the built-in field. No built-in name list to maintain.
31
31
  *
32
- * Returns `null` when there is no sink or no registered extension flags, in
33
- * which case the caller keeps its original startup parse (an extension-aware
34
- * re-parse would be identical anyway).
32
+ * Returns `null` only when there is no sink. Once extensions have loaded, the
33
+ * reparse always runs so `--tools` can be validated against their registered
34
+ * tools even when no extension registered CLI flags.
35
35
  */
36
36
  export declare function applyExtensionFlags(runner: ExtensionFlagSink | undefined, rawArgs: string[]): Args | null;
@@ -45,7 +45,6 @@ export interface ParseDeps {
45
45
  warn: (message: string, meta?: Record<string, unknown>) => void;
46
46
  };
47
47
  parseThinking: (value: string | null | undefined) => ConfiguredThinkingLevel | undefined;
48
- builtinToolNames: readonly string[];
49
48
  normalizeToolNames: (values: Iterable<string>) => string[];
50
49
  thinkingEfforts: readonly string[];
51
50
  }
@@ -11,6 +11,16 @@ export interface SetupCommandArgs {
11
11
  * Returns undefined if not a setup command.
12
12
  */
13
13
  export declare function parseSetupArgs(args: string[]): SetupCommandArgs | undefined;
14
+ export interface PythonCheckResult {
15
+ available: boolean;
16
+ pythonPath?: string;
17
+ usingManagedEnv?: boolean;
18
+ managedEnvPath?: string;
19
+ }
20
+ /**
21
+ * Check Python environment and kernel dependencies.
22
+ */
23
+ export declare function checkPythonSetup(cwd: string, interpreter?: string): Promise<PythonCheckResult>;
14
24
  /**
15
25
  * Install Python packages using uv (preferred) or pip.
16
26
  */
@@ -112,6 +112,8 @@ interface UpdateMethodResolutionOptions {
112
112
  miseBinDirs?: readonly string[];
113
113
  miseDataDir?: string;
114
114
  npmBinDir?: string;
115
+ /** Bun's configured global package directory, independent of its bin directory. */
116
+ bunGlobalDir?: string;
115
117
  /**
116
118
  * Whether the resolved omp path is a plain file (the standalone binary)
117
119
  * rather than a package-manager symlink. Stops a binary install from being
@@ -119,8 +121,34 @@ interface UpdateMethodResolutionOptions {
119
121
  * target directory.
120
122
  */
121
123
  ompIsRegularFile?: boolean;
124
+ /**
125
+ * Absolute path named by the bin entry's first symlink hop. This deliberately
126
+ * preserves a global package symlink instead of resolving into its checkout.
127
+ */
128
+ ompLinkTarget?: string;
122
129
  }
130
+ type UpdateTarget = {
131
+ method: "brew";
132
+ } | {
133
+ method: "mise";
134
+ } | {
135
+ method: "nix";
136
+ } | {
137
+ method: "bun";
138
+ path?: string;
139
+ } | {
140
+ method: "npm";
141
+ path?: string;
142
+ } | {
143
+ method: "binary";
144
+ path: string;
145
+ replacesSymlink: boolean;
146
+ };
123
147
  export declare function resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined, options?: UpdateMethodResolutionOptions): UpdateMethod;
148
+ /** Resolve an update target from the concrete PATH entry selected by the shell. */
149
+ export declare function resolveUpdateTargetFromPath(ompPath: string, bunBinDir: string | undefined, options: UpdateMethodResolutionOptions & {
150
+ allowPackageManagers: boolean;
151
+ }): UpdateTarget;
124
152
  /**
125
153
  * Get the latest release info from the npm registry, following `omp.rename`
126
154
  * pointers ({@link resolveReleaseRename}) when the package has moved to a new
@@ -145,7 +173,13 @@ interface BunInstallCachePruneResult {
145
173
  * cache stays internally consistent.
146
174
  */
147
175
  export declare function pruneBunInstallCache(cacheDir: string, packageNames?: Set<string>): Promise<BunInstallCachePruneResult>;
148
- export declare function resolveBunGlobalNodeModulesDirFromLocations(globalBinDir: string | undefined, cacheDir: string | undefined): string | undefined;
176
+ interface BunGlobalInstallLocations {
177
+ globalDir?: string;
178
+ globalBinDir?: string;
179
+ cacheDir?: string;
180
+ }
181
+ /** Resolve Bun's global node_modules root from explicit, default, or cache locations. */
182
+ export declare function resolveBunGlobalNodeModulesDirFromLocations({ globalDir, globalBinDir, cacheDir, }: BunGlobalInstallLocations): string | undefined;
149
183
  /**
150
184
  * Detect a musl-libc Linux host (Alpine, Void-musl) so self-update replaces a
151
185
  * musl binary with the musl release asset instead of the glibc build, which
@@ -168,16 +202,21 @@ declare function verifyBinaryAtPath(binaryPath: string, expectedVersion: string)
168
202
  */
169
203
  declare function verifyInstalledVersion(expectedVersion: string): Promise<InstalledVersionVerification>;
170
204
  /**
171
- * Best-effort removal of binary-update backups left by earlier runs.
205
+ * Best-effort removal of binary-update leftovers from earlier runs.
172
206
  *
173
- * Each self-update moves the previous executable to `<binary>.<timestamp>.<pid>.bak`
174
- * before swapping the new one in. On Windows that backup cannot be deleted
175
- * while the updating process is alive, so it is left for a later run to reclaim
176
- * once its owning process has exited. Also matches the legacy fixed
177
- * `<binary>.bak` name produced before backups were timestamped, so users
178
- * upgrading from a buggy release get the orphaned file cleaned up.
207
+ * Each self-update writes to `<binary>.<timestamp>.<pid>.new` and moves the
208
+ * previous executable to `<binary>.<timestamp>.<pid>.bak` before swapping the
209
+ * new one in. On Windows a backup cannot be deleted while the updating process
210
+ * is alive (it is the running process image), so it is left for a later run to
211
+ * reclaim once its owning process has exited. A `.new` temp file only survives
212
+ * a hard kill mid-download; it is reaped once older than the download window,
213
+ * which a live download cannot exceed without timing out and cleaning up after
214
+ * itself — so a concurrent run's in-progress temp is never deleted. Legacy
215
+ * fixed `<binary>.bak` / `<binary>.new` names (from before suffixes were made
216
+ * unique) are matched too, so users upgrading from a buggy release get the
217
+ * orphaned files cleaned up.
179
218
  */
180
- export declare function sweepStaleBackups(targetPath: string): Promise<void>;
219
+ export declare function sweepStaleUpdateArtifacts(targetPath: string): Promise<void>;
181
220
  /**
182
221
  * Atomically replace the installed binary and roll back if version verification fails.
183
222
  */
@@ -5,6 +5,9 @@
5
5
  * (see `cli/completion-gen.ts`), so it never drifts from the actual CLI surface.
6
6
  */
7
7
  import { Command } from "@oh-my-pi/pi-utils/cli";
8
+ import { type Shell } from "../cli/completion-gen.js";
9
+ /** Generate a completion script from the live command registry. */
10
+ export declare function generateLiveCompletion(shell: Shell): Promise<string>;
8
11
  export default class Completions extends Command {
9
12
  static description: string;
10
13
  static args: {
@@ -0,0 +1,7 @@
1
+ /** Paths to Claude Code's user data and configuration file. */
2
+ export interface ClaudePaths {
3
+ configDir: string;
4
+ configFile: string;
5
+ }
6
+ /** Resolves Claude Code's user paths, honoring `CLAUDE_CONFIG_DIR`. */
7
+ export declare function resolveClaudePaths(home?: string): ClaudePaths;
@@ -7,13 +7,13 @@ interface UserPathCandidateOptions {
7
7
  }
8
8
  /**
9
9
  * Run a best-effort discovery probe and return its trimmed stdout, or
10
- * `undefined` when the command fails, produces no output, or exceeds
11
- * {@link HOST_PROBE_TIMEOUT_MS}. On timeout the child is killed with SIGKILL so
12
- * a wedged interop pipe cannot hang startup; the killed/non-zero exit is then
13
- * reported as "unavailable" and discovery falls back to the Linux
14
- * `$HOME`/`~/.omp` candidates.
10
+ * `undefined` when the command fails, produces no output, or exceeds the
11
+ * timeout. On timeout the child is killed with SIGKILL so a wedged interop pipe
12
+ * cannot hang startup; the killed/non-zero exit is then reported as
13
+ * "unavailable" and discovery falls back to the Linux `$HOME`/`~/.omp`
14
+ * candidates.
15
15
  */
16
- export declare function runHostProbe(cmd: string[]): string | undefined;
16
+ export declare function runHostProbe(cmd: string[], timeoutMs?: number): string | undefined;
17
17
  /** Resolve the Windows host profile home exposed to WSL, if available. */
18
18
  export declare function getWslWindowsHomeCandidate(options?: UserPathCandidateOptions): string | undefined;
19
19
  /** User-level paths: ~/.agent[s]/<segments>, plus the Windows host profile under WSL. */
@@ -261,11 +261,10 @@ export declare function resolveOrDefaultProjectRegistryPath(cwd: string): Promis
261
261
  /** Register a process-global plugin cache invalidator called whenever plugin roots are cleared. */
262
262
  export declare function registerPluginCacheInvalidator(invalidator: () => void): void;
263
263
  /**
264
- * List all installed Claude Code plugin roots from the plugin cache.
265
- * Reads ~/.claude/plugins/installed_plugins.json and ~/.omp/plugins/installed_plugins.json,
266
- * and optionally the nearest project-scoped registry resolved from `cwd`.
264
+ * List all installed Claude Code plugin roots from its active plugin cache and
265
+ * ~/.omp/plugins/installed_plugins.json, plus the nearest project registry when present.
267
266
  *
268
- * Results are cached per home, project registry, and canonical active project.
267
+ * Results are cached per Claude and OMP config directories, project registry, and canonical active project.
269
268
  */
270
269
  export declare function listClaudePluginRoots(home: string, cwd?: string): Promise<{
271
270
  roots: ClaudePluginRoot[];
@@ -8,7 +8,7 @@ import type { MemoryRuntimeContext } from "../../memory-backend/index.js";
8
8
  import type { AsyncJobSnapshot } from "../../session/agent-session.js";
9
9
  import type { SessionManager } from "../../session/session-manager.js";
10
10
  import type { BranchHandler, NavigateTreeHandler, NewSessionHandler } from "../session-handler-types.js";
11
- import type { AfterProviderResponseEvent, AssistantThinkingRenderer, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, ContextEvent, Extension, ExtensionActions, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFlag, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, InputEvent, InputEventResult, McpNotificationEvent, MessageRenderer, RegisteredCommand, RegisteredTool, ResourcesDiscoverEvent, SessionBeforeBranchResult, SessionBeforeCompactResult, SessionBeforeSwitchResult, SessionBeforeTreeResult, SessionCompactingResult, SessionStopEvent, SessionStopEventResult, ToolCallEvent, ToolCallEventResult, ToolResultEvent, ToolResultEventResult, UserBashEvent, UserBashEventResult, UserPythonEvent, UserPythonEventResult } from "./types.js";
11
+ import type { AfterProviderResponseEvent, AssistantThinkingRenderer, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, ContextEvent, Extension, ExtensionActions, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFlag, ExtensionMode, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, InputEvent, InputEventResult, McpNotificationEvent, MessageRenderer, RegisteredCommand, RegisteredTool, ResourcesDiscoverEvent, SessionBeforeBranchResult, SessionBeforeCompactResult, SessionBeforeSwitchResult, SessionBeforeTreeResult, SessionCompactingResult, SessionStopEvent, SessionStopEventResult, ToolCallEvent, ToolCallEventResult, ToolResultEvent, ToolResultEventResult, UserBashEvent, UserBashEventResult, UserPythonEvent, UserPythonEventResult } from "./types.js";
12
12
  /** Combined result from all before_agent_start handlers */
13
13
  interface BeforeAgentStartCombinedResult {
14
14
  messages?: NonNullable<BeforeAgentStartEventResult["message"]>[];
@@ -115,7 +115,7 @@ export declare class ExtensionRunner {
115
115
  * leak into another's `ctx.cwd` by reading a shared global.
116
116
  */
117
117
  get cwd(): string;
118
- initialize(actions: ExtensionActions, contextActions: ExtensionContextActions, commandContextActions?: ExtensionCommandContextActions, uiContext?: ExtensionUIContext): void;
118
+ initialize(actions: ExtensionActions, contextActions: ExtensionContextActions, commandContextActions?: ExtensionCommandContextActions, uiContext?: ExtensionUIContext, mode?: ExtensionMode): void;
119
119
  /**
120
120
  * Forward a `credential_disabled` event from `AuthStorage` to extension handlers.
121
121
  *
@@ -280,9 +280,13 @@ export interface ExtensionModelQuery {
280
280
  */
281
281
  family(model: Model): string;
282
282
  }
283
+ /** Runtime host mode exposed to Pi-compatible extensions. */
284
+ export type ExtensionMode = "tui" | "rpc" | "json" | "print";
283
285
  export interface ExtensionContext {
284
286
  /** UI methods for user interaction */
285
287
  ui: ExtensionUIContext;
288
+ /** Current run mode. Use `"tui"` to guard terminal-only UI such as custom components. */
289
+ mode: ExtensionMode;
286
290
  /** Get current context usage for the active model. */
287
291
  getContextUsage(): ContextUsage | undefined;
288
292
  /** Get a read-only snapshot of async jobs owned by this session. */
@@ -1,2 +1,6 @@
1
+ export interface DaemonBrokerStartOptions {
2
+ /** Base of the exponential child-restart backoff. */
3
+ restartBackoffBaseMs?: number;
4
+ }
1
5
  /** Start the detached project or global daemon broker selected by the CLI worker host. */
2
- export declare function startDaemonBrokerFromEnvironment(): Promise<void>;
6
+ export declare function startDaemonBrokerFromEnvironment(options?: DaemonBrokerStartOptions): Promise<void>;
@@ -28,7 +28,7 @@ export interface AcpSessionFactoryOptions {
28
28
  sessionDir?: string;
29
29
  authStorage: AuthStorage;
30
30
  modelRegistry: ModelRegistry;
31
- parsedArgs: Pick<Args, "apiKey" | "trustedExtensions">;
31
+ parsedArgs: Pick<Args, "apiKey" | "trustedExtensions" | "tools">;
32
32
  rawArgs: string[];
33
33
  createSession: (options: CreateAgentSessionOptions) => Promise<CreateAgentSessionResult>;
34
34
  }
@@ -110,7 +110,7 @@ interface KillableSubprocess {
110
110
  }
111
111
  /**
112
112
  * Terminate an MCP stdio subprocess: SIGTERM (process-group when `detached`
113
- * on POSIX, direct child otherwise), wait up to `TERM_GRACE_MS` for a
113
+ * on POSIX, direct child otherwise), wait up to `termGraceMs` for a
114
114
  * cooperative exit, then escalate to SIGKILL — waiting up to `KILL_GRACE_MS`
115
115
  * more only when the leader itself hadn't already exited. A detached
116
116
  * leader's cooperative exit does not prove the whole process group is gone
@@ -123,9 +123,12 @@ interface KillableSubprocess {
123
123
  * `detached`/`platform` pair: `StdioTransport.connect()` derives `detached`
124
124
  * from `resolveStdioSpawnCommand()`, which is tied to the host's real
125
125
  * `process.platform`, so a POSIX detached session cannot be reproduced
126
- * end-to-end through `connect()` on a non-Linux dev/CI host.
126
+ * end-to-end through `connect()` on a non-Linux dev/CI host. `termGraceMs`
127
+ * preserves the production grace by default while allowing those real
128
+ * subprocess tests to cover the same transition without sleeping for a
129
+ * production-length shutdown window.
127
130
  */
128
- export declare function terminateStdioProcess(proc: KillableSubprocess, detached: boolean, platform?: NodeJS.Platform): Promise<void>;
131
+ export declare function terminateStdioProcess(proc: KillableSubprocess, detached: boolean, platform?: NodeJS.Platform, termGraceMs?: number): Promise<void>;
129
132
  /**
130
133
  * Stdio transport for MCP servers.
131
134
  * Spawns a subprocess and communicates via stdin/stdout.
@@ -17,8 +17,9 @@ export declare class FooterComponent implements Component {
17
17
  */
18
18
  setExtensionStatus(key: string, text: string | undefined): void;
19
19
  /**
20
- * Set up a file watcher on .git/HEAD to detect branch changes.
21
- * Call the provided callback when branch changes.
20
+ * Watch the repository HEAD for branch changes; invokes the callback so the
21
+ * footer repaints with the new branch. Uses `git.head.watch` (stat-poll) —
22
+ * see that helper for why `fs.watch` cannot track git's atomic HEAD swaps.
22
23
  */
23
24
  watchBranch(onBranchChange: () => void): void;
24
25
  /**
@@ -51,6 +51,7 @@ import type { CompactionQueuedMessage, InteractiveModeContext, InteractiveModeIn
51
51
  * any further, it would only misreport the rows it actually occupies.
52
52
  */
53
53
  export declare function computeEditorMaxHeight(terminalRows: number): number;
54
+ export declare function shouldEnterPlanModeOnStartup(sessionManager: Pick<SessionManager, "buildSessionContext" | "getEntries">, sessionSettings: Pick<Settings, "get">): boolean;
54
55
  /** Options for creating an InteractiveMode instance (for future API use) */
55
56
  export interface InteractiveModeOptions {
56
57
  /** Providers that were migrated during startup */
@@ -334,7 +335,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
334
335
  reuseSettledComponent?: boolean;
335
336
  }): Component[];
336
337
  renderSessionContext(sessionContext: SessionContext, options?: RenderSessionContextOptions): void;
337
- /** Render a session context in bounded chunks so terminal input runs between transcript paints. */
338
+ /** Build a session context in bounded chunks so terminal input runs between event-loop turns. */
338
339
  renderSessionContextIncrementally(sessionContext: SessionContext, options: RenderSessionContextOptions, renderChunk?: () => void): Promise<void>;
339
340
  renderInitialMessages(options?: {
340
341
  preserveExistingChat?: boolean;
@@ -25,6 +25,8 @@ export interface RpcClientOptions {
25
25
  sessionDir?: string;
26
26
  /** Additional CLI arguments */
27
27
  args?: string[];
28
+ /** Grace period before escalating process termination (default: process utility default, 1000ms) */
29
+ terminationGraceMs?: number;
28
30
  /** Custom tools owned by the embedding host and exposed over the RPC transport */
29
31
  customTools?: RpcClientCustomTool[];
30
32
  }
@@ -3,3 +3,8 @@
3
3
  * RPC startup uses this before extension discovery so in-process modules cannot steal protocol input.
4
4
  */
5
5
  export declare function claimRpcInput(): ReadableStream<Uint8Array>;
6
+ /**
7
+ * Parses newline-delimited RPC input without letting one malformed line stop
8
+ * subsequent protocol frames.
9
+ */
10
+ export declare function readRpcInputFrames(input: ReadableStream<Uint8Array>, onFrame: (frame: unknown) => void, onParseError: (message: string) => void): Promise<void>;
@@ -1,4 +1,4 @@
1
- import type { ExtensionError, ExtensionUIContext } from "../extensibility/extensions/types.js";
1
+ import type { ExtensionError, ExtensionMode, ExtensionUIContext } from "../extensibility/extensions/types.js";
2
2
  import type { AgentSession } from "../session/agent-session.js";
3
3
  /** Action name for an extension-originated send failure. */
4
4
  export type ExtensionSendAction = "extension_send" | "extension_send_user";
@@ -9,6 +9,8 @@ export interface InitializeExtensionsOptions {
9
9
  reportRuntimeError: (error: ExtensionError) => void;
10
10
  /** Optional shutdown hook (rpc mode signals its loop; print mode is a no-op). */
11
11
  onShutdown?: () => void;
12
+ /** Pi-compatible mode exposed to extension contexts. Defaults to `"print"`. */
13
+ mode?: ExtensionMode;
12
14
  /** Optional UI context (rpc supplies one; print runs headless). */
13
15
  uiContext?: ExtensionUIContext;
14
16
  /** Optional lifecycle hook for extension-originated messages that can start an agent turn. */
@@ -35,7 +35,7 @@ export declare class UiHelpers {
35
35
  * @param options.populateHistory Add user messages to editor history
36
36
  */
37
37
  renderSessionContext(sessionContext: SessionContext, options?: RenderSessionContextOptions): void;
38
- /** Render a session context in bounded chunks so terminal input runs between transcript paints. */
38
+ /** Build a session context in bounded chunks so terminal input runs between event-loop turns. */
39
39
  renderSessionContextIncrementally(sessionContext: SessionContext, options: RenderSessionContextOptions, renderChunk?: () => void): Promise<void>;
40
40
  renderInitialMessages(options?: RenderInitialMessagesOptions): Promise<void>;
41
41
  clearEditor(): void;
@@ -213,6 +213,8 @@ export interface ExecutorOptions {
213
213
  keepAlive?: boolean;
214
214
  /** Internal ownership handoff for cleanup that outlives the visible Task result. */
215
215
  onCleanupDeferred?: (completion: Promise<void>) => void;
216
+ /** Internal cleanup grace override for deterministic lifecycle tests. */
217
+ cleanupGraceMs?: number;
216
218
  }
217
219
  interface FinalizeSubprocessOutputArgs {
218
220
  rawOutput: string;
@@ -155,6 +155,11 @@ export declare const GIT_COMMAND_OUTPUT_LIMIT_BYTES: number;
155
155
  * degrades instead of freezing the UI indefinitely.
156
156
  */
157
157
  export declare const GIT_SPAWN_SYNC_TIMEOUT_MS = 5000;
158
+ /**
159
+ * Stat-poll interval for {@link head.watch}. One `stat` per interval keeps an
160
+ * always-on status line cheap while surfacing a branch switch within a second.
161
+ */
162
+ export declare const HEAD_WATCH_INTERVAL_MS = 1000;
158
163
  interface CommandOptions {
159
164
  readonly env?: Record<string, string | undefined>;
160
165
  readonly maxOutputBytes?: number;
@@ -458,6 +463,20 @@ export declare const head: {
458
463
  sha(cwd: string, signal?: AbortSignal): Promise<string | null>;
459
464
  /** Abbreviated HEAD commit SHA. */
460
465
  short(cwd: string, length?: number, signal?: AbortSignal): Promise<string | null>;
466
+ /**
467
+ * Watch the repository's HEAD for branch moves. Returns a disposer.
468
+ *
469
+ * Deliberately stat-polls via `fs.watchFile` instead of `fs.watch`: git
470
+ * swaps HEAD with `HEAD.lock` + atomic rename, which unlinks the HEAD inode
471
+ * — and Bun's inotify-backed `fs.watch` permanently stops delivering events
472
+ * after observing a rename in the watched directory (oven-sh/bun#24875), so
473
+ * an event watcher fires once and then freezes on Linux (issue #8412 was
474
+ * the same freeze for file-inode watches on every platform). A path-based
475
+ * stat poll re-resolves the path each interval and survives inode swaps
476
+ * everywhere. Reftable repos keep ref state in `<gitDir>/reftable` (their
477
+ * HEAD file is a static stub), so the poll targets that directory instead.
478
+ */
479
+ watch(repository: GitRepository, onChange: () => void): () => void;
461
480
  };
462
481
  export declare const repo: {
463
482
  /** Resolve the repository root (may be a worktree root). */
@@ -11,5 +11,8 @@ export declare function sanitizeSnapshotForBrush(content: string): {
11
11
  /**
12
12
  * Create a shell snapshot, caching the result.
13
13
  * Returns the path to the snapshot file, or null if creation failed.
14
+ *
15
+ * `timeoutMs` is configurable so callers exercising failure handling do not
16
+ * have to wait out the production startup budget.
14
17
  */
15
- export declare function getOrCreateSnapshot(shell: string, env: Record<string, string | undefined>): Promise<string | null>;
18
+ export declare function getOrCreateSnapshot(shell: string, env: Record<string, string | undefined>, timeoutMs?: number): Promise<string | null>;
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": "17.3.0",
4
+ "version": "17.3.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",
@@ -50,18 +50,18 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@babel/parser": "^7.29.7",
53
- "@oh-my-pi/hashline": "17.3.0",
54
- "@oh-my-pi/omp-stats": "17.3.0",
55
- "@oh-my-pi/omptype": "17.3.0",
56
- "@oh-my-pi/pi-agent-core": "17.3.0",
57
- "@oh-my-pi/pi-ai": "17.3.0",
58
- "@oh-my-pi/pi-catalog": "17.3.0",
59
- "@oh-my-pi/pi-mnemopi": "17.3.0",
60
- "@oh-my-pi/pi-natives": "17.3.0",
61
- "@oh-my-pi/pi-tui": "17.3.0",
62
- "@oh-my-pi/pi-utils": "17.3.0",
63
- "@oh-my-pi/pi-wire": "17.3.0",
64
- "@oh-my-pi/snapcompact": "17.3.0",
53
+ "@oh-my-pi/hashline": "17.3.2",
54
+ "@oh-my-pi/omp-stats": "17.3.2",
55
+ "@oh-my-pi/omptype": "17.3.2",
56
+ "@oh-my-pi/pi-agent-core": "17.3.2",
57
+ "@oh-my-pi/pi-ai": "17.3.2",
58
+ "@oh-my-pi/pi-catalog": "17.3.2",
59
+ "@oh-my-pi/pi-mnemopi": "17.3.2",
60
+ "@oh-my-pi/pi-natives": "17.3.2",
61
+ "@oh-my-pi/pi-tui": "17.3.2",
62
+ "@oh-my-pi/pi-utils": "17.3.2",
63
+ "@oh-my-pi/pi-wire": "17.3.2",
64
+ "@oh-my-pi/snapcompact": "17.3.2",
65
65
  "@opentelemetry/api": "^1.9.1",
66
66
  "@opentelemetry/api-logs": "^0.220.0",
67
67
  "@opentelemetry/context-async-hooks": "^2.9.0",
@@ -152,6 +152,7 @@ export class AsyncJobManager {
152
152
  readonly #maxRunningJobs: number;
153
153
  readonly #retentionMs: number;
154
154
  #deliveryLoop: Promise<void> | undefined;
155
+ #deliveryQueueChanged = Promise.withResolvers<void>();
155
156
  #disposed = false;
156
157
 
157
158
  #filterJobs(jobs: Iterable<AsyncJob>, filter?: AsyncJobFilter): AsyncJob[] {
@@ -335,6 +336,7 @@ export class AsyncJobManager {
335
336
  for (const jobId of uniqueJobIds) {
336
337
  this.#watchedJobs.add(jobId);
337
338
  }
339
+ this.#notifyDeliveryQueueChanged();
338
340
  return uniqueJobIds.length;
339
341
  }
340
342
 
@@ -389,6 +391,7 @@ export class AsyncJobManager {
389
391
  this.#deliveries.length,
390
392
  ...this.#deliveries.filter(delivery => !this.isDeliverySuppressed(delivery.jobId)),
391
393
  );
394
+ this.#notifyDeliveryQueueChanged();
392
395
  return before - this.#deliveries.length;
393
396
  }
394
397
 
@@ -603,6 +606,7 @@ export class AsyncJobManager {
603
606
  this.#clearEvictionTimers();
604
607
  this.#jobs.clear();
605
608
  this.#deliveries.length = 0;
609
+ this.#notifyDeliveryQueueChanged();
606
610
  this.#inFlightDeliveries.length = 0;
607
611
  this.#suppressedDeliveries.clear();
608
612
  this.#watchedJobs.clear();
@@ -704,13 +708,14 @@ export class AsyncJobManager {
704
708
  const now = Date.now();
705
709
  if (selected.nextAttemptAt > now) {
706
710
  if (selected.nextAttemptAt > deadline) return false;
707
- await Bun.sleep(selected.nextAttemptAt - now);
711
+ await this.#waitForDeliveryQueueChange(selected.nextAttemptAt - now);
708
712
  continue;
709
713
  }
710
714
 
711
715
  const index = this.#deliveries.indexOf(selected);
712
716
  if (index === -1) continue;
713
717
  this.#deliveries.splice(index, 1);
718
+ this.#notifyDeliveryQueueChanged();
714
719
  if (this.isDeliverySuppressed(selected.jobId)) continue;
715
720
 
716
721
  return this.#waitForDeliveryPromise(this.#deliverDelivery(selected), deadline);
@@ -726,7 +731,7 @@ export class AsyncJobManager {
726
731
  if (this.isDeliverySuppressed(jobId)) {
727
732
  return;
728
733
  }
729
- this.#deliveries.push({
734
+ this.#queueDelivery({
730
735
  jobId,
731
736
  text,
732
737
  attempt: 0,
@@ -762,7 +767,8 @@ export class AsyncJobManager {
762
767
  }
763
768
  const waitMs = delivery.nextAttemptAt - Date.now();
764
769
  if (waitMs > 0) {
765
- await Bun.sleep(waitMs);
770
+ await this.#waitForDeliveryQueueChange(waitMs);
771
+ continue;
766
772
  }
767
773
  if (this.#deliveries[0] !== delivery) {
768
774
  continue;
@@ -813,7 +819,7 @@ export class AsyncJobManager {
813
819
  delivery.lastError = error instanceof Error ? error.message : String(error);
814
820
  delivery.nextAttemptAt = Date.now() + this.#getRetryDelay(delivery.attempt);
815
821
  if (!this.isDeliverySuppressed(delivery.jobId)) {
816
- this.#deliveries.push(delivery);
822
+ this.#queueDelivery(delivery);
817
823
  }
818
824
  logger.warn("Async job completion delivery failed", {
819
825
  jobId: delivery.jobId,
@@ -831,6 +837,29 @@ export class AsyncJobManager {
831
837
  return promise;
832
838
  }
833
839
 
840
+ #queueDelivery(delivery: AsyncJobDelivery): void {
841
+ const index = this.#deliveries.findIndex(candidate => candidate.nextAttemptAt > delivery.nextAttemptAt);
842
+ if (index === -1) this.#deliveries.push(delivery);
843
+ else this.#deliveries.splice(index, 0, delivery);
844
+ this.#notifyDeliveryQueueChanged();
845
+ }
846
+
847
+ async #waitForDeliveryQueueChange(delayMs: number): Promise<void> {
848
+ const timerElapsed = Promise.withResolvers<void>();
849
+ const timer = setTimeout(timerElapsed.resolve, delayMs);
850
+ timer.unref();
851
+ try {
852
+ await Promise.race([timerElapsed.promise, this.#deliveryQueueChanged.promise]);
853
+ } finally {
854
+ clearTimeout(timer);
855
+ }
856
+ }
857
+
858
+ #notifyDeliveryQueueChanged(): void {
859
+ this.#deliveryQueueChanged.resolve();
860
+ this.#deliveryQueueChanged = Promise.withResolvers<void>();
861
+ }
862
+
834
863
  async #waitForDeliveryPromise(promise: Promise<void> | undefined, deadline: number): Promise<boolean> {
835
864
  if (!promise) return true;
836
865
  if (deadline === Number.POSITIVE_INFINITY) {
package/src/cli/args.ts CHANGED
@@ -6,7 +6,7 @@ import { $env, APP_NAME, logger } from "@oh-my-pi/pi-utils";
6
6
  import chalk from "@oh-my-pi/pi-utils/chalk";
7
7
  import type { ServiceTierOpenAISettingValue } from "../config/service-tier";
8
8
  import { CLI_THINKING_LEVELS, type ConfiguredThinkingLevel, parseCliThinkingLevel } from "../thinking";
9
- import { BUILTIN_TOOL_NAMES, HIDDEN_TOOL_NAMES, normalizeToolNames } from "../tools/builtin-names";
9
+ import { normalizeToolNames } from "../tools/builtin-names";
10
10
  import {
11
11
  OPTIONAL_FLAGS,
12
12
  OPTIONAL_VALUE_FLAGS,
@@ -108,7 +108,6 @@ export interface Args {
108
108
  const PARSE_DEPS: ParseDeps = {
109
109
  logger,
110
110
  parseThinking: parseCliThinkingLevel,
111
- builtinToolNames: [...BUILTIN_TOOL_NAMES, ...HIDDEN_TOOL_NAMES],
112
111
  normalizeToolNames,
113
112
  thinkingEfforts: CLI_THINKING_LEVELS,
114
113
  };
@@ -148,6 +147,7 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
148
147
  // reparse in `runRootCommand` parses it a second time). Mutating the input
149
148
  // would corrupt that later parse, so never touch the caller's array.
150
149
  const args = [...inputArgs];
150
+ const parseDeps = PARSE_DEPS;
151
151
  const result: Args = {
152
152
  messages: [],
153
153
  fileArgs: [],
@@ -214,7 +214,7 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
214
214
  if (i + 1 < args.length && args[i + 1] !== PROFILE_BOOTSTRAP_BOUNDARY_ARG) {
215
215
  const consumed = consumeBuiltInStringValue(arg, args, i + 1);
216
216
  i = consumed.index;
217
- STRING_SETTERS[arg](result, consumed.value, PARSE_DEPS);
217
+ STRING_SETTERS[arg](result, consumed.value, parseDeps);
218
218
  }
219
219
  } else if (OPTIONAL_VALUE_FLAGS.has(arg)) {
220
220
  const config = OPTIONAL_FLAGS[arg];
@@ -332,6 +332,17 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
332
332
  return result;
333
333
  }
334
334
 
335
+ /** Reject requested tool names absent from the fully discovered session registry. */
336
+ export function validateToolNames(requested: readonly string[] | undefined, known: readonly string[]): void {
337
+ if (!requested) return;
338
+ const knownNames = new Set(known);
339
+ const unknown = requested.filter(name => !knownNames.has(name));
340
+ if (unknown.length === 0) return;
341
+ throw new CliUsageError(
342
+ `Unknown tool${unknown.length === 1 ? "" : "s"} in --tools: ${unknown.join(", ")}. Valid tools: ${known.join(", ")}.`,
343
+ );
344
+ }
345
+
335
346
  /**
336
347
  * Emit a stderr error listing the unrecognized flags and return `true` when
337
348
  * there were any. Caller is expected to exit with a non-zero status. Splitting
@@ -29,18 +29,14 @@ export interface ExtensionFlagSink {
29
29
  * semantics and surfaces in `unknownFlags` — without consuming the following
30
30
  * message or overwriting the built-in field. No built-in name list to maintain.
31
31
  *
32
- * Returns `null` when there is no sink or no registered extension flags, in
33
- * which case the caller keeps its original startup parse (an extension-aware
34
- * re-parse would be identical anyway).
32
+ * Returns `null` only when there is no sink. Once extensions have loaded, the
33
+ * reparse always runs so `--tools` can be validated against their registered
34
+ * tools even when no extension registered CLI flags.
35
35
  */
36
36
  export function applyExtensionFlags(runner: ExtensionFlagSink | undefined, rawArgs: string[]): Args | null {
37
- const extensionFlags = runner?.getFlags();
38
- if (!runner || !extensionFlags || extensionFlags.size === 0) {
39
- return null;
40
- }
41
- const parsed = parseArgs(rawArgs, extensionFlags);
42
- // `parseArgs` only records registered extension flags in `unknownFlags`, so
43
- // every entry here is a flag this runner owns that was actually passed.
37
+ if (!runner) return null;
38
+ const parsed = parseArgs(rawArgs, runner.getFlags());
39
+ // `parseArgs` records extension flag values in `unknownFlags`.
44
40
  for (const [name, value] of parsed.unknownFlags) {
45
41
  runner.setFlagValue(name, value);
46
42
  }