@d3ara1n/pi-hashline-edit 0.5.6 → 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.6",
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:
@@ -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));
@@ -163,11 +163,16 @@ test("applies all, exclude, context, and CRLF filtering after rg output", async
163
163
  ],
164
164
  });
165
165
 
166
- 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, {
167
172
  pattern: ["alpha", "beta$"],
168
173
  matchMode: "all",
169
174
  excludePattern: "drop",
170
- context: 1,
175
+ context: 1.9,
171
176
  });
172
177
  assert.equal(
173
178
  text(result),
@@ -182,6 +187,29 @@ test("applies all, exclude, context, and CRLF filtering after rg output", async
182
187
  );
183
188
  });
184
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
+
185
213
  test("passes output flags and formats files and counts", async () => {
186
214
  await withDir(async (dir) =>
187
215
  withEnabled(true, async () => {
@@ -289,6 +317,19 @@ test("delegates only safe fallbacks and rejects extended missing-rg requests", a
289
317
  text(await call(makeGrepOverrideWithBackend(dir, absent.backend), { pattern: "x" })),
290
318
  "delegated",
291
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
+ }
292
333
  await assert.rejects(
293
334
  call(makeGrepOverrideWithBackend(dir, absent.backend), {
294
335
  pattern: ["x", "y"],