pi-readseek 0.9.13 → 0.9.15

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/README.md CHANGED
@@ -1,10 +1,7 @@
1
1
  # pi-readseek
2
2
 
3
- `pi-readseek` adds ReadSeek's anchored file operations, structural search,
4
- symbol navigation, and document tools to Pi. Built-ins remain available unless
5
- `overrideTools` maps `read`, `edit`, `write`, or `grep` to a ReadSeek-backed
6
- implementation. The `read` override runs ReadSeek's `digest` command; it does not
7
- add a separate `readSeek_read` tool.
3
+ Pi extension for anchored file operations, structural search, symbol navigation,
4
+ and document inspection.
8
5
 
9
6
  ## Install
10
7
 
@@ -12,33 +9,18 @@ add a separate `readSeek_read` tool.
12
9
  pi install npm:pi-readseek
13
10
  ```
14
11
 
15
- The package depends on `@jarkkojs/readseek`, which supplies the native binary on
16
- supported platforms. The extension uses that packaged binary exclusively; an
17
- unresolved binary indicates a broken installation rather than falling back to an
18
- unrelated `readseek` executable on `PATH`.
12
+ The extension uses the native binary supplied by `@jarkkojs/readseek`. Set
13
+ `READSEEK_BINARY` to an absolute executable path to override it.
19
14
 
20
15
  ## Tools
21
16
 
22
- | Tool | Purpose |
23
- | ----------------- | ----------------------------------------------------------------------------- |
24
- | `readSeek_edit` | Apply hash-verified edits to an existing text file |
25
- | `readSeek_write` | Create or replace a complete text file and return anchors |
26
- | `readSeek_grep` | Search text or regex and return edit-ready anchors |
27
- | `readSeek_search` | Search AST patterns |
28
- | `readSeek_def` | Find symbol definitions |
29
- | `readSeek_refs` | Find identifier usages, optionally scoped to a cursor binding |
30
- | `readSeek_rename` | Rename a cursor symbol; set `apply: false` to preview the plan |
31
- | `readSeek_digest` | Native `digest` envelope: map, diagnostics, identity, content, metadata |
32
- | `readSeek_view` | View or narrow a structural document index (PDF first); optional asset vision |
33
-
34
- The extension also tells Pi to get fresh anchors before editing and to run
35
- `readSeek_digest` with diagnostics after source edits.
17
+ `readSeek_edit`, `readSeek_write`, `readSeek_grep`, `readSeek_search`,
18
+ `readSeek_def`, `readSeek_refs`, `readSeek_rename`, `readSeek_digest`, and
19
+ `readSeek_view`.
36
20
 
37
21
  ## Settings
38
22
 
39
- Add a `readseek` object to `~/.pi/agent/settings.json` (global) or
40
- `.pi/settings.json` (project). Project settings override matching global values.
41
- Defaults:
23
+ Add `readseek` to `~/.pi/agent/settings.json` or `.pi/settings.json`:
42
24
 
43
25
  ```json
44
26
  {
@@ -47,10 +29,7 @@ Defaults:
47
29
  "imageMode": "auto",
48
30
  "syntaxValidation": "warn",
49
31
  "timeoutMs": 120000,
50
- "grep": {
51
- "maxLines": 2000,
52
- "maxBytes": 51200
53
- },
32
+ "grep": { "maxLines": 2000, "maxBytes": 51200 },
54
33
  "display": {
55
34
  "grep": "compact",
56
35
  "edit": "expanded",
@@ -60,23 +39,8 @@ Defaults:
60
39
  }
61
40
  ```
62
41
 
63
- - `overrideTools`: any of `"read"`, `"edit"`, `"write"`, and `"grep"`.
64
- - `imageMode`: controls standalone images when `digest` overrides `read`.
65
- `"auto"` passes the prepared image to image-capable Pi models and otherwise
66
- runs local analysis, `"on"` always runs local analysis, and `"off"` skips the
67
- image. Local analysis defaults to `all` and also supports `ocr`, `caption`, and
68
- `objects` through `visionMode`. The namespaced `readSeek_digest` tool applies
69
- `visionMode` directly, including `none`. Documents use `readSeek_view` with
70
- optional `visionMode` and `visionLevel`. Local analysis uses
71
- [Qwen3-VL-2B-Instruct](../../docs/models/qwen3-vl-2b-instruct.md).
72
- - `syntaxValidation`: `"warn"` (write and warn), `"block"` (abort), or `"off"`.
73
- Validation triggers only when an edit increases parser error or missing-node
74
- counts.
75
- - `timeoutMs`: native command timeout in milliseconds.
76
- - `grep.maxLines` and `grep.maxBytes`: output limits; larger values are clamped
77
- to the defaults.
78
- - `display`: initial `"compact"` or `"expanded"` state for `grep`,
79
- `edit`, and `write` results.
42
+ `overrideTools` accepts `read`, `edit`, `write`, and `grep`. `imageMode` is
43
+ `auto`, `on`, or `off`; `syntaxValidation` is `warn`, `block`, or `off`.
80
44
 
81
45
  ## Development
82
46
 
@@ -86,8 +50,7 @@ npm test
86
50
  npm run typecheck
87
51
  ```
88
52
 
89
- ## License
90
53
 
91
- `pi-readseek` is Apache-2.0; see [LICENSE](LICENSE).
92
- `@jarkkojs/readseek` is licensed separately under
93
- Apache-2.0 AND LGPL-2.1-or-later.
54
+ ## Licensing
55
+
56
+ [Apache 2.0](../../LICENSE-APACHE-2.0).
package/dist/index.ts CHANGED
@@ -28,6 +28,13 @@ var DOCUMENT_NODE_KINDS = [
28
28
  "section",
29
29
  "structural_section"
30
30
  ];
31
+ var EDIT_VARIANTS = [
32
+ "set_line",
33
+ "replace_lines",
34
+ "insert_after",
35
+ "replace",
36
+ "replace_symbol"
37
+ ];
31
38
  var TOOL_NAMES = [
32
39
  "digest",
33
40
  "edit",
@@ -485,6 +492,25 @@ function renderToolRoutingPolicy(options = {}) {
485
492
  return [title, ...bullets].join(`
486
493
  `);
487
494
  }
495
+ // ../readseek-api/src/hashline.ts
496
+ var HASH_LEN = 6;
497
+ var LINE_ANCHOR_RE = /^(\d+):([0-9a-fA-F]{6})$/;
498
+ function tryParseLineAnchor(anchor) {
499
+ const match = LINE_ANCHOR_RE.exec(anchor.trim());
500
+ if (!match) {
501
+ return { ok: false, error: `invalid LINE:HASH anchor: ${anchor}` };
502
+ }
503
+ const digits = match[1];
504
+ const line = Number.parseInt(digits, 10);
505
+ if (!Number.isSafeInteger(line)) {
506
+ return { ok: false, error: `Line number must be a safe integer, got ${digits} in "${anchor}".` };
507
+ }
508
+ if (line < 1) {
509
+ return { ok: false, error: `Line number must be >= 1, got ${line} in "${anchor}".` };
510
+ }
511
+ return { ok: true, anchor: { line, hash: match[2].toLowerCase() } };
512
+ }
513
+
488
514
  // ../readseek-api/src/validation.ts
489
515
  function validateGitSelection(input) {
490
516
  if (input.ignored && !input.others) {
@@ -516,6 +542,73 @@ function validateVisionLevelVsMode(input) {
516
542
  }
517
543
  return { ok: true };
518
544
  }
545
+ var VARIANT_FIELDS = {
546
+ set_line: ["anchor", "new_text"],
547
+ replace_lines: ["start_anchor", "end_anchor", "new_text"],
548
+ insert_after: ["anchor", "new_text"],
549
+ replace: ["old_text", "new_text"],
550
+ replace_symbol: ["symbol", "new_body"]
551
+ };
552
+ function isEditVariant(value) {
553
+ return EDIT_VARIANTS.includes(value);
554
+ }
555
+ function parseEditItem(item, index) {
556
+ const keys = Object.keys(item);
557
+ const variant = keys[0];
558
+ if (keys.length !== 1 || variant === undefined || !isEditVariant(variant)) {
559
+ return {
560
+ ok: false,
561
+ error: `edits[${index}] must contain exactly one of: ${EDIT_VARIANTS.join(", ")}. Got: [${keys.join(", ")}]`
562
+ };
563
+ }
564
+ const payload = item[variant];
565
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
566
+ return { ok: false, error: `edits[${index}].${variant} must be an object` };
567
+ }
568
+ const expected = VARIANT_FIELDS[variant];
569
+ const allowed = variant === "replace" ? [...expected, "all"] : [...expected];
570
+ const actual = Object.keys(payload);
571
+ if (actual.some((field) => !allowed.includes(field)) || expected.some((field) => !actual.includes(field))) {
572
+ return { ok: false, error: `edits[${index}].${variant} contains invalid fields` };
573
+ }
574
+ const values = payload;
575
+ for (const field of expected) {
576
+ if (typeof values[field] !== "string") {
577
+ return { ok: false, error: `edits[${index}].${variant}.${field} must be a string` };
578
+ }
579
+ }
580
+ if (variant === "replace" && values.all !== undefined && typeof values.all !== "boolean") {
581
+ return { ok: false, error: `edits[${index}].replace.all must be a boolean` };
582
+ }
583
+ for (const field of expected) {
584
+ if (!field.endsWith("anchor"))
585
+ continue;
586
+ const parsed = tryParseLineAnchor(values[field]);
587
+ if (!parsed.ok) {
588
+ return { ok: false, error: `edits[${index}].${variant}.${field}: ${parsed.error}` };
589
+ }
590
+ }
591
+ return { ok: true, edit: item };
592
+ }
593
+ function validateExactlyOneEditVariant(edits) {
594
+ if (!Array.isArray(edits) || edits.length === 0) {
595
+ return { ok: false, error: "edits must contain at least one edit" };
596
+ }
597
+ const normalized = [];
598
+ for (let index = 0;index < edits.length; index++) {
599
+ const item = edits[index];
600
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
601
+ return { ok: false, error: `edits[${index}] must be an edit object` };
602
+ }
603
+ const parsed = parseEditItem(item, index);
604
+ if (!parsed.ok)
605
+ return parsed;
606
+ normalized.push(parsed.edit);
607
+ }
608
+ return { ok: true, edits: normalized };
609
+ }
610
+ var MIN_SAFE_INTEGER = BigInt(Number.MIN_SAFE_INTEGER);
611
+ var MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
519
612
  // ../readseek-api/src/argv.ts
520
613
  function buildLineLocation(line, column) {
521
614
  return column === undefined ? `line:${line}` : `line:${line}:${column}`;
@@ -525,14 +618,12 @@ function appendGitSelectionArgs(args, input) {
525
618
  if (!validated.ok) {
526
619
  throw new Error(validated.error);
527
620
  }
528
- const selection = [
529
- input.cached ? "cached" : undefined,
530
- input.others ? "others" : undefined,
531
- input.ignored ? "ignored" : undefined
532
- ].filter((value) => value !== undefined);
533
- if (selection.length > 0) {
534
- args.push("--git", selection.join(","));
535
- }
621
+ if (input.cached)
622
+ args.push("--cached");
623
+ if (input.others)
624
+ args.push("--others");
625
+ if (input.ignored)
626
+ args.push("--ignored");
536
627
  return args;
537
628
  }
538
629
  function pushOptionalString(args, flag, value) {
@@ -609,15 +700,17 @@ function buildViewArgs(input) {
609
700
  return args;
610
701
  }
611
702
  function buildSearchArgs(input) {
612
- const args = ["search", input.path, input.pattern];
703
+ const args = ["search", input.pattern];
704
+ pushOptionalString(args, "--path", input.path);
613
705
  pushOptionalString(args, "--language", input.language);
706
+ pushOptionalNumber(args, "--limit", input.limit);
614
707
  return appendGitSelectionArgs(args, input);
615
708
  }
616
709
  function buildDefArgs(input) {
617
- const args = ["def", input.path];
710
+ const args = ["def", input.name];
711
+ pushOptionalString(args, "--path", input.path);
618
712
  if (input.format !== undefined)
619
713
  args.push("--format", input.format);
620
- args.push(input.name);
621
714
  pushOptionalString(args, "--language", input.language);
622
715
  return appendGitSelectionArgs(args, input);
623
716
  }
@@ -625,11 +718,14 @@ function buildRefsArgs(input) {
625
718
  const validated = validateRefsNameVsScope(input);
626
719
  if (!validated.ok)
627
720
  throw new Error(validated.error);
628
- const args = ["refs", input.path];
629
- if (input.scope) {
630
- args.push("--at", buildLineLocation(input.line, input.column));
631
- } else if (input.name !== undefined) {
721
+ const args = ["refs"];
722
+ if (input.name !== undefined)
632
723
  args.push(input.name);
724
+ pushOptionalString(args, "--path", input.path);
725
+ if (input.scope) {
726
+ args.push("--scope");
727
+ pushOptionalNumber(args, "--line", input.line);
728
+ pushOptionalNumber(args, "--column", input.column);
633
729
  }
634
730
  if (input.format !== undefined)
635
731
  args.push("--format", input.format);
@@ -645,13 +741,13 @@ function buildEditArgs(input) {
645
741
  });
646
742
  pushOptionalString(args, "--language", input.language);
647
743
  if (input.requestFromStdin)
648
- args.push("--request", "-");
744
+ args.push("--edits", "-");
649
745
  return args;
650
746
  }
651
747
  function buildRenameArgs(input) {
652
748
  const args = ["rename", input.path, "--at", buildLineLocation(input.line, input.column), "--to", input.to];
653
749
  if (input.workspace !== undefined)
654
- args.push("--workspace", input.workspace);
750
+ args.push(`--workspace=${input.workspace}`);
655
751
  appendApplyAndPlanHash(args, {
656
752
  apply: input.apply,
657
753
  planHash: input.planHash,
@@ -661,9 +757,10 @@ function buildRenameArgs(input) {
661
757
  return appendGitSelectionArgs(args, input);
662
758
  }
663
759
  // ../readseek-api/src/platform.ts
664
- import { accessSync, constants as fsConstants } from "node:fs";
760
+ import { accessSync, constants as fsConstants, existsSync, statSync } from "node:fs";
665
761
  import { createRequire } from "node:module";
666
762
  import path from "node:path";
763
+ var READSEEK_BINARY_ENV = "READSEEK_BINARY";
667
764
  var READSEEK_PLATFORM_PACKAGES = {
668
765
  "darwin-arm64": "@jarkkojs/readseek-darwin-arm64",
669
766
  "linux-arm64": "@jarkkojs/readseek-linux-arm64",
@@ -677,6 +774,24 @@ function readSeekBinaryName(platform = process.platform) {
677
774
  function readSeekPlatformKey(platform = process.platform, arch = process.arch) {
678
775
  return `${platform}-${arch}`;
679
776
  }
777
+ function resolveReadSeekBinaryOverride(environment = process.env) {
778
+ const binaryPath = environment[READSEEK_BINARY_ENV];
779
+ if (binaryPath === undefined)
780
+ return null;
781
+ if (!path.isAbsolute(binaryPath)) {
782
+ throw new Error(`${READSEEK_BINARY_ENV} must be an absolute path to the readseek executable`);
783
+ }
784
+ try {
785
+ if (!statSync(binaryPath).isFile())
786
+ throw new Error("not a regular file");
787
+ accessSync(binaryPath, fsConstants.X_OK);
788
+ } catch (error) {
789
+ throw new Error(`${READSEEK_BINARY_ENV} does not point to an executable file: ${binaryPath}`, {
790
+ cause: error
791
+ });
792
+ }
793
+ return binaryPath;
794
+ }
680
795
  function resolveReadSeekBinaryFromPackages(resolver = resolveFromPackage, options = {}) {
681
796
  const resolve = typeof resolver === "function" ? resolver : resolver.resolve.bind(resolver);
682
797
  const platform = options.platform ?? process.platform;
@@ -1033,6 +1148,7 @@ function parseCheckOutput(value) {
1033
1148
  if (!Array.isArray(output.diagnostics))
1034
1149
  throw new Error("invalid readseek diagnostics");
1035
1150
  return {
1151
+ file_hash: optionalString(output.file_hash, "file_hash"),
1036
1152
  errorCount: requireNumber(output.error_count, "error_count"),
1037
1153
  missingCount: requireNumber(output.missing_count, "missing_count"),
1038
1154
  diagnostics: output.diagnostics.map((diagnostic) => {
@@ -1437,6 +1553,7 @@ function createReadSeekClient(runner) {
1437
1553
  path: target,
1438
1554
  pattern,
1439
1555
  language: options.language,
1556
+ limit: options.limit,
1440
1557
  cached: options.cached,
1441
1558
  others: options.others,
1442
1559
  ignored: options.ignored
@@ -1502,7 +1619,8 @@ function createReadSeekClient(runner) {
1502
1619
  path: filePath,
1503
1620
  apply: options.apply,
1504
1621
  planHash: options.planHash,
1505
- language: options.language
1622
+ language: options.language,
1623
+ requestFromStdin: true
1506
1624
  });
1507
1625
  return parseEditOutput(await runner.json(args, {
1508
1626
  signal: options.signal,
@@ -1574,10 +1692,7 @@ import * as Diff from "diff";
1574
1692
 
1575
1693
  // src/hashline.ts
1576
1694
  import xxhashWasm from "xxhash-wasm";
1577
- var HASH_LEN = 3;
1578
- var RADIX = 16;
1579
- var HASH_MOD = RADIX ** HASH_LEN;
1580
- var DICT = Array.from({ length: HASH_MOD }, (_, i) => i.toString(RADIX).padStart(HASH_LEN, "0"));
1695
+ var ANCHOR_MASK = 16777215;
1581
1696
  var HASHLINE_STATE_KEY = Symbol.for("pi-readseek.hashlineState.v1");
1582
1697
  function getHashlineState() {
1583
1698
  const globalObject = globalThis;
@@ -1595,6 +1710,9 @@ async function ensureHashInit() {
1595
1710
  if (!state.initPromise) {
1596
1711
  state.initPromise = xxhashWasm().then((hasher) => {
1597
1712
  state.h32Fn = hasher.h32;
1713
+ }).catch((error) => {
1714
+ state.initPromise = null;
1715
+ throw error;
1598
1716
  });
1599
1717
  }
1600
1718
  await state.initPromise;
@@ -1605,11 +1723,11 @@ function xxh32(input) {
1605
1723
  throw new Error("Hash not initialized — call ensureHashInit() first");
1606
1724
  return state.h32Fn(input, 0) >>> 0;
1607
1725
  }
1726
+ function canonicalizeLine(line) {
1727
+ return line.match(/[^\t\r\n ]+/g)?.join(" ") ?? "";
1728
+ }
1608
1729
  function computeLineHash(line) {
1609
- if (line.endsWith("\r"))
1610
- line = line.slice(0, -1);
1611
- line = line.replace(/\s+/g, "");
1612
- return DICT[xxh32(line) % HASH_MOD];
1730
+ return (xxh32(canonicalizeLine(line)) & ANCHOR_MASK).toString(16).padStart(HASH_LEN, "0");
1613
1731
  }
1614
1732
  var DISPLAY_CONTROL_CHAR_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
1615
1733
  function escapeControlCharsForDisplay(text) {
@@ -1762,27 +1880,10 @@ function generateCompactOrFullDiff(oldContent, newContent, contextLines = 4) {
1762
1880
  }
1763
1881
 
1764
1882
  // src/path-utils.ts
1765
- import { homedir } from "node:os";
1766
- import { isAbsolute, resolve as resolvePath } from "node:path";
1767
- var UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
1768
- function normalizeUnicodeSpaces(str) {
1769
- return str.replace(UNICODE_SPACES, " ");
1770
- }
1771
- function normalizeAtPrefix(filePath) {
1772
- return filePath.startsWith("@") ? filePath.slice(1) : filePath;
1773
- }
1774
- function expandPath(filePath) {
1775
- const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
1776
- if (normalized === "~")
1777
- return homedir();
1778
- if (normalized.startsWith("~/"))
1779
- return homedir() + normalized.slice(1);
1780
- return normalized;
1781
- }
1782
- function resolveToCwd(filePath, cwd) {
1783
- const expanded = expandPath(filePath);
1784
- return isAbsolute(expanded) ? expanded : resolvePath(cwd, expanded);
1785
- }
1883
+ import {
1884
+ expandUserPath as expandUserPath2,
1885
+ resolveToCwd as resolveToCwd2
1886
+ } from "@jarkkojs/readseek-api";
1786
1887
 
1787
1888
  // src/runtime.ts
1788
1889
  function throwIfAborted(signal) {
@@ -2112,41 +2213,14 @@ function classifyEdit(oldContent, newContent) {
2112
2213
  }
2113
2214
 
2114
2215
  // src/readseek-client.ts
2115
- import { existsSync, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
2216
+ import { existsSync as existsSync2, statSync as statSync3 } from "node:fs";
2116
2217
  import { createRequire as createRequire2 } from "node:module";
2117
- import { homedir as homedir3 } from "node:os";
2218
+ import { homedir as homedir2 } from "node:os";
2118
2219
  import path2 from "node:path";
2119
2220
 
2120
- // src/readseek/enums.ts
2121
- var SymbolKind;
2122
- ((SymbolKind2) => {
2123
- SymbolKind2["Class"] = "class";
2124
- SymbolKind2["Function"] = "function";
2125
- SymbolKind2["Method"] = "method";
2126
- SymbolKind2["Variable"] = "variable";
2127
- SymbolKind2["Constant"] = "constant";
2128
- SymbolKind2["Interface"] = "interface";
2129
- SymbolKind2["Type"] = "type";
2130
- SymbolKind2["Enum"] = "enum";
2131
- SymbolKind2["Struct"] = "struct";
2132
- SymbolKind2["Import"] = "import";
2133
- SymbolKind2["Module"] = "module";
2134
- SymbolKind2["Namespace"] = "namespace";
2135
- SymbolKind2["Property"] = "property";
2136
- SymbolKind2["Heading"] = "heading";
2137
- SymbolKind2["Table"] = "table";
2138
- SymbolKind2["View"] = "view";
2139
- SymbolKind2["Procedure"] = "procedure";
2140
- SymbolKind2["Trigger"] = "trigger";
2141
- SymbolKind2["Index"] = "index";
2142
- SymbolKind2["Schema"] = "schema";
2143
- SymbolKind2["Signal"] = "signal";
2144
- SymbolKind2["Unknown"] = "unknown";
2145
- })(SymbolKind ||= {});
2146
-
2147
2221
  // src/readseek-settings.ts
2148
- import { readFileSync, statSync } from "node:fs";
2149
- import { homedir as homedir2 } from "node:os";
2222
+ import { readFileSync, statSync as statSync2 } from "node:fs";
2223
+ import { homedir } from "node:os";
2150
2224
  import { join } from "node:path";
2151
2225
  var READSEEK_TOOL_REPLACEMENTS = {
2152
2226
  read: "readSeek_digest",
@@ -2163,7 +2237,7 @@ var DEFAULT_DISPLAY_MODES = {
2163
2237
  write: "expanded"
2164
2238
  };
2165
2239
  function globalSettingsPath() {
2166
- return join(homedir2(), ".pi/agent/settings.json");
2240
+ return join(homedir(), ".pi/agent/settings.json");
2167
2241
  }
2168
2242
  function projectSettingsPath() {
2169
2243
  return join(process.cwd(), ".pi/settings.json");
@@ -2303,7 +2377,7 @@ function validateSettings(raw, source) {
2303
2377
  var settingsFileCache = new Map;
2304
2378
  function statFileOrNull(path2) {
2305
2379
  try {
2306
- return statSync(path2);
2380
+ return statSync2(path2);
2307
2381
  } catch {
2308
2382
  return null;
2309
2383
  }
@@ -2362,7 +2436,7 @@ function resolveReadSeekToolDisplayMode(tool) {
2362
2436
  // src/readseek-client.ts
2363
2437
  var resolveFromHost = createRequire2(import.meta.url).resolve;
2364
2438
  function readSeekBinaryPath() {
2365
- return resolveReadSeekBinaryFromPackages(resolveFromHost);
2439
+ return resolveReadSeekBinaryOverride() ?? resolveReadSeekBinaryFromPackages(resolveFromHost);
2366
2440
  }
2367
2441
  function readSeekBinaryAvailability() {
2368
2442
  try {
@@ -2372,46 +2446,18 @@ function readSeekBinaryAvailability() {
2372
2446
  return { available: false, reason: classifyReadSeekFailure(err).message };
2373
2447
  }
2374
2448
  }
2375
- var READSEEK_REPO_PACKAGE_NAMES = new Set(["@jarkkojs/readseek", "readseek"]);
2376
2449
  var defaultReadSeekDirInit;
2377
- function directoryExists(dirPath) {
2450
+ function defaultReadSeekDir() {
2451
+ const home = homedir2();
2452
+ return home ? path2.join(home, ".pi", "readseek") : null;
2453
+ }
2454
+ function directoryExists(dir) {
2378
2455
  try {
2379
- return statSync2(dirPath).isDirectory();
2456
+ return statSync3(dir).isDirectory();
2380
2457
  } catch {
2381
2458
  return false;
2382
2459
  }
2383
2460
  }
2384
- var ownRepositoryByDir = new Map;
2385
- function isOwnReadSeekRepository(cwd = process.cwd()) {
2386
- const start = path2.resolve(cwd);
2387
- const cached = ownRepositoryByDir.get(start);
2388
- if (cached !== undefined)
2389
- return cached;
2390
- let dir = start;
2391
- let result = false;
2392
- while (true) {
2393
- const packageJsonPath = path2.join(dir, "package.json");
2394
- if (existsSync(packageJsonPath)) {
2395
- try {
2396
- const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
2397
- if (typeof packageJson.name === "string" && READSEEK_REPO_PACKAGE_NAMES.has(packageJson.name)) {
2398
- result = true;
2399
- break;
2400
- }
2401
- } catch {}
2402
- }
2403
- const parent = path2.dirname(dir);
2404
- if (parent === dir)
2405
- break;
2406
- dir = parent;
2407
- }
2408
- ownRepositoryByDir.set(start, result);
2409
- return result;
2410
- }
2411
- function defaultReadSeekDir() {
2412
- const home = homedir3();
2413
- return home ? path2.join(home, ".pi", "readseek") : null;
2414
- }
2415
2461
  var DEFAULT_READSEEK_VISION_TIMEOUT_MS = 30 * 60000;
2416
2462
  function readSeekTimeoutMs() {
2417
2463
  return resolveReadSeekTimeoutMs() ?? DEFAULT_READSEEK_TIMEOUT_MS;
@@ -2435,8 +2481,19 @@ async function ensureDefaultReadSeekDir() {
2435
2481
  });
2436
2482
  return defaultReadSeekDirInit;
2437
2483
  }
2484
+ function hasProjectReadSeekDir(cwd = process.cwd()) {
2485
+ let dir = path2.resolve(cwd);
2486
+ while (true) {
2487
+ if (existsSync2(path2.join(dir, ".readseek")))
2488
+ return true;
2489
+ const parent = path2.dirname(dir);
2490
+ if (parent === dir)
2491
+ return false;
2492
+ dir = parent;
2493
+ }
2494
+ }
2438
2495
  async function readSeekInvocationArgs(args) {
2439
- if (isOwnReadSeekRepository())
2496
+ if (hasProjectReadSeekDir())
2440
2497
  return [...args];
2441
2498
  const readSeekDir = await ensureDefaultReadSeekDir();
2442
2499
  return readSeekDir ? ["--readseek-dir", readSeekDir, ...args] : [...args];
@@ -2491,7 +2548,7 @@ async function runReadSeekVisionRaw(args, options = {}) {
2491
2548
  }
2492
2549
  }
2493
2550
  async function runReadSeekVision(args, options = {}) {
2494
- return JSON.parse(await runReadSeekVisionRaw(args, options));
2551
+ return parseReadSeekJson(await runReadSeekVisionRaw(args, options));
2495
2552
  }
2496
2553
  var runner = {
2497
2554
  text: (args, options = {}) => options.vision ? runReadSeekVisionRaw(args, options) : runReadSeekRaw(args, options),
@@ -2512,67 +2569,12 @@ var readSeekIdentify = client.identify;
2512
2569
  var readSeekDetect = client.detect;
2513
2570
  var readSeekEdit = client.edit;
2514
2571
  var readSeekRename = client.rename;
2515
- function normalizeLanguage(language) {
2516
- return language === "java" ? "Java" : language;
2517
- }
2518
- function normalizeKind(kind) {
2519
- if (kind === "constructor")
2520
- return "method" /* Method */;
2521
- if (Object.values(SymbolKind).includes(kind))
2522
- return kind;
2523
- return "unknown" /* Unknown */;
2524
- }
2525
- function parentQualifiedNameFor(qualifiedName) {
2526
- const lastDot = qualifiedName.lastIndexOf(".");
2527
- return lastDot === -1 ? "" : qualifiedName.slice(0, lastDot);
2528
- }
2529
- function symbolsFromReadSeek(symbols) {
2530
- const symbolsByQualifiedName = new Map;
2531
- const entries = [];
2532
- for (const symbol of symbols) {
2533
- const parentQualifiedName = parentQualifiedNameFor(symbol.qualified_name);
2534
- const fileSymbol = {
2535
- name: symbol.name,
2536
- kind: normalizeKind(symbol.kind),
2537
- startLine: symbol.start_line,
2538
- endLine: symbol.end_line
2539
- };
2540
- const bucket = symbolsByQualifiedName.get(symbol.qualified_name);
2541
- if (bucket)
2542
- bucket.push(fileSymbol);
2543
- else
2544
- symbolsByQualifiedName.set(symbol.qualified_name, [fileSymbol]);
2545
- entries.push({ parentQualifiedName, symbol: fileSymbol });
2546
- }
2547
- const roots = [];
2548
- for (const entry of entries) {
2549
- const parent = entry.parentQualifiedName ? symbolsByQualifiedName.get(entry.parentQualifiedName)?.[0] : undefined;
2550
- if (!parent) {
2551
- roots.push(entry.symbol);
2552
- continue;
2553
- }
2554
- parent.children ??= [];
2555
- parent.children.push(entry.symbol);
2556
- }
2557
- return roots;
2558
- }
2559
- function fileMapFromReadSeekOutput(output, filePath, totalBytes) {
2560
- if (output.language === "unknown" && output.symbols.length === 0)
2561
- return null;
2562
- return {
2563
- path: filePath,
2564
- totalLines: output.line_count,
2565
- totalBytes,
2566
- language: normalizeLanguage(output.language),
2567
- detailLevel: "full" /* Full */,
2568
- symbols: symbolsFromReadSeek(output.symbols)
2569
- };
2570
- }
2571
- async function readSeekMap(filePath, totalBytes, options = {}) {
2572
+ async function readSeekMap(filePath, options = {}) {
2572
2573
  const envelope = await client.digest(filePath, { select: "map", signal: options.signal });
2573
- if (!envelope.map)
2574
+ const map = envelope.map;
2575
+ if (!map || map.language === "unknown" && map.symbols.length === 0)
2574
2576
  return null;
2575
- return fileMapFromReadSeekOutput(envelope.map, filePath, totalBytes);
2577
+ return map;
2576
2578
  }
2577
2579
 
2578
2580
  // src/edit-syntax-validate.ts
@@ -2677,19 +2679,19 @@ function parseCompactDiffEntries(diff, oldContent, newContent) {
2677
2679
  `);
2678
2680
  const newLines = newContent.split(`
2679
2681
  `);
2680
- const compactDelete = diff.match(/^(\d+):[0-9a-f]{3}\|(.*) → \[deleted\]$/);
2682
+ const compactDelete = diff.match(/^(\d+):[0-9a-f]{6}\|(.*) → \[deleted\]$/);
2681
2683
  if (compactDelete) {
2682
2684
  const oldLine2 = Number(compactDelete[1]);
2683
2685
  const text = compactDelete[2] ?? "";
2684
2686
  if (oldLines[oldLine2 - 1] === text)
2685
2687
  return [{ kind: "remove", oldLine: oldLine2, text }];
2686
2688
  }
2687
- const compactPrefix = diff.match(/^(\d+):[0-9a-f]{3}\|/);
2689
+ const compactPrefix = diff.match(/^(\d+):[0-9a-f]{6}\|/);
2688
2690
  if (!compactPrefix)
2689
2691
  return;
2690
2692
  const oldLine = Number(compactPrefix[1]);
2691
2693
  const oldTextStart = compactPrefix[0].length;
2692
- const separatorPattern = / → (\d+):[0-9a-f]{3}\|/g;
2694
+ const separatorPattern = / → (\d+):[0-9a-f]{6}\|/g;
2693
2695
  let separator;
2694
2696
  while ((separator = separatorPattern.exec(diff)) !== null) {
2695
2697
  const newLine = Number(separator[1]);
@@ -2873,7 +2875,7 @@ function linkToolPath(styledText, rawPath, cwd) {
2873
2875
  try {
2874
2876
  if (!getCapabilities().hyperlinks)
2875
2877
  return styledText;
2876
- const absolutePath = resolveToCwd(rawPath, cwd);
2878
+ const absolutePath = resolveToCwd2(rawPath, cwd);
2877
2879
  return hyperlink(styledText, pathToFileURL(absolutePath).href);
2878
2880
  } catch {
2879
2881
  return styledText;
@@ -3149,11 +3151,6 @@ class DiffPreviewComponent {
3149
3151
  });
3150
3152
  for (const line of body.lines)
3151
3153
  lines.push(line);
3152
- for (const suffix of this.options.suffixLines ?? []) {
3153
- for (const line of suffix.split(`
3154
- `))
3155
- lines.push(clampLineToWidth(line, normalized));
3156
- }
3157
3154
  const clamped = lines.map((line) => visibleWidth3(line) <= normalized ? line : clampLineToWidth(line, normalized));
3158
3155
  this.cachedLines = clamped;
3159
3156
  this.cachedWidth = normalized;
@@ -3251,7 +3248,7 @@ async function executeEdit(opts) {
3251
3248
  const parsed = params;
3252
3249
  const rawPath = parsed.path;
3253
3250
  const path3 = rawPath.replace(/^@/, "");
3254
- const absolutePath = resolveToCwd(path3, cwd);
3251
+ const absolutePath = resolveToCwd2(path3, cwd);
3255
3252
  throwIfAborted(signal);
3256
3253
  try {
3257
3254
  return await withFileMutationQueue(absolutePath, async () => {
@@ -3260,18 +3257,11 @@ async function executeEdit(opts) {
3260
3257
  const hint = `Call readSeek_digest(${JSON.stringify(rawPath)}) first, or use readSeek_grep, readSeek_search, or readSeek_write to produce fresh anchors for this file.`;
3261
3258
  return buildEditError(absolutePath, "file-not-read", `You must get fresh anchors for ${absolutePath} before editing it. ${hint}`, hint);
3262
3259
  }
3263
- const edits = parsed.edits ?? [];
3264
- if (!edits.length) {
3265
- return buildEditError(absolutePath, "invalid-edit-variant", "No edits provided.");
3266
- }
3267
- const variants = new Set(["set_line", "replace_lines", "insert_after", "replace", "replace_symbol"]);
3268
- for (let index = 0;index < edits.length; index++) {
3269
- const edit = edits[index];
3270
- const keys = edit && typeof edit === "object" && !Array.isArray(edit) ? Object.keys(edit) : [];
3271
- if (keys.length !== 1 || !variants.has(keys[0])) {
3272
- return buildEditError(absolutePath, "invalid-edit-variant", `edits[${index}] must contain exactly one of: 'set_line', 'replace_lines', 'insert_after', 'replace', 'replace_symbol'. Got: [${keys.join(", ")}].`);
3273
- }
3260
+ const parsedEdits = validateExactlyOneEditVariant(parsed.edits ?? []);
3261
+ if (!parsedEdits.ok) {
3262
+ return buildEditError(absolutePath, "invalid-edit-variant", parsedEdits.error);
3274
3263
  }
3264
+ const edits = parsedEdits.edits;
3275
3265
  let plan;
3276
3266
  try {
3277
3267
  plan = await readSeekEdit(absolutePath, edits, { signal, language: parsed.language });
@@ -3619,39 +3609,34 @@ ${text}`;
3619
3609
  }
3620
3610
 
3621
3611
  // src/file-map.ts
3622
- import { stat } from "node:fs/promises";
3623
3612
  async function getOrGenerateMap(absPath) {
3624
3613
  try {
3625
- const { size } = await stat(absPath);
3626
- return await readSeekMap(absPath, size);
3614
+ return await readSeekMap(absPath);
3627
3615
  } catch {
3628
3616
  return null;
3629
3617
  }
3630
3618
  }
3631
3619
 
3632
- // src/readseek/symbol-tree.ts
3633
- function traverseSymbolTree(symbols, visit) {
3634
- const result = [];
3635
- const walk = (nodes, parentName) => {
3636
- for (const symbol of nodes) {
3637
- result.push(visit(symbol, parentName));
3638
- if (symbol.children?.length)
3639
- walk(symbol.children, symbol.name);
3640
- }
3641
- };
3642
- walk(symbols);
3643
- return result;
3644
- }
3645
-
3646
3620
  // src/grep-symbol-scope.ts
3647
3621
  function findEnclosingSymbol(map, lineNumber) {
3648
- const candidates = traverseSymbolTree(map.symbols, (symbol, parentName) => ({
3649
- name: symbol.name,
3650
- kind: symbol.kind,
3651
- startLine: symbol.startLine,
3652
- endLine: symbol.endLine,
3653
- parentName
3654
- })).filter((s) => lineNumber >= s.startLine && lineNumber <= s.endLine).sort((a, b) => {
3622
+ const byQualifiedName = new Map;
3623
+ for (const symbol of map.symbols) {
3624
+ const qname = symbol.qualified_name ?? symbol.name;
3625
+ if (!byQualifiedName.has(qname))
3626
+ byQualifiedName.set(qname, symbol);
3627
+ }
3628
+ const candidates = map.symbols.filter((symbol) => lineNumber >= symbol.start_line && lineNumber <= symbol.end_line).map((symbol) => {
3629
+ const qname = symbol.qualified_name ?? symbol.name;
3630
+ const dot = qname.lastIndexOf(".");
3631
+ const parent = dot === -1 ? undefined : byQualifiedName.get(qname.slice(0, dot));
3632
+ return {
3633
+ name: symbol.name,
3634
+ kind: symbol.kind === "constructor" ? "method" : symbol.kind,
3635
+ startLine: symbol.start_line,
3636
+ endLine: symbol.end_line,
3637
+ parentName: parent?.name
3638
+ };
3639
+ }).sort((a, b) => {
3655
3640
  const rangeA = a.endLine - a.startLine;
3656
3641
  const rangeB = b.endLine - b.startLine;
3657
3642
  if (rangeA !== rangeB)
@@ -3660,7 +3645,7 @@ function findEnclosingSymbol(map, lineNumber) {
3660
3645
  return a.startLine - b.startLine;
3661
3646
  return a.name.localeCompare(b.name);
3662
3647
  });
3663
- return candidates.length ? candidates[0] : null;
3648
+ return candidates[0] ?? null;
3664
3649
  }
3665
3650
  function firstLineNumber(group) {
3666
3651
  const first = group.entries.find((e) => e.kind !== "separator");
@@ -3851,38 +3836,9 @@ function formatGrepResultText(input) {
3851
3836
  }
3852
3837
 
3853
3838
  // src/coerce-obvious-int.ts
3854
- var BASE10_INT_RE = /^-?\d+$/;
3855
- var MIN_SAFE_INTEGER = BigInt(Number.MIN_SAFE_INTEGER);
3856
- var MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
3857
- function formatReceived(value) {
3858
- if (typeof value === "number" || typeof value === "bigint")
3859
- return String(value);
3860
- return JSON.stringify(value) ?? String(value);
3861
- }
3862
- function invalidInteger(value, name) {
3863
- return {
3864
- ok: false,
3865
- message: `Invalid ${name}: expected a safe base-10 integer, received ${formatReceived(value)}.`
3866
- };
3867
- }
3868
- function coerceObviousBase10Int(value, name) {
3869
- if (value === undefined || typeof value === "string" && value.trim() === "") {
3870
- return { ok: true, value: undefined };
3871
- }
3872
- if (typeof value === "number") {
3873
- if (Number.isSafeInteger(value)) {
3874
- return { ok: true, value };
3875
- }
3876
- return invalidInteger(value, name);
3877
- }
3878
- if (typeof value === "string" && BASE10_INT_RE.test(value)) {
3879
- const parsed = BigInt(value);
3880
- if (parsed >= MIN_SAFE_INTEGER && parsed <= MAX_SAFE_INTEGER) {
3881
- return { ok: true, value: Number(parsed) };
3882
- }
3883
- }
3884
- return invalidInteger(value, name);
3885
- }
3839
+ import {
3840
+ coerceObviousBase10Int as coerceObviousBase10Int2
3841
+ } from "@jarkkojs/readseek-api";
3886
3842
 
3887
3843
  // src/tui-source-render.ts
3888
3844
  import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
@@ -4019,15 +3975,15 @@ async function executeGrep(opts) {
4019
3975
  const { toolCallId, params, signal, onUpdate, cwd, onFileAnchored } = opts;
4020
3976
  await ensureHashInit();
4021
3977
  const rawParams = params;
4022
- const context = coerceObviousBase10Int(rawParams.context, "context");
3978
+ const context = coerceObviousBase10Int2(rawParams.context, "context");
4023
3979
  if (!context.ok) {
4024
3980
  return buildToolErrorResult("grep", "invalid-params-combo", context.message);
4025
3981
  }
4026
- const limit = coerceObviousBase10Int(rawParams.limit, "limit");
3982
+ const limit = coerceObviousBase10Int2(rawParams.limit, "limit");
4027
3983
  if (!limit.ok) {
4028
3984
  return buildToolErrorResult("grep", "invalid-limit", limit.message);
4029
3985
  }
4030
- const scopeContext = coerceObviousBase10Int(rawParams.scopeContext, "scopeContext");
3986
+ const scopeContext = coerceObviousBase10Int2(rawParams.scopeContext, "scopeContext");
4031
3987
  if (!scopeContext.ok) {
4032
3988
  return buildToolErrorResult("grep", "invalid-params-combo", scopeContext.message);
4033
3989
  }
@@ -4055,7 +4011,7 @@ async function executeGrep(opts) {
4055
4011
  if (!textBlock?.text)
4056
4012
  return result;
4057
4013
  const { path: rawSearchPath } = p;
4058
- const searchPath = resolveToCwd(rawSearchPath || ".", cwd);
4014
+ const searchPath = resolveToCwd2(rawSearchPath || ".", cwd);
4059
4015
  let searchPathIsDirectory = false;
4060
4016
  try {
4061
4017
  searchPathIsDirectory = (await fsStat(searchPath)).isDirectory();
@@ -4481,7 +4437,7 @@ async function executeSearch(opts) {
4481
4437
  const ignoredError = validateIgnoredRequiresOthers("search", p);
4482
4438
  if (ignoredError)
4483
4439
  return ignoredError;
4484
- const searchPath = resolveToCwd(p.path ?? ".", cwd);
4440
+ const searchPath = resolveToCwd2(p.path ?? ".", cwd);
4485
4441
  const statResult = await statSearchPathOrError("search", p.path, searchPath);
4486
4442
  if (!statResult.ok)
4487
4443
  return statResult.error;
@@ -4656,7 +4612,7 @@ async function executeRefs(opts) {
4656
4612
  if (!scopeError.ok) {
4657
4613
  return buildToolErrorResult("refs", "invalid-parameter", `refs parameter: ${scopeError.error}`);
4658
4614
  }
4659
- const searchPath = resolveToCwd(p.path ?? ".", cwd);
4615
+ const searchPath = resolveToCwd2(p.path ?? ".", cwd);
4660
4616
  const statResult = await statSearchPathOrError("refs", p.path, searchPath);
4661
4617
  if (!statResult.ok)
4662
4618
  return statResult.error;
@@ -4762,7 +4718,7 @@ async function executeRename(opts) {
4762
4718
  if (p.column !== undefined && (!Number.isSafeInteger(p.column) || p.column < 1)) {
4763
4719
  return buildToolErrorResult("rename", "invalid-parameter", "rename parameter 'column' must be a positive integer");
4764
4720
  }
4765
- const filePath = resolveToCwd(p.path, cwd);
4721
+ const filePath = resolveToCwd2(p.path, cwd);
4766
4722
  try {
4767
4723
  const output = await readSeekRename(filePath, {
4768
4724
  to: p.to,
@@ -4885,8 +4841,8 @@ import { Text as Text6 } from "@earendil-works/pi-tui";
4885
4841
  import { Type as Type8 } from "@sinclair/typebox";
4886
4842
 
4887
4843
  // src/pending-diff-preview.ts
4888
- import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync, statSync as statSync3 } from "node:fs";
4889
- import { dirname, isAbsolute as isAbsolute2, relative as relative2, resolve, sep } from "node:path";
4844
+ import { existsSync as existsSync3, readFileSync as readFileSync2, realpathSync, statSync as statSync4 } from "node:fs";
4845
+ import { dirname, isAbsolute, relative as relative2, resolve, sep } from "node:path";
4890
4846
  var PENDING_DIFF_MAX_BYTES = 1024 * 1024;
4891
4847
  function skip(reason) {
4892
4848
  return { type: "skip", reason };
@@ -4900,9 +4856,9 @@ function resolveWorkspacePreviewPath(rawPath, cwd, allowMissing) {
4900
4856
  return { type: "skip", reason: "missing path" };
4901
4857
  const workspace = realpathSync(cwd);
4902
4858
  const normalizedPath = rawPath.replace(/^@/, "");
4903
- const explicitAbsolute = isAbsolute2(normalizedPath);
4859
+ const explicitAbsolute = isAbsolute(normalizedPath);
4904
4860
  const requested = resolve(workspace, normalizedPath);
4905
- if (existsSync2(requested)) {
4861
+ if (existsSync3(requested)) {
4906
4862
  const realTarget = realpathSync(requested);
4907
4863
  if (!explicitAbsolute && !isInsidePath(workspace, realTarget))
4908
4864
  return { type: "skip", reason: "path outside workspace" };
@@ -4913,17 +4869,17 @@ function resolveWorkspacePreviewPath(rawPath, cwd, allowMissing) {
4913
4869
  if (!allowMissing)
4914
4870
  return { type: "skip", reason: "file not found" };
4915
4871
  const parent = dirname(requested);
4916
- if (!existsSync2(parent))
4872
+ if (!existsSync3(parent))
4917
4873
  return { type: "skip", reason: "parent directory not found" };
4918
4874
  return { type: "ok", path: requested, existed: false };
4919
4875
  }
4920
4876
  function readUtf8File(filePath) {
4921
- const stat2 = statSync3(filePath);
4922
- if (!stat2.isFile())
4877
+ const stat = statSync4(filePath);
4878
+ if (!stat.isFile())
4923
4879
  return { type: "skip", reason: "not a file" };
4924
- if (stat2.size > PENDING_DIFF_MAX_BYTES)
4880
+ if (stat.size > PENDING_DIFF_MAX_BYTES)
4925
4881
  return { type: "skip", reason: "file too large" };
4926
- const content = readFileSync3(filePath, "utf-8");
4882
+ const content = readFileSync2(filePath, "utf-8");
4927
4883
  if (content.includes("\x00"))
4928
4884
  return { type: "skip", reason: "binary file" };
4929
4885
  return { type: "ok", content };
@@ -5184,7 +5140,7 @@ function registerWriteTool(pi, options = {}) {
5184
5140
  }),
5185
5141
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
5186
5142
  const cwd = ctx?.cwd ?? process.cwd();
5187
- const absolutePath = resolveToCwd(params.path, cwd);
5143
+ const absolutePath = resolveToCwd2(params.path, cwd);
5188
5144
  try {
5189
5145
  return await withFileMutationQueue2(absolutePath, async () => {
5190
5146
  let result;
@@ -5294,7 +5250,7 @@ async function executeDef(opts) {
5294
5250
  if (!p.name || !p.name.trim()) {
5295
5251
  return buildToolErrorResult("def", "invalid-parameter", "readSeek_def requires 'name'");
5296
5252
  }
5297
- const searchPath = resolveToCwd(p.path ?? ".", cwd);
5253
+ const searchPath = resolveToCwd2(p.path ?? ".", cwd);
5298
5254
  const statResult = await statSearchPathOrError("def", p.path, searchPath);
5299
5255
  if (!statResult.ok)
5300
5256
  return statResult.error;
@@ -5506,12 +5462,12 @@ async function executeDigest(opts) {
5506
5462
  }
5507
5463
  const facets = selectedFacets(select);
5508
5464
  const appliesImagePolicy = opts.imageMode !== undefined && facets.includes("content");
5509
- const end = coerceObviousBase10Int(p.end, "end");
5465
+ const end = coerceObviousBase10Int2(p.end, "end");
5510
5466
  if (!end.ok || end.value !== undefined && end.value < 1) {
5511
5467
  const message = end.ok ? `Invalid end: expected a positive integer, received ${end.value}.` : end.message;
5512
5468
  return buildToolErrorResult("digest", "invalid-parameter", message, { path: p.path });
5513
5469
  }
5514
- const limit = coerceObviousBase10Int(p.limit, "limit");
5470
+ const limit = coerceObviousBase10Int2(p.limit, "limit");
5515
5471
  if (!limit.ok || limit.value !== undefined && limit.value < 1) {
5516
5472
  const message = limit.ok ? `Invalid limit: expected a positive integer, received ${limit.value}.` : limit.message;
5517
5473
  return buildToolErrorResult("digest", "invalid-parameter", message, { path: p.path });
@@ -5527,7 +5483,7 @@ async function executeDigest(opts) {
5527
5483
  path: p.path
5528
5484
  });
5529
5485
  }
5530
- const filePath = resolveToCwd(p.path, cwd);
5486
+ const filePath = resolveToCwd2(p.path, cwd);
5531
5487
  const commonOptions = { select, signal };
5532
5488
  const textOptions = {
5533
5489
  ...commonOptions,
@@ -5665,12 +5621,12 @@ var VIEW_PROMPT_METADATA = defineToolPromptMetadata({
5665
5621
  async function executeView(opts) {
5666
5622
  const { params, signal, cwd } = opts;
5667
5623
  const input = params;
5668
- const page = coerceObviousBase10Int(input.page, "page");
5624
+ const page = coerceObviousBase10Int2(input.page, "page");
5669
5625
  if (!page.ok || page.value !== undefined && page.value < 1) {
5670
5626
  const message = page.ok ? `Invalid page: expected a positive integer, received ${page.value}.` : page.message;
5671
5627
  return buildToolErrorResult("view", "invalid-page", message, { path: input.path });
5672
5628
  }
5673
- const depth = coerceObviousBase10Int(input.depth, "depth");
5629
+ const depth = coerceObviousBase10Int2(input.depth, "depth");
5674
5630
  if (!depth.ok || depth.value !== undefined && depth.value < 0) {
5675
5631
  const message = depth.ok ? `Invalid depth: expected a non-negative integer, received ${depth.value}.` : depth.message;
5676
5632
  return buildToolErrorResult("view", "invalid-depth", message, { path: input.path });
@@ -5691,7 +5647,7 @@ async function executeView(opts) {
5691
5647
  path: input.path
5692
5648
  });
5693
5649
  }
5694
- const filePath = resolveToCwd(input.path, cwd);
5650
+ const filePath = resolveToCwd2(input.path, cwd);
5695
5651
  try {
5696
5652
  const output = await readSeekView(filePath, {
5697
5653
  node,
@@ -5823,7 +5779,6 @@ function piReadSeekExtension(pi) {
5823
5779
  const forgetAnchors = (absolutePath) => sessionAnchors.forget(absolutePath);
5824
5780
  const { settings } = resolveReadSeekJsonSettings();
5825
5781
  const replacedBuiltIns = new Set(settings.overrideTools ?? []);
5826
- const binaryAvailable = readSeekBinaryAvailability().available;
5827
5782
  const swap = (builtIn, readSeekName) => replacedBuiltIns.has(builtIn) ? builtIn : readSeekName;
5828
5783
  const readName = swap("read", "readSeek_digest");
5829
5784
  const editName = swap("edit", "readSeek_edit");
@@ -5853,8 +5808,24 @@ function piReadSeekExtension(pi) {
5853
5808
  });
5854
5809
  registerViewTool(pi);
5855
5810
  registerWriteTool(pi, { onFileAnchored: markAnchored, name: writeName });
5811
+ const activeReadSeekNames = READSEEK_TOOL_ENTRIES.map((entry) => entry.builtIn && replacedBuiltIns.has(entry.builtIn) ? entry.builtIn : entry.readSeekName);
5812
+ const inactiveReadSeekNames = new Set(READSEEK_TOOL_ENTRIES.filter((entry) => entry.builtIn !== null && replacedBuiltIns.has(entry.builtIn)).map((entry) => entry.readSeekName));
5813
+ const activateReadSeekTools = () => {
5814
+ if (!readSeekBinaryAvailability().available)
5815
+ return false;
5816
+ const current = pi.getActiveTools();
5817
+ const currentSet = new Set(current);
5818
+ const missing = activeReadSeekNames.some((name) => !currentSet.has(name));
5819
+ const extraInactive = current.some((name) => inactiveReadSeekNames.has(name));
5820
+ if (!missing && !extraInactive)
5821
+ return true;
5822
+ pi.setActiveTools([
5823
+ ...new Set([...current, ...activeReadSeekNames].filter((name) => !inactiveReadSeekNames.has(name)))
5824
+ ]);
5825
+ return true;
5826
+ };
5856
5827
  pi.on("before_agent_start", (event) => {
5857
- if (!binaryAvailable)
5828
+ if (!activateReadSeekTools())
5858
5829
  return;
5859
5830
  return { systemPrompt: `${event.systemPrompt}
5860
5831
 
@@ -5874,12 +5845,7 @@ ${toolRoutingPolicy(policyAliases)}` };
5874
5845
  else
5875
5846
  console.warn(problem);
5876
5847
  }
5877
- if (!availability.available)
5878
- return;
5879
- const activeReadSeekNames = READSEEK_TOOL_ENTRIES.map((entry) => entry.builtIn && replacedBuiltIns.has(entry.builtIn) ? entry.builtIn : entry.readSeekName);
5880
- const inactiveReadSeekNames = new Set(READSEEK_TOOL_ENTRIES.filter((entry) => entry.builtIn !== null && replacedBuiltIns.has(entry.builtIn)).map((entry) => entry.readSeekName));
5881
- const activeTools = [...pi.getActiveTools(), ...activeReadSeekNames].filter((name) => !inactiveReadSeekNames.has(name));
5882
- pi.setActiveTools([...new Set(activeTools)]);
5848
+ activateReadSeekTools();
5883
5849
  });
5884
5850
  }
5885
5851
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-readseek",
3
- "version": "0.9.13",
3
+ "version": "0.9.15",
4
4
  "description": "Pi extension for LINE:HASH-anchored file operations and structural code navigation",
5
5
  "type": "module",
6
6
  "exports": {
@@ -27,7 +27,6 @@
27
27
  "files": [
28
28
  "dist/",
29
29
  "prompts/",
30
- "LICENSE",
31
30
  "README.md"
32
31
  ],
33
32
  "scripts": {
@@ -40,7 +39,8 @@
40
39
  "node": ">=20.0.0"
41
40
  },
42
41
  "dependencies": {
43
- "@jarkkojs/readseek": "^0.9.13",
42
+ "@jarkkojs/readseek": "^0.9.15",
43
+ "@jarkkojs/readseek-api": "^0.9.15",
44
44
  "diff": "^9.0.0",
45
45
  "xxhash-wasm": "^1.1.0"
46
46
  },
@@ -50,13 +50,13 @@
50
50
  "@sinclair/typebox": "*"
51
51
  },
52
52
  "devDependencies": {
53
- "@earendil-works/pi-ai": "^0.82.1",
54
- "@earendil-works/pi-coding-agent": "^0.82.1",
55
- "@earendil-works/pi-tui": "^0.82.1",
53
+ "@earendil-works/pi-ai": "^0.84.2",
54
+ "@earendil-works/pi-coding-agent": "^0.84.2",
55
+ "@earendil-works/pi-tui": "^0.84.2",
56
56
  "@sinclair/typebox": "^0.34.48",
57
57
  "@types/node": "^26.1.1",
58
58
  "jiti": "^2.7.0",
59
- "typescript": "^7.0.2",
59
+ "typescript": "^5.9.3",
60
60
  "vitest": "^4.1.0"
61
61
  },
62
62
  "directories": {
package/prompts/edit.md CHANGED
@@ -24,15 +24,15 @@ plain file content, not a diff or hashline output.
24
24
  {
25
25
  "path": "src/foo.ts",
26
26
  "edits": [
27
- { "set_line": { "anchor": "42:ab1", "new_text": "const x = 2;" } },
27
+ { "set_line": { "anchor": "42:ab1cde", "new_text": "const x = 2;" } },
28
28
  {
29
29
  "replace_lines": {
30
- "start_anchor": "50:c3d",
31
- "end_anchor": "55:e4f",
30
+ "start_anchor": "50:c3d4e5",
31
+ "end_anchor": "55:e4f5a6",
32
32
  "new_text": "const y = 3;\nreturn y;"
33
33
  }
34
34
  },
35
- { "insert_after": { "anchor": "60:f5a", "new_text": "// TODO\n" } }
35
+ { "insert_after": { "anchor": "60:f5a6b7", "new_text": "// TODO\n" } }
36
36
  ]
37
37
  }
38
38
  ```
package/LICENSE DELETED
@@ -1,176 +0,0 @@
1
- Apache License
2
- Version 2.0, January 2004
3
- http://www.apache.org/licenses/
4
-
5
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
-
7
- 1. Definitions.
8
-
9
- "License" shall mean the terms and conditions for use, reproduction,
10
- and distribution as defined by Sections 1 through 9 of this document.
11
-
12
- "Licensor" shall mean the copyright owner or entity authorized by
13
- the copyright owner that is granting the License.
14
-
15
- "Legal Entity" shall mean the union of the acting entity and all
16
- other entities that control, are controlled by, or are under common
17
- control with that entity. For the purposes of this definition,
18
- "control" means (i) the power, direct or indirect, to cause the
19
- direction or management of such entity, whether by contract or
20
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
- outstanding shares, or (iii) beneficial ownership of such entity.
22
-
23
- "You" (or "Your") shall mean an individual or Legal Entity
24
- exercising permissions granted by this License.
25
-
26
- "Source" form shall mean the preferred form for making modifications,
27
- including but not limited to software source code, documentation
28
- source, and configuration files.
29
-
30
- "Object" form shall mean any form resulting from mechanical
31
- transformation or translation of a Source form, including but
32
- not limited to compiled object code, generated documentation,
33
- and conversions to other media types.
34
-
35
- "Work" shall mean the work of authorship, whether in Source or
36
- Object form, made available under the License, as indicated by a
37
- copyright notice that is included in or attached to the work
38
- (an example is provided in the Appendix below).
39
-
40
- "Derivative Works" shall mean any work, whether in Source or Object
41
- form, that is based on (or derived from) the Work and for which the
42
- editorial revisions, annotations, elaborations, or other modifications
43
- represent, as a whole, an original work of authorship. For the purposes
44
- of this License, Derivative Works shall not include works that remain
45
- separable from, or merely link (or bind by name) to the interfaces of,
46
- the Work and Derivative Works thereof.
47
-
48
- "Contribution" shall mean any work of authorship, including
49
- the original version of the Work and any modifications or additions
50
- to that Work or Derivative Works thereof, that is intentionally
51
- submitted to Licensor for inclusion in the Work by the copyright owner
52
- or by an individual or Legal Entity authorized to submit on behalf of
53
- the copyright owner. For the purposes of this definition, "submitted"
54
- means any form of electronic, verbal, or written communication sent
55
- to the Licensor or its representatives, including but not limited to
56
- communication on electronic mailing lists, source code control systems,
57
- and issue tracking systems that are managed by, or on behalf of, the
58
- Licensor for the purpose of discussing and improving the Work, but
59
- excluding communication that is conspicuously marked or otherwise
60
- designated in writing by the copyright owner as "Not a Contribution."
61
-
62
- "Contributor" shall mean Licensor and any individual or Legal Entity
63
- on behalf of whom a Contribution has been received by Licensor and
64
- subsequently incorporated within the Work.
65
-
66
- 2. Grant of Copyright License. Subject to the terms and conditions of
67
- this License, each Contributor hereby grants to You a perpetual,
68
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
- copyright license to reproduce, prepare Derivative Works of,
70
- publicly display, publicly perform, sublicense, and distribute the
71
- Work and such Derivative Works in Source or Object form.
72
-
73
- 3. Grant of Patent License. Subject to the terms and conditions of
74
- this License, each Contributor hereby grants to You a perpetual,
75
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
- (except as stated in this section) patent license to make, have made,
77
- use, offer to sell, sell, import, and otherwise transfer the Work,
78
- where such license applies only to those patent claims licensable
79
- by such Contributor that are necessarily infringed by their
80
- Contribution(s) alone or by combination of their Contribution(s)
81
- with the Work to which such Contribution(s) was submitted. If You
82
- institute patent litigation against any entity (including a
83
- cross-claim or counterclaim in a lawsuit) alleging that the Work
84
- or a Contribution incorporated within the Work constitutes direct
85
- or contributory patent infringement, then any patent licenses
86
- granted to You under this License for that Work shall terminate
87
- as of the date such litigation is filed.
88
-
89
- 4. Redistribution. You may reproduce and distribute copies of the
90
- Work or Derivative Works thereof in any medium, with or without
91
- modifications, and in Source or Object form, provided that You
92
- meet the following conditions:
93
-
94
- (a) You must give any other recipients of the Work or
95
- Derivative Works a copy of this License; and
96
-
97
- (b) You must cause any modified files to carry prominent notices
98
- stating that You changed the files; and
99
-
100
- (c) You must retain, in the Source form of any Derivative Works
101
- that You distribute, all copyright, patent, trademark, and
102
- attribution notices from the Source form of the Work,
103
- excluding those notices that do not pertain to any part of
104
- the Derivative Works; and
105
-
106
- (d) If the Work includes a "NOTICE" text file as part of its
107
- distribution, then any Derivative Works that You distribute must
108
- include a readable copy of the attribution notices contained
109
- within such NOTICE file, excluding those notices that do not
110
- pertain to any part of the Derivative Works, in at least one
111
- of the following places: within a NOTICE text file distributed
112
- as part of the Derivative Works; within the Source form or
113
- documentation, if provided along with the Derivative Works; or,
114
- within a display generated by the Derivative Works, if and
115
- wherever such third-party notices normally appear. The contents
116
- of the NOTICE file are for informational purposes only and
117
- do not modify the License. You may add Your own attribution
118
- notices within Derivative Works that You distribute, alongside
119
- or as an addendum to the NOTICE text from the Work, provided
120
- that such additional attribution notices cannot be construed
121
- as modifying the License.
122
-
123
- You may add Your own copyright statement to Your modifications and
124
- may provide additional or different license terms and conditions
125
- for use, reproduction, or distribution of Your modifications, or
126
- for any such Derivative Works as a whole, provided Your use,
127
- reproduction, and distribution of the Work otherwise complies with
128
- the conditions stated in this License.
129
-
130
- 5. Submission of Contributions. Unless You explicitly state otherwise,
131
- any Contribution intentionally submitted for inclusion in the Work
132
- by You to the Licensor shall be under the terms and conditions of
133
- this License, without any additional terms or conditions.
134
- Notwithstanding the above, nothing herein shall supersede or modify
135
- the terms of any separate license agreement you may have executed
136
- with Licensor regarding such Contributions.
137
-
138
- 6. Trademarks. This License does not grant permission to use the trade
139
- names, trademarks, service marks, or product names of the Licensor,
140
- except as required for reasonable and customary use in describing the
141
- origin of the Work and reproducing the content of the NOTICE file.
142
-
143
- 7. Disclaimer of Warranty. Unless required by applicable law or
144
- agreed to in writing, Licensor provides the Work (and each
145
- Contributor provides its Contributions) on an "AS IS" BASIS,
146
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
- implied, including, without limitation, any warranties or conditions
148
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
- PARTICULAR PURPOSE. You are solely responsible for determining the
150
- appropriateness of using or redistributing the Work and assume any
151
- risks associated with Your exercise of permissions under this License.
152
-
153
- 8. Limitation of Liability. In no event and under no legal theory,
154
- whether in tort (including negligence), contract, or otherwise,
155
- unless required by applicable law (such as deliberate and grossly
156
- negligent acts) or agreed to in writing, shall any Contributor be
157
- liable to You for damages, including any direct, indirect, special,
158
- incidental, or consequential damages of any character arising as a
159
- result of this License or out of the use or inability to use the
160
- Work (including but not limited to damages for loss of goodwill,
161
- work stoppage, computer failure or malfunction, or any and all
162
- other commercial damages or losses), even if such Contributor
163
- has been advised of the possibility of such damages.
164
-
165
- 9. Accepting Warranty or Additional Liability. While redistributing
166
- the Work or Derivative Works thereof, You may choose to offer,
167
- and charge a fee for, acceptance of support, warranty, indemnity,
168
- or other liability obligations and/or rights consistent with this
169
- License. However, in accepting such obligations, You may act only
170
- on Your own behalf and on Your sole responsibility, not on behalf
171
- of any other Contributor, and only if You agree to indemnify,
172
- defend, and hold each Contributor harmless for any liability
173
- incurred by, or claims asserted against, such Contributor by reason
174
- of your accepting any such warranty or additional liability.
175
-
176
- END OF TERMS AND CONDITIONS