@oh-my-pi/pi-coding-agent 14.9.3 → 14.9.7

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 (108) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/package.json +7 -7
  3. package/src/async/job-manager.ts +66 -9
  4. package/src/capability/rule.ts +20 -0
  5. package/src/cli/setup-cli.ts +14 -161
  6. package/src/cli/stats-cli.ts +56 -2
  7. package/src/cli.ts +0 -1
  8. package/src/config/model-registry.ts +13 -0
  9. package/src/config/model-resolver.ts +8 -2
  10. package/src/config/settings-schema.ts +1 -11
  11. package/src/edit/index.ts +8 -0
  12. package/src/edit/renderer.ts +6 -1
  13. package/src/edit/streaming.ts +53 -2
  14. package/src/eval/eval.lark +30 -10
  15. package/src/eval/js/context-manager.ts +334 -601
  16. package/src/eval/js/shared/helpers.ts +237 -0
  17. package/src/eval/js/shared/indirect-eval.ts +30 -0
  18. package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -2
  19. package/src/eval/js/shared/rewrite-imports.ts +211 -0
  20. package/src/eval/js/shared/runtime.ts +168 -0
  21. package/src/eval/js/shared/types.ts +18 -0
  22. package/src/eval/js/tool-bridge.ts +2 -4
  23. package/src/eval/js/worker-core.ts +146 -0
  24. package/src/eval/js/worker-entry.ts +24 -0
  25. package/src/eval/js/worker-protocol.ts +41 -0
  26. package/src/eval/parse.ts +218 -49
  27. package/src/eval/py/display.ts +71 -0
  28. package/src/eval/py/executor.ts +97 -96
  29. package/src/eval/py/index.ts +2 -2
  30. package/src/eval/py/kernel.ts +472 -900
  31. package/src/eval/py/prelude.py +106 -87
  32. package/src/eval/py/runner.py +879 -0
  33. package/src/eval/py/runtime.ts +3 -16
  34. package/src/eval/py/tool-bridge.ts +137 -0
  35. package/src/export/html/template.css +12 -0
  36. package/src/export/html/template.generated.ts +1 -1
  37. package/src/export/html/template.js +113 -7
  38. package/src/extensibility/plugins/loader.ts +31 -6
  39. package/src/extensibility/skills.ts +20 -0
  40. package/src/internal-urls/agent-protocol.ts +63 -52
  41. package/src/internal-urls/artifact-protocol.ts +51 -51
  42. package/src/internal-urls/docs-index.generated.ts +35 -3
  43. package/src/internal-urls/index.ts +6 -19
  44. package/src/internal-urls/local-protocol.ts +49 -7
  45. package/src/internal-urls/mcp-protocol.ts +2 -8
  46. package/src/internal-urls/memory-protocol.ts +89 -59
  47. package/src/internal-urls/router.ts +38 -22
  48. package/src/internal-urls/rule-protocol.ts +2 -20
  49. package/src/internal-urls/skill-protocol.ts +4 -27
  50. package/src/main.ts +1 -1
  51. package/src/mcp/manager.ts +17 -0
  52. package/src/modes/components/session-observer-overlay.ts +2 -2
  53. package/src/modes/components/tool-execution.ts +6 -0
  54. package/src/modes/components/tree-selector.ts +4 -0
  55. package/src/modes/controllers/command-controller.ts +0 -23
  56. package/src/modes/controllers/event-controller.ts +23 -2
  57. package/src/modes/controllers/mcp-command-controller.ts +7 -10
  58. package/src/modes/interactive-mode.ts +2 -2
  59. package/src/modes/theme/theme.ts +27 -27
  60. package/src/modes/types.ts +1 -1
  61. package/src/modes/utils/ui-helpers.ts +14 -9
  62. package/src/prompts/commands/orchestrate.md +1 -0
  63. package/src/prompts/system/project-prompt.md +10 -2
  64. package/src/prompts/system/subagent-system-prompt.md +8 -8
  65. package/src/prompts/system/system-prompt.md +13 -7
  66. package/src/prompts/tools/ask.md +0 -1
  67. package/src/prompts/tools/bash.md +0 -10
  68. package/src/prompts/tools/eval.md +15 -30
  69. package/src/prompts/tools/github.md +6 -5
  70. package/src/prompts/tools/hashline.md +1 -0
  71. package/src/prompts/tools/job.md +14 -6
  72. package/src/prompts/tools/task.md +20 -3
  73. package/src/registry/agent-registry.ts +2 -1
  74. package/src/sdk.ts +87 -89
  75. package/src/session/agent-session.ts +58 -21
  76. package/src/session/artifacts.ts +7 -4
  77. package/src/session/history-storage.ts +77 -19
  78. package/src/session/session-manager.ts +30 -1
  79. package/src/ssh/connection-manager.ts +32 -16
  80. package/src/ssh/sshfs-mount.ts +10 -7
  81. package/src/system-prompt.ts +0 -5
  82. package/src/task/executor.ts +14 -2
  83. package/src/task/index.ts +19 -5
  84. package/src/tool-discovery/tool-index.ts +21 -8
  85. package/src/tools/ast-edit.ts +3 -2
  86. package/src/tools/ast-grep.ts +3 -2
  87. package/src/tools/bash.ts +15 -9
  88. package/src/tools/browser/tab-protocol.ts +4 -0
  89. package/src/tools/browser/tab-supervisor.ts +98 -7
  90. package/src/tools/browser/tab-worker.ts +104 -58
  91. package/src/tools/eval.ts +49 -11
  92. package/src/tools/fetch.ts +1 -1
  93. package/src/tools/gh.ts +140 -4
  94. package/src/tools/index.ts +12 -11
  95. package/src/tools/job.ts +48 -12
  96. package/src/tools/read.ts +5 -4
  97. package/src/tools/search.ts +3 -2
  98. package/src/tools/todo-write.ts +1 -1
  99. package/src/web/scrapers/mastodon.ts +1 -1
  100. package/src/web/scrapers/repology.ts +7 -7
  101. package/src/web/search/index.ts +6 -4
  102. package/src/cli/jupyter-cli.ts +0 -106
  103. package/src/commands/jupyter.ts +0 -32
  104. package/src/eval/py/cancellation.ts +0 -28
  105. package/src/eval/py/gateway-coordinator.ts +0 -424
  106. package/src/internal-urls/jobs-protocol.ts +0 -120
  107. package/src/prompts/system/now-prompt.md +0 -7
  108. /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,6 +1,102 @@
1
1
  # Changelog
2
2
 
3
3
  ## [Unreleased]
4
+ ### Breaking Changes
5
+
6
+ - Changed the `eval` tool input format to a single-line `*** Cell <lang>:"<title>" [t:<duration>] [rst]` header per cell, replacing the `*** Begin <LANG>` / `*** End <LANG>` envelope and the standalone `*** Title:` / `*** Timeout:` / `*** Reset` directives. The lark grammar enforces a fixed attribute order; the runtime parser remains lenient (alias keys, bare positional tokens, single-quoted titles).
7
+
8
+ ### Added
9
+
10
+ - Added support for explicit boolean `rst` values (`rst:true`, `rst:false`, `rst:1`, `rst:0`, `rst:yes`, `rst:no`, `rst:on`, `rst:off`) in `*** Cell` headers
11
+
12
+ ### Changed
13
+
14
+ - Changed the HTML transcript renderer to parse the new `*** Cell` headers while keeping the older `*** Begin <LANG>` and `===== ... =====` formats renderable for historical sessions.
15
+ - Changed the `eval` tool parser so a stray non-marker line between cells no longer crashes with `null is not an object (evaluating 'BEGIN_RE.exec(lines[i])[1]')`; stray content is consumed without aborting parsing.
16
+ - Changed `*** End` to be an optional, undocumented per-cell terminator (kept in the lark to satisfy GPT-trained models' natural terminator habit during constrained sampling).
17
+
18
+ ### Fixed
19
+
20
+ - Improved `*** Cell` header parsing to reject invalid `rst` values with a clear `invalid rst value` error
21
+
22
+ ## [14.9.7] - 2026-05-12
23
+
24
+ ### Breaking Changes
25
+
26
+ - Changed the `timeoutMs` execution option to no longer be enforced during worker-based JS runs, so callers must rely on external cancellation signals for time limits
27
+ - Replaced the Jupyter kernel gateway + WebSocket protocol behind the Python `eval` backend with a subprocess-backed runner that speaks NDJSON over stdin/stdout; removed the `jupyter_kernel_gateway`/`ipykernel` pip dependencies, the `python.sharedGateway` setting, the `omp jupyter` CLI command, and the `PI_PYTHON_GATEWAY_URL` / `PI_PYTHON_GATEWAY_TOKEN` environment variables
28
+
29
+ ### Added
30
+
31
+ - Added Python `tool.<name>(args)` support to `executePython` sessions so evaluated Python code can invoke session tools through the prelude `tool` proxy
32
+ - Added per-execution Python tool bridge session registration and loopback endpoint wiring so Python tool calls resolve to host tools and return tool results
33
+ - Added status-event forwarding for Python tool bridge calls so `tool` invocations can emit execution status updates
34
+ - Added browser-tab JavaScript execution through the shared runtime so tab runs now expose the standard helper globals (`read`, `write`, `sort`, `uniq`, `counter`, `diff`, `tree`, `env`, `output`, `display`, and `tool`)
35
+ - Added static ESM `import` support to browser-tab JavaScript by rewriting top-level imports and resolving them against the tab session context
36
+ - Added substring fallback matching to `HistoryStorage.search` so infix and short-token queries that FTS5 prefix matching misses are still returned
37
+ - Added a live single-line sync progress display to the stats command showing current/total sessions while syncing
38
+ - Added automatic inline JS evaluation fallback when worker creation failed so script execution still works in environments without worker support
39
+
40
+ ### Changed
41
+
42
+ - Changed `setup python` to only verify a reachable Python 3 interpreter instead of installing Jupyter dependencies
43
+ - Changed `info` output to remove the obsolete Python Gateway status block now that shared gateway management is no longer available
44
+ - Changed JavaScript execution in `executeJs` to expose the worker\u2019s real `process` object instead of a restricted, frozen subset
45
+ - Changed JavaScript evaluation to run per session in a worker-backed runner with explicit initialization and teardown handling
46
+ - Changed the Python backend to launch one `python -u runner.py` subprocess per kernel; cancellation now sends `SIGINT` which raises a real `KeyboardInterrupt` in user code, and the same subprocess is reused across cells in session mode
47
+ - Changed Python magic handling so `%pip`, `%cd`, `%env`, `%pwd`, `%ls`, `%time`, `%timeit`, `%who`, `%reset`, `%load`, `%run`, `%%bash`, `%%capture`, `%%timeit`, `%%writefile`, and `!shell` work without depending on IPython
48
+
49
+ ### Fixed
50
+
51
+ - Fixed Python output rendering so `text/markdown` takes precedence over `text/plain` and status bundles are emitted as status updates rather than plain text
52
+ - Fixed query tokenization in `HistoryStorage.search` so punctuation-delimited terms like `git-commit` are aligned with indexing and matched correctly
53
+ - Fixed history search result merging to de-duplicate matches and return full-text matches before substring-only matches while still respecting the requested limit
54
+ - Fixed JS run cancellation so aborting a run now also cancels in-flight tool calls and terminates the active worker session
55
+ - Fixed top-level `const`, `let`, and `class` declarations in evaluated JavaScript to persist across subsequent runs by rewriting top-level declarations
56
+
57
+ ## [14.9.5] - 2026-05-12
58
+ ### Breaking Changes
59
+
60
+ - Removed the `jobs://` internal URL protocol; inspect background jobs via the `job` tool's `list: true` operation instead
61
+
62
+ ### Added
63
+
64
+ - Added `since` and `until` date-range filters to `search_issues`, `search_prs`, `search_commits`, and `search_repos`, accepting relative durations (`m`/`h`/`d`/`w`/`mo`/`y`), ISO dates, and ISO datetimes
65
+ - Added `dateField` support for date filtering (`created` or `updated`) so search results can be constrained by creation, update, pushed (for repos), or committer date (for commits)
66
+ - Added owner-based scoping to async job registration and queries so background jobs can be registered with an `ownerId` and filtered per agent in `getRunningJobs`, `getRecentJobs`, `getAllJobs`, and `cancelAll`
67
+ - Added agent ownership metadata to async jobs started by `task` and `bash` tools so their lifecycle and cancellation is attributed to the creating agent
68
+ - Added `list: true` operation to the `job` tool, returning an immediate snapshot of every job spawned by the calling agent without waiting (replaces the deleted `jobs://` URL)
69
+ - Added per-agent visibility scoping to the `job` tool so `list`, `poll`, and `cancel` only see and act on jobs owned by the calling agent; cross-agent operations now return `not_found`
70
+
71
+ ### Changed
72
+
73
+ - Changed `search_issues`, `search_prs`, `search_commits`, and `search_repos` to allow date-only queries where `query` is omitted if `since`/`until` is provided
74
+ - Changed `search_code` to return a validation error when `since`/`until` is supplied because GitHub code search does not support date qualifiers
75
+ - Changed async job manager ownership so subagents inherit the parent session’s global `AsyncJobManager` instead of creating and owning separate instances
76
+ - Changed session lifecycle cleanup so the global async-job manager is disposed only by the owning top-level session
77
+ - Changed subagent session switches and handoff paths to stop global async-job cancellation and cancel only jobs owned by that session
78
+ - Changed `agent://` and `artifact://` URL resolution to search artifact outputs across all active sessions instead of only the current session, allowing parent and subagent sessions to read each other’s generated outputs by ID
79
+ - Changed `memory://` URL resolution to walk all active sessions’ memory roots and return the first matching file, so worktree-based subagents can access their own memory views as well as shared roots
80
+ - Changed internal URL routing to use a shared process-global `InternalUrlRouter` and protocol handlers, so built-in tools resolve `agent://`, `artifact://`, `memory://`, `skill://`, `rule://`, `mcp://`, and `local://` URLs without requiring session-specific router wiring
81
+ - Changed `mcp://` handler to use the globally registered MCP manager so MCP resource links work for agents sharing session context
82
+
83
+ ### Changed
84
+
85
+ - Changed the `ask.timeout` default from `30` (seconds) to `0` (wait indefinitely). Auto-selecting the recommended option after a fixed delay was surprising users mid-deliberation; the timer is now strictly opt-in. The legacy auto-select behavior is preserved when `ask.timeout` is set to a non-zero value, and the `ask` tool's prompt has been updated so the model expects unlimited reply time by default.
86
+
87
+ ### Fixed
88
+
89
+ - Added `ModelRegistry.hasConfiguredAuth(model)` to mirror the upstream `@mariozechner/pi-coding-agent` API surface; external plugins and downstream wrappers that pre-flight auth before launching a subagent no longer crash with `this._modelRegistry.hasConfiguredAuth is not a function` on the direct agent-launch path. ([#993](https://github.com/can1357/oh-my-pi/issues/993))
90
+ - Fixed an ESM circular-import TDZ that crashed test suites when modules from the `task/` and `tools/` graphs were evaluated together (e.g. `executor-warnings.test.ts` + `task-simple-mode.test.ts`) by deferring `BUILTIN_TOOLS.task`'s `TaskTool.create` dereference to factory-call time and sourcing `truncateTail` from `session/streaming-output` instead of the `tools/` barrel
91
+ - Treat keyless-by-design providers (llama.cpp, ollama, lm-studio) as authenticated in subagent model resolution; fixes silent fallback to parent remote model when a local model is configured. ([#1008](https://github.com/can1357/oh-my-pi/issues/1008))
92
+ - Fixed subagent disposal and session transitions that previously canceled all running async jobs, preventing inadvertent termination of a parent agent’s background work
93
+ - Fixed multi-entry edits silently rendering a fake success when every entry failed (e.g. all hit the auto-generated guard), by surfacing `isError: true` from the single-path edit orchestrator so the renderer takes the error branch instead of falling through to the streaming-preview fallback that displays the *proposed* diff
94
+ - Fixed the auto-generated streaming guard being gated behind `edit.streamingAbort` (default false), so it now pre-empts streaming edit tool calls targeting auto-generated files regardless of that setting
95
+ - Fixed subagents launched in the same parallel batch not seeing each other in their initial `# IRC Peers` system-prompt block by pre-registering the agent in the global `AgentRegistry` before `rebuildSystemPrompt` runs and attaching the live session afterwards
96
+ - Fixed plugin manifest extensions whose entry points at a directory (e.g. `pi-goal`'s `"pi": { "extensions": [".pi/extensions/pi-goal"] }`) failing to load with `Failed to load extension: Directories cannot be read like files`. The plugin path resolver now resolves directory entries to their `index.{ts,js,mjs,cjs}` file, matching the behavior of native auto-discovery via `resolveExtensionEntries`.
97
+ - Fixed the SSH tool on native Windows by avoiding OpenSSH ControlMaster multiplexing, which Win32-OpenSSH does not support and reports as `getsockname failed` ([#154](https://github.com/can1357/oh-my-pi/issues/154)).
98
+ - Fixed `/export` and `/tree` not showing developer-role messages (including the plan content injected after `/plan` approval) so the HTML export and TUI session tree now render developer messages dimmed with their actual content instead of hiding them entirely ([#753](https://github.com/can1357/oh-my-pi/issues/753))
99
+ - Fixed `Timed out initializing browser tab worker` on prebuilt binaries by rewriting `spawnTabWorker` to import the worker entry with `with { type: "file" }` so Bun's `--compile` bundler statically discovers and embeds `tab-worker-entry.ts` in the single-file binary ([#1011](https://github.com/can1357/oh-my-pi/issues/1011))
4
100
 
5
101
  ## [14.9.3] - 2026-05-10
6
102
  ### Breaking Changes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "14.9.3",
4
+ "version": "14.9.7",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -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": "14.9.3",
51
- "@oh-my-pi/pi-agent-core": "14.9.3",
52
- "@oh-my-pi/pi-ai": "14.9.3",
53
- "@oh-my-pi/pi-natives": "14.9.3",
54
- "@oh-my-pi/pi-tui": "14.9.3",
55
- "@oh-my-pi/pi-utils": "14.9.3",
50
+ "@oh-my-pi/omp-stats": "14.9.7",
51
+ "@oh-my-pi/pi-agent-core": "14.9.7",
52
+ "@oh-my-pi/pi-ai": "14.9.7",
53
+ "@oh-my-pi/pi-natives": "14.9.7",
54
+ "@oh-my-pi/pi-tui": "14.9.7",
55
+ "@oh-my-pi/pi-utils": "14.9.7",
56
56
  "@puppeteer/browsers": "^2.13.0",
57
57
  "@sinclair/typebox": "^0.34.49",
58
58
  "@types/turndown": "5.0.6",
@@ -16,6 +16,13 @@ export interface AsyncJob {
16
16
  promise: Promise<void>;
17
17
  resultText?: string;
18
18
  errorText?: string;
19
+ /**
20
+ * Registry id of the agent that registered the job (e.g. "0-Main",
21
+ * "3-AuthLoader"). Used by scoped cancel/list APIs so a subagent's teardown
22
+ * does not cancel its parent's jobs. Undefined for callers that don't
23
+ * supply an id (e.g. legacy tests, SDK consumers without an agent context).
24
+ */
25
+ ownerId?: string;
19
26
  }
20
27
 
21
28
  export interface AsyncJobManagerOptions {
@@ -41,10 +48,38 @@ export interface AsyncJobDeliveryState {
41
48
 
42
49
  export interface AsyncJobRegisterOptions {
43
50
  id?: string;
51
+ /** Registry id of the agent that owns this job; used to scope cancelAll. */
52
+ ownerId?: string;
44
53
  onProgress?: (text: string, details?: Record<string, unknown>) => void | Promise<void>;
45
54
  }
46
55
 
56
+ /**
57
+ * Filter applied to job query/cancel APIs. With `ownerId`, results are
58
+ * restricted to jobs registered by that agent (registry id from
59
+ * `AgentRegistry`, e.g. "0-Main", "3-AuthLoader").
60
+ */
61
+ export interface AsyncJobFilter {
62
+ ownerId?: string;
63
+ }
64
+
47
65
  export class AsyncJobManager {
66
+ static #instance: AsyncJobManager | undefined;
67
+
68
+ /** Process-global instance shared by internal URL protocol handlers and tools. */
69
+ static instance(): AsyncJobManager | undefined {
70
+ return AsyncJobManager.#instance;
71
+ }
72
+
73
+ /** Install or clear the process-global instance. */
74
+ static setInstance(value: AsyncJobManager | undefined): void {
75
+ AsyncJobManager.#instance = value;
76
+ }
77
+
78
+ /** Reset the process-global instance. Test-only. */
79
+ static resetForTests(): void {
80
+ AsyncJobManager.#instance = undefined;
81
+ }
82
+
48
83
  readonly #jobs = new Map<string, AsyncJob>();
49
84
  readonly #deliveries: AsyncJobDelivery[] = [];
50
85
  readonly #suppressedDeliveries = new Set<string>();
@@ -56,6 +91,16 @@ export class AsyncJobManager {
56
91
  #deliveryLoop: Promise<void> | undefined;
57
92
  #disposed = false;
58
93
 
94
+ #filterJobs(jobs: Iterable<AsyncJob>, filter?: AsyncJobFilter): AsyncJob[] {
95
+ const ownerId = filter?.ownerId;
96
+ if (!ownerId) return Array.from(jobs);
97
+ const out: AsyncJob[] = [];
98
+ for (const job of jobs) {
99
+ if (job.ownerId === ownerId) out.push(job);
100
+ }
101
+ return out;
102
+ }
103
+
59
104
  constructor(options: AsyncJobManagerOptions) {
60
105
  this.#onJobComplete = options.onJobComplete;
61
106
  this.#maxRunningJobs = Math.max(1, Math.floor(options.maxRunningJobs ?? DEFAULT_MAX_RUNNING_JOBS));
@@ -95,6 +140,7 @@ export class AsyncJobManager {
95
140
  label,
96
141
  abortController,
97
142
  promise: Promise.resolve(),
143
+ ownerId: options?.ownerId,
98
144
  };
99
145
 
100
146
  const reportProgress = async (text: string, details?: Record<string, unknown>): Promise<void> => {
@@ -138,9 +184,15 @@ export class AsyncJobManager {
138
184
  return id;
139
185
  }
140
186
 
141
- cancel(id: string): boolean {
187
+ /**
188
+ * Cancel a single job by id. When `filter.ownerId` is set and does not
189
+ * match the job's owner, the call is treated as not-found (returns false)
190
+ * so cross-agent cancellation is rejected at the manager level.
191
+ */
192
+ cancel(id: string, filter?: AsyncJobFilter): boolean {
142
193
  const job = this.#jobs.get(id);
143
194
  if (!job) return false;
195
+ if (filter?.ownerId && job.ownerId !== filter.ownerId) return false;
144
196
  if (job.status !== "running") return false;
145
197
  job.status = "cancelled";
146
198
  job.abortController.abort();
@@ -152,19 +204,19 @@ export class AsyncJobManager {
152
204
  return this.#jobs.get(id);
153
205
  }
154
206
 
155
- getRunningJobs(): AsyncJob[] {
156
- return Array.from(this.#jobs.values()).filter(job => job.status === "running");
207
+ getRunningJobs(filter?: AsyncJobFilter): AsyncJob[] {
208
+ return this.#filterJobs(this.#jobs.values(), filter).filter(job => job.status === "running");
157
209
  }
158
210
 
159
- getRecentJobs(limit = 10): AsyncJob[] {
160
- return Array.from(this.#jobs.values())
211
+ getRecentJobs(limit = 10, filter?: AsyncJobFilter): AsyncJob[] {
212
+ return this.#filterJobs(this.#jobs.values(), filter)
161
213
  .filter(job => job.status !== "running")
162
214
  .sort((a, b) => b.startTime - a.startTime)
163
215
  .slice(0, limit);
164
216
  }
165
217
 
166
- getAllJobs(): AsyncJob[] {
167
- return Array.from(this.#jobs.values());
218
+ getAllJobs(filter?: AsyncJobFilter): AsyncJob[] {
219
+ return this.#filterJobs(this.#jobs.values(), filter);
168
220
  }
169
221
 
170
222
  getDeliveryState(): AsyncJobDeliveryState {
@@ -221,8 +273,13 @@ export class AsyncJobManager {
221
273
  return before - this.#deliveries.length;
222
274
  }
223
275
 
224
- cancelAll(): void {
225
- for (const job of this.getRunningJobs()) {
276
+ /**
277
+ * Cancel running jobs. With `filter.ownerId` set, cancels only jobs the
278
+ * matching agent registered; with no filter, cancels every running job
279
+ * (used by `dispose()` to nuke the manager's state).
280
+ */
281
+ cancelAll(filter?: AsyncJobFilter): void {
282
+ for (const job of this.getRunningJobs(filter)) {
226
283
  job.status = "cancelled";
227
284
  job.abortController.abort();
228
285
  this.#scheduleEviction(job.id);
@@ -209,6 +209,26 @@ export function parseRuleConditionAndScope(frontmatter: RuleFrontmatter): Pick<R
209
209
  };
210
210
  }
211
211
 
212
+ let activeRules: readonly Rule[] = [];
213
+
214
+ /**
215
+ * Process-global snapshot of rules the active session loaded.
216
+ * Read by internal URL protocol handlers (rule://).
217
+ */
218
+ export function getActiveRules(): readonly Rule[] {
219
+ return activeRules;
220
+ }
221
+
222
+ /** Replace the active rule snapshot. Called once per top-level session. */
223
+ export function setActiveRules(value: readonly Rule[]): void {
224
+ activeRules = value;
225
+ }
226
+
227
+ /** Reset the active rule snapshot. Test-only. */
228
+ export function resetActiveRulesForTests(): void {
229
+ activeRules = [];
230
+ }
231
+
212
232
  export const ruleCapability = defineCapability<Rule>({
213
233
  id: "rules",
214
234
  displayName: "Rules",
@@ -21,7 +21,6 @@ export interface SetupCommandArgs {
21
21
 
22
22
  const VALID_COMPONENTS: SetupComponent[] = ["python", "stt"];
23
23
 
24
- const PYTHON_PACKAGES = ["jupyter_kernel_gateway", "ipykernel"];
25
24
  const MANAGED_PYTHON_ENV = getPythonEnvDir();
26
25
 
27
26
  /**
@@ -65,10 +64,6 @@ export function parseSetupArgs(args: string[]): SetupCommandArgs | undefined {
65
64
  interface PythonCheckResult {
66
65
  available: boolean;
67
66
  pythonPath?: string;
68
- uvPath?: string;
69
- pipPath?: string;
70
- missingPackages: string[];
71
- installedPackages: string[];
72
67
  usingManagedEnv?: boolean;
73
68
  managedEnvPath?: string;
74
69
  }
@@ -85,8 +80,6 @@ function managedPythonPath(): string {
85
80
  async function checkPythonSetup(): Promise<PythonCheckResult> {
86
81
  const result: PythonCheckResult = {
87
82
  available: false,
88
- missingPackages: [],
89
- installedPackages: [],
90
83
  managedEnvPath: MANAGED_PYTHON_ENV,
91
84
  };
92
85
 
@@ -94,109 +87,24 @@ async function checkPythonSetup(): Promise<PythonCheckResult> {
94
87
  const managedPath = managedPythonPath();
95
88
  const hasManagedEnv = await Bun.file(managedPath).exists();
96
89
 
97
- result.uvPath = $which("uv") ?? undefined;
98
- result.pipPath = $which("pip3") ?? $which("pip") ?? undefined;
99
-
100
- const candidates = [systemPythonPath, hasManagedEnv ? managedPath : undefined].filter(
101
- (candidate): candidate is string => !!candidate,
102
- );
103
- if (candidates.length === 0) {
90
+ const pythonPath = systemPythonPath ?? (hasManagedEnv ? managedPath : undefined);
91
+ if (!pythonPath) {
104
92
  return result;
105
93
  }
106
-
107
- result.pythonPath = systemPythonPath ?? managedPath;
108
- let bestMatch = {
109
- pythonPath: candidates[0],
110
- missingPackages: [...PYTHON_PACKAGES],
111
- installedPackages: [] as string[],
112
- usingManagedEnv: candidates[0] === managedPath,
113
- };
114
-
115
- for (const pythonPath of candidates) {
116
- const installedPackages: string[] = [];
117
- const missingPackages: string[] = [];
118
- for (const pkg of PYTHON_PACKAGES) {
119
- const moduleName = pkg === "jupyter_kernel_gateway" ? "kernel_gateway" : pkg;
120
- const script = `import importlib.util; raise SystemExit(0 if importlib.util.find_spec('${moduleName}') else 1)`;
121
- const check = await $`${pythonPath} -c ${script}`.quiet().nothrow();
122
- if (check.exitCode === 0) {
123
- installedPackages.push(pkg);
124
- } else {
125
- missingPackages.push(pkg);
126
- }
127
- }
128
-
129
- if (missingPackages.length < bestMatch.missingPackages.length) {
130
- bestMatch = {
131
- pythonPath,
132
- missingPackages,
133
- installedPackages,
134
- usingManagedEnv: pythonPath === managedPath,
135
- };
136
- }
137
-
138
- if (missingPackages.length === 0) {
139
- result.available = true;
140
- result.pythonPath = pythonPath;
141
- result.missingPackages = missingPackages;
142
- result.installedPackages = installedPackages;
143
- result.usingManagedEnv = pythonPath === managedPath;
144
- return result;
145
- }
146
- }
147
-
148
- result.pythonPath = bestMatch.pythonPath;
149
- result.missingPackages = bestMatch.missingPackages;
150
- result.installedPackages = bestMatch.installedPackages;
151
- result.usingManagedEnv = bestMatch.usingManagedEnv;
94
+ const probe = await $`${pythonPath} -c "import sys;sys.exit(0)"`.quiet().nothrow();
95
+ result.pythonPath = pythonPath;
96
+ result.available = probe.exitCode === 0;
97
+ result.usingManagedEnv = pythonPath === managedPath;
152
98
  return result;
153
99
  }
154
100
 
155
101
  /**
156
102
  * Install Python packages using uv (preferred) or pip.
157
103
  */
158
- async function installPythonPackages(
159
- packages: string[],
160
- pythonPath: string,
161
- uvPath?: string,
162
- pipPath?: string,
163
- ): Promise<{ success: boolean; usedManagedEnv: boolean }> {
164
- if (uvPath) {
165
- console.log(chalk.dim(`Installing via uv: ${packages.join(" ")}`));
166
- const result = await $`${uvPath} pip install ${packages}`.nothrow();
167
- if (result.exitCode === 0) {
168
- return { success: true, usedManagedEnv: false };
169
- }
170
- }
171
-
172
- if (pipPath) {
173
- console.log(chalk.dim(`Installing via pip: ${packages.join(" ")}`));
174
- const result = await $`${pipPath} install ${packages}`.nothrow();
175
- if (result.exitCode === 0) {
176
- return { success: true, usedManagedEnv: false };
177
- }
178
- }
179
-
180
- console.log(chalk.dim(`Falling back to managed virtual environment: ${MANAGED_PYTHON_ENV}`));
181
-
182
- if (uvPath) {
183
- const createEnv = await $`${uvPath} venv ${MANAGED_PYTHON_ENV}`.quiet().nothrow();
184
- if (createEnv.exitCode !== 0) {
185
- return { success: false, usedManagedEnv: true };
186
- }
187
- const installInManagedEnv = await $`${uvPath} pip install --python ${MANAGED_PYTHON_ENV} ${packages}`.nothrow();
188
- return { success: installInManagedEnv.exitCode === 0, usedManagedEnv: true };
189
- }
190
-
191
- const createEnv = await $`${pythonPath} -m venv ${MANAGED_PYTHON_ENV}`.quiet().nothrow();
192
- if (createEnv.exitCode !== 0) {
193
- return { success: false, usedManagedEnv: true };
194
- }
195
-
196
- const managedPython = managedPythonPath();
197
- const installInManagedEnv = await $`${managedPython} -m pip install ${packages}`.nothrow();
198
- return { success: installInManagedEnv.exitCode === 0, usedManagedEnv: true };
199
- }
104
+ // Python installation helper removed: the subprocess runner has no Python
105
+ // package dependencies beyond a working interpreter. `omp setup python --check`
106
+ // remains as a probe; users install optional libs (pandas, matplotlib, ...)
107
+ // directly via pip or the in-process `%pip` magic.
200
108
 
201
109
  /**
202
110
  * Run the setup command.
@@ -232,67 +140,13 @@ async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Pr
232
140
  console.log(chalk.dim(`Using managed environment: ${check.managedEnvPath}`));
233
141
  }
234
142
 
235
- if (check.uvPath) {
236
- console.log(chalk.dim(`uv: ${check.uvPath}`));
237
- } else if (check.pipPath) {
238
- console.log(chalk.dim(`pip: ${check.pipPath}`));
239
- }
240
-
241
- if (check.installedPackages.length > 0) {
242
- console.log(chalk.green(`${theme.status.success} Installed: ${check.installedPackages.join(", ")}`));
243
- }
244
-
245
- if (check.missingPackages.length === 0) {
143
+ if (check.available) {
246
144
  console.log(chalk.green(`\n${theme.status.success} Python execution is ready`));
247
145
  return;
248
146
  }
249
147
 
250
- console.log(chalk.yellow(`${theme.status.warning} Missing: ${check.missingPackages.join(", ")}`));
251
-
252
- if (flags.check) {
253
- process.exit(1);
254
- }
255
-
256
- if (!check.uvPath && !check.pipPath) {
257
- console.error(chalk.red(`\n${theme.status.error} No package manager found`));
258
- console.error(chalk.dim("Install uv (recommended) or pip:"));
259
- console.error(chalk.dim(" curl -LsSf https://astral.sh/uv/install.sh | sh"));
260
- process.exit(1);
261
- }
262
-
263
- console.log("");
264
- const install = await installPythonPackages(check.missingPackages, check.pythonPath, check.uvPath, check.pipPath);
265
-
266
- if (!install.success) {
267
- console.error(chalk.red(`\n${theme.status.error} Installation failed`));
268
- console.error(chalk.dim("Try installing manually:"));
269
- if (install.usedManagedEnv) {
270
- if (check.uvPath) {
271
- console.error(chalk.dim(` uv venv ${MANAGED_PYTHON_ENV}`));
272
- console.error(
273
- chalk.dim(` uv pip install --python ${MANAGED_PYTHON_ENV} ${check.missingPackages.join(" ")}`),
274
- );
275
- } else {
276
- console.error(chalk.dim(` ${check.pythonPath} -m venv ${MANAGED_PYTHON_ENV}`));
277
- console.error(chalk.dim(` ${managedPythonPath()} -m pip install ${check.missingPackages.join(" ")}`));
278
- }
279
- } else {
280
- console.error(chalk.dim(` ${check.uvPath ? "uv pip" : "pip"} install ${check.missingPackages.join(" ")}`));
281
- }
282
- process.exit(1);
283
- }
284
-
285
- const recheck = await checkPythonSetup();
286
- if (recheck.available) {
287
- console.log(chalk.green(`\n${theme.status.success} Python execution is ready`));
288
- if (recheck.usingManagedEnv) {
289
- console.log(chalk.dim(`Managed Python environment: ${recheck.managedEnvPath}`));
290
- }
291
- } else {
292
- console.error(chalk.red(`\n${theme.status.error} Setup incomplete`));
293
- console.error(chalk.dim(`Still missing: ${recheck.missingPackages.join(", ")}`));
294
- process.exit(1);
295
- }
148
+ console.error(chalk.red(`\n${theme.status.error} Python interpreter reported failure`));
149
+ process.exit(1);
296
150
  }
297
151
 
298
152
  async function handleSttSetup(flags: { json?: boolean; check?: boolean }): Promise<void> {
@@ -359,9 +213,8 @@ ${chalk.bold("Usage:")}
359
213
  ${APP_NAME} setup <component> [options]
360
214
 
361
215
  ${chalk.bold("Components:")}
362
- python Install Jupyter kernel dependencies for Python code execution
216
+ python Verify a Python 3 interpreter is reachable for code execution
363
217
  stt Install speech-to-text dependencies (openai-whisper, recording tools)
364
- Packages: ${PYTHON_PACKAGES.join(", ")}
365
218
 
366
219
  ${chalk.bold("Options:")}
367
220
  -c, --check Check if dependencies are installed without installing
@@ -8,6 +8,58 @@ import { APP_NAME, formatDuration, formatNumber, formatPercent } from "@oh-my-pi
8
8
  import chalk from "chalk";
9
9
  import { openPath } from "../utils/open";
10
10
 
11
+ /**
12
+ * Single-line TTY progress bar. On a non-TTY stream we just stay quiet -
13
+ * the final "Synced ..." summary still prints either way.
14
+ */
15
+ function createSyncProgressReporter(): {
16
+ onProgress: (event: { current: number; total: number; sessionFile: string }) => void;
17
+ finish: () => void;
18
+ } {
19
+ const stream = process.stderr;
20
+ const isTty = stream.isTTY === true;
21
+ let lastWidth = 0;
22
+ let lastRender = 0;
23
+ return {
24
+ onProgress(event) {
25
+ if (!isTty) return;
26
+ const now = Date.now();
27
+ // Throttle to ~30 fps and always force a render for the last file.
28
+ if (event.current < event.total && now - lastRender < 33) return;
29
+ lastRender = now;
30
+ const label = chalk.dim(shortenSessionFile(event.sessionFile));
31
+ const pct = ((event.current / event.total) * 100).toFixed(0).padStart(3, " ");
32
+ const counter = chalk.cyan(`[${event.current}/${event.total}]`);
33
+ const line = `${counter} ${pct}% ${label}`;
34
+ const columns = stream.columns ?? 120;
35
+ const trimmed = truncateToColumns(line, columns - 1);
36
+ stream.write(`\r${trimmed.padEnd(lastWidth)}`);
37
+ lastWidth = trimmed.length;
38
+ },
39
+ finish() {
40
+ if (!isTty || lastWidth === 0) return;
41
+ stream.write(`\r${" ".repeat(lastWidth)}\r`);
42
+ lastWidth = 0;
43
+ },
44
+ };
45
+ }
46
+
47
+ function shortenSessionFile(p: string): string {
48
+ const marker = "/sessions/";
49
+ const idx = p.indexOf(marker);
50
+ return idx >= 0 ? p.slice(idx + marker.length) : p;
51
+ }
52
+
53
+ function truncateToColumns(s: string, max: number): string {
54
+ if (max <= 0) return "";
55
+ const width = Bun.stringWidth(s, { countAnsiEscapeCodes: false });
56
+ if (width <= max) return s;
57
+ // Cheap right-trim with an ellipsis - we don't need ANSI-aware slicing
58
+ // because the colored prefix is short and the truncated tail is the
59
+ // dim filename, where dropping bytes is fine.
60
+ return `${s.slice(0, Math.max(0, max - 1))}\u2026`;
61
+ }
62
+
11
63
  // =============================================================================
12
64
  // Types
13
65
  // =============================================================================
@@ -74,8 +126,10 @@ export async function runStatsCommand(cmd: StatsCommandArgs): Promise<void> {
74
126
  );
75
127
 
76
128
  // Sync session files first
77
- console.log("Syncing session files...");
78
- const { processed, files } = await syncAllSessions();
129
+ const progress = createSyncProgressReporter();
130
+ process.stderr.write("Syncing session files...\n");
131
+ const { processed, files } = await syncAllSessions({ onProgress: progress.onProgress });
132
+ progress.finish();
79
133
  const total = await getTotalMessageCount();
80
134
  console.log(`Synced ${processed} new entries from ${files} files (${total} total)\n`);
81
135
 
package/src/cli.ts CHANGED
@@ -55,7 +55,6 @@ const commands: CommandEntry[] = [
55
55
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
56
56
  { name: "grep", load: () => import("./commands/grep").then(m => m.default) },
57
57
  { name: "grievances", load: () => import("./commands/grievances").then(m => m.default) },
58
- { name: "jupyter", load: () => import("./commands/jupyter").then(m => m.default) },
59
58
  { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
60
59
  { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
61
60
  { name: "shell", load: () => import("./commands/shell").then(m => m.default) },
@@ -2072,6 +2072,19 @@ export class ModelRegistry {
2072
2072
  return this.#models.filter(model => this.#isModelAvailable(model));
2073
2073
  }
2074
2074
 
2075
+ /**
2076
+ * Check whether auth is configured for a model's provider.
2077
+ *
2078
+ * Mirrors the upstream `@mariozechner/pi-coding-agent` API surface so that
2079
+ * external plugins/extensions and downstream wrappers (e.g. subagent launch
2080
+ * paths that pre-flight auth before model resolution) can probe a model
2081
+ * without resolving an API key. Returns true for keyless providers as well
2082
+ * as providers with stored credentials. See issue #993.
2083
+ */
2084
+ hasConfiguredAuth(model: Model<Api>): boolean {
2085
+ return this.#keylessProviders.has(model.provider) || this.authStorage.hasAuth(model.provider);
2086
+ }
2087
+
2075
2088
  getDiscoverableProviders(): string[] {
2076
2089
  const disabledProviders = getDisabledProviderIdsFromSettings();
2077
2090
  return this.#discoverableProviders
@@ -16,7 +16,7 @@ import chalk from "chalk";
16
16
  import MODEL_PRIO from "../priority.json" with { type: "json" };
17
17
  import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking";
18
18
  import { fuzzyMatch } from "../utils/fuzzy";
19
- import { isAuthenticated, MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
19
+ import { isAuthenticated, kNoAuth, MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
20
20
  import type { Settings } from "./settings";
21
21
 
22
22
  /** Default model IDs for each known provider */
@@ -743,6 +743,12 @@ export function resolveModelOverride(
743
743
  * `modelRoles.task` pointing at an unqualified id whose only available
744
744
  * provider variant has no configured credentials — see #985).
745
745
  *
746
+ * Keyless-by-design providers (llama.cpp, ollama, lm-studio) advertise the
747
+ * `kNoAuth` sentinel from `getApiKey` to signal that they do not require
748
+ * credentials. Those are treated as authenticated here so an explicitly
749
+ * configured local model is never silently rerouted to the parent's remote
750
+ * provider (see #1008).
751
+ *
746
752
  * If neither the subagent nor the parent has working auth, returns the
747
753
  * primary resolution unchanged so the existing error path still surfaces
748
754
  * a meaningful failure downstream.
@@ -764,7 +770,7 @@ export async function resolveModelOverrideWithAuthFallback(
764
770
  }
765
771
 
766
772
  const primaryKey = await modelRegistry.getApiKey(primary.model);
767
- if (isAuthenticated(primaryKey)) {
773
+ if (primaryKey === kNoAuth || isAuthenticated(primaryKey)) {
768
774
  return { ...primary, authFallbackUsed: false };
769
775
  }
770
776