@oh-my-pi/pi-coding-agent 13.9.4 → 13.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.9.6] - 2026-03-08
6
+
7
+ ### Added
8
+
9
+ - Added `glob` parameter to `ast_grep` and `ast_edit` tools for additional glob filtering relative to the `path` parameter
10
+ - Added `combineSearchGlobs` utility function to merge glob patterns from `path` and `glob` parameters
11
+
12
+ ### Changed
13
+
14
+ - Renamed `patterns` parameter to `pat` in `ast_grep` tool for consistency
15
+ - Renamed `selector` parameter to `sel` in `ast_grep` and `ast_edit` tools for brevity
16
+ - Updated tool documentation with expanded guidance on AST pattern syntax, metavariable usage, and contextual matching strategies
17
+ - Updated `grep` tool to combine glob patterns from `path` and `glob` parameters instead of throwing an error when both are provided
18
+
5
19
  ## [13.9.4] - 2026-03-07
6
20
  ### Added
7
21
 
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": "13.9.4",
4
+ "version": "13.9.6",
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",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.9.4",
45
- "@oh-my-pi/pi-agent-core": "13.9.4",
46
- "@oh-my-pi/pi-ai": "13.9.4",
47
- "@oh-my-pi/pi-natives": "13.9.4",
48
- "@oh-my-pi/pi-tui": "13.9.4",
49
- "@oh-my-pi/pi-utils": "13.9.4",
44
+ "@oh-my-pi/omp-stats": "13.9.6",
45
+ "@oh-my-pi/pi-agent-core": "13.9.6",
46
+ "@oh-my-pi/pi-ai": "13.9.6",
47
+ "@oh-my-pi/pi-natives": "13.9.6",
48
+ "@oh-my-pi/pi-tui": "13.9.6",
49
+ "@oh-my-pi/pi-utils": "13.9.6",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -2,10 +2,16 @@ Performs structural AST-aware rewrites via native ast-grep.
2
2
 
3
3
  <instruction>
4
4
  - Use for codemods and structural rewrites where plain text replace is unsafe
5
- - Narrow scope with `path` before replacing (`path` accepts files, directories, or glob patterns)
6
- - Default to language-scoped rewrites in mixed repositories: set `lang` and keep `path` narrow
7
- - Treat parse issues as a scoping signal: tighten `path`/`lang` before retrying
5
+ - Narrow scope with `path` before replacing (`path` accepts files, directories, or glob patterns; use `glob` for an additional filter relative to `path`)
6
+ - Default to language-scoped rewrites in mixed repositories: set `lang` and keep `path`/`glob` narrow
7
+ - Treat parse issues as a scoping or pattern-shape signal: tighten `path`/`lang`, or rewrite the pattern into valid syntax before retrying
8
8
  - Metavariables captured in each rewrite pattern (`$A`, `$$$ARGS`) are substituted into that entry's rewrite template
9
+ - For variadic captures, use `$$$NAME` (not `$$NAME`)
10
+ - Rewrite patterns must parse as valid AST for the target language; if a method or declaration does not parse standalone, wrap it in valid context or switch to a contextual `sel`
11
+ - When using contextual `sel`, the match and replacement target the selected node, not the outer wrapper you used to make the pattern parse
12
+ - For TypeScript declarations and methods, prefer patterns that tolerate annotations you do not care about, e.g. `async function $NAME($$$ARGS): $_ { $$$BODY }` or `class $_ { method($$$ARGS): $_ { $$$BODY } }`
13
+ - Metavariables must be the sole content of an AST node; partial-text metavariables like `prefix$VAR` or `"hello $NAME"` do NOT work in patterns or rewrites
14
+ - To delete matched code, use an empty `out` string: `{"pat":"console.log($$$)","out":""}`
9
15
  - Each matched rewrite is a 1:1 structural substitution; you cannot split one capture into multiple nodes or merge multiple captures into one node
10
16
  </instruction>
11
17
 
@@ -17,14 +23,23 @@ Performs structural AST-aware rewrites via native ast-grep.
17
23
  <examples>
18
24
  - Rename a call site across a directory:
19
25
  `{"ops":[{"pat":"oldApi($$$ARGS)","out":"newApi($$$ARGS)"}],"lang":"typescript","path":"src/"}`
20
- - Multi-op codemod:
21
- `{"ops":[{"pat":"require($A)","out":"import $A"},{"pat":"module.exports = $E","out":"export default $E"}],"lang":"javascript","path":"src/"}`
26
+ - Delete all matching calls (empty `out` removes the matched node):
27
+ `{"ops":[{"pat":"console.log($$$ARGS)","out":""}],"lang":"typescript","path":"src/"}`
28
+ - Rewrite an import source path:
29
+ `{"ops":[{"pat":"import { $$$IMPORTS } from \"old-package\"","out":"import { $$$IMPORTS } from \"new-package\""}],"lang":"typescript","path":"src/"}`
30
+ - Modernize to optional chaining (same metavariable enforces identity):
31
+ `{"ops":[{"pat":"$A && $A()","out":"$A?.()"}],"lang":"typescript","path":"src/"}`
22
32
  - Swap two arguments using captures:
23
33
  `{"ops":[{"pat":"assertEqual($A, $B)","out":"assertEqual($B, $A)"}],"lang":"typescript","path":"tests/"}`
34
+ - Rename a TypeScript function declaration while tolerating any return type annotation:
35
+ `{"ops":[{"pat":"async function fetchData($$$ARGS): $_ { $$$BODY }","out":"async function loadData($$$ARGS): $_ { $$$BODY }"}],"sel":"function_declaration","lang":"typescript","path":"src/api.ts"}`
36
+ - Convert Python print calls to logging:
37
+ `{"ops":[{"pat":"print($$$ARGS)","out":"logger.info($$$ARGS)"}],"lang":"python","path":"src/"}`
24
38
  </examples>
25
39
 
26
40
  <critical>
27
41
  - `ops` **MUST** contain at least one concrete `{ pat, out }` entry
28
42
  - If the path pattern spans multiple languages, set `lang` explicitly for deterministic rewrites
43
+ - Parse issues mean the rewrite request is malformed or mis-scoped; do not assume a clean no-op until the pattern parses successfully
29
44
  - For one-off local text edits, prefer the Edit tool instead of AST edit
30
45
  </critical>
@@ -2,14 +2,22 @@ Performs structural code search using AST matching via native ast-grep.
2
2
 
3
3
  <instruction>
4
4
  - Use this when syntax shape matters more than raw text (calls, declarations, specific language constructs)
5
- - Prefer a precise `path` scope to keep results targeted and deterministic (`path` accepts files, directories, or glob patterns)
6
- - Default to language-scoped search in mixed repositories: pair `path` glob + explicit `lang` to avoid parse-noise from non-source files
7
- - `patterns` is required and must include at least one non-empty AST pattern; `lang` is optional (`lang` is inferred per file extension when omitted)
5
+ - Prefer a precise `path` scope to keep results targeted and deterministic (`path` accepts files, directories, or glob patterns; use `glob` for an additional filter relative to `path`)
6
+ - Default to language-scoped search in mixed repositories: pair `path` + `glob` + explicit `lang` to avoid parse-noise from non-source files
7
+ - `pat` is required and must include at least one non-empty AST pattern; `lang` is optional (`lang` is inferred per file extension when omitted)
8
8
  - Multiple patterns run in one native pass; results are merged and then `offset`/`limit` are applied to the combined match set
9
- - Use `selector` only for contextual pattern mode; otherwise provide direct patterns
9
+ - Use `sel` only for contextual pattern mode; otherwise provide direct patterns
10
+ - In contextual pattern mode, results are returned for the selected node (`sel`), not the outer wrapper used to make the pattern parse
10
11
  - For variadic arguments/fields, use `$$$NAME` (not `$$NAME`)
12
+ - Patterns must parse as a single valid AST node for the target language; if a bare pattern fails, wrap it in valid context or use `sel`
11
13
  - Patterns match AST structure, not text — whitespace/formatting differences are ignored
12
14
  - When the same metavariable appears multiple times, all occurrences must match identical code
15
+ - For TypeScript declarations and methods, prefer shapes that tolerate annotations you do not care about, e.g. `async function $NAME($$$ARGS): $_ { $$$BODY }` or `class $_ { method($$$ARGS): $_ { $$$BODY } }` instead of omitting the return type entirely
16
+ - Metavariables must be the sole content of an AST node; partial-text metavariables like `prefix$VAR`, `"hello $NAME"`, or `a $OP b` do NOT work — match the whole node instead
17
+ - `$$$` captures are lazy (non-greedy): they stop when the next element in the pattern can match; place the most specific node after `$$$` to control where capture ends
18
+ - `$_` is a non-capturing wildcard (matches any single node without binding); use it when you need to tolerate a node but don't need its value
19
+ - Search the right declaration form before concluding absence: top-level function, class method, and variable-assigned function are different AST shapes
20
+ - If you only need to prove a symbol exists, prefer a looser contextual search such as `pat: ["executeBash"]` with `sel: "identifier"`
13
21
  </instruction>
14
22
 
15
23
  <output>
@@ -19,16 +27,25 @@ Performs structural code search using AST matching via native ast-grep.
19
27
 
20
28
  <examples>
21
29
  - Find all console logging calls in one pass (multi-pattern, scoped):
22
- `{"patterns":["console.log($$$)","console.error($$$)"],"lang":"typescript","path":"src/"}`
23
- - Capture and inspect metavariable bindings from a pattern:
24
- `{"patterns":["require($MOD)"],"lang":"javascript","path":"src/"}`
30
+ `{"pat":["console.log($$$)","console.error($$$)"],"lang":"typescript","path":"src/"}`
31
+ - Find all named imports from a specific package:
32
+ `{"pat":["import { $$$IMPORTS } from \"react\""],"lang":"typescript","path":"src/"}`
33
+ - Match arrow functions assigned to a const (different AST shape than function declarations):
34
+ `{"pat":["const $NAME = ($$$ARGS) => $BODY"],"lang":"typescript","path":"src/utils/"}`
35
+ - Match any method call on an object using wildcard `$_` (ignores method name):
36
+ `{"pat":["logger.$_($$$ARGS)"],"lang":"typescript","path":"src/"}`
25
37
  - Contextual pattern with selector — match only the identifier `foo`, not the whole call:
26
- `{"patterns":["foo()"],"selector":"identifier","lang":"typescript","path":"src/utils.ts"}`
38
+ `{"pat":["foo()"],"sel":"identifier","lang":"typescript","path":"src/utils.ts"}`
39
+ - Match a TypeScript function declaration without caring about its exact return type:
40
+ `{"pat":["async function processItems($$$ARGS): $_ { $$$BODY }"],"sel":"function_declaration","lang":"typescript","path":"src/worker.ts"}`
41
+ - Loosest existence check for a symbol in one file:
42
+ `{"pat":["processItems"],"sel":"identifier","lang":"typescript","path":"src/worker.ts"}`
27
43
  </examples>
28
44
 
29
45
  <critical>
30
- - `patterns` is required
46
+ - `pat` is required
31
47
  - Set `lang` explicitly to constrain matching when path pattern spans mixed-language trees
32
48
  - Avoid repo-root AST scans when the target is language-specific; narrow `path` first
49
+ - Treat parse issues as query failure, not evidence of absence: repair the pattern or tighten `path`/`glob`/`lang` before concluding "no matches"
33
50
  - If exploration is broad/open-ended across subsystems, use Task tool with explore subagent first
34
51
  </critical>
@@ -14,7 +14,7 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
14
14
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
15
15
  import type { ToolSession } from ".";
16
16
  import type { OutputMeta } from "./output-meta";
17
- import { hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
17
+ import { combineSearchGlobs, hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
18
18
  import {
19
19
  dedupeParseErrors,
20
20
  formatCount,
@@ -38,7 +38,8 @@ const astEditSchema = Type.Object({
38
38
  }),
39
39
  lang: Type.Optional(Type.String({ description: "Language override" })),
40
40
  path: Type.Optional(Type.String({ description: "File, directory, or glob pattern to rewrite (default: cwd)" })),
41
- selector: Type.Optional(Type.String({ description: "Optional selector for contextual pattern mode" })),
41
+ glob: Type.Optional(Type.String({ description: "Optional glob filter relative to path" })),
42
+ sel: Type.Optional(Type.String({ description: "Optional selector for contextual pattern mode" })),
42
43
  limit: Type.Optional(Type.Number({ description: "Max total replacements" })),
43
44
  });
44
45
 
@@ -98,7 +99,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
98
99
  const maxFiles = parseInt(process.env.PI_MAX_AST_FILES ?? "", 10) || 1000;
99
100
 
100
101
  let searchPath: string | undefined;
101
- let globFilter: string | undefined;
102
+ let globFilter = params.glob?.trim() || undefined;
102
103
  const rawPath = params.path?.trim();
103
104
  if (rawPath) {
104
105
  const internalRouter = this.session.internalRouter;
@@ -114,7 +115,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
114
115
  } else {
115
116
  const parsedPath = parseSearchPath(rawPath);
116
117
  searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
117
- globFilter = parsedPath.glob;
118
+ globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
118
119
  }
119
120
  }
120
121
 
@@ -133,7 +134,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
133
134
  lang: params.lang?.trim(),
134
135
  path: resolvedSearchPath,
135
136
  glob: globFilter,
136
- selector: params.selector?.trim(),
137
+ selector: params.sel?.trim(),
137
138
  dryRun: true,
138
139
  maxReplacements,
139
140
  maxFiles,
@@ -277,7 +278,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
277
278
  lang: params.lang?.trim(),
278
279
  path: resolvedSearchPath,
279
280
  glob: globFilter,
280
- selector: params.selector?.trim(),
281
+ selector: params.sel?.trim(),
281
282
  dryRun: false,
282
283
  maxReplacements,
283
284
  maxFiles,
@@ -320,7 +321,7 @@ interface AstEditRenderArgs {
320
321
  ops?: Array<{ pat?: string; out?: string }>;
321
322
  lang?: string;
322
323
  path?: string;
323
- selector?: string;
324
+ sel?: string;
324
325
  limit?: number;
325
326
  }
326
327
 
@@ -14,7 +14,7 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, t
14
14
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
15
15
  import type { ToolSession } from ".";
16
16
  import type { OutputMeta } from "./output-meta";
17
- import { hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
17
+ import { combineSearchGlobs, hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
18
18
  import {
19
19
  dedupeParseErrors,
20
20
  formatCount,
@@ -28,10 +28,11 @@ import { ToolError } from "./tool-errors";
28
28
  import { toolResult } from "./tool-result";
29
29
 
30
30
  const astGrepSchema = Type.Object({
31
- patterns: Type.Array(Type.String(), { minItems: 1, description: "AST patterns to match" }),
31
+ pat: Type.Array(Type.String(), { minItems: 1, description: "AST patterns to match" }),
32
32
  lang: Type.Optional(Type.String({ description: "Language override" })),
33
33
  path: Type.Optional(Type.String({ description: "File, directory, or glob pattern to search (default: cwd)" })),
34
- selector: Type.Optional(Type.String({ description: "Optional selector for contextual pattern mode" })),
34
+ glob: Type.Optional(Type.String({ description: "Optional glob filter relative to path" })),
35
+ sel: Type.Optional(Type.String({ description: "Optional selector for contextual pattern mode" })),
35
36
  limit: Type.Optional(Type.Number({ description: "Max matches (default: 50)" })),
36
37
  offset: Type.Optional(Type.Number({ description: "Skip first N matches (default: 0)" })),
37
38
  context: Type.Optional(Type.Number({ description: "Context lines around each match" })),
@@ -68,11 +69,9 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
68
69
  _context?: AgentToolContext,
69
70
  ): Promise<AgentToolResult<AstGrepToolDetails>> {
70
71
  return untilAborted(signal, async () => {
71
- const patterns = [
72
- ...new Set(params.patterns.map(pattern => pattern.trim()).filter(pattern => pattern.length > 0)),
73
- ];
72
+ const patterns = [...new Set(params.pat.map(pattern => pattern.trim()).filter(pattern => pattern.length > 0))];
74
73
  if (patterns.length === 0) {
75
- throw new ToolError("`patterns` must include at least one non-empty pattern");
74
+ throw new ToolError("`pat` must include at least one non-empty pattern");
76
75
  }
77
76
  const limit = params.limit === undefined ? 50 : Math.floor(params.limit);
78
77
  if (!Number.isFinite(limit) || limit < 1) {
@@ -88,7 +87,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
88
87
  }
89
88
 
90
89
  let searchPath: string | undefined;
91
- let globFilter: string | undefined;
90
+ let globFilter = params.glob?.trim() || undefined;
92
91
  const rawPath = params.path?.trim();
93
92
  if (rawPath) {
94
93
  const internalRouter = this.session.internalRouter;
@@ -104,7 +103,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
104
103
  } else {
105
104
  const parsedPath = parseSearchPath(rawPath);
106
105
  searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
107
- globFilter = parsedPath.glob;
106
+ globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
108
107
  }
109
108
  }
110
109
 
@@ -126,7 +125,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
126
125
  lang: params.lang?.trim(),
127
126
  path: resolvedSearchPath,
128
127
  glob: globFilter,
129
- selector: params.selector?.trim(),
128
+ selector: params.sel?.trim(),
130
129
  limit,
131
130
  offset,
132
131
  context,
@@ -273,10 +272,10 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
273
272
  // =============================================================================
274
273
 
275
274
  interface AstGrepRenderArgs {
276
- patterns?: string[];
275
+ pat?: string[];
277
276
  lang?: string;
278
277
  path?: string;
279
- selector?: string;
278
+ sel?: string;
280
279
  limit?: number;
281
280
  offset?: number;
282
281
  context?: number;
@@ -290,14 +289,13 @@ export const astGrepToolRenderer = {
290
289
  const meta: string[] = [];
291
290
  if (args.lang) meta.push(`lang:${args.lang}`);
292
291
  if (args.path) meta.push(`in ${args.path}`);
293
- if (args.selector) meta.push("selector");
292
+ if (args.sel) meta.push("selector");
294
293
  if (args.limit !== undefined && args.limit > 0) meta.push(`limit:${args.limit}`);
295
294
  if (args.offset !== undefined && args.offset > 0) meta.push(`offset:${args.offset}`);
296
295
  if (args.context !== undefined) meta.push(`context:${args.context}`);
297
- if (args.patterns && args.patterns.length > 1) meta.push(`${args.patterns.length} patterns`);
296
+ if (args.pat && args.pat.length > 1) meta.push(`${args.pat.length} patterns`);
298
297
 
299
- const description =
300
- args.patterns?.length === 1 ? args.patterns[0] : args.patterns ? `${args.patterns.length} patterns` : "?";
298
+ const description = args.pat?.length === 1 ? args.pat[0] : args.pat ? `${args.pat.length} patterns` : "?";
301
299
  const text = renderStatusLine({ icon: "pending", title: "AST Grep", description, meta }, uiTheme);
302
300
  return new Text(text, 0, 0);
303
301
  },
@@ -321,7 +319,7 @@ export const astGrepToolRenderer = {
321
319
  const limitReached = details?.limitReached ?? false;
322
320
 
323
321
  if (matchCount === 0) {
324
- const description = args?.patterns?.length === 1 ? args.patterns[0] : undefined;
322
+ const description = args?.pat?.length === 1 ? args.pat[0] : undefined;
325
323
  const meta = ["0 matches"];
326
324
  if (details?.scopePath) meta.push(`in ${details.scopePath}`);
327
325
  if (filesSearched > 0) meta.push(`searched ${filesSearched}`);
@@ -344,7 +342,7 @@ export const astGrepToolRenderer = {
344
342
  if (details?.scopePath) meta.push(`in ${details.scopePath}`);
345
343
  meta.push(`searched ${filesSearched}`);
346
344
  if (limitReached) meta.push(uiTheme.fg("warning", "limit reached"));
347
- const description = args?.patterns?.length === 1 ? args.patterns[0] : undefined;
345
+ const description = args?.pat?.length === 1 ? args.pat[0] : undefined;
348
346
  const header = renderStatusLine(
349
347
  { icon: limitReached ? "warning" : "success", title: "AST Grep", description, meta },
350
348
  uiTheme,
package/src/tools/grep.ts CHANGED
@@ -16,7 +16,7 @@ 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 { hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
19
+ import { combineSearchGlobs, hasGlobPathChars, parseSearchPath, resolveToCwd } from "./path-utils";
20
20
  import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
21
21
  import { ToolError } from "./tool-errors";
22
22
  import { toolResult } from "./tool-result";
@@ -125,12 +125,7 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
125
125
  const parsedPath = parseSearchPath(rawPath);
126
126
  searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
127
127
  if (parsedPath.glob) {
128
- if (globFilter) {
129
- throw new ToolError(
130
- "When path already includes glob characters, omit the separate glob parameter",
131
- );
132
- }
133
- globFilter = parsedPath.glob;
128
+ globFilter = combineSearchGlobs(parsedPath.glob, globFilter);
134
129
  }
135
130
  }
136
131
  } else {
@@ -128,6 +128,16 @@ export function parseSearchPath(filePath: string): ParsedSearchPath {
128
128
  };
129
129
  }
130
130
 
131
+ export function combineSearchGlobs(prefixGlob?: string, suffixGlob?: string): string | undefined {
132
+ if (!prefixGlob) return suffixGlob;
133
+ if (!suffixGlob) return prefixGlob;
134
+
135
+ const normalizedPrefix = prefixGlob.replace(/\/+$/, "");
136
+ const normalizedSuffix = suffixGlob.replace(/^\/+/, "");
137
+
138
+ return `${normalizedPrefix}/${normalizedSuffix}`;
139
+ }
140
+
131
141
  export function resolveReadPath(filePath: string, cwd: string): string {
132
142
  const resolved = resolveToCwd(filePath, cwd);
133
143
  const shellEscapedVariant = tryShellEscapedPath(resolved);