@oh-my-pi/pi-coding-agent 13.10.0 → 13.11.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 (69) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/package.json +7 -7
  3. package/src/commit/agentic/agent.ts +3 -1
  4. package/src/commit/agentic/index.ts +7 -1
  5. package/src/commit/analysis/conventional.ts +5 -1
  6. package/src/commit/analysis/summary.ts +5 -1
  7. package/src/commit/changelog/generate.ts +5 -1
  8. package/src/commit/changelog/index.ts +4 -0
  9. package/src/commit/map-reduce/index.ts +5 -0
  10. package/src/commit/map-reduce/map-phase.ts +17 -2
  11. package/src/commit/map-reduce/reduce-phase.ts +5 -1
  12. package/src/commit/model-selection.ts +38 -26
  13. package/src/commit/pipeline.ts +22 -11
  14. package/src/config/settings-schema.ts +20 -0
  15. package/src/config.ts +10 -3
  16. package/src/discovery/helpers.ts +7 -3
  17. package/src/internal-urls/docs-index.generated.ts +1 -1
  18. package/src/lsp/index.ts +4 -4
  19. package/src/lsp/utils.ts +81 -0
  20. package/src/main.ts +25 -14
  21. package/src/mcp/manager.ts +40 -2
  22. package/src/mcp/oauth-flow.ts +41 -0
  23. package/src/mcp/transports/http.ts +23 -0
  24. package/src/mcp/types.ts +6 -0
  25. package/src/modes/components/mcp-add-wizard.ts +12 -0
  26. package/src/modes/components/settings-defs.ts +2 -1
  27. package/src/modes/components/todo-reminder.ts +8 -1
  28. package/src/modes/controllers/command-controller.ts +75 -3
  29. package/src/modes/controllers/input-controller.ts +2 -3
  30. package/src/modes/controllers/mcp-command-controller.ts +9 -1
  31. package/src/modes/interactive-mode.ts +11 -7
  32. package/src/modes/theme/theme.ts +30 -27
  33. package/src/modes/types.ts +2 -1
  34. package/src/patch/hashline.ts +3 -6
  35. package/src/prompts/system/eager-todo.md +13 -0
  36. package/src/prompts/tools/ast-edit.md +1 -1
  37. package/src/prompts/tools/ast-grep.md +1 -1
  38. package/src/prompts/tools/find.md +1 -0
  39. package/src/prompts/tools/grep.md +1 -0
  40. package/src/prompts/tools/hashline.md +23 -111
  41. package/src/prompts/tools/todo-write.md +11 -1
  42. package/src/sdk.ts +1 -1
  43. package/src/session/agent-session.ts +85 -7
  44. package/src/session/session-manager.ts +5 -9
  45. package/src/slash-commands/builtin-registry.ts +10 -2
  46. package/src/task/executor.ts +9 -18
  47. package/src/task/index.ts +8 -4
  48. package/src/task/render.ts +5 -10
  49. package/src/task/template.ts +4 -1
  50. package/src/task/types.ts +2 -0
  51. package/src/tools/ast-edit.ts +26 -7
  52. package/src/tools/ast-grep.ts +26 -9
  53. package/src/tools/fetch.ts +36 -5
  54. package/src/tools/find.ts +13 -64
  55. package/src/tools/grep.ts +27 -10
  56. package/src/tools/json-tree.ts +1 -1
  57. package/src/tools/output-meta.ts +2 -1
  58. package/src/tools/path-utils.ts +348 -0
  59. package/src/tools/todo-write.ts +27 -4
  60. package/src/utils/commit-message-generator.ts +27 -22
  61. package/src/utils/image-input.ts +1 -1
  62. package/src/utils/image-resize.ts +4 -4
  63. package/src/utils/title-generator.ts +36 -23
  64. package/src/utils/tool-choice.ts +28 -0
  65. package/src/web/parallel.ts +346 -0
  66. package/src/web/scrapers/youtube.ts +29 -0
  67. package/src/web/search/provider.ts +4 -1
  68. package/src/web/search/providers/parallel.ts +63 -0
  69. package/src/web/search/types.ts +1 -0
@@ -7,6 +7,7 @@ import { ptree, truncate } from "@oh-my-pi/pi-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
8
  import { parseHTML } from "linkedom";
9
9
  import { renderPromptTemplate } from "../config/prompt-templates";
10
+ import type { Settings } from "../config/settings";
10
11
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
12
  import { type Theme, theme } from "../modes/theme/theme";
12
13
  import fetchDescription from "../prompts/tools/fetch.md" with { type: "text" };
@@ -15,6 +16,7 @@ import { renderStatusLine } from "../tui";
15
16
  import { CachedOutputBlock } from "../tui/output-block";
16
17
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
17
18
  import { ensureTool } from "../utils/tools-manager";
19
+ import { extractWithParallel, findParallelApiKey, getParallelExtractContent } from "../web/parallel";
18
20
  import { specialHandlers } from "../web/scrapers";
19
21
  import type { RenderResult } from "../web/scrapers/types";
20
22
  import { finalizeOutput, loadPage, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
@@ -465,12 +467,13 @@ function parseFeedToMarkdown(content: string, maxItems = 10): string {
465
467
  }
466
468
 
467
469
  /**
468
- * Render HTML to markdown using jina, trafilatura, lynx (in order of preference)
470
+ * Render HTML to markdown using Parallel, jina, trafilatura, lynx (in order of preference)
469
471
  */
470
472
  async function renderHtmlToText(
471
473
  url: string,
472
474
  html: string,
473
475
  timeout: number,
476
+ settings: Settings,
474
477
  userSignal?: AbortSignal,
475
478
  ): Promise<{ content: string; ok: boolean; method: string }> {
476
479
  const signal = ptree.combineSignals(userSignal, timeout * 1000);
@@ -482,6 +485,28 @@ async function renderHtmlToText(
482
485
  signal,
483
486
  };
484
487
 
488
+ // Try Parallel extract first when credentials are configured
489
+ if (settings.get("providers.parallelFetch") && (await findParallelApiKey())) {
490
+ try {
491
+ const parallelResult = await extractWithParallel([url], {
492
+ objective: "Extract the main content",
493
+ excerpts: true,
494
+ fullContent: false,
495
+ signal,
496
+ });
497
+ const firstDocument = parallelResult.results[0];
498
+ if (firstDocument) {
499
+ const content = getParallelExtractContent(firstDocument);
500
+ if (content.trim().length > 100 && !isLowQualityOutput(content)) {
501
+ return { content, ok: true, method: "parallel" };
502
+ }
503
+ }
504
+ } catch {
505
+ // Parallel extract failed, continue to next method
506
+ signal?.throwIfAborted();
507
+ }
508
+ }
509
+
485
510
  // Try jina first (reader API)
486
511
  try {
487
512
  const jinaUrl = `https://r.jina.ai/${url}`;
@@ -608,7 +633,13 @@ async function handleSpecialUrls(
608
633
  /**
609
634
  * Main render function implementing the full pipeline
610
635
  */
611
- async function renderUrl(url: string, timeout: number, raw: boolean, signal?: AbortSignal): Promise<FetchRenderResult> {
636
+ async function renderUrl(
637
+ url: string,
638
+ timeout: number,
639
+ raw: boolean,
640
+ settings: Settings,
641
+ signal?: AbortSignal,
642
+ ): Promise<FetchRenderResult> {
612
643
  const notes: string[] = [];
613
644
  const fetchedAt = new Date().toISOString();
614
645
  if (signal?.aborted) {
@@ -714,7 +745,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean, signal?: Ab
714
745
  }
715
746
 
716
747
  const resized = await resizeImage(
717
- { type: "image", data: binary.buffer.toBase64(), mimeType: imageMimeType },
748
+ { type: "image", data: Buffer.from(binary.buffer).toBase64(), mimeType: imageMimeType },
718
749
  { maxBytes: MAX_INLINE_IMAGE_OUTPUT_BYTES },
719
750
  );
720
751
  const isDecodedImage =
@@ -952,7 +983,7 @@ async function renderUrl(url: string, timeout: number, raw: boolean, signal?: Ab
952
983
  }
953
984
 
954
985
  // 5E: Render HTML with lynx or html2text
955
- const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, signal);
986
+ const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, settings, signal);
956
987
  if (!htmlResult.ok) {
957
988
  notes.push("html rendering failed (lynx/html2text unavailable)");
958
989
  const output = finalizeOutput(rawContent);
@@ -1092,7 +1123,7 @@ export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails
1092
1123
  throw new ToolAbortError();
1093
1124
  }
1094
1125
 
1095
- const result = await renderUrl(url, effectiveTimeout, raw, signal);
1126
+ const result = await renderUrl(url, effectiveTimeout, raw, this.session.settings, signal);
1096
1127
  const truncation = truncateHead(result.content, {
1097
1128
  maxBytes: DEFAULT_MAX_BYTES,
1098
1129
  maxLines: FETCH_DEFAULT_MAX_LINES,
package/src/tools/find.ts CHANGED
@@ -24,7 +24,7 @@ import {
24
24
  import type { ToolSession } from ".";
25
25
  import { applyListLimit } from "./list-limit";
26
26
  import { formatFullOutputReference, type OutputMeta } from "./output-meta";
27
- import { resolveToCwd } from "./path-utils";
27
+ import { parseFindPattern, resolveMultiFindPattern, resolveToCwd } from "./path-utils";
28
28
  import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
29
29
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
30
30
  import { toolResult } from "./tool-result";
@@ -40,56 +40,6 @@ export type FindToolInput = Static<typeof findSchema>;
40
40
  const DEFAULT_LIMIT = 1000;
41
41
  const GLOB_TIMEOUT_MS = 5000;
42
42
 
43
- /**
44
- * Parse a pattern to extract the base directory path and glob pattern.
45
- * Examples:
46
- * "src/app/**\/*.tsx" → { basePath: "src/app", globPattern: "**\/*.tsx" }
47
- * "src/app/*.tsx" → { basePath: "src/app", globPattern: "*.tsx" }
48
- * "*.ts" → { basePath: ".", globPattern: "**\/*.ts" }
49
- * "**\/*.json" → { basePath: ".", globPattern: "**\/*.json" }
50
- * "/abs/path/**\/*.ts" → { basePath: "/abs/path", globPattern: "**\/*.ts" }
51
- */
52
- function parsePatternPath(pattern: string): { basePath: string; globPattern: string } {
53
- // Find the first segment containing glob characters
54
- const segments = pattern.split("/");
55
- const globChars = ["*", "?", "[", "{"];
56
-
57
- let firstGlobIndex = -1;
58
- for (let i = 0; i < segments.length; i++) {
59
- if (globChars.some(c => segments[i].includes(c))) {
60
- firstGlobIndex = i;
61
- break;
62
- }
63
- }
64
-
65
- // No glob characters found - treat as literal path with implicit **/*
66
- if (firstGlobIndex === -1) {
67
- // Pattern is a directory path like "src/app" - search recursively in it
68
- return { basePath: pattern, globPattern: "**/*" };
69
- }
70
-
71
- // Glob starts at first segment - no base path
72
- if (firstGlobIndex === 0) {
73
- // Simple pattern like "*.ts" needs **/ prefix for recursive search
74
- const needsRecursive = !pattern.startsWith("**/");
75
- return {
76
- basePath: ".",
77
- globPattern: needsRecursive ? `**/${pattern}` : pattern,
78
- };
79
- }
80
-
81
- // Split at the glob boundary
82
- const basePath = segments.slice(0, firstGlobIndex).join("/");
83
- const globPattern = segments.slice(firstGlobIndex).join("/");
84
-
85
- return { basePath, globPattern };
86
- }
87
-
88
- function hasGlobChars(pattern: string): boolean {
89
- const globChars = ["*", "?", "[", "{"];
90
- return globChars.some(char => pattern.includes(char));
91
- }
92
-
93
43
  export interface FindToolDetails {
94
44
  truncation?: TruncationResult;
95
45
  resultLimitReached?: number;
@@ -149,27 +99,26 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
149
99
  const { pattern, limit, hidden } = params;
150
100
 
151
101
  return untilAborted(signal, async () => {
152
- // Parse pattern to extract base directory and glob pattern
153
- // e.g., "src/app/**/*.tsx" basePath: "src/app", globPattern: "**/*.tsx"
154
- // e.g., "*.ts" basePath: ".", globPattern: "**/*.ts"
102
+ const formatScopePath = (targetPath: string): string => {
103
+ const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
104
+ return relative.length === 0 ? "." : relative;
105
+ };
155
106
  const normalizedPattern = pattern.trim().replace(/\\/g, "/");
156
107
  if (!normalizedPattern) {
157
108
  throw new ToolError("Pattern must not be empty");
158
109
  }
159
110
 
160
- const hasGlob = hasGlobChars(normalizedPattern);
161
- const { basePath, globPattern } = parsePatternPath(normalizedPattern);
162
- const searchPath = resolveToCwd(basePath, this.session.cwd);
111
+ const multiPattern = await resolveMultiFindPattern(normalizedPattern, this.session.cwd);
112
+ const parsedPattern = multiPattern ? null : parseFindPattern(normalizedPattern);
113
+ const hasGlob = multiPattern ? true : (parsedPattern?.hasGlob ?? false);
114
+ const globPattern = multiPattern?.globPattern ?? parsedPattern?.globPattern ?? "**/*";
115
+ const searchPath = resolveToCwd(multiPattern?.basePath ?? parsedPattern?.basePath ?? ".", this.session.cwd);
116
+ const scopePath = multiPattern?.scopePath ?? formatScopePath(searchPath);
163
117
 
164
118
  if (searchPath === "/") {
165
119
  throw new ToolError("Searching from root directory '/' is not allowed");
166
120
  }
167
121
 
168
- const scopePath = (() => {
169
- const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
170
- return relative.length === 0 ? "." : relative;
171
- })();
172
-
173
122
  const rawLimit = limit ?? DEFAULT_LIMIT;
174
123
  const effectiveLimit = Number.isFinite(rawLimit) ? Math.floor(rawLimit) : Number.NaN;
175
124
  if (!Number.isFinite(effectiveLimit) || effectiveLimit <= 0) {
@@ -180,7 +129,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
180
129
  // If custom operations provided with glob, use that instead of fd
181
130
  if (this.#customOps?.glob) {
182
131
  if (!(await this.#customOps.exists(searchPath))) {
183
- throw new ToolError(`Path not found: ${searchPath}`);
132
+ throw new ToolError(`Path not found: ${scopePath}`);
184
133
  }
185
134
 
186
135
  if (!hasGlob && this.#customOps.stat) {
@@ -245,7 +194,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
245
194
  searchStat = await fs.promises.stat(searchPath);
246
195
  } catch (err) {
247
196
  if (isEnoent(err)) {
248
- throw new ToolError(`Path not found: ${searchPath}`);
197
+ throw new ToolError(`Path not found: ${scopePath}`);
249
198
  }
250
199
  throw err;
251
200
  }
package/src/tools/grep.ts CHANGED
@@ -16,7 +16,13 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
16
16
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
17
17
  import type { ToolSession } from ".";
18
18
  import { formatFullOutputReference, type OutputMeta } from "./output-meta";
19
- import { combineSearchGlobs, hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
19
+ import {
20
+ combineSearchGlobs,
21
+ hasGlobPathChars,
22
+ parseSearchPath,
23
+ resolveMultiSearchPath,
24
+ resolveToCwd,
25
+ } from "./path-utils";
20
26
  import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
21
27
  import { ToolError } from "./tool-errors";
22
28
  import { toolResult } from "./tool-result";
@@ -107,7 +113,12 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
107
113
  const effectiveMultiline = multiline ?? patternHasNewline;
108
114
 
109
115
  const useHashLines = resolveFileDisplayMode(this.session).hashLines;
116
+ const formatScopePath = (targetPath: string): string => {
117
+ const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
118
+ return relative.length === 0 ? "." : relative;
119
+ };
110
120
  let searchPath: string;
121
+ let scopePath: string;
111
122
  let globFilter = glob?.trim() || undefined;
112
123
  const internalRouter = this.session.internalRouter;
113
124
  if (searchDir?.trim()) {
@@ -121,27 +132,33 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
121
132
  throw new ToolError(`Cannot grep internal URL without a backing file: ${rawPath}`);
122
133
  }
123
134
  searchPath = resource.sourcePath;
135
+ scopePath = formatScopePath(searchPath);
124
136
  } else {
125
- const parsedPath = parseSearchPath(rawPath);
126
- searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
127
- if (parsedPath.glob) {
128
- globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
137
+ const multiSearchPath = await resolveMultiSearchPath(rawPath, this.session.cwd, globFilter);
138
+ if (multiSearchPath) {
139
+ searchPath = multiSearchPath.basePath;
140
+ globFilter = multiSearchPath.glob;
141
+ scopePath = multiSearchPath.scopePath;
142
+ } else {
143
+ const parsedPath = parseSearchPath(rawPath);
144
+ searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
145
+ if (parsedPath.glob) {
146
+ globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
147
+ }
148
+ scopePath = formatScopePath(searchPath);
129
149
  }
130
150
  }
131
151
  } else {
132
152
  searchPath = resolveToCwd(".", this.session.cwd);
153
+ scopePath = ".";
133
154
  }
134
- const scopePath = (() => {
135
- const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
136
- return relative.length === 0 ? "." : relative;
137
- })();
138
155
 
139
156
  let isDirectory: boolean;
140
157
  try {
141
158
  const stat = await Bun.file(searchPath).stat();
142
159
  isDirectory = stat.isDirectory();
143
160
  } catch {
144
- throw new ToolError(`Path not found: ${searchPath}`);
161
+ throw new ToolError(`Path not found: ${scopePath}`);
145
162
  }
146
163
 
147
164
  const effectiveOutputMode = "content";
@@ -14,7 +14,7 @@ export const JSON_TREE_SCALAR_LEN_COLLAPSED = 60;
14
14
  export const JSON_TREE_SCALAR_LEN_EXPANDED = 2000;
15
15
 
16
16
  /** Keys injected by the harness that should not be displayed to users */
17
- const HIDDEN_ARG_KEYS = new Set([INTENT_FIELD]);
17
+ const HIDDEN_ARG_KEYS = new Set([INTENT_FIELD, "__partialJson"]);
18
18
 
19
19
  /** Strip harness-internal keys from tool args for display */
20
20
  export function stripInternalArgs(args: Record<string, unknown>): Record<string, unknown> {
@@ -12,6 +12,7 @@ import type {
12
12
  AgentToolUpdateCallback,
13
13
  } from "@oh-my-pi/pi-agent-core";
14
14
  import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
15
+ import { formatGroupedDiagnosticMessages } from "../lsp/utils";
15
16
  import type { Theme } from "../modes/theme/theme";
16
17
  import type { OutputSummary, TruncationResult } from "../session/streaming-output";
17
18
  import { formatBytes, wrapBrackets } from "./render-utils";
@@ -386,7 +387,7 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
386
387
  let diagnosticsNotice = "";
387
388
  if (meta.diagnostics && meta.diagnostics.messages.length > 0) {
388
389
  const d = meta.diagnostics;
389
- diagnosticsNotice = `\n\nLSP Diagnostics (${d.summary}):\n ${d.messages.join("\n ")}`;
390
+ diagnosticsNotice = `\n\nLSP Diagnostics (${d.summary}):\n${formatGroupedDiagnosticMessages(d.messages)}`;
390
391
  }
391
392
 
392
393
  const notice = parts.length ? `\n\n[${parts.join(". ")}]` : "";