@oh-my-pi/pi-coding-agent 15.0.1 → 15.0.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.
- package/CHANGELOG.md +38 -0
- package/package.json +8 -8
- package/src/commands/commit.ts +10 -0
- package/src/config/model-registry.ts +31 -1
- package/src/config/settings-schema.ts +11 -0
- package/src/discovery/claude-plugins.ts +19 -7
- package/src/eval/py/runner.py +42 -11
- package/src/eval/py/runtime.ts +1 -0
- package/src/extensibility/extensions/get-commands-handler.ts +77 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
- package/src/hashline/input.ts +2 -1
- package/src/hashline/parser.ts +27 -3
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/internal-urls/router.ts +8 -0
- package/src/internal-urls/types.ts +21 -0
- package/src/lsp/config.ts +15 -6
- package/src/lsp/defaults.json +6 -2
- package/src/modes/acp/acp-agent.ts +248 -50
- package/src/modes/components/status-line/segments.ts +38 -4
- package/src/modes/controllers/extension-ui-controller.ts +3 -2
- package/src/modes/rpc/host-uris.ts +235 -0
- package/src/modes/rpc/rpc-mode.ts +27 -1
- package/src/modes/rpc/rpc-types.ts +57 -0
- package/src/modes/runtime-init.ts +2 -1
- package/src/modes/theme/defaults/dark-poimandres.json +1 -0
- package/src/modes/theme/defaults/light-poimandres.json +1 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/prompts/tools/github.md +4 -4
- package/src/prompts/tools/hashline.md +22 -26
- package/src/prompts/tools/read.md +55 -37
- package/src/task/discovery.ts +5 -2
- package/src/task/executor.ts +2 -1
- package/src/tools/bash-command-fixup.ts +47 -0
- package/src/tools/bash.ts +39 -15
- package/src/tools/browser/render.ts +2 -2
- package/src/tools/eval.ts +10 -2
- package/src/tools/gh.ts +37 -4
- package/src/tools/job.ts +16 -7
- package/src/tools/output-meta.ts +26 -0
- package/src/tools/read.ts +32 -4
- package/src/tools/ssh.ts +3 -2
- package/src/tools/write.ts +20 -0
- package/src/web/search/providers/anthropic.ts +5 -0
- package/src/web/search/providers/exa.ts +3 -0
- package/src/web/search/providers/gemini.ts +5 -0
- package/src/web/search/providers/jina.ts +5 -2
- package/src/web/search/providers/zai.ts +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.0.2] - 2026-05-15
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added the `set_host_uri_schemes` RPC command so hosts can register and replace writable/read-only internal URI schemes with scheme metadata (`writable`, `immutable`) at runtime
|
|
10
|
+
- Enabled the `write` tool to dispatch `write(url, content)` to registered internal URL handlers, allowing edits to non-filesystem resources via host-managed URI schemes
|
|
11
|
+
- Added host-owned internal URI read/write over RPC, including abort support, so URI operations are resolved by the host transport for `read` and `write` requests
|
|
12
|
+
- Added handling of host URI request results in RPC mode so host services can stream completion frames for internal URI operations
|
|
13
|
+
- Added scratch-directory awareness to the status-line `path` segment. When the project directory is inside an OS-level scratch root (the platform `os.tmpdir()`, `/tmp` and `/var/tmp` plus their macOS `/private/...` aliases, `~/tmp`, or — on Windows — `%TEMP%` / `%TMP%` / `%SystemRoot%\Temp`), the segment now (1) renders the new `icon.scratchFolder` symbol instead of `icon.folder`, and (2) strips the scratch root from the displayed path so only the trailing folder (and any subpath beneath it) is shown — mirroring how `/work` and `~/Projects` are already abbreviated. Both behaviors honor the existing `stripWorkPrefix` option. Icon defaults: 🗑 (emoji), `` (nf-fa-trash) for Nerd Font, `[T]` for ASCII, `◌` in the poimandres themes; themes can override `icon.scratchFolder` independently of `icon.folder`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Changed the `github` tool's search ops (`search_issues`, `search_prs`, `search_code`, `search_commits`) to default the `repo` scope to the current checkout's `owner/repo` when `repo` is omitted. The auto-scope is skipped when the query already carries an explicit `repo:`/`org:`/`user:`/`owner:` qualifier or when `gh repo view` cannot resolve a github remote (in which case the search proceeds across all of GitHub as before). `search_repos` is unchanged — repository-scoping there must live in the query.
|
|
18
|
+
|
|
19
|
+
- Changed bash command preprocessing to strip trailing `| head` and `| tail` pipelines (including `|&`) from each top-level segment in command chains separated by `;`, `&&`, `||`, or `&`
|
|
20
|
+
- Changed bash fixup notices to state that stderr is already merged into stdout and to reflect that fixes were applied for multiple stripped segments when several transforms fire
|
|
21
|
+
- Changed shell-minimizer per-line truncation marker from a bare `…` to `…[+N]`, where `N` is the count of dropped Unicode scalars. The bracketed tally disambiguates minimizer-driven cuts from genuine `…` characters in the source (paths, JSON, stack traces, etc.) and gives the agent an exact count so it can decide whether the missing tail is recoverable inline or warrants reading the `[raw output: artifact://<id>]` footer the bash wrapper already emits when the minimizer rewrites output. Affects pipeline Stage 5 (`truncate_lines_at` in `defs/*.toml`) and the internal callers in `filters/git.rs`, `filters/listing.rs`, and `filters/lint.rs`. ([#1046](https://github.com/can1357/oh-my-pi/issues/1046))
|
|
22
|
+
- Changed bash command preprocessing to use the real `brush-parser` AST via `pi-natives` `applyBashFixups` instead of a hand-rolled top-level mask scanner. The previous regex/character-walking implementation reimplemented quote/heredoc/`$(...)` tracking with conservative bail-outs (notably refusing to fixup commands containing here-strings); the AST-driven version inherits the full shell parser, so semantics-preserving rewrites like stripping `| head -5` off `cat <<<'content' | head -5` now succeed instead of being skipped. No public API change — `applyBashFixups(command)` returns the same `{ command, stripped }` shape.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Fixed bash command fixups to remove a redundant standalone trailing `2>&1` redirect when no other pipe or redirection remains
|
|
27
|
+
- Fixed command-fixup notices to list all stripped segments instead of reporting only one
|
|
28
|
+
- Fixed summarized `read` output stalling agents on elided regions by appending an explicit footer like `[NN lines across MM elided regions; read <path>:raw or a line range like <path>:1-9999 for verbatim content]`. The footer fires whenever the structural summarizer elided at least one span, so the model gets a concrete recovery selector instead of having to guess from a bare `...` / `{ .. }` marker. Surfaces `elidedLines` on `ReadToolDetails.summary` alongside the existing `elidedSpans`. ([#1046](https://github.com/can1357/oh-my-pi/issues/1046))
|
|
29
|
+
- Updated the `read` tool prompt to describe the new elision footer and instruct the model to follow `:raw` (or an explicit line range) when the elided body is actually needed, rather than guessing.
|
|
30
|
+
- Fixed plugin extensions failing to load when their `peerDependencies` reference internal `pi-*` packages under any scope other than `@mariozechner` (e.g. `Cannot find module '@earendil-works/pi-tui'` from `@juicesharp/rpiv-ask-user-question`, or `Cannot find module '@oh-my-pi/pi-utils'` from `@oh-my-pi/swarm-extension`). The legacy-pi specifier shim now treats `@mariozechner`, `@earendil-works`, **and** the canonical `@oh-my-pi` itself as aliases for the same set of bundled in-process packages (`pi-agent-core`, `pi-ai`, `pi-coding-agent`, `pi-natives`, `pi-tui`, `pi-utils`), and additionally rewrites the upstream-only `pi-ai/oauth` subpath onto our `pi-ai/utils/oauth` layout. Restored the `Key` runtime helper export on `@oh-my-pi/pi-tui` to match upstream — plugins using `Key.enter` / `Key.ctrl("c")` (e.g. `@plannotator/pi-extension`, `@juicesharp/rpiv-ask-user-question`) no longer fail with `Export named 'Key' not found`. End-to-end verified against `@juicesharp/rpiv-ask-user-question`, `@oh-my-pi/swarm-extension`, and `@plannotator/pi-extension` — each now loads cleanly with all of its tools/commands/handlers registered. Plugins importing any of those scopes are remapped to the omp binary's own copy at load time, so peer deps are no longer dragged in from npm and there is exactly one module instance per package regardless of which scope name the plugin's manifest happened to declare.
|
|
31
|
+
- Fixed `omp commit` hanging after a successful commit instead of returning to the shell. The command now mirrors the `runPrintMode` exit pattern and calls `postmortem.quit(0)` once the pipeline resolves so lingering HTTP/2 keep-alive sockets, the Settings autosave timer, and other AgentSession background handles don't keep the event loop pinned. ([#1041](https://github.com/can1357/oh-my-pi/issues/1041))
|
|
32
|
+
- Fixed hashline payload parsing to silently treat truly-blank lines as empty `~`-prefixed payload lines when more payload follows in the same run. The previous behavior broke at the blank ("payload line has no preceding +, <, or = operation.") even though the intent is obvious — the only ambiguity is between in-payload blanks and end-of-section blanks, and a one-line lookahead resolves it: blanks that precede a non-payload op still end the run cleanly as section separators. Recovers the common case of forgetting the leading separator on a blank inserted line without changing how trailing blanks between ops behave.
|
|
33
|
+
- Rewrote the hashline edit prompt examples to use an ASCII-only `TITLE = "Mr"` → `"Mrs"` / `"Dr"` motif instead of the previous `" • "` and `"·"` separators. Some agents had been copying the middle-dot literal characters into real edits as if they were format scaffolding (e.g. emitting payload lines like `~ ·`), since the demo inserts were near-twins of the existing string. The new example keeps every original op shape (single-line replace, multiline replace, insert AFTER/BEFORE, append, delete, blank, plus both anti-patterns) but uses content that is obviously domain-specific and clearly distinct from any payload separator. Pure prompt change; no parser, schema, or runtime behavior is affected.
|
|
34
|
+
- Fixed startup fallback-chain validation to recognize cached runtime-discovered standard provider models, including Ollama Cloud models listed by `--list-models`, so `retry.fallbackChains` no longer warns that valid `ollama-cloud/<model>` selectors are unknown. ([#1052](https://github.com/can1357/oh-my-pi/issues/1052))
|
|
35
|
+
- Fixed `discoverAgents()` ignoring `disabledProviders` for the `claude-plugins` provider. Plugin roots from `~/.claude/plugins/` were scanned unconditionally, so agents from Claude Code marketplace plugins continued to appear in `/agents` and the Agent Control Center even when `disabledProviders: [claude-plugins]` was set. The discovery path now checks `isProviderEnabled("claude-plugins")` before calling `listClaudePluginRoots()`, matching how every other capability respects the disabled-providers set. ([#1075](https://github.com/can1357/oh-my-pi/issues/1075))
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Fixed `$env:VAR` PowerShell variables being mangled on Windows when commands invoked PowerShell as a subprocess (e.g. `powershell -Command "Write-Host $env:SystemRoot"`). Brush-core applied POSIX parameter expansion to `$env` before spawning the child, leaving a dangling `:NAME`. The fix lives in `pi-shell` at env-var application time: every brush session now defines `env=$env` as an internal shell variable so `$env:NAME` expands to the literal `$env:NAME` token that PowerShell expects. The fallback is not exported, only influences brush's own expansion, and is shadowed by any user assignment to `env` (e.g. `env=prod; echo "$env:8080"` still prints `prod:8080`), so the POSIX bash contract is preserved. ([#1079](https://github.com/can1357/oh-my-pi/issues/1079))
|
|
40
|
+
|
|
41
|
+
|
|
5
42
|
## [15.0.1] - 2026-05-14
|
|
6
43
|
### Breaking Changes
|
|
7
44
|
|
|
@@ -15,6 +52,7 @@
|
|
|
15
52
|
- Added per-line column cap shared across streaming tool outputs (`bash`, `ssh`, `python`, `js eval`) and the `read` tool. Lines wider than `tools.outputMaxColumns` bytes (default **768**) are ellipsis-truncated at write time and remaining bytes up to the next `\n` are dropped — bounded memory even on multi-MB single-line outputs (e.g. `cat /dev/urandom`). The cap lives on `OutputSink` as the new `maxColumns` option, persists state across chunk boundaries so split-mid-line writes still respect the budget, and exposes `columnDroppedBytes` / `columnTruncatedLines` on `OutputSummary`. Middle-elision byte math subtracts column drops so the "elided from middle" count stays honest. `read` reuses the same setting but trims its already-collected lines via `truncateLine`. Skipped when the read selector is `:raw`. The artifact file (`artifact://<id>`) keeps the full uncapped stream. Set `tools.outputMaxColumns = 0` to disable.
|
|
16
53
|
- Added Bun HTTP/2 fetch opt-in. Dev scripts (`bun run dev`, `bun run stats`) now pass `bun --experimental-http2-fetch` so every `fetch()` advertises `h2` in the TLS ALPN list and falls back to HTTP/1.1 when the server doesn't select it. Multiplexing collapses parallel requests to the same origin onto one TLS connection. For the installed `omp` binary, export `BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP2_CLIENT=1` in your shell to enable the same behavior (the flag has to be set before Bun starts; `process.env` from inside JS is too late). Requires Bun **1.3.14**.
|
|
17
54
|
- Added per-subagent cost display (`$X.XX` in the task progress tree and the session-observer stats line). Cost is accumulated incrementally from `message_end` events and shown only when non-zero, using the `statusLineCost` theme color. Providers that do not report per-turn cost data (e.g. subscription/OAuth usage) continue to show nothing.
|
|
55
|
+
- Added ACP elicitation bridge so skills/extensions calling `select`, `confirm`, or `input` on the extension UI context now produce real `unstable_createElicitation` form requests to the ACP client (rather than always resolving to `undefined` / `false`). The `acpExtensionUiContext` constant is promoted to `createAcpExtensionUiContext(connection, getSessionId, clientCapabilities)` — invoked once per session inside `#configureExtensions`, with `getSessionId: () => string` so the live `record.session.sessionId` is read on every elicitation (the underlying id mutates when an extension command calls `ctx.newSession` / `ctx.switchSession`). Each method maps to a single-property `value` schema: `select` → `{type: "string", enum}`, `confirm` → `{type: "boolean"}` (joined `title` + `message` when the trimmed message is non-empty; otherwise just `title`), `input` → `{type: "string", description: placeholder?}` (ACP has no `placeholder` field on `StringPropertySchema`; empty / whitespace-only placeholders are treated as absent). `accept` responses narrow the returned `ElicitationContentValue` back to the method's declared type with a runtime `typeof` guard; `decline` / `cancel` / transport failures fall back to the prior stub return values. `dialogOptions.signal` is honored: an already-aborted signal short-circuits before any SDK round-trip, and an abort mid-flight races against the elicitation so the caller's promise resolves to the stub fallback (the ACP request itself keeps running on the client side — the SDK exposes no form-mode cancel surface; `unstable_completeElicitation` is URL-mode only — matching the in-flight pattern used by `requestRpcEditor`). `dialogOptions.timeout` is honored on parity with `RpcExtensionUIContext`: when the timer fires before the client responds, `onTimeout` is invoked and the caller resolves to the stub fallback. A throwing `onTimeout` is caught and logged (`logger.warn`) so the elicitation promise still settles. Late SDK rejections that arrive after abort/timeout are dropped silently to keep operator logs clean; transport failures still emit `logger.warn` with `{ sessionId, method, error }`. Calls are skipped when the client did not advertise `clientCapabilities.elicitation.form` during `initialize`, so non-elicitation clients are unaffected. `createAcpExtensionUiContext` is exported for tests.
|
|
18
56
|
|
|
19
57
|
### Changed
|
|
20
58
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "15.0.
|
|
4
|
+
"version": "15.0.2",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
|
-
"homepage": "https://
|
|
6
|
+
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
8
8
|
"contributors": [
|
|
9
9
|
"Mario Zechner"
|
|
@@ -47,12 +47,12 @@
|
|
|
47
47
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
48
48
|
"@babel/parser": "^7.29.3",
|
|
49
49
|
"@mozilla/readability": "^0.6.0",
|
|
50
|
-
"@oh-my-pi/omp-stats": "15.0.
|
|
51
|
-
"@oh-my-pi/pi-agent-core": "15.0.
|
|
52
|
-
"@oh-my-pi/pi-ai": "15.0.
|
|
53
|
-
"@oh-my-pi/pi-natives": "15.0.
|
|
54
|
-
"@oh-my-pi/pi-tui": "15.0.
|
|
55
|
-
"@oh-my-pi/pi-utils": "15.0.
|
|
50
|
+
"@oh-my-pi/omp-stats": "15.0.2",
|
|
51
|
+
"@oh-my-pi/pi-agent-core": "15.0.2",
|
|
52
|
+
"@oh-my-pi/pi-ai": "15.0.2",
|
|
53
|
+
"@oh-my-pi/pi-natives": "15.0.2",
|
|
54
|
+
"@oh-my-pi/pi-tui": "15.0.2",
|
|
55
|
+
"@oh-my-pi/pi-utils": "15.0.2",
|
|
56
56
|
"@puppeteer/browsers": "^2.13.0",
|
|
57
57
|
"@sinclair/typebox": "^0.34.49",
|
|
58
58
|
"@types/turndown": "5.0.6",
|
package/src/commands/commit.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Generate and optionally push a commit with changelog updates.
|
|
3
3
|
*/
|
|
4
|
+
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
4
5
|
import { Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
5
6
|
import { runCommitCommand } from "../commit";
|
|
6
7
|
import type { CommitCommandArgs } from "../commit/types";
|
|
@@ -31,6 +32,15 @@ export default class Commit extends Command {
|
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
await initTheme();
|
|
35
|
+
// The agentic commit flow opens HTTP/2 keep-alive sockets to the model
|
|
36
|
+
// provider (via `installH2Fetch`) and spins up an AgentSession with
|
|
37
|
+
// background async-job + extension machinery. `session.dispose()` releases
|
|
38
|
+
// what it can, but Bun's fetch keeps idle connections warm and a few
|
|
39
|
+
// timers (Settings autosave, OAuth refresh) stay armed long enough to
|
|
40
|
+
// pin the event loop after the commit is already written. Mirror the
|
|
41
|
+
// `runPrintMode` exit pattern from `main.ts` so the CLI returns to the
|
|
42
|
+
// shell instead of stranding the user on Ctrl+C (issue #1041).
|
|
34
43
|
await runCommitCommand(cmd);
|
|
44
|
+
await postmortem.quit(0);
|
|
35
45
|
}
|
|
36
46
|
}
|
|
@@ -1015,8 +1015,12 @@ export class ModelRegistry {
|
|
|
1015
1015
|
|
|
1016
1016
|
this.#addImplicitDiscoverableProviders(configuredProviders);
|
|
1017
1017
|
const builtInModels = this.#applyHardcodedModelPolicies(this.#loadBuiltInModels(overrides));
|
|
1018
|
+
const cachedStandardModels = this.#applyHardcodedModelPolicies(this.#loadCachedStandardProviderModels());
|
|
1018
1019
|
const cachedDiscoveries = this.#applyHardcodedModelPolicies(this.#loadCachedDiscoverableModels());
|
|
1019
|
-
const resolvedDefaults = this.#mergeResolvedModels(
|
|
1020
|
+
const resolvedDefaults = this.#mergeResolvedModels(
|
|
1021
|
+
this.#mergeResolvedModels(builtInModels, cachedStandardModels),
|
|
1022
|
+
cachedDiscoveries,
|
|
1023
|
+
);
|
|
1020
1024
|
const withConfigModels = this.#mergeCustomModels(resolvedDefaults, this.#customModelOverlays);
|
|
1021
1025
|
// Merge runtime extension models so they survive refresh() cycles
|
|
1022
1026
|
const combined = this.#mergeCustomModels(withConfigModels, this.#runtimeModelOverlays);
|
|
@@ -1115,6 +1119,32 @@ export class ModelRegistry {
|
|
|
1115
1119
|
return merged;
|
|
1116
1120
|
}
|
|
1117
1121
|
|
|
1122
|
+
#loadCachedStandardProviderModels(): Model<Api>[] {
|
|
1123
|
+
const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(provider => provider.provider));
|
|
1124
|
+
const cachedModels: Model<Api>[] = [];
|
|
1125
|
+
for (const descriptor of PROVIDER_DESCRIPTORS) {
|
|
1126
|
+
if (configuredDiscoveryProviders.has(descriptor.providerId)) {
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
const cache = readModelCache<Api>(descriptor.providerId, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath);
|
|
1130
|
+
if (!cache) {
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
const models = cache.models.map(model =>
|
|
1134
|
+
model.provider === descriptor.providerId ? model : { ...model, provider: descriptor.providerId },
|
|
1135
|
+
);
|
|
1136
|
+
const providerOverride = this.#providerOverrides.get(descriptor.providerId);
|
|
1137
|
+
const withTransport = providerOverride
|
|
1138
|
+
? models.map(model => this.#applyProviderTransportOverride(model, providerOverride))
|
|
1139
|
+
: models;
|
|
1140
|
+
const withCompat = providerOverride?.compat
|
|
1141
|
+
? withTransport.map(model => ({ ...model, compat: mergeCompat(model.compat, providerOverride.compat) }))
|
|
1142
|
+
: withTransport;
|
|
1143
|
+
cachedModels.push(...this.#applyProviderModelOverrides(descriptor.providerId, withCompat));
|
|
1144
|
+
}
|
|
1145
|
+
return cachedModels;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1118
1148
|
#loadCachedDiscoverableModels(): Model<Api>[] {
|
|
1119
1149
|
const cachedModels: Model<Api>[] = [];
|
|
1120
1150
|
for (const providerConfig of this.#discoverableProviders) {
|
|
@@ -1648,6 +1648,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
1648
1648
|
},
|
|
1649
1649
|
"bashInterceptor.patterns": { type: "array", default: DEFAULT_BASH_INTERCEPTOR_RULES },
|
|
1650
1650
|
|
|
1651
|
+
"bash.stripTrailingHeadTail": {
|
|
1652
|
+
type: "boolean",
|
|
1653
|
+
default: true,
|
|
1654
|
+
ui: {
|
|
1655
|
+
tab: "editing",
|
|
1656
|
+
label: "Strip Trailing head/tail",
|
|
1657
|
+
description:
|
|
1658
|
+
"Silently drop trailing `| head`/`| tail` pipes from single-line bash commands. Output is already truncated automatically.",
|
|
1659
|
+
},
|
|
1660
|
+
},
|
|
1661
|
+
|
|
1651
1662
|
// Shell output minimizer
|
|
1652
1663
|
"shellMinimizer.enabled": {
|
|
1653
1664
|
type: "boolean",
|
|
@@ -31,6 +31,7 @@ const PRIORITY = 70; // Below claude.ts (80) so user .claude/ overrides win
|
|
|
31
31
|
interface ClaudePluginManifest {
|
|
32
32
|
skills?: string;
|
|
33
33
|
"slash-commands"?: string;
|
|
34
|
+
commands?: string;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
interface ResolvedPluginDir {
|
|
@@ -59,24 +60,35 @@ function isWithinPluginRoot(rootPath: string, targetPath: string): boolean {
|
|
|
59
60
|
|
|
60
61
|
async function resolvePluginDir(
|
|
61
62
|
root: ClaudePluginRoot,
|
|
62
|
-
|
|
63
|
+
manifestKeys: ReadonlyArray<keyof ClaudePluginManifest>,
|
|
63
64
|
fallback: string,
|
|
64
65
|
): Promise<ResolvedPluginDir> {
|
|
65
66
|
const manifest = await readPluginManifest(root);
|
|
66
67
|
const fallbackDir = path.join(root.path, fallback);
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
|
|
69
|
+
let configured: string | undefined;
|
|
70
|
+
let matchedKey: keyof ClaudePluginManifest | undefined;
|
|
71
|
+
for (const key of manifestKeys) {
|
|
72
|
+
const val = manifest?.[key];
|
|
73
|
+
if (typeof val === "string" && val.trim()) {
|
|
74
|
+
configured = val.trim();
|
|
75
|
+
matchedKey = key;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (configured === undefined) {
|
|
69
81
|
return { dir: fallbackDir };
|
|
70
82
|
}
|
|
71
83
|
|
|
72
|
-
const resolved = path.resolve(root.path, configured
|
|
84
|
+
const resolved = path.resolve(root.path, configured);
|
|
73
85
|
if (isWithinPluginRoot(root.path, resolved)) {
|
|
74
86
|
return { dir: resolved };
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
return {
|
|
78
90
|
dir: fallbackDir,
|
|
79
|
-
warning: `[claude-plugins] Ignoring ${String(
|
|
91
|
+
warning: `[claude-plugins] Ignoring ${String(matchedKey)} path outside plugin root for ${root.id}: ${configured}`,
|
|
80
92
|
};
|
|
81
93
|
}
|
|
82
94
|
|
|
@@ -93,7 +105,7 @@ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
|
93
105
|
|
|
94
106
|
const results = await Promise.all(
|
|
95
107
|
roots.map(async root => {
|
|
96
|
-
const { dir: skillsDir, warning } = await resolvePluginDir(root, "skills", "skills");
|
|
108
|
+
const { dir: skillsDir, warning } = await resolvePluginDir(root, ["skills"], "skills");
|
|
97
109
|
const result = await scanSkillsFromDir(ctx, {
|
|
98
110
|
dir: skillsDir,
|
|
99
111
|
providerId: PROVIDER_ID,
|
|
@@ -128,7 +140,7 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
|
|
|
128
140
|
|
|
129
141
|
const results = await Promise.all(
|
|
130
142
|
roots.map(async root => {
|
|
131
|
-
const { dir: commandsDir, warning } = await resolvePluginDir(root, "slash-commands", "commands");
|
|
143
|
+
const { dir: commandsDir, warning } = await resolvePluginDir(root, ["commands", "slash-commands"], "commands");
|
|
132
144
|
const result = await loadFilesFromDir<SlashCommand>(ctx, commandsDir, PROVIDER_ID, root.scope, {
|
|
133
145
|
extensions: ["md"],
|
|
134
146
|
transform: (name, content, filePath, source) => {
|
package/src/eval/py/runner.py
CHANGED
|
@@ -25,9 +25,11 @@ when installed.
|
|
|
25
25
|
|
|
26
26
|
from __future__ import annotations
|
|
27
27
|
|
|
28
|
+
import asyncio
|
|
28
29
|
import ast
|
|
29
30
|
import base64
|
|
30
31
|
import builtins
|
|
32
|
+
import inspect
|
|
31
33
|
import io
|
|
32
34
|
import json
|
|
33
35
|
import os
|
|
@@ -120,6 +122,7 @@ class _RunnerState:
|
|
|
120
122
|
"__builtins__": builtins,
|
|
121
123
|
}
|
|
122
124
|
self.last_install_marker: int = 0
|
|
125
|
+
self.loop: asyncio.AbstractEventLoop | None = None
|
|
123
126
|
|
|
124
127
|
|
|
125
128
|
_STATE = _RunnerState()
|
|
@@ -688,13 +691,41 @@ _install_builtins(_STATE.user_ns)
|
|
|
688
691
|
# ---------------------------------------------------------------------------
|
|
689
692
|
|
|
690
693
|
|
|
694
|
+
_TLA_FLAG = getattr(ast, "PyCF_ALLOW_TOP_LEVEL_AWAIT", 0x2000)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _get_event_loop() -> asyncio.AbstractEventLoop:
|
|
698
|
+
loop = _STATE.loop
|
|
699
|
+
if loop is None or loop.is_closed():
|
|
700
|
+
loop = asyncio.new_event_loop()
|
|
701
|
+
asyncio.set_event_loop(loop)
|
|
702
|
+
_STATE.loop = loop
|
|
703
|
+
return loop
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def _run_compiled(code, ns: dict, *, want_value: bool) -> Any:
|
|
707
|
+
"""Execute a code object, awaiting it if compiled as a coroutine.
|
|
708
|
+
|
|
709
|
+
``want_value`` is True for the trailing expression — we return ``eval``'s
|
|
710
|
+
result (or the awaited coroutine's value). For statement blocks the
|
|
711
|
+
return is always ``None``.
|
|
712
|
+
"""
|
|
713
|
+
if code.co_flags & inspect.CO_COROUTINE:
|
|
714
|
+
coro = eval(code, ns)
|
|
715
|
+
result = _get_event_loop().run_until_complete(coro)
|
|
716
|
+
return result if want_value else None
|
|
717
|
+
if want_value:
|
|
718
|
+
return eval(code, ns)
|
|
719
|
+
exec(code, ns)
|
|
720
|
+
return None
|
|
721
|
+
|
|
722
|
+
|
|
691
723
|
def _exec_source(source: str, ns: dict) -> None:
|
|
692
724
|
"""Compile + execute ``source``; if the last node is an expression, route
|
|
693
|
-
its value through ``__omp_display`` so dataframes/figures render rich.
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
raise
|
|
725
|
+
its value through ``__omp_display`` so dataframes/figures render rich.
|
|
726
|
+
Top-level ``await`` / ``async for`` / ``async with`` is permitted; the
|
|
727
|
+
cell is driven through the runner's persistent event loop."""
|
|
728
|
+
module = ast.parse(source, mode="exec")
|
|
698
729
|
|
|
699
730
|
if not module.body:
|
|
700
731
|
return
|
|
@@ -704,16 +735,16 @@ def _exec_source(source: str, ns: dict) -> None:
|
|
|
704
735
|
body_module = ast.Module(body=module.body[:-1], type_ignores=[])
|
|
705
736
|
expr_module = ast.Expression(body=last.value)
|
|
706
737
|
ast.copy_location(expr_module, last)
|
|
707
|
-
body_code = compile(body_module, "<cell>", "exec")
|
|
708
|
-
expr_code = compile(expr_module, "<cell>", "eval")
|
|
709
|
-
|
|
710
|
-
value =
|
|
738
|
+
body_code = compile(body_module, "<cell>", "exec", flags=_TLA_FLAG)
|
|
739
|
+
expr_code = compile(expr_module, "<cell>", "eval", flags=_TLA_FLAG)
|
|
740
|
+
_run_compiled(body_code, ns, want_value=False)
|
|
741
|
+
value = _run_compiled(expr_code, ns, want_value=True)
|
|
711
742
|
if value is not None:
|
|
712
743
|
__omp_display(value, kind="result")
|
|
713
744
|
return
|
|
714
745
|
|
|
715
|
-
code = compile(module, "<cell>", "exec")
|
|
716
|
-
|
|
746
|
+
code = compile(module, "<cell>", "exec", flags=_TLA_FLAG)
|
|
747
|
+
_run_compiled(code, ns, want_value=False)
|
|
717
748
|
|
|
718
749
|
|
|
719
750
|
# ---------------------------------------------------------------------------
|
package/src/eval/py/runtime.ts
CHANGED
|
@@ -151,6 +151,7 @@ export function filterEnv(env: Record<string, string | undefined>): Record<strin
|
|
|
151
151
|
*/
|
|
152
152
|
export function resolveVenvPath(cwd: string): string | undefined {
|
|
153
153
|
if ($env.VIRTUAL_ENV) return $env.VIRTUAL_ENV;
|
|
154
|
+
if ($env.CONDA_PREFIX) return $env.CONDA_PREFIX;
|
|
154
155
|
const candidates = [path.join(cwd, ".venv"), path.join(cwd, "venv")];
|
|
155
156
|
for (const candidate of candidates) {
|
|
156
157
|
if (fs.existsSync(candidate)) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper for wiring the `getCommands` action of {@link ExtensionAPI}.
|
|
3
|
+
*
|
|
4
|
+
* Centralizes the union over the three slash-command sources the runtime
|
|
5
|
+
* exposes so the five wiring sites (interactive UI, ACP, RPC, print, child
|
|
6
|
+
* task executor) cannot drift:
|
|
7
|
+
* - extension-registered hook commands (`source: "extension"`)
|
|
8
|
+
* - prompt commands loaded as `LoadedCustomCommand` — user/project/bundled
|
|
9
|
+
* custom commands and MCP prompts (`source: "prompt"`)
|
|
10
|
+
* - skill commands derived from `session.skills`, gated on
|
|
11
|
+
* `skillsSettings.enableSkillCommands` (`source: "skill"`)
|
|
12
|
+
*
|
|
13
|
+
* Built-in slash commands are intentionally excluded; `getCommands()` is the
|
|
14
|
+
* surface extensions use to discover dynamic commands they did not register
|
|
15
|
+
* themselves. Each frontend (interactive-mode, ACP) prepends its own builtins.
|
|
16
|
+
*/
|
|
17
|
+
import type { SkillsSettings } from "../../config/settings";
|
|
18
|
+
import type { CustomCommandSource, LoadedCustomCommand } from "../custom-commands";
|
|
19
|
+
import { getSkillSlashCommandName, type Skill } from "../skills";
|
|
20
|
+
import type { SlashCommandInfo, SlashCommandLocation } from "../slash-commands";
|
|
21
|
+
import type { ExtensionRunner } from "./runner";
|
|
22
|
+
|
|
23
|
+
interface CommandsCapableSession {
|
|
24
|
+
readonly extensionRunner?: ExtensionRunner;
|
|
25
|
+
readonly customCommands: ReadonlyArray<LoadedCustomCommand>;
|
|
26
|
+
readonly skills: ReadonlyArray<Skill>;
|
|
27
|
+
readonly skillsSettings?: SkillsSettings;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getSessionSlashCommands(session: CommandsCapableSession): SlashCommandInfo[] {
|
|
31
|
+
const out: SlashCommandInfo[] = [];
|
|
32
|
+
|
|
33
|
+
const runner = session.extensionRunner;
|
|
34
|
+
if (runner) {
|
|
35
|
+
for (const cmd of runner.getRegisteredCommands()) {
|
|
36
|
+
out.push({
|
|
37
|
+
name: cmd.name,
|
|
38
|
+
description: cmd.description,
|
|
39
|
+
source: "extension",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const cmd of session.customCommands) {
|
|
45
|
+
out.push({
|
|
46
|
+
name: cmd.command.name,
|
|
47
|
+
description: cmd.command.description,
|
|
48
|
+
source: "prompt",
|
|
49
|
+
location: customCommandLocation(cmd.source),
|
|
50
|
+
path: cmd.resolvedPath,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (session.skillsSettings?.enableSkillCommands) {
|
|
55
|
+
for (const skill of session.skills) {
|
|
56
|
+
out.push({
|
|
57
|
+
name: getSkillSlashCommandName(skill),
|
|
58
|
+
description: skill.description || undefined,
|
|
59
|
+
source: "skill",
|
|
60
|
+
path: skill.filePath,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function customCommandLocation(source: CustomCommandSource): SlashCommandLocation | undefined {
|
|
69
|
+
switch (source) {
|
|
70
|
+
case "user":
|
|
71
|
+
return "user";
|
|
72
|
+
case "project":
|
|
73
|
+
return "project";
|
|
74
|
+
case "bundled":
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -3,21 +3,46 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as url from "node:url";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
// Canonical scope for in-process pi packages. Plugins published against any of
|
|
7
|
+
// the aliased scopes below (mariozechner's original publish, earendil-works'
|
|
8
|
+
// fork, or the canonical @oh-my-pi scope itself) are remapped to this scope and
|
|
9
|
+
// resolved against the bundled copy that ships inside the omp binary. This
|
|
10
|
+
// keeps plugins running against the exact runtime state of the host (single
|
|
11
|
+
// module registry, single tool registry, etc.) regardless of which historical
|
|
12
|
+
// scope name they happened to declare in their peerDependencies.
|
|
13
|
+
const CANONICAL_PI_SCOPE = "@oh-my-pi";
|
|
14
|
+
|
|
15
|
+
// Scopes that have historically been used to publish (or alias) the same set
|
|
16
|
+
// of internal pi-* packages. `@oh-my-pi` is intentionally included so that
|
|
17
|
+
// direct imports of the canonical name still flow through `Bun.resolveSync`
|
|
18
|
+
// against the host binary, avoiding a duplicate copy being pulled in from a
|
|
19
|
+
// plugin's own node_modules tree at install time.
|
|
20
|
+
const PI_SCOPE_ALIASES = ["oh-my-pi", "mariozechner", "earendil-works"] as const;
|
|
21
|
+
|
|
22
|
+
// Internal pi-* package basenames bundled inside the omp binary.
|
|
23
|
+
const PI_PACKAGE_NAMES = ["pi-agent-core", "pi-ai", "pi-coding-agent", "pi-natives", "pi-tui", "pi-utils"] as const;
|
|
24
|
+
|
|
25
|
+
const PI_SCOPE_ALTERNATION = PI_SCOPE_ALIASES.join("|");
|
|
26
|
+
const PI_PACKAGE_ALTERNATION = PI_PACKAGE_NAMES.join("|");
|
|
27
|
+
|
|
28
|
+
// Upstream `@mariozechner/*` packages exposed a few subpaths at the package
|
|
29
|
+
// root that we relocated under a different folder. Each entry rewrites
|
|
30
|
+
// `<pkg>/<from>` → `<pkg>/<to>` after the scope has been canonicalised, so
|
|
31
|
+
// plugins importing the upstream layout still resolve to a real file in our
|
|
32
|
+
// bundled copy. Add new entries as `pkg/from -> pkg/to` whenever a plugin
|
|
33
|
+
// surfaces another upstream-only subpath that breaks resolution.
|
|
34
|
+
const PI_SUBPATH_REMAPS: ReadonlyMap<string, string> = new Map<string, string>([
|
|
35
|
+
// `@mariozechner/pi-ai/oauth` re-exported `./utils/oauth/index.js`.
|
|
36
|
+
// Our pi-ai keeps the implementation under `utils/oauth` but never added a
|
|
37
|
+
// root-level re-export, so map the upstream subpath onto it directly.
|
|
38
|
+
["pi-ai/oauth", "pi-ai/utils/oauth"],
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const LEGACY_PI_SPECIFIER_FILTER = new RegExp(`^@(?:${PI_SCOPE_ALTERNATION})/(?:${PI_PACKAGE_ALTERNATION})(?:/.*)?$`);
|
|
42
|
+
const LEGACY_PI_IMPORT_SPECIFIER_REGEX = new RegExp(
|
|
43
|
+
`((?:from\\s+|import\\s*\\(\\s*)["'])(@(?:${PI_SCOPE_ALTERNATION})/(?:${PI_PACKAGE_ALTERNATION})(?:/[^"'()\\s]+)?)(["'])`,
|
|
44
|
+
"g",
|
|
45
|
+
);
|
|
21
46
|
const LEGACY_PI_FILE_PREFIX = "omp-legacy-pi-file:";
|
|
22
47
|
const LEGACY_PI_FILE_NAMESPACE = "omp-legacy-pi-file";
|
|
23
48
|
const resolvedSpecifierFallbacks = new Map<string, string>();
|
|
@@ -25,25 +50,17 @@ const resolvedSpecifierFallbacks = new Map<string, string>();
|
|
|
25
50
|
let isLegacyPiSpecifierShimInstalled = false;
|
|
26
51
|
|
|
27
52
|
function remapLegacyPiSpecifier(specifier: string): string | null {
|
|
28
|
-
|
|
29
|
-
const legacyPackageName = `${legacyScope}/${packageName}`;
|
|
30
|
-
const mappedPackageName = LEGACY_PI_PACKAGE_MAP[legacyPackageName as keyof typeof LEGACY_PI_PACKAGE_MAP];
|
|
31
|
-
if (!mappedPackageName) {
|
|
53
|
+
if (!LEGACY_PI_SPECIFIER_FILTER.test(specifier)) {
|
|
32
54
|
return null;
|
|
33
55
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const subpath = subpathParts.join("/");
|
|
39
|
-
if (legacyPackageName === "@mariozechner/pi-coding-agent") {
|
|
40
|
-
return (
|
|
41
|
-
LEGACY_PI_CODING_AGENT_SUBPATH_MAP[subpath as keyof typeof LEGACY_PI_CODING_AGENT_SUBPATH_MAP] ??
|
|
42
|
-
`${mappedPackageName}/${subpath}`
|
|
43
|
-
);
|
|
56
|
+
const slashIdx = specifier.indexOf("/", 1);
|
|
57
|
+
// Filter guarantees a slash exists, but guard anyway to keep the type narrow.
|
|
58
|
+
if (slashIdx === -1) {
|
|
59
|
+
return null;
|
|
44
60
|
}
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
const rest = specifier.slice(slashIdx + 1);
|
|
62
|
+
const remappedSubpath = PI_SUBPATH_REMAPS.get(rest) ?? rest;
|
|
63
|
+
return `${CANONICAL_PI_SCOPE}/${remappedSubpath}`;
|
|
47
64
|
}
|
|
48
65
|
|
|
49
66
|
function getResolvedSpecifier(specifier: string): string {
|
package/src/hashline/input.ts
CHANGED
|
@@ -108,7 +108,8 @@ export function splitHashlineInputs(input: string, options: SplitHashlineOptions
|
|
|
108
108
|
|
|
109
109
|
const flush = () => {
|
|
110
110
|
if (currentPath.length === 0) return;
|
|
111
|
-
|
|
111
|
+
const hasOps = currentLines.some(rawLine => stripTrailingCarriageReturn(rawLine).trim().length > 0);
|
|
112
|
+
if (hasOps) sections.push({ path: currentPath, diff: currentLines.join("\n") });
|
|
112
113
|
currentLines = [];
|
|
113
114
|
};
|
|
114
115
|
|
package/src/hashline/parser.ts
CHANGED
|
@@ -86,9 +86,33 @@ function collectPayload(
|
|
|
86
86
|
let index = startIndex;
|
|
87
87
|
while (index < lines.length) {
|
|
88
88
|
const line = stripTrailingCarriageReturn(lines[index]);
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
if (line.startsWith(HL_EDIT_SEP)) {
|
|
90
|
+
payload.push(line.slice(1));
|
|
91
|
+
index++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Silently recover from a missing payload prefix on an otherwise blank
|
|
95
|
+
// line: if more payload follows (possibly past further blanks), treat
|
|
96
|
+
// each intervening blank as an empty `${HL_EDIT_SEP}` payload line.
|
|
97
|
+
// Additionally, when the op explicitly requires payload (`+`/`<`) and
|
|
98
|
+
// we have not collected any yet, accept the blank(s) themselves as the
|
|
99
|
+
// empty payload — common typo of forgetting the `${HL_EDIT_SEP}` prefix
|
|
100
|
+
// when inserting a blank line.
|
|
101
|
+
if (line.length === 0) {
|
|
102
|
+
let lookahead = index + 1;
|
|
103
|
+
while (lookahead < lines.length && stripTrailingCarriageReturn(lines[lookahead]).length === 0) {
|
|
104
|
+
lookahead++;
|
|
105
|
+
}
|
|
106
|
+
const followedByPayload =
|
|
107
|
+
lookahead < lines.length && stripTrailingCarriageReturn(lines[lookahead]).startsWith(HL_EDIT_SEP);
|
|
108
|
+
const acceptBareBlank = requirePayload && payload.length === 0;
|
|
109
|
+
if (followedByPayload || acceptBareBlank) {
|
|
110
|
+
for (let j = index; j < lookahead; j++) payload.push("");
|
|
111
|
+
index = lookahead;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
92
116
|
}
|
|
93
117
|
if (payload.length === 0 && requirePayload) {
|
|
94
118
|
throw new Error(`line ${opLineNum}: + and < operations require at least one ${HL_EDIT_SEP}TEXT payload line.`);
|