@oh-my-pi/pi-coding-agent 14.2.0 → 14.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/package.json +19 -19
  3. package/src/cli/args.ts +10 -1
  4. package/src/cli/shell-cli.ts +15 -3
  5. package/src/config/settings-schema.ts +60 -1
  6. package/src/dap/session.ts +8 -2
  7. package/src/debug/system-info.ts +6 -2
  8. package/src/discovery/claude.ts +58 -36
  9. package/src/discovery/opencode.ts +20 -2
  10. package/src/edit/index.ts +3 -1
  11. package/src/edit/modes/chunk.ts +133 -53
  12. package/src/edit/modes/hashline.ts +36 -11
  13. package/src/edit/renderer.ts +98 -133
  14. package/src/edit/streaming.ts +351 -0
  15. package/src/exec/bash-executor.ts +60 -5
  16. package/src/internal-urls/docs-index.generated.ts +5 -5
  17. package/src/internal-urls/pi-protocol.ts +0 -2
  18. package/src/lsp/client.ts +22 -6
  19. package/src/lsp/defaults.json +2 -1
  20. package/src/lsp/index.ts +53 -10
  21. package/src/lsp/types.ts +2 -0
  22. package/src/modes/acp/acp-agent.ts +76 -2
  23. package/src/modes/components/assistant-message.ts +1 -34
  24. package/src/modes/components/hook-editor.ts +1 -1
  25. package/src/modes/components/tool-execution.ts +111 -101
  26. package/src/modes/controllers/input-controller.ts +1 -1
  27. package/src/modes/interactive-mode.ts +0 -2
  28. package/src/modes/theme/mermaid-cache.ts +13 -52
  29. package/src/modes/theme/theme.ts +2 -2
  30. package/src/prompts/system/system-prompt.md +1 -1
  31. package/src/prompts/tools/ast-grep.md +1 -0
  32. package/src/prompts/tools/browser.md +1 -0
  33. package/src/prompts/tools/chunk-edit.md +25 -22
  34. package/src/prompts/tools/gh-pr-push.md +2 -1
  35. package/src/prompts/tools/grep.md +4 -3
  36. package/src/prompts/tools/lsp.md +6 -0
  37. package/src/prompts/tools/read-chunk.md +46 -7
  38. package/src/prompts/tools/read.md +7 -4
  39. package/src/sdk.ts +8 -5
  40. package/src/session/agent-session.ts +36 -20
  41. package/src/session/session-manager.ts +228 -57
  42. package/src/session/streaming-output.ts +11 -0
  43. package/src/system-prompt.ts +7 -2
  44. package/src/task/executor.ts +1 -0
  45. package/src/tools/ast-edit.ts +37 -2
  46. package/src/tools/bash.ts +75 -12
  47. package/src/tools/find.ts +19 -26
  48. package/src/tools/gh.ts +6 -16
  49. package/src/tools/grep.ts +94 -37
  50. package/src/tools/path-utils.ts +31 -3
  51. package/src/tools/resolve.ts +12 -3
  52. package/src/tools/sqlite-reader.ts +116 -3
  53. package/src/tools/vim.ts +1 -1
  54. package/src/web/search/providers/codex.ts +129 -6
package/CHANGELOG.md CHANGED
@@ -2,6 +2,65 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [14.3.0] - 2026-04-25
6
+
7
+ ### Added
8
+
9
+ - Added Markdown pipe-table `row_N` chunk selectors for row-level table edits.
10
+ - Added `resolveToolAlias` export so tool names in CLI and session setup are normalized to canonical names, including mapping legacy `read` references to `open`
11
+ - Added new `open` and `open-chunk` tool prompt documentation pages to describe canonical `open` usage for local files/directories, chunk reads, and URLs
12
+ - Added full-output retrieval metadata to minimized shell command output by appending an `artifact://<id>` footer with byte counts, allowing users to open the original unminimized command output
13
+ - Added streaming preview API exports from the package (`resolveEditMode`, `EDIT_MODE_STRATEGIES`, and chunk preview helpers) so editors can reuse mode-aware edit preview logic programmatically
14
+ - Added `shellMinimizer` configuration options (`enabled`, `settingsPath`, `only`, `except`, and `maxCaptureBytes`) so users can control shell output minimization behavior
15
+
16
+ ### Changed
17
+
18
+ - Changed the canonical file/URL reader tool from `read` to `open` across default tool lists and routing, including system prompts, plan mode, cursor handlers, and runtime tool registration
19
+ - Changed runtime and UI handling to render and track `open` tool calls as first-class (with `read` accepted as legacy alias), including ACP mapping, session observers, and streaming message groups
20
+ - Changed chunk edit guidance to document parser-specific region behavior, including TypeScript decorator/JSDoc sibling chunks, Python docstrings as body content, Python opaque nested chunks, Markdown whole-chunk fallbacks, ID volatility, and indentation display differences
21
+ - Changed chunk deletion in chunk edit mode to require explicit `delete: true`; `write: null` and bare `{ path }` entries now fail with guidance instead of deleting content.
22
+ - Changed chunk edit validation to reject entries with multiple operation fields instead of choosing one and ignoring the rest.
23
+ - Changed chunk edit validation to reject `write: ""` as an accidental destructive empty replacement; use the open tool for inspection or `delete: true` for deletion.
24
+ - Changed chunk edit responses to warn when appending or prepending to a container without `~`, since that inserts outside the container rather than inside its body.
25
+ - Changed fetch output logging so URL-fetch artifacts now use `.open.log` naming instead of `.read.log`
26
+ - Changed Bash interception guidance and errors to recommend `open` in place of `read` for cat/head/tail-style commands
27
+ - Changed exported SDK tool surface to expose `OpenTool` as canonical and keep `ReadTool` as a compatibility alias
28
+ - Changed session list loading to use parallel workers and fixed-size prefix reads per session file, reducing latency when loading many or large sessions
29
+ - Changed edit call rendering to use mode-aware streaming diff previews, including multi-file chunk edit previews grouped by file path while arguments are still streaming
30
+ - Changed shell execution in both interactive and non-interactive modes to route command output through the configured shell output minimizer
31
+ - Changed default behavior so shell output minimization can now be toggled from settings without code changes
32
+ - Changed shell output minimization to leave compound and piped commands unchanged; only a single eligible whole command is captured and minimized after it exits
33
+
34
+ ### Removed
35
+
36
+ - Removed the chunk edit `read: true` operation; use the open tool to inspect chunks without modifying files.
37
+ - Removed the `replace: { old, new }` chunk edit operation. Use `write` or `insert` for chunk edits instead.
38
+
39
+ ### Fixed
40
+
41
+ - Fixed startup crashes on Linux systems where Bun's `os.cpus()` fails on non-contiguous CPU numbering ([#779](https://github.com/can1357/oh-my-pi/issues/779))
42
+ - Fixed `gh_pr_push` so branches without `gh_pr_checkout` metadata fail instead of falling back to the tracked merge branch, and updated the GitHub tool setting copy to stop calling the tool group read-only ([#778](https://github.com/can1357/oh-my-pi/issues/778))
43
+ - Fixed session list metadata extraction to better populate session titles and first-user summaries from partial session data when full JSONL parsing is unavailable
44
+ - Fixed shell execution output to replace raw streamed bash output with the minimizer’s rewritten text before final output while still preserving the full original output as artifact metadata
45
+ - Fixed bash command minimization to save the full unminimized output as a `bash-original` artifact during AgentSession shell execution, enabling `artifact://` access to complete command output
46
+ - Fixed streaming chunk previews that could display an incomplete trailing edit as a deletion when partial JSON temporarily converted in-flight values to `null`
47
+ - Fixed edit streaming preview updates to cancel obsolete in-flight computations and avoid rendering stale previews as args change
48
+ - Fixed chunk edits to reject unsafe `^`/`~` writes on code leaf chunks instead of falling back to whole-chunk replacement and risking structural indentation corruption
49
+ - Fixed chunk `replace` operations to dedent multiline replacement snippets before reapplying the matched source indentation, preventing Python nested replacements from compounding indentation on repeated edits.
50
+ - Fixed Go chunk trees to classify `package` clauses separately from imports and to avoid duplicating method receivers in method summaries.
51
+ - Fixed chunk path-not-found guidance so it recommends `sel="?"` without claiming the already-shown listing must be re-read.
52
+ - Fixed Markdown chunk appends to preserve blank-line separators after line-oriented inserts such as table rows
53
+ - Fixed Markdown section region-fallback warnings to call out child chunks that will be replaced by whole-section edits.
54
+ - Fixed rejected chunk-edit errors to distinguish current file content from hypothetical post-edit parse-error previews and to state when a same-file batch was rolled back.
55
+ - Fixed unsafe Python head-region edits by rejecting decorated Python `^` writes and Python `^` deletes that can orphan indented bodies while still parsing.
56
+ - Fixed Markdown table-row appends so row-shaped content lands inside the table block instead of after the trailing blank-line separator.
57
+ - Fixed Markdown root writes to preserve fenced-code indentation verbatim.
58
+ - Fixed Rust enum-variant replacement matching so trailing commas are included consistently with whole-variant writes.
59
+ - Fixed streaming edit call headers to keep showing the target file path while the edit arguments are still arriving
60
+ - Fixed Mermaid fenced markdown rendering in assistant messages on terminals without image protocol support ([#650](https://github.com/can1357/oh-my-pi/issues/650))
61
+ - Fixed chunk edit path parsing so plan-mode edits to section-addressed `local://PLAN.md:<selector>` paths are classified as writes to the plan file
62
+ - Fixed SQLite `read` helper queries to reject `where=` clauses with SQL control syntax that could override the structured selector's pagination; raw SQL remains available through `q=SELECT ...`
63
+
5
64
  ## [14.2.0] - 2026-04-23
6
65
 
7
66
  ### Added
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.2.0",
4
+ "version": "14.3.0",
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",
@@ -44,31 +44,31 @@
44
44
  "generate-template": "bun scripts/generate-template.ts"
45
45
  },
46
46
  "dependencies": {
47
- "@agentclientprotocol/sdk": "0.16.1",
48
- "@mozilla/readability": "^0.6",
49
- "@oh-my-pi/omp-stats": "14.2.0",
50
- "@oh-my-pi/pi-agent-core": "14.2.0",
51
- "@oh-my-pi/pi-ai": "14.2.0",
52
- "@oh-my-pi/pi-natives": "14.2.0",
53
- "@oh-my-pi/pi-tui": "14.2.0",
54
- "@oh-my-pi/pi-utils": "14.2.0",
55
- "@sinclair/typebox": "^0.34",
56
- "@xterm/headless": "^6.0",
57
- "ajv": "^8.18",
58
- "chalk": "^5.6",
59
- "diff": "^8.0",
47
+ "@agentclientprotocol/sdk": "0.20.0",
48
+ "@mozilla/readability": "^0.6.0",
49
+ "@oh-my-pi/omp-stats": "14.3.0",
50
+ "@oh-my-pi/pi-agent-core": "14.3.0",
51
+ "@oh-my-pi/pi-ai": "14.3.0",
52
+ "@oh-my-pi/pi-natives": "14.3.0",
53
+ "@oh-my-pi/pi-tui": "14.3.0",
54
+ "@oh-my-pi/pi-utils": "14.3.0",
55
+ "@sinclair/typebox": "^0.34.49",
56
+ "@xterm/headless": "^6.0.0",
57
+ "ajv": "^8.20.0",
58
+ "chalk": "^5.6.2",
59
+ "diff": "^9.0.0",
60
60
  "fflate": "0.8.2",
61
61
  "handlebars": "^4.7.9",
62
- "linkedom": "^0.18",
63
- "lru-cache": "11.3.1",
64
- "markit-ai": "0.5.0",
65
- "puppeteer": "^24.37",
62
+ "linkedom": "^0.18.12",
63
+ "lru-cache": "11.3.5",
64
+ "markit-ai": "0.5.3",
65
+ "puppeteer": "^24.42.0",
66
66
  "turndown": "7.2.4",
67
67
  "turndown-plugin-gfm": "1.0.2",
68
68
  "zod": "4.3.6"
69
69
  },
70
70
  "devDependencies": {
71
- "@types/bun": "^1.3",
71
+ "@types/bun": "^1.3.13",
72
72
  "@types/turndown": "5.0.6"
73
73
  },
74
74
  "engines": {
package/src/cli/args.ts CHANGED
@@ -60,7 +60,16 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
60
60
  };
61
61
 
62
62
  for (let i = 0; i < args.length; i++) {
63
- const arg = args[i];
63
+ let arg = args[i];
64
+
65
+ // Support --flag=value syntax (e.g. --tools=ask,read)
66
+ if (arg.startsWith("--") && arg.includes("=")) {
67
+ const eqIdx = arg.indexOf("=");
68
+ const value = arg.slice(eqIdx + 1);
69
+ arg = arg.slice(0, eqIdx);
70
+ // Insert the value so the existing "args[++i]" logic picks it up
71
+ args.splice(i + 1, 0, value);
72
+ }
64
73
 
65
74
  if (arg === "--help" || arg === "-h") {
66
75
  result.help = true;
@@ -5,10 +5,10 @@
5
5
  */
6
6
  import * as path from "node:path";
7
7
  import { createInterface } from "node:readline/promises";
8
- import { Shell } from "@oh-my-pi/pi-natives";
8
+ import { type MinimizerOptions, Shell } from "@oh-my-pi/pi-natives";
9
9
  import { APP_NAME, getProjectDir } from "@oh-my-pi/pi-utils";
10
10
  import chalk from "chalk";
11
- import { Settings } from "../config/settings";
11
+ import { Settings, type ShellMinimizerSettings } from "../config/settings";
12
12
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
13
13
 
14
14
  export interface ShellCommandArgs {
@@ -41,6 +41,17 @@ export function parseShellArgs(args: string[]): ShellCommandArgs | undefined {
41
41
  return result;
42
42
  }
43
43
 
44
+ function buildMinimizerOptions(group: ShellMinimizerSettings): MinimizerOptions | undefined {
45
+ if (!group.enabled) return undefined;
46
+ return {
47
+ enabled: true,
48
+ settingsPath: group.settingsPath || undefined,
49
+ only: group.only.length > 0 ? group.only : undefined,
50
+ except: group.except.length > 0 ? group.except : undefined,
51
+ maxCaptureBytes: group.maxCaptureBytes,
52
+ };
53
+ }
54
+
44
55
  export async function runShellCommand(cmd: ShellCommandArgs): Promise<void> {
45
56
  if (!process.stdin.isTTY) {
46
57
  process.stderr.write("Error: shell console requires an interactive TTY.\n");
@@ -51,7 +62,8 @@ export async function runShellCommand(cmd: ShellCommandArgs): Promise<void> {
51
62
  const settings = await Settings.init({ cwd });
52
63
  const { shell, env: shellEnv } = settings.getShellConfig();
53
64
  const snapshotPath = cmd.noSnapshot || !shell.includes("bash") ? null : await getOrCreateSnapshot(shell, shellEnv);
54
- const shellSession = new Shell({ sessionEnv: shellEnv, snapshotPath: snapshotPath ?? undefined });
65
+ const minimizer = buildMinimizerOptions(settings.getGroup("shellMinimizer"));
66
+ const shellSession = new Shell({ sessionEnv: shellEnv, snapshotPath: snapshotPath ?? undefined, minimizer });
55
67
 
56
68
  let active = false;
57
69
  let lastChar: string | null = null;
@@ -1123,6 +1123,39 @@ export const SETTINGS_SCHEMA = {
1123
1123
  },
1124
1124
  "bashInterceptor.patterns": { type: "array", default: DEFAULT_BASH_INTERCEPTOR_RULES },
1125
1125
 
1126
+ // Shell output minimizer
1127
+ "shellMinimizer.enabled": {
1128
+ type: "boolean",
1129
+ default: true,
1130
+ ui: {
1131
+ tab: "editing",
1132
+ label: "Shell Minimizer",
1133
+ description: "Compress verbose shell output (git, npm, cargo, etc.) before returning it to the agent",
1134
+ },
1135
+ },
1136
+ "shellMinimizer.settingsPath": {
1137
+ type: "string",
1138
+ default: undefined,
1139
+ ui: {
1140
+ tab: "editing",
1141
+ label: "Minimizer Settings Path",
1142
+ description: "Optional TOML file with per-command minimizer overrides",
1143
+ submenu: true,
1144
+ },
1145
+ },
1146
+ "shellMinimizer.only": { type: "array", default: EMPTY_STRING_ARRAY },
1147
+ "shellMinimizer.except": { type: "array", default: EMPTY_STRING_ARRAY },
1148
+ "shellMinimizer.maxCaptureBytes": {
1149
+ type: "number",
1150
+ default: 4 * 1024 * 1024,
1151
+ ui: {
1152
+ tab: "editing",
1153
+ label: "Minimizer Capture Limit",
1154
+ description: "Maximum captured output bytes before falling back to raw streaming",
1155
+ submenu: true,
1156
+ },
1157
+ },
1158
+
1126
1159
  // Python
1127
1160
  "python.toolMode": {
1128
1161
  type: "enum",
@@ -1315,7 +1348,8 @@ export const SETTINGS_SCHEMA = {
1315
1348
  ui: {
1316
1349
  tab: "tools",
1317
1350
  label: "GitHub CLI",
1318
- description: "Enable read-only gh_* tools for GitHub repository, issue, pull request, diff, and search access",
1351
+ description:
1352
+ "Enable gh_* tools for GitHub repository, issue, pull request, diff, search, checkout, and PR push workflows",
1319
1353
  },
1320
1354
  },
1321
1355
 
@@ -1612,6 +1646,22 @@ export const SETTINGS_SCHEMA = {
1612
1646
  ui: { tab: "tasks", label: "Claude Project Commands", description: "Load commands from .claude/commands/" },
1613
1647
  },
1614
1648
 
1649
+ "commands.enableOpencodeUser": {
1650
+ type: "boolean",
1651
+ default: true,
1652
+ ui: {
1653
+ tab: "tasks",
1654
+ label: "OpenCode User Commands",
1655
+ description: "Load commands from ~/.config/opencode/commands/",
1656
+ },
1657
+ },
1658
+
1659
+ "commands.enableOpencodeProject": {
1660
+ type: "boolean",
1661
+ default: true,
1662
+ ui: { tab: "tasks", label: "OpenCode Project Commands", description: "Load commands from .opencode/commands/" },
1663
+ },
1664
+
1615
1665
  // ────────────────────────────────────────────────────────────────────────
1616
1666
  // Providers
1617
1667
  // ────────────────────────────────────────────────────────────────────────
@@ -1956,6 +2006,14 @@ export interface BashInterceptorRule {
1956
2006
  allowSubcommands?: string[];
1957
2007
  }
1958
2008
 
2009
+ export interface ShellMinimizerSettings {
2010
+ enabled: boolean;
2011
+ settingsPath: string | undefined;
2012
+ only: string[];
2013
+ except: string[];
2014
+ maxCaptureBytes: number;
2015
+ }
2016
+
1959
2017
  /** Map group prefix -> typed settings interface */
1960
2018
  export interface GroupTypeMap {
1961
2019
  compaction: CompactionSettings;
@@ -1973,6 +2031,7 @@ export interface GroupTypeMap {
1973
2031
  modelRoles: Record<string, string>;
1974
2032
  modelTags: ModelTagsSettings;
1975
2033
  cycleOrder: string[];
2034
+ shellMinimizer: ShellMinimizerSettings;
1976
2035
  }
1977
2036
 
1978
2037
  export type GroupPrefix = keyof GroupTypeMap;
@@ -1075,11 +1075,17 @@ export class DapSessionManager {
1075
1075
  * MUST be called before the command that triggers the event.
1076
1076
  */
1077
1077
  #prepareStopOutcome(session: DapSession, signal?: AbortSignal, timeoutMs: number = 30_000): Promise<unknown> {
1078
- return Promise.race([
1078
+ const promises = [
1079
1079
  session.client.waitForEvent("stopped", undefined, signal, timeoutMs),
1080
1080
  session.client.waitForEvent("terminated", undefined, signal, timeoutMs),
1081
1081
  session.client.waitForEvent("exited", undefined, signal, timeoutMs),
1082
- ]);
1082
+ ];
1083
+ // Promise.race leaves the losing waiters pending; their timeouts would
1084
+ // otherwise surface as unhandled rejections once they fire.
1085
+ for (const p of promises) {
1086
+ p.catch(() => {});
1087
+ }
1088
+ return Promise.race(promises);
1083
1089
  }
1084
1090
 
1085
1091
  /**
@@ -40,8 +40,12 @@ function macosMarketingName(release: string): string | undefined {
40
40
 
41
41
  /** Collect system information */
42
42
  export async function collectSystemInfo(): Promise<SystemInfo> {
43
- const cpus = os.cpus();
44
- const cpuModel = cpus[0]?.model ?? "Unknown CPU";
43
+ let cpuModel = "Unknown CPU";
44
+ try {
45
+ cpuModel = os.cpus()[0]?.model ?? cpuModel;
46
+ } catch {
47
+ // Keep debug report collection best-effort when CPU probing fails.
48
+ }
45
49
 
46
50
  // Try to get shell from environment
47
51
  const shell = Bun.env.SHELL ?? Bun.env.ComSpec ?? "unknown";
@@ -18,6 +18,7 @@ import { type SlashCommand, slashCommandCapability } from "../capability/slash-c
18
18
  import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
19
19
  import { type CustomTool, toolCapability } from "../capability/tool";
20
20
  import type { LoadContext, LoadResult } from "../capability/types";
21
+ import { settings } from "../config/settings";
21
22
  import {
22
23
  calculateDepth,
23
24
  createSourceMeta,
@@ -252,48 +253,69 @@ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<Extens
252
253
  // Slash Commands
253
254
  // =============================================================================
254
255
 
256
+ /**
257
+ * Read the Claude command-loading toggles from settings.
258
+ * Falls back to true (current behavior) when settings are not initialized,
259
+ * e.g. inside discovery unit tests that run without Settings.init().
260
+ */
261
+ function readClaudeCommandToggles(): { enableUser: boolean; enableProject: boolean } {
262
+ try {
263
+ return {
264
+ enableUser: settings.get("commands.enableClaudeUser") ?? true,
265
+ enableProject: settings.get("commands.enableClaudeProject") ?? true,
266
+ };
267
+ } catch {
268
+ return { enableUser: true, enableProject: true };
269
+ }
270
+ }
271
+
255
272
  async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
256
273
  const items: SlashCommand[] = [];
257
274
  const warnings: string[] = [];
275
+ const { enableUser, enableProject } = readClaudeCommandToggles();
276
+
277
+ if (enableUser) {
278
+ const userBase = getUserClaude(ctx);
279
+ const userCommandsDir = path.join(userBase, "commands");
280
+
281
+ const userResult = await loadFilesFromDir<SlashCommand>(ctx, userCommandsDir, PROVIDER_ID, "user", {
282
+ extensions: ["md"],
283
+ transform: (name, content, path, source) => {
284
+ const cmdName = name.replace(/\.md$/, "");
285
+ return {
286
+ name: cmdName,
287
+ path,
288
+ content,
289
+ level: "user",
290
+ _source: source,
291
+ };
292
+ },
293
+ });
258
294
 
259
- const userBase = getUserClaude(ctx);
260
- const userCommandsDir = path.join(userBase, "commands");
261
-
262
- const userResult = await loadFilesFromDir<SlashCommand>(ctx, userCommandsDir, PROVIDER_ID, "user", {
263
- extensions: ["md"],
264
- transform: (name, content, path, source) => {
265
- const cmdName = name.replace(/\.md$/, "");
266
- return {
267
- name: cmdName,
268
- path,
269
- content,
270
- level: "user",
271
- _source: source,
272
- };
273
- },
274
- });
275
-
276
- items.push(...userResult.items);
277
- if (userResult.warnings) warnings.push(...userResult.warnings);
278
-
279
- const projectCommandsDir = path.join(ctx.cwd, CONFIG_DIR, "commands");
295
+ items.push(...userResult.items);
296
+ if (userResult.warnings) warnings.push(...userResult.warnings);
297
+ }
280
298
 
281
- const projectResult = await loadFilesFromDir<SlashCommand>(ctx, projectCommandsDir, PROVIDER_ID, "project", {
282
- extensions: ["md"],
283
- transform: (name, content, path, source) => {
284
- const cmdName = name.replace(/\.md$/, "");
285
- return {
286
- name: cmdName,
287
- path,
288
- content,
289
- level: "project",
290
- _source: source,
291
- };
292
- },
293
- });
299
+ if (enableProject) {
300
+ const projectCommandsDir = path.join(ctx.cwd, CONFIG_DIR, "commands");
301
+
302
+ const projectResult = await loadFilesFromDir<SlashCommand>(ctx, projectCommandsDir, PROVIDER_ID, "project", {
303
+ extensions: ["md"],
304
+ transform: (name, content, path, source) => {
305
+ const cmdName = name.replace(/\.md$/, "");
306
+ return {
307
+ name: cmdName,
308
+ path,
309
+ content,
310
+ level: "project",
311
+ _source: source,
312
+ };
313
+ },
314
+ });
294
315
 
295
- items.push(...projectResult.items);
296
- if (projectResult.warnings) warnings.push(...projectResult.warnings);
316
+ items.push(...projectResult.items);
317
+ if (projectResult.warnings) warnings.push(...projectResult.warnings);
318
+ }
297
319
 
298
320
  return { items, warnings };
299
321
  }
@@ -26,6 +26,7 @@ import { type Settings, settingsCapability } from "../capability/settings";
26
26
  import { type Skill, skillCapability } from "../capability/skill";
27
27
  import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
28
28
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
29
+ import { settings } from "../config/settings";
29
30
 
30
31
  import {
31
32
  buildExtensionModuleItems,
@@ -236,9 +237,26 @@ async function loadExtensionModules(ctx: LoadContext): Promise<LoadResult<Extens
236
237
  // Slash Commands (commands/)
237
238
  // =============================================================================
238
239
 
240
+ /**
241
+ * Read the OpenCode command-loading toggles from settings.
242
+ * Falls back to true (current behavior) when settings are not initialized,
243
+ * e.g. inside discovery unit tests that run without Settings.init().
244
+ */
245
+ function readOpencodeCommandToggles(): { enableUser: boolean; enableProject: boolean } {
246
+ try {
247
+ return {
248
+ enableUser: settings.get("commands.enableOpencodeUser") ?? true,
249
+ enableProject: settings.get("commands.enableOpencodeProject") ?? true,
250
+ };
251
+ } catch {
252
+ return { enableUser: true, enableProject: true };
253
+ }
254
+ }
255
+
239
256
  async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
240
- const userCommandsDir = getUserPath(ctx, "opencode", "commands");
241
- const projectCommandsDir = getProjectPath(ctx, "opencode", "commands");
257
+ const { enableUser, enableProject } = readOpencodeCommandToggles();
258
+ const userCommandsDir = enableUser ? getUserPath(ctx, "opencode", "commands") : null;
259
+ const projectCommandsDir = enableProject ? getProjectPath(ctx, "opencode", "commands") : null;
242
260
 
243
261
  const transformCommand =
244
262
  (level: "user" | "project") => (name: string, content: string, filePath: string, source: SourceMeta) => {
package/src/edit/index.ts CHANGED
@@ -68,6 +68,7 @@ export * from "./modes/patch";
68
68
  export * from "./modes/replace";
69
69
  export * from "./normalize";
70
70
  export * from "./renderer";
71
+ export * from "./streaming";
71
72
 
72
73
  type TInput =
73
74
  | typeof replaceEditSchema
@@ -322,7 +323,8 @@ export class EditTool implements AgentTool<TInput> {
322
323
  chunkAutoIndent: resolveChunkAutoIndent(),
323
324
  }),
324
325
  parameters: chunkEditParamsSchema,
325
- invalidParamsMessage: "Invalid edit parameters for chunk mode.",
326
+ invalidParamsMessage:
327
+ "Invalid edit parameters for chunk mode. Expected `{ edits: [{ path: 'file:selector', ...op }, ...] }` with at least one edit. Each edit needs a `path`; supply exactly one of `write: 'content'`, `insert: { loc, body }`, or `delete: true`.",
326
328
  validate: isChunkParams,
327
329
  execute: (
328
330
  tool: EditTool,