@d3ara1n/pi-hashline-edit 0.5.5 → 0.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d3ara1n/pi-hashline-edit",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "type": "module",
5
5
  "description": "Hashline-style file editing for pi — line-anchored edits verified by content hash, replacing oldText/newText matching",
6
6
  "homepage": "https://github.com/d3ara1n/pi-extensions/tree/main/packages/pi-hashline-edit#readme",
@@ -52,6 +52,7 @@ import { parseHashline } from "./render.ts";
52
52
  const DEFAULT_LIMIT = 100;
53
53
  /** Max chars per result line for display (mirrors pi's truncate.ts; not exported there). */
54
54
  const GREP_MAX_LINE_LENGTH = 500;
55
+ const GREP_CONTEXT_MAX = 20;
55
56
 
56
57
  /** Locate ripgrep: pi's bundled bin first, then PATH. Returns null if not found. */
57
58
  async function findRg(): Promise<string | null> {
@@ -107,6 +108,11 @@ function toArray(v: string | string[] | undefined): string[] {
107
108
  return Array.isArray(v) ? v : [v];
108
109
  }
109
110
 
111
+ function clampContext(context: number | undefined): number {
112
+ if (!context || !Number.isFinite(context) || context < 0) return 0;
113
+ return Math.min(Math.floor(context), GREP_CONTEXT_MAX);
114
+ }
115
+
110
116
  const grepOverrideSchema = Type.Object({
111
117
  pattern: Type.Union([Type.String(), Type.Array(Type.String())], {
112
118
  description:
@@ -129,10 +135,10 @@ const grepOverrideSchema = Type.Object({
129
135
  }),
130
136
  ),
131
137
  wordMatch: Type.Optional(Type.Boolean({ description: "Match whole words only (rg -w)" })),
132
- path: Type.Union([Type.String(), Type.Array(Type.String())], {
138
+ path: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())], {
133
139
  description:
134
140
  "Directory or file to search (string or array of paths; default: current directory)",
135
- }),
141
+ })),
136
142
  glob: Type.Optional(
137
143
  Type.String({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'" }),
138
144
  ),
@@ -145,9 +151,10 @@ const grepOverrideSchema = Type.Object({
145
151
  }),
146
152
  ),
147
153
  context: Type.Optional(
148
- Type.Number({
149
- description:
150
- "Number of lines to show before and after each match (default: 0); context lines are anchored too",
154
+ Type.Integer({
155
+ minimum: 0,
156
+ maximum: GREP_CONTEXT_MAX,
157
+ description: `Number of lines on each side of a match (0-${GREP_CONTEXT_MAX}; default: 0); context lines are anchored too`,
151
158
  }),
152
159
  ),
153
160
  limit: Type.Optional(
@@ -365,8 +372,10 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
365
372
  onUpdate: any,
366
373
  ): Promise<any> {
367
374
  const state = getState();
375
+ const ctx = clampContext(params.context);
376
+ const delegatedParams = params.context === undefined ? params : { ...params, context: ctx };
368
377
  // aborted → built-in grep (it handles abort itself)
369
- if (signal?.aborted) return backend.delegate(toolCallId, params, signal, onUpdate);
378
+ if (signal?.aborted) return backend.delegate(toolCallId, delegatedParams, signal, onUpdate);
370
379
 
371
380
  const patterns = toArray(params.pattern);
372
381
  if (patterns.length === 0) throw new Error("pattern is required (got an empty array)");
@@ -384,7 +393,7 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
384
393
  const rgPath = await backend.findRg();
385
394
  // ripgrep unavailable → built-in (it can auto-download rg), but only for plain params
386
395
  if (!rgPath) {
387
- if (legacyShaped) return backend.delegate(toolCallId, params, signal, onUpdate);
396
+ if (legacyShaped) return backend.delegate(toolCallId, delegatedParams, signal, onUpdate);
388
397
  throw new Error(
389
398
  "ripgrep (rg) not found; extended grep params cannot fall back to the built-in grep. Retry with a simple pattern first, or use bash",
390
399
  );
@@ -393,8 +402,7 @@ export function makeGrepOverrideWithBackend(cwd: string, overrides: Partial<Grep
393
402
  const excludes = toArray(params.excludePattern);
394
403
  const matchMode: "any" | "all" = params.matchMode ?? "any";
395
404
  const outputMode: "content" | "files" | "count" = params.outputMode ?? "content";
396
- const { glob, ignoreCase, literal, wordMatch, context, limit } = params;
397
- const ctx = context && context > 0 ? context : 0;
405
+ const { glob, ignoreCase, literal, wordMatch, limit } = params;
398
406
  const searchPaths = (() => {
399
407
  const raw = toArray(params.path);
400
408
  return (raw.length ? raw : ["."]).map((p) => canonicalPath(cwd, p));
@@ -96,7 +96,9 @@ test("formats parsed rg matches with full-line hash anchors", async () => {
96
96
  ],
97
97
  });
98
98
 
99
- const result = await call(makeGrepOverrideWithBackend(dir, fake.backend), {
99
+ const tool = makeGrepOverrideWithBackend(dir, fake.backend);
100
+ assert.deepEqual(tool.parameters.required, ["pattern"]);
101
+ const result = await call(tool, {
100
102
  pattern: "alpha",
101
103
  });
102
104
  const output = text(result);
@@ -161,11 +163,16 @@ test("applies all, exclude, context, and CRLF filtering after rg output", async
161
163
  ],
162
164
  });
163
165
 
164
- const result = await call(makeGrepOverrideWithBackend(dir, fake.backend), {
166
+ const tool = makeGrepOverrideWithBackend(dir, fake.backend);
167
+ const contextSchema: any = tool.parameters.properties.context;
168
+ assert.equal(contextSchema.type, "integer");
169
+ assert.equal(contextSchema.minimum, 0);
170
+ assert.equal(contextSchema.maximum, 20);
171
+ const result = await call(tool, {
165
172
  pattern: ["alpha", "beta$"],
166
173
  matchMode: "all",
167
174
  excludePattern: "drop",
168
- context: 1,
175
+ context: 1.9,
169
176
  });
170
177
  assert.equal(
171
178
  text(result),
@@ -180,6 +187,29 @@ test("applies all, exclude, context, and CRLF filtering after rg output", async
180
187
  );
181
188
  });
182
189
 
190
+ test("context bounds apply to each side of a match, including direct runtime calls", async () => {
191
+ await withDir(async (dir) => {
192
+ const file = join(dir, "a.ts");
193
+ const lines = Array.from({ length: 45 }, (_, i) => i === 22 ? "needle" : `line ${i + 1}`);
194
+ await writeFile(file, lines.join("\n"));
195
+ const fake = fakeBackend({ lines: [rgMatch(file, 23, "needle\n")] });
196
+ const tool = makeGrepOverrideWithBackend(dir, fake.backend);
197
+
198
+ for (const context of [20, 1_000]) {
199
+ const output = text(await call(tool, { pattern: "needle", context })).split("\n");
200
+ assert.equal(output.length, 42);
201
+ assert.match(output[1], /^3#[0-9A-Z]+│line 3$/);
202
+ assert.match(output[21], /^23#[0-9A-Z]+│needle$/);
203
+ assert.match(output[41], /^43#[0-9A-Z]+│line 43$/);
204
+ }
205
+ for (const context of [-1, Number.NaN, Number.POSITIVE_INFINITY]) {
206
+ const output = text(await call(tool, { pattern: "needle", context })).split("\n");
207
+ assert.equal(output.length, 2);
208
+ assert.match(output[1], /^23#[0-9A-Z]+│needle$/);
209
+ }
210
+ });
211
+ });
212
+
183
213
  test("passes output flags and formats files and counts", async () => {
184
214
  await withDir(async (dir) =>
185
215
  withEnabled(true, async () => {
@@ -287,6 +317,19 @@ test("delegates only safe fallbacks and rejects extended missing-rg requests", a
287
317
  text(await call(makeGrepOverrideWithBackend(dir, absent.backend), { pattern: "x" })),
288
318
  "delegated",
289
319
  );
320
+ assert.deepEqual(absent.delegates.at(-1)?.[1], { pattern: "x" });
321
+ const tool = makeGrepOverrideWithBackend(dir, absent.backend);
322
+ for (const [context, expected] of [
323
+ [1_000, 20],
324
+ [1.9, 1],
325
+ [-1, 0],
326
+ [Number.POSITIVE_INFINITY, 0],
327
+ ]) {
328
+ const params = { pattern: "x", context };
329
+ await call(tool, params);
330
+ assert.equal(absent.delegates.at(-1)?.[1].context, expected);
331
+ assert.equal(params.context, context);
332
+ }
290
333
  await assert.rejects(
291
334
  call(makeGrepOverrideWithBackend(dir, absent.backend), {
292
335
  pattern: ["x", "y"],