@oh-my-pi/pi-coding-agent 12.0.0 → 12.1.0

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 (76) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +4 -29
  3. package/docs/fs-scan-cache-architecture.md +50 -0
  4. package/docs/models.md +234 -0
  5. package/package.json +7 -11
  6. package/src/cli/args.ts +1 -1
  7. package/src/cli/config-cli.ts +2 -1
  8. package/src/cli/grep-cli.ts +1 -1
  9. package/src/cli/jupyter-cli.ts +2 -1
  10. package/src/cli/plugin-cli.ts +2 -1
  11. package/src/cli/setup-cli.ts +117 -22
  12. package/src/cli/shell-cli.ts +1 -1
  13. package/src/cli/stats-cli.ts +2 -1
  14. package/src/cli/update-cli.ts +1 -1
  15. package/src/cli/web-search-cli.ts +2 -1
  16. package/src/cli.ts +1 -1
  17. package/src/commands/launch.ts +1 -1
  18. package/src/commit/agentic/index.ts +1 -0
  19. package/src/commit/pipeline.ts +1 -0
  20. package/src/config/keybindings.ts +1 -1
  21. package/src/config/model-registry.ts +210 -11
  22. package/src/config/model-resolver.ts +3 -3
  23. package/src/config/prompt-templates.ts +4 -4
  24. package/src/config/settings.ts +1 -1
  25. package/src/config.ts +3 -62
  26. package/src/debug/index.ts +1 -1
  27. package/src/debug/report-bundle.ts +1 -12
  28. package/src/debug/system-info.ts +1 -1
  29. package/src/discovery/claude-plugins.ts +205 -0
  30. package/src/discovery/helpers.ts +139 -3
  31. package/src/discovery/index.ts +1 -0
  32. package/src/export/custom-share.ts +1 -1
  33. package/src/export/html/index.ts +1 -1
  34. package/src/extensibility/custom-commands/loader.ts +2 -1
  35. package/src/extensibility/plugins/index.ts +0 -7
  36. package/src/extensibility/plugins/installer.ts +1 -1
  37. package/src/extensibility/plugins/loader.ts +3 -7
  38. package/src/extensibility/plugins/manager.ts +4 -4
  39. package/src/index.ts +1 -1
  40. package/src/ipy/executor.ts +1 -1
  41. package/src/ipy/gateway-coordinator.ts +1 -1
  42. package/src/ipy/modules.ts +5 -6
  43. package/src/ipy/runtime.ts +27 -0
  44. package/src/main.ts +3 -1
  45. package/src/mcp/config-writer.ts +1 -13
  46. package/src/modes/components/welcome.ts +1 -1
  47. package/src/modes/controllers/mcp-command-controller.ts +2 -7
  48. package/src/modes/controllers/selector-controller.ts +1 -1
  49. package/src/modes/interactive-mode.ts +2 -2
  50. package/src/modes/theme/theme.ts +1 -1
  51. package/src/patch/hashline.ts +0 -12
  52. package/src/patch/index.ts +14 -0
  53. package/src/sdk.ts +2 -1
  54. package/src/session/agent-session.ts +1 -1
  55. package/src/session/agent-storage.ts +1 -1
  56. package/src/session/auth-storage.ts +2 -2
  57. package/src/session/history-storage.ts +1 -1
  58. package/src/session/session-manager.ts +1 -1
  59. package/src/ssh/connection-manager.ts +3 -4
  60. package/src/ssh/sshfs-mount.ts +2 -3
  61. package/src/system-prompt.ts +2 -2
  62. package/src/task/discovery.ts +14 -1
  63. package/src/task/executor.ts +1 -0
  64. package/src/task/worktree.ts +2 -1
  65. package/src/tools/bash-interactive.ts +33 -1
  66. package/src/tools/fs-cache-invalidation.ts +28 -0
  67. package/src/tools/grep.ts +1 -0
  68. package/src/tools/read.ts +2 -3
  69. package/src/tools/write.ts +2 -0
  70. package/src/utils/file-mentions.ts +128 -7
  71. package/src/utils/tools-manager.ts +1 -1
  72. package/src/web/search/auth.ts +1 -1
  73. package/src/web/search/providers/codex.ts +1 -1
  74. package/src/web/search/providers/gemini.ts +1 -1
  75. package/src/web/search/providers/perplexity.ts +1 -1
  76. package/src/extensibility/plugins/paths.ts +0 -37
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [12.1.0] - 2026-02-13
6
+
7
+ ### Added
8
+
9
+ - Filesystem scan cache invalidation helpers (`invalidateFsScanAfterWrite`, `invalidateFsScanAfterDelete`, `invalidateFsScanAfterRename`) to properly invalidate shared caches after file mutations
10
+ - Named discovery profile for file mention candidates to standardize cache visibility and ignore semantics across callers
11
+ - Comprehensive `models.yml` provider integration guide documenting custom model registration, provider overrides, API adapters, merge behavior, and practical integration patterns for Ollama, vLLM, LM Studio, and proxy endpoints
12
+ - Claude Code marketplace plugin discovery: automatically loads skills, commands, hooks, tools, and agents from `~/.claude/plugins/cache/` based on `installed_plugins.json` registry ([#48](https://github.com/can1357/oh-my-pi/issues/48))
13
+
14
+ ### Changed
15
+
16
+ - Moved directory path utilities from `src/config.ts` to `@oh-my-pi/pi-utils/dirs` for shared use across packages
17
+ - Updated imports throughout codebase to use centralized directory path functions from `@oh-my-pi/pi-utils/dirs`
18
+ - Updated interactive bash terminal UI label from 'InteractiveTerm' to 'Console' for clarity
19
+ - Enhanced bash execution environment with comprehensive non-interactive defaults for pagers, editors, and package managers to prevent command blocking and interactive prompts
20
+ - Updated custom models configuration to use `~/.omp/agent/models.yml` (YAML format) while maintaining backward compatibility with legacy `models.json`
21
+
5
22
  ## [12.0.0] - 2026-02-12
6
23
 
7
24
  ### Added
package/README.md CHANGED
@@ -447,7 +447,9 @@ The `--system-prompt` CLI flag overrides both files. Use `--append-system-prompt
447
447
 
448
448
  ### Custom Models and Providers
449
449
 
450
- Add custom models (Ollama, vLLM, LM Studio, etc.) via `~/.omp/agent/models.json`:
450
+ Add custom models (Ollama, vLLM, LM Studio, etc.) via `~/.omp/agent/models.yml` (`models.json` is still supported for legacy configs):
451
+
452
+ > See [models.yml provider integration guide](docs/models.md) for full schema, merge behavior, and provider integration patterns.
451
453
 
452
454
  ```json
453
455
  {
@@ -750,7 +752,7 @@ export default function (omp: HookAPI) {
750
752
  content,
751
753
  display: true,
752
754
  },
753
- true
755
+ true,
754
756
  ); // triggerTurn: start agent loop
755
757
  }
756
758
  });
@@ -1035,33 +1037,6 @@ Omp is a fork of [Pi](https://github.com/badlogic/pi) by [Mario Zechner](https:/
1035
1037
 
1036
1038
  ## Development
1037
1039
 
1038
- ### Forking / Rebranding
1039
-
1040
- Configure via `package.json`:
1041
-
1042
- ```json
1043
- {
1044
- "ompConfig": {
1045
- "name": "omp",
1046
- "configDir": ".omp"
1047
- }
1048
- }
1049
- ```
1050
-
1051
- Change `name`, `configDir`, and `bin` field for your fork. Affects CLI banner, config paths, and environment variable names.
1052
-
1053
- ### Path Resolution
1054
-
1055
- Three execution modes: npm install, standalone binary, tsx from source.
1056
-
1057
- **Always use `src/config.ts`** for package assets:
1058
-
1059
- ```typescript
1060
- import { getPackageDir } from "./config.js";
1061
- ```
1062
-
1063
- Never use `__dirname` directly for package assets.
1064
-
1065
1040
  ### Debug Command
1066
1041
 
1067
1042
  `/debug` (hidden) writes rendered lines with ANSI codes to `~/.omp/agent/omp-debug.log` for TUI debugging, as well as the last set of messages that were sent to the LLM.
@@ -0,0 +1,50 @@
1
+ # FS scan cache architecture
2
+
3
+ This document defines the shared filesystem-scan cache contract used by `pi-natives` discovery/search callers.
4
+
5
+ ## Cache key contract
6
+
7
+ Cache entries are keyed by:
8
+
9
+ - `root` (absolute search root path)
10
+ - `include_hidden` (hidden-file visibility)
11
+ - `use_gitignore` (ignore-rule behavior)
12
+
13
+ Callers with different visibility/ignore semantics must use different profiles so they do not share incompatible cache entries.
14
+
15
+ ## Freshness and recheck contract
16
+
17
+ `crates/pi-natives/src/fs_cache.rs` owns global policy:
18
+
19
+ - `FS_SCAN_CACHE_TTL_MS` (default `1000`)
20
+ - `FS_SCAN_EMPTY_RECHECK_MS` (default `200`)
21
+ - `FS_SCAN_CACHE_MAX_ENTRIES` (default `16`)
22
+
23
+ `get_or_scan()` returns `cache_age_ms` so callers can decide whether an empty filtered result should trigger `force_rescan()`.
24
+
25
+ Current callers using this contract:
26
+
27
+ - `fd` (`fuzzyFind`) uses empty-result fast recheck.
28
+ - `grep` consumes shared scan entries and applies grep-specific glob/type filtering on top.
29
+
30
+ ## Invalidation contract
31
+
32
+ Mutation-triggered invalidation is explicit and path-based via `invalidateFsScanCache`.
33
+
34
+ Coding-agent routes invalidation through `packages/coding-agent/src/tools/fs-cache-invalidation.ts`:
35
+
36
+ - `invalidateFsScanAfterWrite(path)`
37
+ - `invalidateFsScanAfterDelete(path)`
38
+ - `invalidateFsScanAfterRename(oldPath, newPath)` (invalidates both paths)
39
+
40
+ Write/edit flows call these helpers after successful filesystem mutation.
41
+
42
+ ## Caller discovery profiles
43
+
44
+ Callers should not build ad-hoc discovery flags inline. Use named profile/policy helpers at callsites.
45
+
46
+ Current profile boundaries:
47
+
48
+ - File mention candidate discovery (`file-mentions.ts`): hidden on, gitignore on, node_modules included.
49
+ - TUI fuzzy `@` discovery (`autocomplete.ts`): hidden on, gitignore on, bounded result count.
50
+ - TUI local path prefix completion keeps a separate per-directory `readdir` cache as an intentional latency fast-path; global fuzzy discovery remains on natives shared scan cache.
package/docs/models.md ADDED
@@ -0,0 +1,234 @@
1
+ # `models.yml` provider integration guide
2
+
3
+ `models.yml` lets you register custom model providers (local or hosted), override built-in providers, and tune model metadata.
4
+
5
+ Default location:
6
+
7
+ - `~/.omp/agent/models.yml`
8
+
9
+ Legacy support:
10
+
11
+ - `models.json` is still read and auto-migrated to `models.yml` when possible.
12
+
13
+ ## Top-level shape
14
+
15
+ ```yaml
16
+ providers:
17
+ <provider-name>:
18
+ # Provider config
19
+ ```
20
+
21
+ `<provider-name>` is the provider ID used everywhere else (selection, auth lookup, etc.).
22
+
23
+ ## Provider fields
24
+
25
+ ```yaml
26
+ providers:
27
+ my-provider:
28
+ baseUrl: https://api.example.com/v1
29
+ apiKey: MY_PROVIDER_API_KEY
30
+ api: openai-responses
31
+ headers:
32
+ X-Custom-Header: value
33
+ authHeader: true
34
+ auth: apiKey
35
+ discovery:
36
+ type: ollama
37
+ modelOverrides:
38
+ <model-id-within-provider>:
39
+ name: Friendly Name
40
+ models:
41
+ - id: model-id
42
+ name: My Model
43
+ api: openai-responses
44
+ reasoning: false
45
+ input: [text]
46
+ cost:
47
+ input: 0
48
+ output: 0
49
+ cacheRead: 0
50
+ cacheWrite: 0
51
+ contextWindow: 128000
52
+ maxTokens: 16384
53
+ headers:
54
+ X-Model-Header: value
55
+ compat:
56
+ supportsStore: true
57
+ supportsDeveloperRole: true
58
+ supportsReasoningEffort: true
59
+ maxTokensField: max_completion_tokens
60
+ openRouterRouting:
61
+ only: [anthropic]
62
+ vercelGatewayRouting:
63
+ order: [openai, anthropic]
64
+ ```
65
+
66
+ ### `api` values
67
+
68
+ Supported API adapters:
69
+
70
+ - `openai-completions`
71
+ - `openai-responses`
72
+ - `openai-codex-responses`
73
+ - `azure-openai-responses`
74
+ - `anthropic-messages`
75
+ - `google-generative-ai`
76
+ - `google-vertex`
77
+
78
+ `auth` values:
79
+
80
+ - `apiKey` (default)
81
+ - `none`
82
+
83
+ `discovery.type` values:
84
+
85
+ - `ollama`
86
+
87
+ If `discovery` is set, provider-level `api` is required.
88
+
89
+ ## Required vs optional
90
+
91
+ ### Full custom provider (defines `models`)
92
+
93
+ If `models` is non-empty, you must set:
94
+
95
+ - `baseUrl`
96
+ - `apiKey` (unless `auth: none`)
97
+ - `api` at provider level or per model
98
+
99
+ If `auth: none` is set, `apiKey` is optional even when `models` are defined.
100
+
101
+ ### Override-only provider (no `models`)
102
+
103
+ If `models` is empty/missing, set at least one of:
104
+
105
+ - `baseUrl`
106
+ - `modelOverrides`
107
+ - `discovery`
108
+
109
+ Use this to modify built-in providers without redefining all models.
110
+
111
+ Default values when omitted in a model definition:
112
+
113
+ - `reasoning: false`
114
+ - `input: [text]`
115
+ - `cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }`
116
+ - `contextWindow: 128000`
117
+ - `maxTokens: 16384`
118
+
119
+ Header merge behavior:
120
+
121
+ - Provider `headers` are applied first
122
+ - Model-level `headers` override provider headers on key conflicts
123
+ ## Merge behavior
124
+
125
+ `models.yml` does not replace the built-in registry.
126
+
127
+ 1. Built-in models load first.
128
+ 2. Provider-level overrides (`baseUrl`, `headers`) are applied.
129
+ 3. `modelOverrides` are applied by model ID within each provider.
130
+ 4. Custom `models` are merged in.
131
+ 5. If a custom model has the same `provider + id` as an existing model, it replaces that model.
132
+
133
+ ## API key behavior
134
+
135
+ `apiKey` resolution is:
136
+
137
+ 1. Treat value as env var name (preferred)
138
+ 2. If env var not found, treat value as literal key
139
+
140
+ Example:
141
+
142
+ ```yaml
143
+ apiKey: OPENROUTER_API_KEY
144
+ ```
145
+
146
+ If `OPENROUTER_API_KEY` exists, that value is used. Otherwise, the literal string `OPENROUTER_API_KEY` is used as the token.
147
+
148
+ Use `authHeader: true` when your endpoint expects:
149
+
150
+ ```http
151
+ Authorization: Bearer <apiKey>
152
+ ```
153
+
154
+ Set `auth: none` for keyless providers (local gateways, unauthenticated dev endpoints).
155
+
156
+ ## Practical integration patterns
157
+
158
+ ### 1) OpenAI-compatible endpoint (vLLM / LM Studio / gateway)
159
+
160
+ ```yaml
161
+ providers:
162
+ local-openai:
163
+ baseUrl: http://127.0.0.1:8000/v1
164
+ auth: none
165
+ api: openai-completions
166
+ models:
167
+ - id: Qwen/Qwen2.5-Coder-32B-Instruct
168
+ name: Qwen 2.5 Coder 32B (local)
169
+ ```
170
+
171
+ ### 2) Anthropic-compatible proxy
172
+
173
+ ```yaml
174
+ providers:
175
+ anthropic-proxy:
176
+ baseUrl: https://proxy.example.com/anthropic
177
+ apiKey: ANTHROPIC_PROXY_KEY
178
+ api: anthropic-messages
179
+ authHeader: true
180
+ models:
181
+ - id: claude-sonnet-4-20250514
182
+ name: Claude Sonnet 4 (Proxy)
183
+ reasoning: true
184
+ input: [text, image]
185
+ ```
186
+
187
+ ### 3) Override built-in provider without redefining models
188
+
189
+ ```yaml
190
+ providers:
191
+ openrouter:
192
+ baseUrl: https://my-corp-proxy.example.com/v1
193
+ headers:
194
+ X-Team: platform
195
+ modelOverrides:
196
+ anthropic/claude-sonnet-4:
197
+ name: Sonnet 4 (Corp Route)
198
+ ```
199
+
200
+ ### 4) Runtime discovery for Ollama
201
+
202
+ ```yaml
203
+ providers:
204
+ ollama:
205
+ baseUrl: http://127.0.0.1:11434
206
+ api: openai-completions
207
+ auth: none
208
+ discovery:
209
+ type: ollama
210
+ ```
211
+
212
+ The agent will query `GET /api/tags` and register discovered models dynamically.
213
+
214
+ ## Validation failures to watch for
215
+
216
+ Common schema/validation errors:
217
+
218
+ - Provider with `models` but missing `baseUrl`
219
+ - Provider with `models` and `auth != none` but missing `apiKey`
220
+ - Model missing `api` when neither provider-level nor model-level `api` is set
221
+ - Non-positive `contextWindow` or `maxTokens`
222
+ - `discovery` configured without provider-level `api`
223
+
224
+ When `models.yml` has errors, the agent falls back to built-in models and reports a load error.
225
+
226
+ ## Quick start
227
+
228
+ 1. Create `~/.omp/agent/models.yml`
229
+ 2. Add one provider with one model
230
+ 3. Start the agent and open `/model`
231
+ 4. Confirm your provider/model appears
232
+ 5. If auth fails, check env vars and `authHeader`
233
+
234
+ For SDK usage, `ModelRegistry` also accepts a custom path so you can load non-default `models.yml` files programmatically.
package/package.json CHANGED
@@ -1,12 +1,8 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "12.0.0",
3
+ "version": "12.1.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
- "ompConfig": {
7
- "name": "omp",
8
- "configDir": ".omp"
9
- },
10
6
  "bin": {
11
7
  "omp": "src/cli.ts"
12
8
  },
@@ -88,12 +84,12 @@
88
84
  },
89
85
  "dependencies": {
90
86
  "@mozilla/readability": "0.6.0",
91
- "@oh-my-pi/omp-stats": "12.0.0",
92
- "@oh-my-pi/pi-agent-core": "12.0.0",
93
- "@oh-my-pi/pi-ai": "12.0.0",
94
- "@oh-my-pi/pi-natives": "12.0.0",
95
- "@oh-my-pi/pi-tui": "12.0.0",
96
- "@oh-my-pi/pi-utils": "12.0.0",
87
+ "@oh-my-pi/omp-stats": "12.1.0",
88
+ "@oh-my-pi/pi-agent-core": "12.1.0",
89
+ "@oh-my-pi/pi-ai": "12.1.0",
90
+ "@oh-my-pi/pi-natives": "12.1.0",
91
+ "@oh-my-pi/pi-tui": "12.1.0",
92
+ "@oh-my-pi/pi-utils": "12.1.0",
97
93
  "@sinclair/typebox": "^0.34.48",
98
94
  "@xterm/headless": "^6.0.0",
99
95
  "ajv": "^8.17.1",
package/src/cli/args.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  */
4
4
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
5
5
  import { logger } from "@oh-my-pi/pi-utils";
6
+ import { APP_NAME, CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils/dirs";
6
7
  import chalk from "chalk";
7
- import { APP_NAME, CONFIG_DIR_NAME } from "../config";
8
8
  import { BUILTIN_TOOLS } from "../tools";
9
9
 
10
10
  export type Mode = "text" | "json" | "rpc";
@@ -4,8 +4,9 @@
4
4
  * Handles `omp config <command>` subcommands for managing settings.
5
5
  * Uses settings-defs as the source of truth for available settings.
6
6
  */
7
+
8
+ import { APP_NAME, getAgentDir } from "@oh-my-pi/pi-utils/dirs";
7
9
  import chalk from "chalk";
8
- import { APP_NAME, getAgentDir } from "../config";
9
10
  import {
10
11
  getDefault,
11
12
  getEnumValues,
@@ -5,8 +5,8 @@
5
5
  */
6
6
  import * as path from "node:path";
7
7
  import { grep } from "@oh-my-pi/pi-natives";
8
+ import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
8
9
  import chalk from "chalk";
9
- import { APP_NAME } from "../config";
10
10
 
11
11
  export interface GrepCommandArgs {
12
12
  pattern: string;
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Handles `omp jupyter` subcommand for managing the shared Python gateway.
5
5
  */
6
+
7
+ import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
6
8
  import chalk from "chalk";
7
- import { APP_NAME } from "../config";
8
9
  import { getGatewayStatus, shutdownSharedGateway } from "../ipy/gateway-coordinator";
9
10
 
10
11
  export type JupyterAction = "kill" | "status";
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Handles `omp plugin <command>` subcommands for plugin lifecycle management.
5
5
  */
6
+
7
+ import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
6
8
  import chalk from "chalk";
7
- import { APP_NAME } from "../config";
8
9
  import { PluginManager, parseSettingValue, validateSetting } from "../extensibility/plugins";
9
10
  import { theme } from "../modes/theme/theme";
10
11
 
@@ -3,9 +3,10 @@
3
3
  *
4
4
  * Handles `omp setup <component>` to install dependencies for optional features.
5
5
  */
6
+ import * as path from "node:path";
7
+ import { APP_NAME, getPythonEnvDir } from "@oh-my-pi/pi-utils/dirs";
6
8
  import { $ } from "bun";
7
9
  import chalk from "chalk";
8
- import { APP_NAME } from "../config";
9
10
  import { theme } from "../modes/theme/theme";
10
11
 
11
12
  export type SetupComponent = "python";
@@ -21,6 +22,7 @@ export interface SetupCommandArgs {
21
22
  const VALID_COMPONENTS: SetupComponent[] = ["python"];
22
23
 
23
24
  const PYTHON_PACKAGES = ["jupyter_kernel_gateway", "ipykernel"];
25
+ const MANAGED_PYTHON_ENV = getPythonEnvDir();
24
26
 
25
27
  /**
26
28
  * Parse setup subcommand arguments.
@@ -67,6 +69,14 @@ interface PythonCheckResult {
67
69
  pipPath?: string;
68
70
  missingPackages: string[];
69
71
  installedPackages: string[];
72
+ usingManagedEnv?: boolean;
73
+ managedEnvPath?: string;
74
+ }
75
+
76
+ function managedPythonPath(): string {
77
+ return process.platform === "win32"
78
+ ? path.join(MANAGED_PYTHON_ENV, "Scripts", "python.exe")
79
+ : path.join(MANAGED_PYTHON_ENV, "bin", "python");
70
80
  }
71
81
 
72
82
  /**
@@ -77,48 +87,115 @@ async function checkPythonSetup(): Promise<PythonCheckResult> {
77
87
  available: false,
78
88
  missingPackages: [],
79
89
  installedPackages: [],
90
+ managedEnvPath: MANAGED_PYTHON_ENV,
80
91
  };
81
92
 
82
- const pythonPath = Bun.which("python") ?? Bun.which("python3");
83
- if (!pythonPath) {
84
- return result;
85
- }
86
- result.pythonPath = pythonPath;
93
+ const systemPythonPath = Bun.which("python") ?? Bun.which("python3");
94
+ const managedPath = managedPythonPath();
95
+ const hasManagedEnv = await Bun.file(managedPath).exists();
96
+
87
97
  result.uvPath = Bun.which("uv") ?? undefined;
88
98
  result.pipPath = Bun.which("pip3") ?? Bun.which("pip") ?? undefined;
89
99
 
90
- for (const pkg of PYTHON_PACKAGES) {
91
- const moduleName = pkg === "jupyter_kernel_gateway" ? "kernel_gateway" : pkg;
92
- const script = `import importlib.util; raise SystemExit(0 if importlib.util.find_spec('${moduleName}') else 1)`;
93
- const check = await $`${pythonPath} -c ${script}`.quiet().nothrow();
94
- if (check.exitCode === 0) {
95
- result.installedPackages.push(pkg);
96
- } else {
97
- result.missingPackages.push(pkg);
100
+ const candidates = [systemPythonPath, hasManagedEnv ? managedPath : undefined].filter(
101
+ (candidate): candidate is string => !!candidate,
102
+ );
103
+ if (candidates.length === 0) {
104
+ return result;
105
+ }
106
+
107
+ result.pythonPath = systemPythonPath ?? managedPath;
108
+ let bestMatch = {
109
+ pythonPath: candidates[0],
110
+ missingPackages: [...PYTHON_PACKAGES],
111
+ installedPackages: [] as string[],
112
+ usingManagedEnv: candidates[0] === managedPath,
113
+ };
114
+
115
+ for (const pythonPath of candidates) {
116
+ const installedPackages: string[] = [];
117
+ const missingPackages: string[] = [];
118
+ for (const pkg of PYTHON_PACKAGES) {
119
+ const moduleName = pkg === "jupyter_kernel_gateway" ? "kernel_gateway" : pkg;
120
+ const script = `import importlib.util; raise SystemExit(0 if importlib.util.find_spec('${moduleName}') else 1)`;
121
+ const check = await $`${pythonPath} -c ${script}`.quiet().nothrow();
122
+ if (check.exitCode === 0) {
123
+ installedPackages.push(pkg);
124
+ } else {
125
+ missingPackages.push(pkg);
126
+ }
127
+ }
128
+
129
+ if (missingPackages.length < bestMatch.missingPackages.length) {
130
+ bestMatch = {
131
+ pythonPath,
132
+ missingPackages,
133
+ installedPackages,
134
+ usingManagedEnv: pythonPath === managedPath,
135
+ };
136
+ }
137
+
138
+ if (missingPackages.length === 0) {
139
+ result.available = true;
140
+ result.pythonPath = pythonPath;
141
+ result.missingPackages = missingPackages;
142
+ result.installedPackages = installedPackages;
143
+ result.usingManagedEnv = pythonPath === managedPath;
144
+ return result;
98
145
  }
99
146
  }
100
147
 
101
- result.available = result.missingPackages.length === 0;
148
+ result.pythonPath = bestMatch.pythonPath;
149
+ result.missingPackages = bestMatch.missingPackages;
150
+ result.installedPackages = bestMatch.installedPackages;
151
+ result.usingManagedEnv = bestMatch.usingManagedEnv;
102
152
  return result;
103
153
  }
104
154
 
105
155
  /**
106
156
  * Install Python packages using uv (preferred) or pip.
107
157
  */
108
- async function installPythonPackages(packages: string[], uvPath?: string, pipPath?: string): Promise<boolean> {
158
+ async function installPythonPackages(
159
+ packages: string[],
160
+ pythonPath: string,
161
+ uvPath?: string,
162
+ pipPath?: string,
163
+ ): Promise<{ success: boolean; usedManagedEnv: boolean }> {
109
164
  if (uvPath) {
110
165
  console.log(chalk.dim(`Installing via uv: ${packages.join(" ")}`));
111
166
  const result = await $`${uvPath} pip install ${packages}`.nothrow();
112
- return result.exitCode === 0;
167
+ if (result.exitCode === 0) {
168
+ return { success: true, usedManagedEnv: false };
169
+ }
113
170
  }
114
171
 
115
172
  if (pipPath) {
116
173
  console.log(chalk.dim(`Installing via pip: ${packages.join(" ")}`));
117
174
  const result = await $`${pipPath} install ${packages}`.nothrow();
118
- return result.exitCode === 0;
175
+ if (result.exitCode === 0) {
176
+ return { success: true, usedManagedEnv: false };
177
+ }
178
+ }
179
+
180
+ console.log(chalk.dim(`Falling back to managed virtual environment: ${MANAGED_PYTHON_ENV}`));
181
+
182
+ if (uvPath) {
183
+ const createEnv = await $`${uvPath} venv ${MANAGED_PYTHON_ENV}`.quiet().nothrow();
184
+ if (createEnv.exitCode !== 0) {
185
+ return { success: false, usedManagedEnv: true };
186
+ }
187
+ const installInManagedEnv = await $`${uvPath} pip install --python ${MANAGED_PYTHON_ENV} ${packages}`.nothrow();
188
+ return { success: installInManagedEnv.exitCode === 0, usedManagedEnv: true };
189
+ }
190
+
191
+ const createEnv = await $`${pythonPath} -m venv ${MANAGED_PYTHON_ENV}`.quiet().nothrow();
192
+ if (createEnv.exitCode !== 0) {
193
+ return { success: false, usedManagedEnv: true };
119
194
  }
120
195
 
121
- return false;
196
+ const managedPython = managedPythonPath();
197
+ const installInManagedEnv = await $`${managedPython} -m pip install ${packages}`.nothrow();
198
+ return { success: installInManagedEnv.exitCode === 0, usedManagedEnv: true };
122
199
  }
123
200
 
124
201
  /**
@@ -148,6 +225,9 @@ async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Pr
148
225
  }
149
226
 
150
227
  console.log(chalk.dim(`Python: ${check.pythonPath}`));
228
+ if (check.usingManagedEnv) {
229
+ console.log(chalk.dim(`Using managed environment: ${check.managedEnvPath}`));
230
+ }
151
231
 
152
232
  if (check.uvPath) {
153
233
  console.log(chalk.dim(`uv: ${check.uvPath}`));
@@ -178,18 +258,33 @@ async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Pr
178
258
  }
179
259
 
180
260
  console.log("");
181
- const success = await installPythonPackages(check.missingPackages, check.uvPath, check.pipPath);
261
+ const install = await installPythonPackages(check.missingPackages, check.pythonPath, check.uvPath, check.pipPath);
182
262
 
183
- if (!success) {
263
+ if (!install.success) {
184
264
  console.error(chalk.red(`\n${theme.status.error} Installation failed`));
185
265
  console.error(chalk.dim("Try installing manually:"));
186
- console.error(chalk.dim(` ${check.uvPath ? "uv pip" : "pip"} install ${check.missingPackages.join(" ")}`));
266
+ if (install.usedManagedEnv) {
267
+ if (check.uvPath) {
268
+ console.error(chalk.dim(` uv venv ${MANAGED_PYTHON_ENV}`));
269
+ console.error(
270
+ chalk.dim(` uv pip install --python ${MANAGED_PYTHON_ENV} ${check.missingPackages.join(" ")}`),
271
+ );
272
+ } else {
273
+ console.error(chalk.dim(` ${check.pythonPath} -m venv ${MANAGED_PYTHON_ENV}`));
274
+ console.error(chalk.dim(` ${managedPythonPath()} -m pip install ${check.missingPackages.join(" ")}`));
275
+ }
276
+ } else {
277
+ console.error(chalk.dim(` ${check.uvPath ? "uv pip" : "pip"} install ${check.missingPackages.join(" ")}`));
278
+ }
187
279
  process.exit(1);
188
280
  }
189
281
 
190
282
  const recheck = await checkPythonSetup();
191
283
  if (recheck.available) {
192
284
  console.log(chalk.green(`\n${theme.status.success} Python execution is ready`));
285
+ if (recheck.usingManagedEnv) {
286
+ console.log(chalk.dim(`Managed Python environment: ${recheck.managedEnvPath}`));
287
+ }
193
288
  } else {
194
289
  console.error(chalk.red(`\n${theme.status.error} Setup incomplete`));
195
290
  console.error(chalk.dim(`Still missing: ${recheck.missingPackages.join(", ")}`));
@@ -6,8 +6,8 @@
6
6
  import * as path from "node:path";
7
7
  import { createInterface } from "node:readline/promises";
8
8
  import { Shell } from "@oh-my-pi/pi-natives";
9
+ import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
9
10
  import chalk from "chalk";
10
- import { APP_NAME } from "../config";
11
11
  import { Settings } from "../config/settings";
12
12
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
13
13
 
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * Handles `omp stats` subcommand for viewing AI usage statistics.
5
5
  */
6
+
7
+ import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
6
8
  import chalk from "chalk";
7
- import { APP_NAME } from "../config";
8
9
  import { openPath } from "../utils/open";
9
10
 
10
11
  // =============================================================================