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

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 +16 -15
  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
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Claude Code Marketplace Plugin Provider
3
+ *
4
+ * Loads configuration from ~/.claude/plugins/cache/ based on installed_plugins.json registry.
5
+ * Priority: 70 (below claude.ts at 80, so user overrides in .claude/ take precedence)
6
+ */
7
+ import * as path from "node:path";
8
+ import { registerProvider } from "../capability";
9
+ import { type Hook, hookCapability } from "../capability/hook";
10
+ import { type Skill, skillCapability } from "../capability/skill";
11
+ import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
12
+ import { type CustomTool, toolCapability } from "../capability/tool";
13
+ import type { LoadContext, LoadResult } from "../capability/types";
14
+ import { type ClaudePluginRoot, listClaudePluginRoots, loadFilesFromDir, loadSkillsFromDir } from "./helpers";
15
+
16
+ const PROVIDER_ID = "claude-plugins";
17
+ const DISPLAY_NAME = "Claude Code Marketplace";
18
+ const PRIORITY = 70; // Below claude.ts (80) so user .claude/ overrides win
19
+
20
+ // =============================================================================
21
+ // Skills
22
+ // =============================================================================
23
+
24
+ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
25
+ const items: Skill[] = [];
26
+ const warnings: string[] = [];
27
+
28
+ const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home);
29
+ warnings.push(...rootWarnings);
30
+
31
+ const results = await Promise.all(
32
+ roots.map(async root => {
33
+ const skillsDir = path.join(root.path, "skills");
34
+ return loadSkillsFromDir(ctx, {
35
+ dir: skillsDir,
36
+ providerId: PROVIDER_ID,
37
+ level: root.scope,
38
+ });
39
+ }),
40
+ );
41
+
42
+ for (const result of results) {
43
+ items.push(...result.items);
44
+ if (result.warnings) warnings.push(...result.warnings);
45
+ }
46
+
47
+ return { items, warnings };
48
+ }
49
+
50
+ // =============================================================================
51
+ // Slash Commands
52
+ // =============================================================================
53
+
54
+ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
55
+ const items: SlashCommand[] = [];
56
+ const warnings: string[] = [];
57
+
58
+ const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home);
59
+ warnings.push(...rootWarnings);
60
+
61
+ const results = await Promise.all(
62
+ roots.map(async root => {
63
+ const commandsDir = path.join(root.path, "commands");
64
+ return loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, root.scope, {
65
+ extensions: ["md"],
66
+ transform: (name, content, filePath, source) => {
67
+ const cmdName = name.replace(/\.md$/, "");
68
+ return {
69
+ name: cmdName,
70
+ path: filePath,
71
+ content,
72
+ level: root.scope,
73
+ _source: source,
74
+ };
75
+ },
76
+ });
77
+ }),
78
+ );
79
+
80
+ for (const result of results) {
81
+ items.push(...result.items);
82
+ if (result.warnings) warnings.push(...result.warnings);
83
+ }
84
+
85
+ return { items, warnings };
86
+ }
87
+
88
+ // =============================================================================
89
+ // Hooks
90
+ // =============================================================================
91
+
92
+ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
93
+ const items: Hook[] = [];
94
+ const warnings: string[] = [];
95
+
96
+ const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home);
97
+ warnings.push(...rootWarnings);
98
+
99
+ const hookTypes = ["pre", "post"] as const;
100
+
101
+ const loadTasks: { root: ClaudePluginRoot; hookType: "pre" | "post" }[] = [];
102
+ for (const root of roots) {
103
+ for (const hookType of hookTypes) {
104
+ loadTasks.push({ root, hookType });
105
+ }
106
+ }
107
+
108
+ const results = await Promise.all(
109
+ loadTasks.map(async ({ root, hookType }) => {
110
+ const hooksDir = path.join(root.path, "hooks", hookType);
111
+ return loadFilesFromDir<Hook>(ctx, hooksDir, PROVIDER_ID, root.scope, {
112
+ transform: (name, _content, filePath, source) => {
113
+ const toolName = name.replace(/\.(sh|bash|zsh|fish)$/, "");
114
+ return {
115
+ name,
116
+ path: filePath,
117
+ type: hookType,
118
+ tool: toolName,
119
+ level: root.scope,
120
+ _source: source,
121
+ };
122
+ },
123
+ });
124
+ }),
125
+ );
126
+
127
+ for (const result of results) {
128
+ items.push(...result.items);
129
+ if (result.warnings) warnings.push(...result.warnings);
130
+ }
131
+
132
+ return { items, warnings };
133
+ }
134
+
135
+ // =============================================================================
136
+ // Custom Tools
137
+ // =============================================================================
138
+
139
+ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
140
+ const items: CustomTool[] = [];
141
+ const warnings: string[] = [];
142
+
143
+ const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home);
144
+ warnings.push(...rootWarnings);
145
+
146
+ const results = await Promise.all(
147
+ roots.map(async root => {
148
+ const toolsDir = path.join(root.path, "tools");
149
+ return loadFilesFromDir<CustomTool>(ctx, toolsDir, PROVIDER_ID, root.scope, {
150
+ transform: (name, _content, filePath, source) => {
151
+ const toolName = name.replace(/\.(ts|js|sh|bash|py)$/, "");
152
+ return {
153
+ name: toolName,
154
+ path: filePath,
155
+ level: root.scope,
156
+ _source: source,
157
+ };
158
+ },
159
+ });
160
+ }),
161
+ );
162
+
163
+ for (const result of results) {
164
+ items.push(...result.items);
165
+ if (result.warnings) warnings.push(...result.warnings);
166
+ }
167
+
168
+ return { items, warnings };
169
+ }
170
+
171
+ // =============================================================================
172
+ // Provider Registration
173
+ // =============================================================================
174
+
175
+ registerProvider<Skill>(skillCapability.id, {
176
+ id: PROVIDER_ID,
177
+ displayName: DISPLAY_NAME,
178
+ description: "Load skills from Claude Code marketplace plugins (~/.claude/plugins/cache/)",
179
+ priority: PRIORITY,
180
+ load: loadSkills,
181
+ });
182
+
183
+ registerProvider<SlashCommand>(slashCommandCapability.id, {
184
+ id: PROVIDER_ID,
185
+ displayName: DISPLAY_NAME,
186
+ description: "Load slash commands from Claude Code marketplace plugins",
187
+ priority: PRIORITY,
188
+ load: loadSlashCommands,
189
+ });
190
+
191
+ registerProvider<Hook>(hookCapability.id, {
192
+ id: PROVIDER_ID,
193
+ displayName: DISPLAY_NAME,
194
+ description: "Load hooks from Claude Code marketplace plugins",
195
+ priority: PRIORITY,
196
+ load: loadHooks,
197
+ });
198
+
199
+ registerProvider<CustomTool>(toolCapability.id, {
200
+ id: PROVIDER_ID,
201
+ displayName: DISPLAY_NAME,
202
+ description: "Load custom tools from Claude Code marketplace plugins",
203
+ priority: PRIORITY,
204
+ load: loadTools,
205
+ });
@@ -4,6 +4,7 @@
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
+ import { CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils/dirs";
7
8
  import { readDirEntries, readFile } from "../capability/fs";
8
9
  import type { Skill, SkillFrontmatter } from "../capability/skill";
9
10
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
@@ -39,9 +40,9 @@ export function expandPath(p: string): string {
39
40
  */
40
41
  export const SOURCE_PATHS = {
41
42
  native: {
42
- userBase: ".omp",
43
- userAgent: ".omp/agent",
44
- projectDir: ".omp",
43
+ userBase: CONFIG_DIR_NAME,
44
+ userAgent: `${CONFIG_DIR_NAME}/agent`,
45
+ projectDir: CONFIG_DIR_NAME,
45
46
  },
46
47
  claude: {
47
48
  userBase: ".claude",
@@ -546,3 +547,138 @@ export function getExtensionNameFromPath(extensionPath: string): string {
546
547
 
547
548
  return base;
548
549
  }
550
+
551
+ // =============================================================================
552
+ // Claude Code Plugin Cache Helpers
553
+ // =============================================================================
554
+
555
+ /**
556
+ * Entry for an installed Claude Code plugin.
557
+ */
558
+ export interface ClaudePluginEntry {
559
+ scope: "user" | "project";
560
+ installPath: string;
561
+ version: string;
562
+ installedAt: string;
563
+ lastUpdated: string;
564
+ gitCommitSha?: string;
565
+ }
566
+
567
+ /**
568
+ * Claude Code installed_plugins.json registry format.
569
+ */
570
+ export interface ClaudePluginsRegistry {
571
+ version: number;
572
+ plugins: Record<string, ClaudePluginEntry[]>;
573
+ }
574
+
575
+ /**
576
+ * Resolved plugin root for loading.
577
+ */
578
+ export interface ClaudePluginRoot {
579
+ /** Plugin ID (e.g., "simpleclaude-core@simpleclaude") */
580
+ id: string;
581
+ /** Marketplace name */
582
+ marketplace: string;
583
+ /** Plugin name */
584
+ plugin: string;
585
+ /** Version string */
586
+ version: string;
587
+ /** Absolute path to plugin root */
588
+ path: string;
589
+ /** Whether this is a user or project scope plugin */
590
+ scope: "user" | "project";
591
+ }
592
+
593
+ /**
594
+ * Parse Claude Code installed_plugins.json content.
595
+ */
596
+ export function parseClaudePluginsRegistry(content: string): ClaudePluginsRegistry | null {
597
+ const data = parseJSON<ClaudePluginsRegistry>(content);
598
+ if (!data || typeof data !== "object") return null;
599
+ if (
600
+ typeof data.version !== "number" ||
601
+ !data.plugins ||
602
+ typeof data.plugins !== "object" ||
603
+ Array.isArray(data.plugins)
604
+ )
605
+ return null;
606
+ return data;
607
+ }
608
+
609
+ /**
610
+ * List all installed Claude Code plugin roots from the plugin cache.
611
+ * Reads ~/.claude/plugins/installed_plugins.json and resolves plugin paths.
612
+ *
613
+ * Results are cached per home directory to avoid repeated parsing.
614
+ */
615
+ const pluginRootsCache = new Map<string, { roots: ClaudePluginRoot[]; warnings: string[] }>();
616
+
617
+ export async function listClaudePluginRoots(home: string): Promise<{ roots: ClaudePluginRoot[]; warnings: string[] }> {
618
+ const cached = pluginRootsCache.get(home);
619
+ if (cached) return cached;
620
+
621
+ const roots: ClaudePluginRoot[] = [];
622
+ const warnings: string[] = [];
623
+
624
+ const registryPath = path.join(home, ".claude", "plugins", "installed_plugins.json");
625
+ const content = await readFile(registryPath);
626
+
627
+ if (!content) {
628
+ // No registry file - not an error, just no plugins
629
+ const result = { roots, warnings };
630
+ pluginRootsCache.set(home, result);
631
+ return result;
632
+ }
633
+
634
+ const registry = parseClaudePluginsRegistry(content);
635
+ if (!registry) {
636
+ warnings.push(`Failed to parse Claude Code plugin registry: ${registryPath}`);
637
+ const result = { roots, warnings };
638
+ pluginRootsCache.set(home, result);
639
+ return result;
640
+ }
641
+
642
+ for (const [pluginId, entries] of Object.entries(registry.plugins)) {
643
+ if (!Array.isArray(entries) || entries.length === 0) continue;
644
+
645
+ // Parse plugin ID format: "plugin-name@marketplace"
646
+ const atIndex = pluginId.lastIndexOf("@");
647
+ if (atIndex === -1) {
648
+ warnings.push(`Invalid plugin ID format (missing @marketplace): ${pluginId}`);
649
+ continue;
650
+ }
651
+
652
+ const pluginName = pluginId.slice(0, atIndex);
653
+ const marketplace = pluginId.slice(atIndex + 1);
654
+
655
+ // Process all valid entries, not just the first one.
656
+ // This handles plugins with multiple installs (different scopes/versions).
657
+ for (const entry of entries) {
658
+ if (!entry.installPath || typeof entry.installPath !== "string") {
659
+ warnings.push(`Plugin ${pluginId} entry has no installPath`);
660
+ continue;
661
+ }
662
+
663
+ roots.push({
664
+ id: pluginId,
665
+ marketplace,
666
+ plugin: pluginName,
667
+ version: entry.version || "unknown",
668
+ path: entry.installPath,
669
+ scope: entry.scope || "user",
670
+ });
671
+ }
672
+ }
673
+
674
+ const result = { roots, warnings };
675
+ pluginRootsCache.set(home, result);
676
+ return result;
677
+ }
678
+
679
+ /**
680
+ * Clear the plugin roots cache (useful for testing or when plugins change).
681
+ */
682
+ export function clearClaudePluginRootsCache(): void {
683
+ pluginRootsCache.clear();
684
+ }
@@ -23,6 +23,7 @@ import "../capability/tool";
23
23
  import "./agents-md";
24
24
  import "./builtin";
25
25
  import "./claude";
26
+ import "./claude-plugins";
26
27
  import "./cline";
27
28
  import "./agents";
28
29
  import "./codex";
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
- import { getAgentDir } from "../config";
9
+ import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
10
10
 
11
11
  export interface CustomShareResult {
12
12
  /** URL to display/open (optional - script may handle everything itself) */
@@ -1,7 +1,7 @@
1
1
  import * as path from "node:path";
2
2
  import type { AgentState } from "@oh-my-pi/pi-agent-core";
3
3
  import { isEnoent } from "@oh-my-pi/pi-utils";
4
- import { APP_NAME } from "../../config";
4
+ import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
5
5
  import { getResolvedThemeColors, getThemeExportColors } from "../../modes/theme/theme";
6
6
  import { type SessionEntry, type SessionHeader, SessionManager } from "../../session/session-manager";
7
7
  // Pre-generated template (created by scripts/generate-template.ts at publish time)
@@ -8,8 +8,9 @@ import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
10
10
  import { isEnoent, logger } from "@oh-my-pi/pi-utils";
11
+ import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
11
12
  import * as typebox from "@sinclair/typebox";
12
- import { getAgentDir, getConfigDirs } from "../../config";
13
+ import { getConfigDirs } from "../../config";
13
14
  import { execCommand } from "../../exec/exec";
14
15
  import { ReviewCommand } from "./bundled/review";
15
16
  import type {
@@ -14,13 +14,6 @@ export {
14
14
  } from "./loader";
15
15
  export { PluginManager, parseSettingValue, validateSetting } from "./manager";
16
16
  export { extractPackageName, formatPluginSpec, parsePluginSpec } from "./parser";
17
- export {
18
- getPluginsDir,
19
- getPluginsLockfile,
20
- getPluginsNodeModules,
21
- getPluginsPackageJson,
22
- getProjectPluginOverrides,
23
- } from "./paths";
24
17
  export type {
25
18
  BooleanSetting,
26
19
  DoctorCheck,
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { isEnoent } from "@oh-my-pi/pi-utils";
4
- import { getAgentDir } from "../../config";
4
+ import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
5
5
  import type { InstalledPlugin } from "./types";
6
6
 
7
7
  const PLUGINS_DIR = path.join(getAgentDir(), "plugins");
@@ -7,12 +7,8 @@
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import { isEnoent } from "@oh-my-pi/pi-utils";
10
- import {
11
- getAllProjectPluginOverridePaths,
12
- getPluginsLockfile,
13
- getPluginsNodeModules,
14
- getPluginsPackageJson,
15
- } from "./paths";
10
+ import { getPluginsLockfile, getPluginsNodeModules, getPluginsPackageJson } from "@oh-my-pi/pi-utils/dirs";
11
+ import { getConfigDirPaths } from "../../config";
16
12
  import type { InstalledPlugin, PluginManifest, PluginRuntimeConfig, ProjectPluginOverrides } from "./types";
17
13
 
18
14
  // =============================================================================
@@ -36,7 +32,7 @@ async function loadRuntimeConfig(): Promise<PluginRuntimeConfig> {
36
32
  * Load project-local plugin overrides (checks .omp and .pi directories).
37
33
  */
38
34
  async function loadProjectOverrides(cwd: string): Promise<ProjectPluginOverrides> {
39
- for (const overridesPath of getAllProjectPluginOverridePaths(cwd)) {
35
+ for (const overridesPath of getConfigDirPaths("plugin-overrides.json", { user: false, cwd })) {
40
36
  try {
41
37
  return await Bun.file(overridesPath).json();
42
38
  } catch (err) {
@@ -1,14 +1,14 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { isEnoent, logger } from "@oh-my-pi/pi-utils";
4
- import { extractPackageName, parsePluginSpec } from "./parser";
5
4
  import {
6
5
  getPluginsDir,
7
6
  getPluginsLockfile,
8
7
  getPluginsNodeModules,
9
8
  getPluginsPackageJson,
10
- getProjectPluginOverrides,
11
- } from "./paths";
9
+ getProjectPluginOverridesPath,
10
+ } from "@oh-my-pi/pi-utils/dirs";
11
+ import { extractPackageName, parsePluginSpec } from "./parser";
12
12
  import type {
13
13
  DoctorCheck,
14
14
  DoctorOptions,
@@ -82,7 +82,7 @@ export class PluginManager {
82
82
  }
83
83
 
84
84
  async #loadProjectOverrides(): Promise<ProjectPluginOverrides> {
85
- const overridesPath = getProjectPluginOverrides(this.#cwd);
85
+ const overridesPath = getProjectPluginOverridesPath(this.#cwd);
86
86
  try {
87
87
  return await Bun.file(overridesPath).json();
88
88
  } catch (err) {
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ export { StringEnum } from "@oh-my-pi/pi-ai";
7
7
  export { Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
8
8
  // Logging
9
9
  export { logger } from "@oh-my-pi/pi-utils";
10
- export { getAgentDir, VERSION } from "./config";
10
+ export { getAgentDir, VERSION } from "@oh-my-pi/pi-utils/dirs";
11
11
  export { formatKeyHint, formatKeyHints } from "./config/keybindings";
12
12
  export { ModelRegistry } from "./config/model-registry";
13
13
  // Prompt templates
@@ -1,6 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { $env, isEnoent, logger } from "@oh-my-pi/pi-utils";
3
- import { getAgentDir } from "../config";
3
+ import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
4
4
  import { OutputSink } from "../session/streaming-output";
5
5
  import { time } from "../utils/timings";
6
6
  import { shutdownSharedGateway } from "./gateway-coordinator";
@@ -2,8 +2,8 @@ import * as fs from "node:fs";
2
2
  import { createServer } from "node:net";
3
3
  import * as path from "node:path";
4
4
  import { isEnoent, logger, procmgr } from "@oh-my-pi/pi-utils";
5
+ import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
5
6
  import type { Subprocess } from "bun";
6
- import { getAgentDir } from "../config";
7
7
  import { Settings } from "../config/settings";
8
8
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
9
9
  import { time } from "../utils/timings";
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
- import * as os from "node:os";
3
2
  import * as path from "node:path";
3
+ import { getAgentModulesDir, getProjectModulesDir } from "@oh-my-pi/pi-utils/dirs";
4
4
 
5
5
  export type PythonModuleSource = "user" | "project";
6
6
 
@@ -26,8 +26,8 @@ export interface PythonModuleExecutor {
26
26
  export interface DiscoverPythonModulesOptions {
27
27
  /** Working directory for project-level modules. Default: process.cwd() */
28
28
  cwd?: string;
29
- /** Home directory for user-level modules. Default: os.homedir() */
30
- homeDir?: string;
29
+ /** Agent directory for user-level modules. Default: from getAgentDir() */
30
+ agentDir?: string;
31
31
  }
32
32
 
33
33
  interface ModuleCandidate {
@@ -66,10 +66,9 @@ async function readModuleContent(candidate: ModuleCandidate): Promise<PythonModu
66
66
  */
67
67
  export async function discoverPythonModules(options: DiscoverPythonModulesOptions = {}): Promise<PythonModuleEntry[]> {
68
68
  const cwd = options.cwd ?? process.cwd();
69
- const homeDir = options.homeDir ?? os.homedir();
70
69
 
71
- const userDir = path.join(homeDir, ".omp", "agent", "modules");
72
- const projectDir = path.resolve(cwd, ".omp", "modules");
70
+ const userDir = getAgentModulesDir(options.agentDir);
71
+ const projectDir = getProjectModulesDir(cwd);
73
72
 
74
73
  const userCandidates = await listModuleCandidates(userDir, "user");
75
74
  const projectCandidates = await listModuleCandidates(projectDir, "project");
@@ -8,6 +8,7 @@ import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
 
10
10
  import { $env } from "@oh-my-pi/pi-utils";
11
+ import { getPythonEnvDir } from "@oh-my-pi/pi-utils/dirs";
11
12
 
12
13
  const DEFAULT_ENV_ALLOWLIST = new Set([
13
14
  "PATH",
@@ -103,6 +104,17 @@ function resolvePathKey(env: Record<string, string | undefined>): string {
103
104
  return match ?? "PATH";
104
105
  }
105
106
 
107
+ function resolveManagedPythonEnv(): string {
108
+ return getPythonEnvDir();
109
+ }
110
+
111
+ function resolveManagedPythonCandidate(): { venvPath: string; pythonPath: string } {
112
+ const venvPath = resolveManagedPythonEnv();
113
+ const binDir = process.platform === "win32" ? path.join(venvPath, "Scripts") : path.join(venvPath, "bin");
114
+ const pythonPath = path.join(binDir, process.platform === "win32" ? "python.exe" : "python");
115
+ return { venvPath, pythonPath };
116
+ }
117
+
106
118
  export interface PythonRuntime {
107
119
  /** Path to python executable */
108
120
  pythonPath: string;
@@ -184,6 +196,21 @@ export function resolvePythonRuntime(cwd: string, baseEnv: Record<string, string
184
196
  }
185
197
  }
186
198
 
199
+ const managed = resolveManagedPythonCandidate();
200
+ if (fs.existsSync(managed.pythonPath)) {
201
+ env.VIRTUAL_ENV = managed.venvPath;
202
+ const pathKey = resolvePathKey(env);
203
+ const currentPath = env[pathKey];
204
+ const managedBin =
205
+ process.platform === "win32" ? path.join(managed.venvPath, "Scripts") : path.join(managed.venvPath, "bin");
206
+ env[pathKey] = currentPath ? `${managedBin}${path.delimiter}${currentPath}` : managedBin;
207
+ return {
208
+ pythonPath: resolveWindowlessPython(managed.pythonPath),
209
+ env,
210
+ venvPath: managed.venvPath,
211
+ };
212
+ }
213
+
187
214
  const pythonPath = Bun.which("python") ?? Bun.which("python3");
188
215
  if (!pythonPath) {
189
216
  throw new Error("Python executable not found on PATH");
package/src/main.ts CHANGED
@@ -10,12 +10,13 @@ import * as path from "node:path";
10
10
  import { createInterface } from "node:readline/promises";
11
11
  import { type ImageContent, supportsXhigh } from "@oh-my-pi/pi-ai";
12
12
  import { $env, postmortem } from "@oh-my-pi/pi-utils";
13
+ import { VERSION } from "@oh-my-pi/pi-utils/dirs";
13
14
  import chalk from "chalk";
14
15
  import type { Args } from "./cli/args";
15
16
  import { processFileArguments } from "./cli/file-processor";
16
17
  import { listModels } from "./cli/list-models";
17
18
  import { selectSession } from "./cli/session-picker";
18
- import { findConfigFile, VERSION } from "./config";
19
+ import { findConfigFile } from "./config";
19
20
  import { ModelRegistry, ModelsConfigFile } from "./config/model-registry";
20
21
  import { parseModelPattern, parseModelString, resolveModelScope, type ScopedModel } from "./config/model-resolver";
21
22
  import { Settings, settings } from "./config/settings";
@@ -489,6 +490,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
489
490
  // Create AuthStorage and ModelRegistry upfront
490
491
  const authStorage = await discoverAuthStorage();
491
492
  const modelRegistry = new ModelRegistry(authStorage);
493
+ await modelRegistry.refresh();
492
494
  debugStartup("main:discoverModels");
493
495
  time("discoverModels");
494
496
 
@@ -4,24 +4,12 @@
4
4
  * Utilities for reading/writing .omp/mcp.json files at user or project level.
5
5
  */
6
6
  import * as fs from "node:fs";
7
- import * as os from "node:os";
8
7
  import * as path from "node:path";
9
8
  import { isEnoent } from "@oh-my-pi/pi-utils";
9
+
10
10
  import { validateServerConfig } from "./config";
11
11
  import type { MCPConfigFile, MCPServerConfig } from "./types";
12
12
 
13
- /**
14
- * Get the path to the MCP config file.
15
- * @param scope - "user" for ~/.omp/mcp.json or "project" for .omp/mcp.json
16
- * @param cwd - Current working directory (used for project scope)
17
- */
18
- export function getMCPConfigPath(scope: "user" | "project", cwd: string): string {
19
- if (scope === "user") {
20
- return path.join(os.homedir(), ".omp", "mcp.json");
21
- }
22
- return path.join(cwd, ".omp", "mcp.json");
23
- }
24
-
25
13
  /**
26
14
  * Read an MCP config file.
27
15
  * Returns empty config if file doesn't exist.
@@ -1,5 +1,5 @@
1
1
  import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
2
- import { APP_NAME } from "../../config";
2
+ import { APP_NAME } from "@oh-my-pi/pi-utils/dirs";
3
3
  import { theme } from "../../modes/theme/theme";
4
4
 
5
5
  export interface RecentSession {
@@ -4,16 +4,11 @@
4
4
  * Handles /mcp subcommands for managing MCP servers.
5
5
  */
6
6
  import { Spacer, Text } from "@oh-my-pi/pi-tui";
7
+ import { getMCPConfigPath } from "@oh-my-pi/pi-utils/dirs";
7
8
  import type { SourceMeta } from "../../capability/types";
8
9
  import { analyzeAuthError, discoverOAuthEndpoints, MCPManager } from "../../mcp";
9
10
  import { connectToServer, disconnectServer, listTools } from "../../mcp/client";
10
- import {
11
- addMCPServer,
12
- getMCPConfigPath,
13
- readMCPConfigFile,
14
- removeMCPServer,
15
- updateMCPServer,
16
- } from "../../mcp/config-writer";
11
+ import { addMCPServer, readMCPConfigFile, removeMCPServer, updateMCPServer } from "../../mcp/config-writer";
17
12
  import { MCPOAuthFlow } from "../../mcp/oauth-flow";
18
13
  import type { MCPServerConfig, MCPServerConnection } from "../../mcp/types";
19
14
  import type { OAuthCredential } from "../../session/auth-storage";