@oh-my-pi/pi-coding-agent 16.3.11 → 16.3.12

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 (80) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/cli.js +3119 -3028
  3. package/dist/types/advisor/runtime.d.ts +11 -0
  4. package/dist/types/config/model-registry.d.ts +4 -0
  5. package/dist/types/config/settings-schema.d.ts +6 -0
  6. package/dist/types/config/settings.d.ts +2 -0
  7. package/dist/types/discovery/helpers.d.ts +9 -0
  8. package/dist/types/exec/bash-executor.d.ts +1 -0
  9. package/dist/types/extensibility/shared-events.d.ts +2 -2
  10. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +1 -0
  11. package/dist/types/internal-urls/registry-helpers.d.ts +7 -5
  12. package/dist/types/modes/components/model-selector.d.ts +2 -1
  13. package/dist/types/modes/github-ref-autocomplete.d.ts +35 -0
  14. package/dist/types/modes/utils/context-usage.d.ts +0 -12
  15. package/dist/types/modes/workflow.d.ts +5 -1
  16. package/dist/types/session/agent-session.d.ts +5 -0
  17. package/dist/types/system-prompt.d.ts +1 -1
  18. package/dist/types/tools/bash-interactive.d.ts +1 -1
  19. package/dist/types/tools/bash-skill-urls.d.ts +1 -0
  20. package/dist/types/tools/bash.d.ts +2 -1
  21. package/dist/types/tools/browser/launch.d.ts +1 -0
  22. package/dist/types/tools/grep.d.ts +2 -0
  23. package/dist/types/tools/index.d.ts +4 -0
  24. package/dist/types/tools/path-utils.d.ts +24 -0
  25. package/dist/types/tools/read.d.ts +2 -0
  26. package/dist/types/utils/local-date.d.ts +2 -0
  27. package/package.json +12 -12
  28. package/src/advisor/__tests__/advisor.test.ts +145 -0
  29. package/src/advisor/runtime.ts +19 -0
  30. package/src/config/api-key-resolver.ts +7 -2
  31. package/src/config/model-registry.ts +9 -0
  32. package/src/config/settings-schema.ts +11 -1
  33. package/src/config/settings.ts +11 -0
  34. package/src/discovery/builtin.ts +2 -1
  35. package/src/discovery/claude-plugins.ts +167 -46
  36. package/src/discovery/helpers.ts +16 -1
  37. package/src/edit/renderer.ts +20 -6
  38. package/src/eval/js/worker-core.ts +163 -6
  39. package/src/exec/bash-executor.ts +14 -9
  40. package/src/extensibility/plugins/legacy-pi-compat.ts +6 -2
  41. package/src/extensibility/plugins/marketplace/fetcher.ts +15 -14
  42. package/src/extensibility/shared-events.ts +2 -2
  43. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +68 -0
  44. package/src/internal-urls/docs-index.generated.txt +1 -1
  45. package/src/internal-urls/registry-helpers.ts +9 -6
  46. package/src/modes/components/model-selector.ts +30 -6
  47. package/src/modes/components/settings-defs.ts +1 -1
  48. package/src/modes/components/status-line/component.ts +14 -2
  49. package/src/modes/controllers/command-controller.ts +13 -23
  50. package/src/modes/controllers/event-controller.ts +12 -12
  51. package/src/modes/controllers/extension-ui-controller.ts +6 -35
  52. package/src/modes/controllers/input-controller.ts +18 -2
  53. package/src/modes/controllers/mcp-command-controller.ts +10 -9
  54. package/src/modes/controllers/selector-controller.ts +16 -5
  55. package/src/modes/github-ref-autocomplete.ts +75 -0
  56. package/src/modes/interactive-mode.ts +54 -10
  57. package/src/modes/prompt-action-autocomplete.ts +35 -0
  58. package/src/modes/utils/context-usage.ts +58 -5
  59. package/src/modes/utils/hotkeys-markdown.ts +2 -1
  60. package/src/modes/utils/ui-helpers.ts +2 -2
  61. package/src/modes/workflow.ts +14 -8
  62. package/src/prompts/system/plan-mode-active.md +5 -2
  63. package/src/prompts/system/system-prompt.md +1 -2
  64. package/src/prompts/system/workflow-notice.md +69 -50
  65. package/src/prompts/tools/bash.md +18 -7
  66. package/src/prompts/tools/grep.md +1 -1
  67. package/src/prompts/tools/read.md +4 -3
  68. package/src/sdk.ts +11 -0
  69. package/src/session/agent-session.ts +128 -14
  70. package/src/system-prompt.ts +3 -2
  71. package/src/tools/bash-interactive.ts +1 -1
  72. package/src/tools/bash-skill-urls.ts +39 -7
  73. package/src/tools/bash.ts +69 -39
  74. package/src/tools/browser/launch.ts +31 -4
  75. package/src/tools/grep.ts +53 -14
  76. package/src/tools/index.ts +11 -0
  77. package/src/tools/path-utils.ts +46 -1
  78. package/src/tools/read.ts +108 -50
  79. package/src/utils/local-date.ts +7 -0
  80. package/src/utils/open.ts +36 -10
package/src/tools/read.ts CHANGED
@@ -99,10 +99,12 @@ import {
99
99
  type LineRange,
100
100
  parseLineRanges,
101
101
  pathTargetsSsh,
102
+ probeLiteralPathExists,
102
103
  resolveReadPath,
103
104
  splitDelimitedPathEntry,
104
105
  splitInternalUrlSel,
105
106
  splitPathAndSel,
107
+ splitPathAndSelPreferringLiteral,
106
108
  } from "./path-utils";
107
109
  import { formatBytes, replaceTabs, shortenPath, wrapBrackets } from "./render-utils";
108
110
  import {
@@ -746,7 +748,10 @@ function splitPdfImageMemberReadPath(readPath: string): { pdfPath: string; membe
746
748
 
747
749
  const readSchema = type({
748
750
  path: type("string").describe(
749
- 'Local path, internal URI (e.g. "omp://", "issue://123", "pr://123"), or URL; append :<sel> for line ranges or raw mode (e.g. "src/foo.ts:50-100")',
751
+ 'Local path, internal URI (e.g. "omp://", "issue://123", "pr://123"), or URL. Inline :<sel> is still accepted for compatibility.',
752
+ ),
753
+ "selector?": type("string").describe(
754
+ 'selector without a leading colon (e.g. "50-100", "raw", "raw:50-100", "conflicts"); keeps `path` literal when filenames contain colons',
750
755
  ),
751
756
  });
752
757
 
@@ -2113,6 +2118,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2113
2118
  _toolContext?: AgentToolContext,
2114
2119
  ): Promise<AgentToolResult<ReadToolDetails>> {
2115
2120
  let { path: readPath } = params;
2121
+ let explicitSelector = params.selector?.trim();
2122
+ let explicitParsedSelector = explicitSelector === undefined ? undefined : parseSel(explicitSelector);
2123
+ if (
2124
+ params.selector !== undefined &&
2125
+ (explicitSelector === undefined || explicitSelector.length === 0 || explicitParsedSelector?.kind === "none")
2126
+ ) {
2127
+ throw invalidSelector(params.selector);
2128
+ }
2116
2129
  if (readPath.startsWith("file://")) {
2117
2130
  readPath = expandPath(readPath);
2118
2131
  }
@@ -2133,40 +2146,55 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2133
2146
  if (!this.session.settings.get("fetch.enabled")) {
2134
2147
  throw new ToolError("URL reads are disabled by settings.");
2135
2148
  }
2136
- if (parsedUrlTarget.ranges !== undefined) {
2149
+ if (explicitParsedSelector?.kind === "conflicts") {
2150
+ throw new ToolError("The explicit read selector `conflicts` is only supported for local files.");
2151
+ }
2152
+ const urlRaw =
2153
+ explicitParsedSelector === undefined ? parsedUrlTarget.raw : isRawSelector(explicitParsedSelector);
2154
+ const urlRanges =
2155
+ explicitParsedSelector?.kind === "lines" ? explicitParsedSelector.ranges : parsedUrlTarget.ranges;
2156
+ if (urlRanges !== undefined && urlRanges.length > 1) {
2137
2157
  const cached = await loadReadUrlCacheEntry(
2138
2158
  this.session,
2139
- { path: parsedUrlTarget.path, raw: parsedUrlTarget.raw },
2159
+ { path: parsedUrlTarget.path, raw: urlRaw },
2140
2160
  signal,
2141
2161
  { ensureArtifact: true, preferCached: true },
2142
2162
  );
2143
- return this.#buildInMemoryMultiRangeResult(cached.output, parsedUrlTarget.ranges, {
2163
+ return this.#buildInMemoryMultiRangeResult(cached.output, urlRanges, {
2144
2164
  details: { ...cached.details },
2145
2165
  sourceUrl: cached.details.finalUrl,
2146
2166
  entityLabel: "URL output",
2147
- raw: parsedUrlTarget.raw,
2167
+ raw: urlRaw,
2148
2168
  immutable: true,
2149
2169
  });
2150
2170
  }
2151
- if (parsedUrlTarget.offset !== undefined || parsedUrlTarget.limit !== undefined) {
2171
+ const urlRange = urlRanges?.[0];
2172
+ const urlOffset = explicitParsedSelector?.kind === "lines" ? urlRange?.startLine : parsedUrlTarget.offset;
2173
+ const urlLimit =
2174
+ explicitParsedSelector?.kind === "lines" && urlRange
2175
+ ? urlRange.endLine !== undefined
2176
+ ? urlRange.endLine - urlRange.startLine + 1
2177
+ : undefined
2178
+ : parsedUrlTarget.limit;
2179
+ if (urlOffset !== undefined || urlLimit !== undefined) {
2152
2180
  const cached = await loadReadUrlCacheEntry(
2153
2181
  this.session,
2154
- { path: parsedUrlTarget.path, raw: parsedUrlTarget.raw },
2182
+ { path: parsedUrlTarget.path, raw: urlRaw },
2155
2183
  signal,
2156
2184
  {
2157
2185
  ensureArtifact: true,
2158
2186
  preferCached: true,
2159
2187
  },
2160
2188
  );
2161
- return this.#buildInMemoryTextResult(cached.output, parsedUrlTarget.offset, parsedUrlTarget.limit, {
2189
+ return this.#buildInMemoryTextResult(cached.output, urlOffset, urlLimit, {
2162
2190
  details: { ...cached.details },
2163
2191
  sourceUrl: cached.details.finalUrl,
2164
2192
  entityLabel: "URL output",
2165
- raw: parsedUrlTarget.raw,
2193
+ raw: urlRaw,
2166
2194
  immutable: true,
2167
2195
  });
2168
2196
  }
2169
- return executeReadUrl(this.session, { path: parsedUrlTarget.path, raw: parsedUrlTarget.raw }, signal);
2197
+ return executeReadUrl(this.session, { path: parsedUrlTarget.path, raw: urlRaw }, signal);
2170
2198
  }
2171
2199
 
2172
2200
  // Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://, omp://, issue://, pr://).
@@ -2174,8 +2202,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2174
2202
  // off the URL and surfaced via parseSel rather than confusing handlers.
2175
2203
  const internalRouter = InternalUrlRouter.instance();
2176
2204
  if (internalRouter.canHandle(readPath)) {
2177
- const internalTarget = splitInternalUrlSel(readPath);
2178
- const parsed = parseSel(internalTarget.sel);
2205
+ const internalTarget =
2206
+ explicitSelector === undefined ? splitInternalUrlSel(readPath) : { path: readPath, sel: explicitSelector };
2207
+ const parsed = explicitParsedSelector ?? parseSel(internalTarget.sel);
2179
2208
  if (internalTarget.sel !== undefined && parsed.kind === "none") {
2180
2209
  throw new ToolError(
2181
2210
  `Invalid selector ':${internalTarget.sel}' on '${internalTarget.path}'. Use :N, :N-M, :N+K, :N- (open-ended), a comma-separated list of ranges, :raw, or a range combined with raw (e.g. :raw:50-100).`,
@@ -2192,7 +2221,16 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2192
2221
  skills: this.session.skills,
2193
2222
  });
2194
2223
  if (localFile) {
2195
- readPath = internalTarget.sel === undefined ? localFile.path : `${localFile.path}:${internalTarget.sel}`;
2224
+ readPath = localFile.path;
2225
+ // Promote the URL-embedded selector into the explicit-selector state so
2226
+ // downstream literal-preferring routing does NOT re-split the synthesized
2227
+ // `${localFile.path}:${sel}` string — a sibling literal file at that name
2228
+ // would otherwise shadow the intended local:// URL selector semantics
2229
+ // (issue #4618 reviewer feedback on c493d12).
2230
+ if (explicitSelector === undefined && internalTarget.sel !== undefined) {
2231
+ explicitSelector = internalTarget.sel;
2232
+ explicitParsedSelector = parsed;
2233
+ }
2196
2234
  } else {
2197
2235
  return this.#handleInternalUrl(internalTarget.path, parsed, signal);
2198
2236
  }
@@ -2205,48 +2243,68 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2205
2243
  // resolution share misses instead of re-globbing the workspace.
2206
2244
  const suffixCache: SuffixMatchCache = new Map();
2207
2245
 
2208
- const archivePath = await this.#resolveArchiveReadPath(readPath, suffixCache, signal);
2209
- if (archivePath) {
2210
- const archiveSubPath = splitPathAndSel(archivePath.archiveSubPath);
2211
- const archiveParsed = parseSel(archiveSubPath.sel);
2212
- return this.#readArchive(
2213
- readPath,
2214
- archiveParsed,
2215
- { ...archivePath, archiveSubPath: archiveSubPath.path },
2216
- signal,
2217
- );
2218
- }
2246
+ // Prefer a literal filesystem match over selector interpretation so real
2247
+ // POSIX filenames containing selector-looking suffixes win over structured
2248
+ // archive / sqlite / pdf-image dispatch. With explicit `selector`, `path`
2249
+ // is exact: `path: "test:1-2", selector: "1-2"` means "lines 1-2 from
2250
+ // the literal file test:1-2", without recursively depending on whether a
2251
+ // longer `test:1-2:1-2` filename also exists (issue #4618).
2252
+ const literalSplit =
2253
+ explicitSelector === undefined
2254
+ ? await splitPathAndSelPreferringLiteral(readPath, this.session.cwd)
2255
+ : { path: readPath, sel: explicitSelector };
2256
+ const rawPathIsLiteral =
2257
+ explicitSelector !== undefined
2258
+ ? readPath.includes(":") && (await probeLiteralPathExists(readPath, this.session.cwd)) !== "missing"
2259
+ : literalSplit.sel === undefined && splitPathAndSel(readPath).sel !== undefined;
2260
+
2261
+ if (!rawPathIsLiteral) {
2262
+ const archivePath = await this.#resolveArchiveReadPath(readPath, suffixCache, signal);
2263
+ if (archivePath) {
2264
+ const archiveSubPath =
2265
+ explicitSelector === undefined
2266
+ ? splitPathAndSel(archivePath.archiveSubPath)
2267
+ : { path: archivePath.archiveSubPath, sel: explicitSelector };
2268
+ const archiveParsed = parseSel(archiveSubPath.sel);
2269
+ return this.#readArchive(
2270
+ readPath,
2271
+ archiveParsed,
2272
+ { ...archivePath, archiveSubPath: archiveSubPath.path },
2273
+ signal,
2274
+ );
2275
+ }
2219
2276
 
2220
- const sqlitePath = await this.#resolveSqliteReadPath(readPath, suffixCache, signal);
2221
- if (sqlitePath) {
2222
- return this.#readSqlite(sqlitePath, signal);
2223
- }
2277
+ const sqlitePath = await this.#resolveSqliteReadPath(readPath, suffixCache, signal);
2278
+ if (sqlitePath) {
2279
+ return this.#readSqlite(sqlitePath, signal);
2280
+ }
2224
2281
 
2225
- const pdfImageMemberPath = splitPdfImageMemberReadPath(readPath);
2226
- if (pdfImageMemberPath) {
2227
- let absolutePdfPath = resolveReadPath(pdfImageMemberPath.pdfPath, this.session.cwd);
2228
- let suffixResolution: { from: string; to: string } | undefined;
2229
- try {
2230
- const stat = await Bun.file(absolutePdfPath).stat();
2231
- if (stat.isDirectory())
2232
- throw new ToolError(`Path '${pdfImageMemberPath.pdfPath}' is a directory, not a PDF file`);
2233
- } catch (error) {
2234
- if (!isNotFoundError(error) || isRemoteMountPath(absolutePdfPath)) throw error;
2235
- const suffixMatch = await this.#findSuffixMatchCached(suffixCache, pdfImageMemberPath.pdfPath, signal);
2236
- if (!suffixMatch) throw new ToolError(`Path '${pdfImageMemberPath.pdfPath}' not found`);
2237
- absolutePdfPath = suffixMatch.absolutePath;
2238
- suffixResolution = { from: pdfImageMemberPath.pdfPath, to: suffixMatch.displayPath };
2282
+ const pdfImageMemberPath = splitPdfImageMemberReadPath(readPath);
2283
+ if (pdfImageMemberPath) {
2284
+ let absolutePdfPath = resolveReadPath(pdfImageMemberPath.pdfPath, this.session.cwd);
2285
+ let suffixResolution: { from: string; to: string } | undefined;
2286
+ try {
2287
+ const stat = await Bun.file(absolutePdfPath).stat();
2288
+ if (stat.isDirectory())
2289
+ throw new ToolError(`Path '${pdfImageMemberPath.pdfPath}' is a directory, not a PDF file`);
2290
+ } catch (error) {
2291
+ if (!isNotFoundError(error) || isRemoteMountPath(absolutePdfPath)) throw error;
2292
+ const suffixMatch = await this.#findSuffixMatchCached(suffixCache, pdfImageMemberPath.pdfPath, signal);
2293
+ if (!suffixMatch) throw new ToolError(`Path '${pdfImageMemberPath.pdfPath}' not found`);
2294
+ absolutePdfPath = suffixMatch.absolutePath;
2295
+ suffixResolution = { from: pdfImageMemberPath.pdfPath, to: suffixMatch.displayPath };
2296
+ }
2297
+ return this.#readPdfImageMember(
2298
+ absolutePdfPath,
2299
+ pdfImageMemberPath.pdfPath,
2300
+ pdfImageMemberPath.member,
2301
+ suffixResolution,
2302
+ signal,
2303
+ );
2239
2304
  }
2240
- return this.#readPdfImageMember(
2241
- absolutePdfPath,
2242
- pdfImageMemberPath.pdfPath,
2243
- pdfImageMemberPath.member,
2244
- suffixResolution,
2245
- signal,
2246
- );
2247
2305
  }
2248
2306
 
2249
- const localTarget = splitPathAndSel(readPath);
2307
+ const localTarget = literalSplit;
2250
2308
  const localReadPath = localTarget.path;
2251
2309
  const parsed = parseSel(localTarget.sel);
2252
2310
 
@@ -0,0 +1,7 @@
1
+ /** formatLocalCalendarDate formats a Date as YYYY-MM-DD in the host local timezone. */
2
+ export function formatLocalCalendarDate(date: Date = new Date()): string {
3
+ const year = date.getFullYear();
4
+ const month = String(date.getMonth() + 1).padStart(2, "0");
5
+ const day = String(date.getDate()).padStart(2, "0");
6
+ return `${year}-${month}-${day}`;
7
+ }
package/src/utils/open.ts CHANGED
@@ -32,22 +32,48 @@ function getExistingWslLocalPath(urlOrPath: string): string | undefined {
32
32
  }
33
33
 
34
34
  /**
35
- * Resolve the Windows `rundll32.exe` command used to hand a URL/path to the
36
- * user's registered protocol handler. Anchoring to `%SystemRoot%\System32`
37
- * (rather than relying on `rundll32` being on `PATH`) survives environments
38
- * where the machine `PATH` no longer references `System32` — a common
39
- * real-world misconfiguration where `System32\Wbem` / `WindowsPowerShell` /
40
- * `OpenSSH` survive but `System32` itself is dropped. Bare `rundll32` on
41
- * such boxes throws `Executable not found in $PATH: "rundll32"` from
42
- * `Bun.spawn` before ShellExecute ever sees the URL.
35
+ * Resolve the Windows opener used to hand a URL/path to the user's registered
36
+ * protocol handler. PowerShell's `Start-Process` goes through ShellExecute
37
+ * like the previous `rundll32 url.dll,FileProtocolHandler`, with two
38
+ * advantages that make the delayed-failure telemetry in {@link openPath}
39
+ * actually observable on Windows:
40
+ *
41
+ * - `rundll32` exits 0 unconditionally, so no launch failure ever reaches the
42
+ * non-zero-exit logging below. `Start-Process` surfaces the failures
43
+ * ShellExecute itself reports — missing target file, no handler executable,
44
+ * access denied — as exit code 1 (verified live: a nonexistent file path
45
+ * exits 1; `$ErrorActionPreference='Stop'` additionally promotes any
46
+ * non-terminating error classes). Known limitation shared by every opener:
47
+ * an unregistered URL scheme exits 0 because Windows "handles" it by
48
+ * offering the app-picker.
49
+ * - `-EncodedCommand` carries the target as a UTF-16LE/base64 payload, so no
50
+ * cmd/PowerShell metacharacter parsing ever sees it (OAuth authorize URLs
51
+ * carry `&`); inside the decoded script the target is a single-quoted
52
+ * literal (no `$` expansion) with embedded quotes doubled.
53
+ *
54
+ * PowerShell is anchored to `%SystemRoot%\System32` for the same reason the
55
+ * previous revision anchored `rundll32`: machine PATHs that dropped
56
+ * `System32` are a real-world occurrence, and bare names throw
57
+ * `Executable not found in $PATH` from `Bun.spawn`. A bare-name fallback
58
+ * remains for exotic SystemRoot layouts.
43
59
  */
44
60
  function windowsOpenerCommand(target: string): string[] {
45
61
  const systemRoot = process.env.SystemRoot?.trim() || process.env.SYSTEMROOT?.trim() || "C:\\Windows";
46
62
  // `path.win32` (not the platform-adaptive `path.join`) keeps Windows path
47
63
  // separators when tests run under a POSIX host and matches Windows call
48
64
  // conventions on the real target.
49
- const rundll32 = path.win32.join(systemRoot, "System32", "rundll32.exe");
50
- return [rundll32, "url.dll,FileProtocolHandler", target];
65
+ const absolute = path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
66
+ const powershell = fs.existsSync(absolute) ? absolute : "powershell.exe";
67
+ const script = `$ErrorActionPreference='Stop';Start-Process '${target.replaceAll("'", "''")}'`;
68
+ return [
69
+ powershell,
70
+ "-NoProfile",
71
+ "-NonInteractive",
72
+ "-WindowStyle",
73
+ "Hidden",
74
+ "-EncodedCommand",
75
+ Buffer.from(script, "utf16le").toString("base64"),
76
+ ];
51
77
  }
52
78
  /** Open a URL or file path in the default browser/application. Best-effort, never throws. */
53
79
  export function openPath(urlOrPath: string): void {