- Defined `EmbeddingRow` and `EmbeddingOutput` in runtime options and exported them from core embeddings.
- Updated `EmbeddingProvider`, `MnemosyneEmbeddingProvider`, and `provider` runtime option types to return `EmbeddingOutput` instead of `unknown`.
- Refactored embedding result normalization to accept typed rows and sync/async batches and coerce them into validated `Float32Array` vectors.
Codex review on #1527 flagged that the documented forms
`git+https://github.com/user/repo` and `git@github.com:user/repo` still
fell through to the npm install path. `git+https` was rejected by the
package-name validator; scp-style `git@…` passed the validator but then
resolved `actualName` via `extractPackageName` to `git` (everything before
the `@`), causing the post-install package.json lookup to fail at
`node_modules/git/package.json`.
- `parseGitUrl`: strip leading `git+` (forwarded to bun/git as-is) and
extend the protocol gate to also accept scp-like `git@host:user/repo`.
The scp form is unambiguous — no local path starts with `git@` — and
matches what `git clone` itself takes.
- `isGitSpec` now returns true for both forms, routing them through the
snapshot/diff path in `PluginManager.install` so the real package name
is discovered correctly.
- Tests: flip the two cases that asserted rejection, add ref and
`git+ssh` coverage. Verified end-to-end:
`PluginManager.install('git+https://github.com/oldschoola/omp-insights')`
installs `@oldschoola/omp-insights@1.2.3`.
Extends `omp plugin install` to accept git sources alongside npm specs and
marketplace refs. Bun's installer already understands git URLs; the blocker
was `PluginManager.install`'s strict npm-name validator and the assumption
that the actual package name could be derived from the spec.
- `git-url.ts`: `parseGitUrl` now recognizes npm-style namespaced shorthand
(`github:user/repo`, `gitlab:`, `bitbucket:`, `codeberg:`, `sourcehut:` /
`srht:`), with optional `#ref` and `.git` suffix. Exposes `isGitSpec` as
`parseGitUrl(s) !== null`. Existing protocol-URL and `git:` shorthand paths
are untouched.
- `manager.ts`: `install()` branches on `isGitSpec`. Git specs go through a
separate `validateGitSpec` (shell-metachar rejection only — `/`, `:`, `@`,
`#`, `+` are legal) and the real package name is discovered by snapshotting
`plugins/package.json` deps before `bun install` and diffing afterwards.
Falls back to value-match on force-reinstall where the key already exists.
- Help text in `plugin-cli` documents the new sources and adds a github:
example.
Smoke tested end-to-end on Windows with both forms against the test repo:
PluginManager.install('github:oldschoola/omp-insights')
PluginManager.install('https://github.com/oldschoola/omp-insights')
both resolve `@oldschoola/omp-insights@1.2.3` and write a correct lock entry.
Shell-injection probe (`github:foo/bar; rm -rf /`) is rejected.
Bun 1.3 compiled modules report the bunfs mount root from import.meta.dir, not their source-layout module directory. The prior helper walked four directories above that value and escaped the embedded root.
Append packages to the compiled bunfs root, while keeping a guarded suffix path for future module-specific import.meta.dir semantics. Update regression tests to pin the compiled-root behavior observed by a bun build --compile probe.
Fixes#1514
External extensions importing @oh-my-pi/pi-* values (e.g. AssistantMessageEventStream from @oh-my-pi/pi-ai) failed on Windows compiled binaries with "Cannot find package $bunfs\\root\\packages\\...". Shim paths in LEGACY_PI_PACKAGE_ROOT_OVERRIDES were built from a hardcoded POSIX literal "/$bunfs/root/packages"; Win32 normalised the leading slash to a backslash and the path never resolved against the real bunfs mount (<drive>:\\~BUN\\root\\...).
Derive the bunfs package root by walking four directories up from import.meta.dir (which oven-sh/bun#15766 confirms returns the platform-native bunfs path inside the binary). All override targets now go through path.join, so separators stay native on Windows, Linux, and macOS.
Fixes#1514
PluginManager.link symlinks the package into <plugins>/node_modules
and records it in omp-plugins.lock.json, but never writes to
<plugins>/package.json#dependencies. getEnabledPlugins iterated only
the dependency map, so the documented `omp install ./local-extension`
workflow (delegated to plugin link) succeeded but its sibling skills/,
hooks/, tools/, etc. stayed invisible after install.
Iterate the union of package.json#dependencies and
omp-plugins.lock.json#plugins so symlinked-only packages surface
alongside npm/marketplace installs. Lockfile entries whose
node_modules tree has since been deleted (stale link) are skipped
silently. Linked-only setups with no <plugins>/package.json at all
now work too.
Per-PR review feedback: https://github.com/can1357/oh-my-pi/pull/1498
Marketplace and `omp plugin link` installs write to
`<plugins>/node_modules/` rather than to `extensions:` in settings,
so the original PR still missed their sibling skills/, hooks/,
tools/, commands/, rules/, prompts/, .mcp.json sub-trees. Wire
listOmpExtensionRoots to enumerate getEnabledPlugins(cwd, { home })
in addition to CLI-injected and settings-driven roots.
Adds an optional { home } parameter to getEnabledPlugins so the
discovery loader can pass through LoadContext.home for tempdir-rooted
tests. The getPluginsNodeModules/getPluginsPackageJson/
getPluginsLockfile helpers gain the same optional home overload so
they mirror getPluginsDir.
Per-PR review feedback: https://github.com/can1357/oh-my-pi/pull/1498
- Mocked the Vertex stream E2E test to override the home directory and clear GOOGLE_APPLICATION_CREDENTIALS so token resolution uses metadata credentials instead of local ADC files.
- Updated wafer and model-registry test expectations to match current model metadata values (Qwen3.7 Max and claude-opus-4-8).
Limited bunfs package-root overrides to compiled-binary mode so non-compiled installs (monorepo, source-link, node_modules) keep resolving legacy pi roots through Bun's package resolver instead of a hardcoded source-tree path.
Refs #1474
Retried original legacy specifiers after canonical peer fallback fails so direct plugin imports with only legacy-scoped peer dependencies continue to load.
Listing the coding-agent's own ./src/index.ts as a bun --compile extra entrypoint silently breaks the CLI binary startup. Added a dedicated legacy-pi-coding-agent-shim.ts that re-exports the canonical barrel, registered the shim instead of the package index, and updated the compat resolver to point pi-coding-agent at the shim path.
Added bundled root overrides for legacy pi package imports in compiled binaries and corrected fallback resolution to use canonical @oh-my-pi specifiers.
Fixes#1474
Reviewer caught that the new `legacy-pi-ai-shim.ts` is only referenced
via a computed string path, so Bun's `--compile` static analyzer cannot
trace it into bunfs. The same gap existed for the pre-existing
`typebox.ts` shim — `path.resolve(import.meta.dir, "../typebox.ts")`
collapses to `/$bunfs/typebox.ts` in compiled mode (where
`import.meta.dir` is `/$bunfs/root`), which does not exist in bunfs.
Legacy plugins importing bare `@sinclair/typebox` or `@(scope)/pi-ai`
therefore hit "Cannot find module" in release builds.
- Branch `TYPEBOX_SHIM_PATH` and `LEGACY_PI_AI_SHIM_PATH` on
`isCompiledBinary()`: in compiled mode they point at the
`--root`-relative bunfs entry path with a `.js` extension
(`/$bunfs/root/packages/coding-agent/src/extensibility/<file>.js`);
in dev they keep the `path.resolve(import.meta.dir, "../<file>.ts")`
source path so tests still resolve against the workspace tree.
- List both shims as additional `--compile` entrypoints in
`scripts/build-binary.ts` so Bun actually emits them into bunfs at the
paths the runtime expects.
Verified with a focused `bun build --compile` probe: `Bun.resolveSync`
returns the expected bunfs path for both shims when they are listed as
entries, and fails when they are not. Dev-mode test suite unchanged
(17 pass / 0 fail across the four legacy-pi compat test files).
Plannotator-class legacy extensions still import `Type` from
`@(scope)/pi-ai` (e.g. `@earendil-works/pi-ai` rewritten to
`@oh-my-pi/pi-ai`). pi-ai 15.1.0 removed the root `Type` runtime
export, so extension load crashed with `Export named 'Type' not found`
even though the `@sinclair/typebox` Zod-backed shim still ships in the
coding agent.
Routed bare `@oh-my-pi/pi-ai` root specifiers — used by both the
mirrored-source rewriter and the Bun.plugin onResolve hook — through a
new sibling shim that re-exports the canonical pi-ai surface plus the
`Type` runtime from the existing TypeBox shim. Subpath imports such as
`@oh-my-pi/pi-ai/utils/oauth` continue to resolve directly against the
bundled pi-ai package.
Fixes#1437
Loaded marketplace lspServers metadata from Claude plugin caches and embedded it for OMP marketplace installs so config-only plugins register without package code.
Fixes#1352
In a compiled binary, Bun.resolveSync(spec, import.meta.dir) throws
'Cannot find module' because import.meta.dir is inside /$bunfs/root
and the virtual FS exposes no node_modules tree at runtime.
Previously this throw propagated through rewriteLegacyPiImports ->
rewriteLegacyPiImportsForRuntime -> mirrorLegacyPiFile ->
loadLegacyPiModule -> loadExtension, which swallowed it as 'Failed to
load extension' and silently dropped any plugin whose files imported
@mariozechner/pi-ai (or any @mariozechner/pi-* whose bundled
counterpart isn't reachable via resolveSync in the binary).
Fix: wrap the resolution call in rewriteLegacyPiImports in a try/catch
and return the original match on failure. rewriteBareImportsForLegacyExtension
runs immediately afterwards in every call path and already resolves bare
specifiers against the importer's real filesystem directory, so it picks
up @mariozechner/pi-ai from the plugin's installed peer deps instead.
Apply the same fallback to resolveLegacyPiSpecifier (the Bun plugin
shim's onResolve handler) for tool/hook files loaded directly via Bun's
import system rather than through loadLegacyPiModule.
Fixes#1215
- Added a dedicated `@sinclair/typebox` specifier remap path and routed bare legacy imports to the in-repo shim during extension rewriting and module resolution.
- Added resolve hooks for both `file` and legacy-file namespaces so legacy extensions load the shim consistently at runtime.
isUrlLikeSpecifier matched Windows absolute paths (e.g. `C:\foo`) because the URL-scheme regex `^[A-Za-z][A-Za-z\d+.-]*:` happily eats a single drive letter. When a legacy plugin extension imported a bare-specifier dep from its own `node_modules`, rewriteBareImportsForLegacyExtension resolved it to an absolute path, then toRewrittenImportSpecifier short-circuited pathToFileURL and embedded the raw Windows path into the mirrored TS source.
The TS string-literal parser then ate \n, \U, \y and friends, producing nonsense package specifiers like `C:Usersjames.ompagentextensionssupipowers\node_modulesyamldistindex.js` that Bun rejected with `Cannot find package …`. Net effect: every legacy extension that pulls in any node_modules dep failed to load on Windows.
Fix: reject `^[A-Za-z]:[\\/]` in isUrlLikeSpecifier before the URL-scheme test so drive-letter paths flow through pathToFileURL and reach the mirror as proper `file:///C:/...` URLs.
- Fixed legacy `pi-*` scope alias remapping to canonical packages, including `pi-ai/oauth` rewrites.
- Fixed restoration of `Key` on `@oh-my-pi/pi-tui` with canonical key strings and typed modifier helpers.
- Updated both package changelogs with unreleased notes for compatibility and `Key` restoration.
- Added `pi-scope-aliases.test.ts` coverage for alias remaps using fixture plugins and `loadExtensions`.
Plugin manifests that declare a directory entry (e.g. pi-goal's
`"pi": { "extensions": [".pi/extensions/pi-goal"] }`) were
pushed through to the legacy module loader unchanged. `Bun.file()`
on a directory throws "Directories cannot be read like files",
so every such plugin failed to load.
`resolvePluginPaths` now routes each joined manifest entry through
`resolveManifestEntryFile`, which:
- returns the path as-is when it is a file,
- returns the directory's `index.{ts,js,mjs,cjs}` when it is a
directory containing one of those files,
- otherwise returns null (skip).
This mirrors the documented native auto-discovery behavior in
`resolveExtensionEntries` and the candidate list in
`extensibility/custom-commands/loader.ts`.
Closes#1001.
Files loaded via the omp-legacy-pi-file: namespace bypass Bun's normal
node_modules lookup because the importer lives in a custom namespace,
so an extension that imports its own bundled dependencies (e.g.
`import parseDuration from "local-duration-parser"`) failed with
`Cannot find package`. Pre-resolve bare specifiers in the onLoad step
against the importer's directory so extensions can ship their own
node_modules. Relative, absolute, node:, and URL specifiers are left
untouched, preserving the existing legacy @mariozechner/pi-* remap
path.
Some extensions/plugins still import the legacy @mariozechner/pi-* package names (pi-agent-core, pi-ai, pi-coding-agent, pi-tui) instead of @oh-my-pi/pi-*. Register a single Bun.plugin on first plugin/extension load that rewrites those specifiers to the current @oh-my-pi/pi-* equivalents, including the @mariozechner/pi-coding-agent/extensibility/{extensions,hooks} sub-exports. No filesystem mutation, no symlinks, no proxy files.
Fixes#973
- Replaced all Bun.which() calls with $which() utility from @oh-my-pi/pi-utils across 22 files.
- Removed findBashOnPath() wrapper function from procmgr.ts, consolidating binary path resolution.
- Updated AGENTS.md documentation to reflect new $which() API usage pattern.
- Centralized binary detection logic through shared utility, reducing code duplication.
- Extracted git operations from ControlledGit class into centralized utils/git module with 1276 lines of typed command wrappers.
- Replaced dependency injection of ControlledGit instances with direct cwd string parameters across commit agent tools and workflows.
- Migrated all git command execution from inline shell calls and custom helpers to structured git module API (diff, status, branch, worktree, patch, etc.).
- Removed ControlledGit class, operations.ts, and helper functions (findGitHeadPath, mergeStdoutStderr, joinPatch) now provided by git module.
- Exported git utilities from main package entry point for extension and plugin use.
listInstalledPlugins() was marking user-scope entries as shadowedBy:
"project" based on plugin ID presence in the project registry alone.
A disabled project copy (enabled: false) is not active in discovery, so
the user copy is the effective install and must not appear shadowed.
activeProjectIds now only includes project plugins where the first entry
does not have enabled: false.
Plugins can now be installed at user scope (global) or project scope
(per-project, higher capability priority). Scope is encoded in registry
file location, not a metadata field:
user: ~/.omp/plugins/installed_plugins.json
project: <nearest-project>/.omp/plugins/installed_plugins.json
cache: ~/.omp/plugins/cache/plugins/ (shared, path-referenced)
Project root discovery: resolveActiveProjectRegistryPath(cwd) walks up
from cwd looking for the nearest .omp/ directory, falling back to the
nearest .git root. This is the single resolver used by install, uninstall,
list, upgrade, discovery, and doctor.
Discovery: listClaudePluginRoots(home, cwd?) reads both registries when
cwd is provided. Project entries shadow user entries for the same plugin
ID. Cache key is canonical ("${home}:${resolvedProjectPath}") so nested
cwds within the same project share a cache entry.
Manager changes:
- installPlugin({ scope? }): routes registry reads/writes by scope;
checks collectReferencedPaths() across both registries before deleting
any cached plugin dir to prevent cross-scope data loss
- uninstallPlugin(id, scope?), setPluginEnabled(id, enabled, scope?),
upgradePlugin(id, scope?): throw a disambiguation error when the plugin
exists in both scopes and no scope is specified
- upgradePluginAcrossScopes(id): new; upgrades all scopes the plugin is
installed in; returns InstalledPluginEntry[]
- upgradeAllPlugins(): uses upgradePluginAcrossScopes; result includes scope
- listInstalledPlugins(): returns InstalledPluginSummary[] merged from both
registries; user entries marked shadowedBy: "project" when overridden
CLI: omp plugin install|uninstall|upgrade|enable|disable --scope user|project
Slash: /marketplace install [--scope user|project] name@marketplace
MarketplaceManager constructed with projectInstalledRegistryPath in all
CLI handlers and builtin-registry.ts via resolveActiveProjectRegistryPath.
.gitignore: .omp/plugins/ added (local runtime state, not committed).
preloadPluginRoots/clearClaudePluginRootsCache carry cwd through for LSP.
main.ts passes getProjectDir() at startup.
Tests: 226 pass across 13 files. New: project-scope.test.ts (resolver
walk-up, .git fallback, null return, canonical path, shadow precedence);
manager scope tests (registry isolation, disambiguation errors, cross-scope
cache-ref protection, upgradePluginAcrossScopes, shadowedBy marking).
fixes#581
- Added error handling for marketplace plugin selector failures with user-facing error messages.
- Fixed marketplace clone promotion timing by extracting logic into separate function and applying consistently across addMarketplace and refreshMarketplace methods.
- Added cleanup of temporary clone directories on source verification failures to prevent orphaned files.
- Improved plugin selector UI rendering by expanding onCancel callback to trigger requestRender after completion.
- Fixed LSP config path handling by replacing hardcoded filename array with variable reference.
- Enhanced error handling for unknown plugin source variants in marketplace validation.
- addMarketplace reads registry before fetching; if the fetched catalog
name collides with an existing entry, cleans up the clone and throws
before any cache corruption occurs
- installPlugin rejects relative string sources when the marketplace was
added via URL (only marketplace.json is cached, not the plugin tree)
- /reload-plugins now calls refreshSlashCommandState() so newly
installed/removed plugin commands take effect without restart
- parseMarketplaceCatalog rejects string sources that don't start
with './' at parse time instead of deferring to install-time crash
- classifyInstallTarget skips known npm dist-tags (latest, next, beta,
etc.) and semver-like strings before checking marketplace names
- updateMarketplace throws if fetched catalog name differs from the
registered name, preventing stale data on upstream renames
- parseMarketplaceCatalog validates variant-specific required fields
(github.repo, url.url, git-subdir.url+path, npm.package) at parse
time instead of deferring to install-time crashes
- parseMarketplaceCatalog rejects plugin names that fail isValidNameSegment
instead of deferring the error to install time
- cachePlugin copies to staging dir first, then renames into place;
a failed copy no longer destroys the active install
- Fix ReturnType<> usage in cloneAndReadCatalog
- Carry over enabled:false from existing entries during force reinstall
and upgrade; disabled plugins stay disabled without user intent change
- After clearing plugin roots cache, fire-and-forget preloadPluginRoots()
so sync LSP config reads stay valid for the rest of the session
- Store injected --plugin-dir roots in module-level variable; survive
cache clears by re-merging on next listClaudePluginRoots() call
- Defer old plugin deletion in force-reinstall until new cache succeeds;
transient failures no longer leave the plugin uninstalled
- Classify HTTP URLs by pathname instead of full string; query params
no longer cause .json URLs to be misclassified as git
- Honor enabled:false flag in listClaudePluginRoots (skip disabled plugins)
- Normalize local source URIs to absolute paths in addMarketplace
- Resolver returns explicit tempCloneRoot instead of path-based inference
- Added `toExtensionId()` method to all capability types for granular extension identification and disabling.
- Added `disabledExtensions` and `includeDisabled` options to LoadOptions for filtering disabled capabilities by extension ID.
- Added plugin manifest support for `extensions` entry points with automatic discovery from installed plugins.
- Fixed skill loading to properly respect disabled skill names from custom directories.
- Improved cross-platform path handling by using `path.basename()` instead of string splitting in context file and state manager.
- Refactored capability index loading to validate source metadata and filter disabled extensions before processing.
- Consolidated @oh-my-pi/pi-utils subpath imports into single package root import across 100+ files.
- Moved tryParseJson utility from local web scrapers module to @oh-my-pi/pi-utils package for centralized JSON parsing.
- Renamed loadSkillsFromDir to scanSkillsFromDir and refactored skill discovery to use fs.promises.readdir instead of glob-based approach.
- Replaced custom parseJSON with tryParseJson across discovery modules for consistent error handling.
- Removed emitCustomToolSessionEvent method and cleanupSshResources function, consolidating shutdown logic into dispose method.
- Updated glob pattern construction to use GlobBuilder with literal_separator(true) for improved path handling.
- Replaced all direct `process.cwd()` calls with `getProjectDir()` utility function across 40+ files to centralize project directory resolution logic.
- Added `getProjectDir()` and `setProjectDir()` functions to `@oh-my-pi/pi-utils/dirs` module to provide abstracted project directory management.
- Made `SessionManager.list()` method asynchronous to support asynchronous session discovery operations.
- Updated default working directory resolution throughout codebase to use `getProjectDir()` instead of `process.cwd()` for improved project directory detection.