@fitchmultz/pi-mcp-adapter 8.0.6 → 8.0.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +128 -8
  2. package/OAUTH.md +13 -16
  3. package/README.md +8 -8
  4. package/cli.js +67 -28
  5. package/dist/abort.js +18 -9
  6. package/dist/client-operations.js +4 -2
  7. package/dist/commands.js +77 -43
  8. package/dist/config.js +194 -105
  9. package/dist/connection-runtime.d.ts +2 -2
  10. package/dist/connection-runtime.js +29 -18
  11. package/dist/consent-manager.d.ts +2 -2
  12. package/dist/consent-manager.js +5 -2
  13. package/dist/direct-tools.d.ts +1 -1
  14. package/dist/direct-tools.js +13 -7
  15. package/dist/elicitation-handler.d.ts +2 -3
  16. package/dist/elicitation-handler.js +113 -58
  17. package/dist/gateway-arguments.d.ts +1 -1
  18. package/dist/gateway-arguments.js +90 -23
  19. package/dist/glimpse-ui.js +10 -5
  20. package/dist/host-html-template.d.ts +3 -1
  21. package/dist/host-html-template.js +9 -12
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +264 -119
  24. package/dist/init.d.ts +1 -3
  25. package/dist/init.js +91 -51
  26. package/dist/legacy-migration.d.ts +1 -1
  27. package/dist/legacy-migration.js +17 -12
  28. package/dist/lifecycle.d.ts +3 -3
  29. package/dist/lifecycle.js +33 -17
  30. package/dist/logger.d.ts +2 -2
  31. package/dist/logger.js +4 -2
  32. package/dist/mcp-auth-flow.js +150 -58
  33. package/dist/mcp-auth.d.ts +7 -7
  34. package/dist/mcp-auth.js +189 -138
  35. package/dist/mcp-callback-server.js +19 -8
  36. package/dist/mcp-code.js +168 -54
  37. package/dist/mcp-keyring-helper.cjs +57 -23
  38. package/dist/mcp-oauth-provider.d.ts +6 -7
  39. package/dist/mcp-oauth-provider.js +146 -72
  40. package/dist/mcp-oauth-runtime.js +29 -16
  41. package/dist/mcp-output-guard.js +193 -65
  42. package/dist/mcp-output-preview.js +41 -20
  43. package/dist/mcp-panel.d.ts +9 -9
  44. package/dist/mcp-panel.js +141 -70
  45. package/dist/mcp-script-worker.mjs +135 -43
  46. package/dist/mcp-setup-panel.d.ts +8 -8
  47. package/dist/mcp-setup-panel.js +130 -44
  48. package/dist/mcp-status.js +14 -7
  49. package/dist/mcp-trace.d.ts +10 -5
  50. package/dist/mcp-trace.js +127 -61
  51. package/dist/metadata-cache.d.ts +3 -4
  52. package/dist/metadata-cache.js +83 -43
  53. package/dist/npx-resolver.js +101 -60
  54. package/dist/onboarding-state.js +4 -2
  55. package/dist/plain-result-reader.js +78 -31
  56. package/dist/prompts.js +36 -20
  57. package/dist/proxy-modes.js +670 -193
  58. package/dist/resource-tools.js +30 -9
  59. package/dist/runtime-owner.js +36 -18
  60. package/dist/sampling-handler.d.ts +1 -2
  61. package/dist/sampling-handler.js +41 -21
  62. package/dist/search-ranking.d.ts +1 -1
  63. package/dist/search-ranking.js +67 -34
  64. package/dist/server-manager.d.ts +11 -11
  65. package/dist/server-manager.js +283 -166
  66. package/dist/session-recovery.js +113 -61
  67. package/dist/tool-approval.js +9 -3
  68. package/dist/tool-loader.js +154 -55
  69. package/dist/tool-metadata.d.ts +1 -1
  70. package/dist/tool-metadata.js +51 -23
  71. package/dist/tool-registrar.js +32 -10
  72. package/dist/tool-result-renderer.js +58 -33
  73. package/dist/types.d.ts +2 -2
  74. package/dist/types.js +39 -18
  75. package/dist/ui-message-history.js +33 -13
  76. package/dist/ui-resource-handler.d.ts +3 -3
  77. package/dist/ui-resource-handler.js +37 -21
  78. package/dist/ui-server.js +101 -43
  79. package/dist/ui-session.js +66 -29
  80. package/dist/ui-stream-types.js +14 -4
  81. package/dist/ui-tool-visibility.js +12 -6
  82. package/dist/unix-socket-transport.d.ts +1 -2
  83. package/dist/unix-socket-transport.js +19 -12
  84. package/dist/utils.d.ts +2 -0
  85. package/dist/utils.js +60 -35
  86. package/package.json +26 -2
  87. package/skills/mcp-scripting/SKILL.md +6 -6
@@ -1,5 +1,5 @@
1
1
  // npx-resolver.ts - Resolve npx/npm exec binaries to avoid npm parent processes
2
- import { existsSync, readFileSync, realpathSync, readdirSync, statSync, writeFileSync, renameSync, mkdirSync, openSync, readSync, closeSync } from "node:fs";
2
+ import { existsSync, readFileSync, realpathSync, readdirSync, statSync, writeFileSync, renameSync, mkdirSync, openSync, readSync, closeSync, } from "node:fs";
3
3
  import { join, dirname, extname, resolve, sep } from "node:path";
4
4
  import { getAdapterPath } from "./agent-dir.js";
5
5
  import { throwIfAborted } from "./abort.js";
@@ -9,23 +9,21 @@ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
9
9
  const EXACT_PACKAGE_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?$/;
10
10
  export async function resolveNpxBinary(command, args, signal) {
11
11
  throwIfAborted(signal);
12
- const parsed = command === "npx"
13
- ? parseNpxArgs(args)
14
- : command === "npm"
15
- ? parseNpmExecArgs(args)
16
- : null;
17
- if (!parsed)
12
+ const parsed = command === "npx" ? parseNpxArgs(args) : command === "npm" ? parseNpmExecArgs(args) : null;
13
+ if (!parsed) {
18
14
  return null;
15
+ }
19
16
  const packageSpec = parsePackageSpec(parsed.packageSpec);
20
- if (!packageSpec)
17
+ if (!packageSpec) {
21
18
  return null;
19
+ }
22
20
  const cacheKey = JSON.stringify([command, ...args]);
23
21
  const cache = loadCache();
24
22
  const cached = cache?.entries?.[cacheKey];
25
- if (cached
26
- && Date.now() - cached.resolvedAt < CACHE_TTL_MS
27
- && existsSync(cached.resolvedBin)
28
- && (!packageSpec.exactVersion || cached.packageVersion === packageSpec.exactVersion)) {
23
+ if (cached &&
24
+ Date.now() - cached.resolvedAt < CACHE_TTL_MS &&
25
+ existsSync(cached.resolvedBin) &&
26
+ (!packageSpec.exactVersion || cached.packageVersion === packageSpec.exactVersion)) {
29
27
  return { binPath: cached.resolvedBin, extraArgs: parsed.extraArgs, isJs: cached.isJs };
30
28
  }
31
29
  const resolved = resolveFromNpmCache(parsed.packageSpec, parsed.binName);
@@ -38,7 +36,11 @@ export async function resolveNpxBinary(command, args, signal) {
38
36
  const resolvedAfterInstall = resolveFromNpmCache(parsed.packageSpec, parsed.binName);
39
37
  if (resolvedAfterInstall) {
40
38
  saveCacheEntry(cacheKey, resolvedAfterInstall);
41
- return { binPath: resolvedAfterInstall.resolvedBin, extraArgs: parsed.extraArgs, isJs: resolvedAfterInstall.isJs };
39
+ return {
40
+ binPath: resolvedAfterInstall.resolvedBin,
41
+ extraArgs: parsed.extraArgs,
42
+ isJs: resolvedAfterInstall.isJs,
43
+ };
42
44
  }
43
45
  return null;
44
46
  }
@@ -52,30 +54,36 @@ function parseNpxArgs(args) {
52
54
  let foundFirstPositional = false;
53
55
  for (let i = 0; i < before.length; i++) {
54
56
  const arg = before[i];
55
- if (arg === undefined)
57
+ if (arg === undefined) {
56
58
  return null;
59
+ }
57
60
  if (foundFirstPositional) {
58
61
  positionals.push(arg);
59
62
  continue;
60
63
  }
61
- if (arg === "-y" || arg === "--yes")
64
+ if (arg === "-y" || arg === "--yes") {
62
65
  continue;
66
+ }
63
67
  if (arg === "-p" || arg === "--package") {
64
68
  const value = before[i + 1];
65
- if (!value || value.startsWith("-"))
69
+ if (!value || value.startsWith("-")) {
66
70
  return null;
67
- if (!packageSpec)
71
+ }
72
+ if (!packageSpec) {
68
73
  packageSpec = value;
74
+ }
69
75
  sawPackageFlag = true;
70
76
  i++;
71
77
  continue;
72
78
  }
73
79
  if (arg.startsWith("--package=")) {
74
80
  const value = arg.slice("--package=".length);
75
- if (!value)
81
+ if (!value) {
76
82
  return null;
77
- if (!packageSpec)
83
+ }
84
+ if (!packageSpec) {
78
85
  packageSpec = value;
86
+ }
79
87
  sawPackageFlag = true;
80
88
  continue;
81
89
  }
@@ -88,48 +96,58 @@ function parseNpxArgs(args) {
88
96
  const separatedAfter = separatorIndex >= 0 && after.length > 0 ? ["--", ...after] : after;
89
97
  if (sawPackageFlag) {
90
98
  const binName = positionals[0];
91
- if (!packageSpec || !binName)
99
+ if (!packageSpec || !binName) {
92
100
  return null;
101
+ }
93
102
  const extraArgs = positionals.slice(1).concat(separatedAfter);
94
103
  return { packageSpec, binName, extraArgs };
95
104
  }
96
105
  const packagePositional = positionals[0];
97
- if (!packagePositional)
106
+ if (!packagePositional) {
98
107
  return null;
108
+ }
99
109
  const extraArgs = positionals.slice(1).concat(separatedAfter);
100
110
  return { packageSpec: packagePositional, extraArgs };
101
111
  }
102
112
  function parseNpmExecArgs(args) {
103
- if (args[0] !== "exec")
113
+ if (args[0] !== "exec") {
104
114
  return null;
115
+ }
105
116
  const execArgs = args.slice(1);
106
117
  const separatorIndex = execArgs.indexOf("--");
107
- if (separatorIndex < 0)
118
+ if (separatorIndex < 0) {
108
119
  return null;
120
+ }
109
121
  const before = execArgs.slice(0, separatorIndex);
110
122
  const after = execArgs.slice(separatorIndex + 1);
111
123
  let packageSpec;
112
124
  for (let i = 0; i < before.length; i++) {
113
125
  const arg = before[i];
114
- if (arg === undefined)
126
+ if (arg === undefined) {
115
127
  return null;
116
- if (arg === "-y" || arg === "--yes")
128
+ }
129
+ if (arg === "-y" || arg === "--yes") {
117
130
  continue;
131
+ }
118
132
  if (arg === "--package") {
119
133
  const value = before[i + 1];
120
- if (!value || value.startsWith("-"))
134
+ if (!value || value.startsWith("-")) {
121
135
  return null;
122
- if (!packageSpec)
136
+ }
137
+ if (!packageSpec) {
123
138
  packageSpec = value;
139
+ }
124
140
  i++;
125
141
  continue;
126
142
  }
127
143
  if (arg.startsWith("--package=")) {
128
144
  const value = arg.slice("--package=".length);
129
- if (!value)
145
+ if (!value) {
130
146
  return null;
131
- if (!packageSpec)
147
+ }
148
+ if (!packageSpec) {
132
149
  packageSpec = value;
150
+ }
133
151
  continue;
134
152
  }
135
153
  if (arg.startsWith("-")) {
@@ -137,25 +155,30 @@ function parseNpmExecArgs(args) {
137
155
  }
138
156
  }
139
157
  const binName = after[0];
140
- if (!packageSpec || !binName)
158
+ if (!packageSpec || !binName) {
141
159
  return null;
160
+ }
142
161
  const extraArgs = after.slice(1);
143
162
  return { packageSpec, binName, extraArgs };
144
163
  }
145
164
  function resolveFromNpmCache(packageSpec, binName) {
146
165
  const cacheDir = getNpmCacheDir();
147
- if (!cacheDir)
166
+ if (!cacheDir) {
148
167
  return null;
168
+ }
149
169
  const parsedSpec = parsePackageSpec(packageSpec);
150
- if (!parsedSpec)
170
+ if (!parsedSpec) {
151
171
  return null;
172
+ }
152
173
  const { packageName, exactVersion } = parsedSpec;
153
174
  const packageDir = findCachedPackageDir(cacheDir, packageName, exactVersion);
154
- if (!packageDir)
175
+ if (!packageDir) {
155
176
  return null;
177
+ }
156
178
  const packageJsonPath = join(packageDir, "package.json");
157
- if (!existsSync(packageJsonPath))
179
+ if (!existsSync(packageJsonPath)) {
158
180
  return null;
181
+ }
159
182
  let pkg = null;
160
183
  try {
161
184
  pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
@@ -164,8 +187,9 @@ function resolveFromNpmCache(packageSpec, binName) {
164
187
  return null;
165
188
  }
166
189
  const binField = pkg?.bin;
167
- if (!binField)
190
+ if (!binField) {
168
191
  return null;
192
+ }
169
193
  const candidates = buildBinCandidates(packageName, binName);
170
194
  let chosenBinName;
171
195
  let binRel;
@@ -189,15 +213,17 @@ function resolveFromNpmCache(packageSpec, binName) {
189
213
  }
190
214
  }
191
215
  }
192
- if (!binRel)
216
+ if (!binRel) {
193
217
  return null;
218
+ }
194
219
  const nodeModulesDir = findNodeModulesDir(packageDir);
195
220
  const binLink = chosenBinName ? join(nodeModulesDir, ".bin", chosenBinName) : null;
196
221
  let resolvedBin = binLink && existsSync(binLink) ? safeRealpath(binLink) : "";
197
222
  if (!resolvedBin) {
198
223
  resolvedBin = resolve(packageDir, binRel);
199
- if (!existsSync(resolvedBin))
224
+ if (!existsSync(resolvedBin)) {
200
225
  return null;
226
+ }
201
227
  }
202
228
  const isJs = detectJsBinary(resolvedBin);
203
229
  return {
@@ -236,23 +262,27 @@ async function forceNpxCache(packageSpec, signal) {
236
262
  });
237
263
  }
238
264
  catch (error) {
239
- if (signal?.aborted)
265
+ if (signal?.aborted) {
240
266
  throwIfAborted(signal);
267
+ }
241
268
  // Ignore failures, resolution will fall back to original command
242
269
  }
243
270
  throwIfAborted(signal);
244
271
  }
245
272
  function buildBinCandidates(packageName, explicitBin) {
246
273
  const candidates = [];
247
- if (explicitBin)
274
+ if (explicitBin) {
248
275
  candidates.push(explicitBin);
276
+ }
249
277
  if (packageName.startsWith("@")) {
250
278
  const namePart = packageName.split("/")[1] ?? "";
251
279
  const scopePart = packageName.split("/")[0]?.replace("@", "") ?? "";
252
- if (namePart)
280
+ if (namePart) {
253
281
  candidates.push(namePart);
254
- if (scopePart && namePart)
282
+ }
283
+ if (scopePart && namePart) {
255
284
  candidates.push(`${scopePart}-${namePart}`);
285
+ }
256
286
  }
257
287
  else {
258
288
  candidates.push(packageName);
@@ -261,14 +291,16 @@ function buildBinCandidates(packageName, explicitBin) {
261
291
  }
262
292
  function parsePackageSpec(spec) {
263
293
  const trimmed = spec.trim();
264
- if (!trimmed)
294
+ if (!trimmed) {
265
295
  return null;
296
+ }
266
297
  let packageName;
267
298
  let requestedVersion;
268
299
  if (trimmed.startsWith("@")) {
269
300
  const slashIndex = trimmed.indexOf("/");
270
- if (slashIndex < 0)
301
+ if (slashIndex < 0) {
271
302
  return null;
303
+ }
272
304
  const atIndex = trimmed.lastIndexOf("@");
273
305
  if (atIndex > slashIndex) {
274
306
  packageName = trimmed.slice(0, atIndex);
@@ -288,11 +320,13 @@ function parsePackageSpec(spec) {
288
320
  packageName = trimmed;
289
321
  }
290
322
  }
291
- if (!packageName)
323
+ if (!packageName) {
292
324
  return null;
325
+ }
293
326
  const normalizedVersion = requestedVersion?.replace(/^=/, "").replace(/^v/i, "");
294
327
  // Let npm resolve ranges and tags rather than choosing an arbitrary cached version.
295
- if (requestedVersion !== undefined && (!normalizedVersion || !EXACT_PACKAGE_VERSION_RE.test(normalizedVersion))) {
328
+ if (requestedVersion !== undefined &&
329
+ (!normalizedVersion || !EXACT_PACKAGE_VERSION_RE.test(normalizedVersion))) {
296
330
  return null;
297
331
  }
298
332
  return {
@@ -309,14 +343,13 @@ function defaultBinName(packageName) {
309
343
  }
310
344
  function findCachedPackageDir(cacheDir, packageName, exactVersion) {
311
345
  const npxDir = join(cacheDir, "_npx");
312
- if (!existsSync(npxDir))
346
+ if (!existsSync(npxDir)) {
313
347
  return null;
314
- const packagePathParts = packageName.startsWith("@")
315
- ? packageName.split("/")
316
- : [packageName];
348
+ }
349
+ const packagePathParts = packageName.startsWith("@") ? packageName.split("/") : [packageName];
317
350
  const candidates = readdirSync(npxDir, { withFileTypes: true })
318
- .filter(entry => entry.isDirectory())
319
- .map(entry => {
351
+ .filter((entry) => entry.isDirectory())
352
+ .map((entry) => {
320
353
  const full = join(npxDir, entry.name);
321
354
  const mtime = safeStatMtime(full);
322
355
  return { name: entry.name, mtime };
@@ -325,13 +358,15 @@ function findCachedPackageDir(cacheDir, packageName, exactVersion) {
325
358
  for (const entry of candidates) {
326
359
  const pkgDir = join(npxDir, entry.name, "node_modules", ...packagePathParts);
327
360
  const packageJsonPath = join(pkgDir, "package.json");
328
- if (!existsSync(packageJsonPath))
361
+ if (!existsSync(packageJsonPath)) {
329
362
  continue;
363
+ }
330
364
  if (exactVersion) {
331
365
  try {
332
366
  const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
333
- if (pkg.version !== exactVersion)
367
+ if (pkg.version !== exactVersion) {
334
368
  continue;
369
+ }
335
370
  }
336
371
  catch {
337
372
  continue;
@@ -351,8 +386,9 @@ function findNodeModulesDir(packageDir) {
351
386
  }
352
387
  function detectJsBinary(binPath) {
353
388
  const ext = extname(binPath).toLowerCase();
354
- if (ext === ".js" || ext === ".mjs" || ext === ".cjs")
389
+ if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
355
390
  return true;
391
+ }
356
392
  try {
357
393
  const fd = openSync(binPath, "r");
358
394
  try {
@@ -371,8 +407,9 @@ function detectJsBinary(binPath) {
371
407
  }
372
408
  let npmCacheDirCached;
373
409
  function getNpmCacheDir() {
374
- if (npmCacheDirCached !== undefined)
410
+ if (npmCacheDirCached !== undefined) {
375
411
  return npmCacheDirCached;
412
+ }
376
413
  if (process.env.NPM_CONFIG_CACHE) {
377
414
  npmCacheDirCached = process.env.NPM_CONFIG_CACHE;
378
415
  return npmCacheDirCached;
@@ -397,16 +434,20 @@ function getNpxCachePath() {
397
434
  }
398
435
  function loadCache() {
399
436
  const cachePath = getNpxCachePath();
400
- if (!existsSync(cachePath))
437
+ if (!existsSync(cachePath)) {
401
438
  return null;
439
+ }
402
440
  try {
403
441
  const raw = JSON.parse(readFileSync(cachePath, "utf-8"));
404
- if (!raw || typeof raw !== "object")
442
+ if (!raw || typeof raw !== "object") {
405
443
  return null;
406
- if (raw.version !== CACHE_VERSION)
444
+ }
445
+ if (raw.version !== CACHE_VERSION) {
407
446
  return null;
408
- if (!raw.entries || typeof raw.entries !== "object")
447
+ }
448
+ if (!raw.entries || typeof raw.entries !== "object") {
409
449
  return null;
450
+ }
410
451
  return raw;
411
452
  }
412
453
  catch {
@@ -417,7 +458,7 @@ function saveCacheEntry(key, entry) {
417
458
  const cachePath = getNpxCachePath();
418
459
  const dir = dirname(cachePath);
419
460
  mkdirSync(dir, { recursive: true });
420
- let merged = { version: CACHE_VERSION, entries: {} };
461
+ const merged = { version: CACHE_VERSION, entries: {} };
421
462
  try {
422
463
  if (existsSync(cachePath)) {
423
464
  const existing = JSON.parse(readFileSync(cachePath, "utf-8"));
@@ -11,12 +11,14 @@ export function getOnboardingStatePath() {
11
11
  }
12
12
  export function loadOnboardingState() {
13
13
  const path = getOnboardingStatePath();
14
- if (!existsSync(path))
14
+ if (!existsSync(path)) {
15
15
  return { ...DEFAULT_STATE };
16
+ }
16
17
  try {
17
18
  const raw = JSON.parse(readFileSync(path, "utf-8"));
18
- if (!raw || typeof raw !== "object")
19
+ if (!raw || typeof raw !== "object") {
19
20
  return { ...DEFAULT_STATE };
21
+ }
20
22
  return {
21
23
  version: 1,
22
24
  sharedConfigHintShown: raw.sharedConfigHintShown === true,
@@ -8,8 +8,11 @@ const readers = new Map();
8
8
  const MAX_INDEXES = 8;
9
9
  const MAX_POINTS = 1024;
10
10
  function sameRevision(a, b) {
11
- return a.dev === b.dev && a.ino === b.ino && a.size === b.size
12
- && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs;
11
+ return (a.dev === b.dev &&
12
+ a.ino === b.ino &&
13
+ a.size === b.size &&
14
+ a.mtimeNs === b.mtimeNs &&
15
+ a.ctimeNs === b.ctimeNs);
13
16
  }
14
17
  /** Raw full-content SHA-256. Returns "" on a short read so a truncated verify never matches. */
15
18
  async function digestOf(file, size) {
@@ -18,16 +21,18 @@ async function digestOf(file, size) {
18
21
  let position = 0;
19
22
  while (position < size) {
20
23
  const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, size - position), position);
21
- if (!bytesRead)
24
+ if (!bytesRead) {
22
25
  return "";
26
+ }
23
27
  hash.update(buffer.subarray(0, bytesRead));
24
28
  position += bytesRead;
25
29
  }
26
30
  return hash.digest("hex");
27
31
  }
28
32
  function checkpoint(index, point) {
29
- if (point.bytes - index.points.at(-1).bytes < index.stride)
33
+ if (point.bytes - index.points.at(-1).bytes < index.stride) {
30
34
  return;
35
+ }
31
36
  index.points.push(point);
32
37
  if (index.points.length > MAX_POINTS) {
33
38
  index.points = index.points.filter((_, i) => i % 2 === 0);
@@ -37,8 +42,9 @@ function checkpoint(index, point) {
37
42
  function remember(ref, index) {
38
43
  indexes.delete(ref);
39
44
  indexes.set(ref, index);
40
- if (indexes.size > MAX_INDEXES)
45
+ if (indexes.size > MAX_INDEXES) {
41
46
  indexes.delete(indexes.keys().next().value);
47
+ }
42
48
  }
43
49
  function extend(index, after, text) {
44
50
  // Clone before updating so a shared cached hasher never advances.
@@ -49,8 +55,9 @@ function extend(index, after, text) {
49
55
  // Index only the accepted delta, keeping checkpoints on complete UTF-8 code points.
50
56
  for (let start = 0; start < text.length;) {
51
57
  let end = Math.min(text.length, start + 16 * 1024);
52
- if (end < text.length && text.codePointAt(end - 1) > 0xffff)
58
+ if (end < text.length && text.codePointAt(end - 1) > 0xffff) {
53
59
  end++;
60
+ }
54
61
  position += byteLength(text.slice(start, end));
55
62
  checkpoint(index, { bytes: position, characters: index.total + end });
56
63
  start = end;
@@ -66,32 +73,42 @@ export function appendPlainOutput(ref, text) {
66
73
  key = indexes.size || readers.size ? realpathSync(ref) : ref;
67
74
  cached = indexes.get(key);
68
75
  indexes.delete(key);
69
- if (cached || readers.has(key))
76
+ if (cached || readers.has(key)) {
70
77
  before = statSync(ref, { bigint: true });
78
+ }
79
+ }
80
+ catch {
81
+ /* Cache bookkeeping must not change write acceptance. */
71
82
  }
72
- catch { /* Cache bookkeeping must not change write acceptance. */ }
73
83
  appendFileSync(ref, text, { flush: true });
74
- if (!before)
84
+ if (!before) {
75
85
  return;
86
+ }
76
87
  try {
77
88
  const after = statSync(ref, { bigint: true });
78
- if (after.dev !== before.dev || after.ino !== before.ino
79
- || after.size !== before.size + BigInt(byteLength(text)))
89
+ if (after.dev !== before.dev ||
90
+ after.ino !== before.ino ||
91
+ after.size !== before.size + BigInt(byteLength(text))) {
80
92
  return;
93
+ }
81
94
  if (cached && sameRevision(cached.revision, before) && !text.includes("\ufffd")) {
82
95
  const next = { ...cached, points: [...cached.points] };
83
96
  extend(next, after, text);
84
97
  remember(key, next);
85
98
  }
86
99
  for (const progress of readers.get(key) ?? []) {
87
- if (!sameRevision(progress.revision, before))
100
+ if (!sameRevision(progress.revision, before)) {
88
101
  continue;
102
+ }
89
103
  extend(progress, after, text);
90
- if (text.includes("\ufffd"))
104
+ if (text.includes("\ufffd")) {
91
105
  progress.cacheable = false;
106
+ }
92
107
  }
93
108
  }
94
- catch { /* The durable append already succeeded; leave its index invalidated. */ }
109
+ catch {
110
+ /* The durable append already succeeded; leave its index invalidated. */
111
+ }
95
112
  }
96
113
  /** Sparse byte/UTF16 checkpoints, never retained file contents. Changed/uncacheable files scan afresh. */
97
114
  export async function readPlainResult(ref, offset, limit, maxBytes, maxLines) {
@@ -103,12 +120,25 @@ export async function readPlainResult(ref, offset, limit, maxBytes, maxLines) {
103
120
  const revision = fstatSync(file.fd, { bigint: true });
104
121
  const cached = indexes.get(ref);
105
122
  const matched = cached !== undefined && sameRevision(cached.revision, revision);
106
- const fresh = { revision, total: 0, points: [{ bytes: 0, characters: 0 }],
107
- stride: 64 * 1024, digest: "", hasher: createHash("sha256") };
123
+ const fresh = {
124
+ revision,
125
+ total: 0,
126
+ points: [{ bytes: 0, characters: 0 }],
127
+ stride: 64 * 1024,
128
+ digest: "",
129
+ hasher: createHash("sha256"),
130
+ };
108
131
  let index = matched ? cached : fresh;
109
132
  // Append characters stay relative until the cold scan knows the opened prefix's total.
110
- progress = { revision, total: 0, points: [{ bytes: Number(revision.size), characters: 0 }],
111
- stride: index.stride, cacheable: true, digest: "", hasher: index.hasher.copy() };
133
+ progress = {
134
+ revision,
135
+ total: 0,
136
+ points: [{ bytes: Number(revision.size), characters: 0 }],
137
+ stride: index.stride,
138
+ cacheable: true,
139
+ digest: "",
140
+ hasher: index.hasher.copy(),
141
+ };
112
142
  const active = readers.get(ref) ?? new Set();
113
143
  active.add(progress);
114
144
  readers.set(ref, active);
@@ -125,8 +155,9 @@ export async function readPlainResult(ref, offset, limit, maxBytes, maxLines) {
125
155
  let point = index.points[0];
126
156
  if (valid) {
127
157
  for (const candidate of index.points) {
128
- if (candidate.characters > offset)
158
+ if (candidate.characters > offset) {
129
159
  break;
160
+ }
130
161
  point = candidate;
131
162
  }
132
163
  }
@@ -139,26 +170,31 @@ export async function readPlainResult(ref, offset, limit, maxBytes, maxLines) {
139
170
  let readPosition = point.bytes;
140
171
  while (readPosition < Number(revision.size)) {
141
172
  const { bytesRead } = await file.read(buffer, 0, Math.min(buffer.length, Number(revision.size) - readPosition), readPosition);
142
- if (!bytesRead)
173
+ if (!bytesRead) {
143
174
  break;
175
+ }
144
176
  readPosition += bytesRead;
145
- const chunk = decoder.write(buffer.subarray(0, bytesRead)) + (readPosition === Number(revision.size) ? decoder.end() : "");
177
+ const chunk = decoder.write(buffer.subarray(0, bytesRead)) +
178
+ (readPosition === Number(revision.size) ? decoder.end() : "");
146
179
  if (!valid) {
147
180
  progress.hasher.update(buffer.subarray(0, bytesRead));
148
181
  checkpoint(index, { bytes: position, characters: total });
149
182
  // Invalid/incomplete UTF8 can re-decode differently after append. Keep the full-scan fallback.
150
- if (chunk.includes("\ufffd"))
183
+ if (chunk.includes("\ufffd")) {
151
184
  cacheable = false;
185
+ }
152
186
  position += byteLength(chunk);
153
187
  }
154
188
  const start = Math.max(0, offset - total);
155
189
  if (!finished && start < chunk.length) {
156
- if (start > 0 && chunk.codePointAt(start - 1) > 0xffff)
190
+ if (start > 0 && chunk.codePointAt(start - 1) > 0xffff) {
157
191
  throw new Error("offset must not split a Unicode surrogate pair");
192
+ }
158
193
  for (const char of chunk.slice(start)) {
159
194
  const nextBytes = bytes + byteLength(char);
160
195
  const nextLines = lines + (char === "\n" ? 1 : 0);
161
- if (page && (nextBytes > maxBytes || nextLines > maxLines || page.length + char.length > limit)) {
196
+ if (page &&
197
+ (nextBytes > maxBytes || nextLines > maxLines || page.length + char.length > limit)) {
162
198
  finished = true;
163
199
  break;
164
200
  }
@@ -168,21 +204,31 @@ export async function readPlainResult(ref, offset, limit, maxBytes, maxLines) {
168
204
  }
169
205
  }
170
206
  total += chunk.length;
171
- if (valid && finished)
207
+ if (valid && finished) {
172
208
  break;
209
+ }
173
210
  }
174
211
  }
175
212
  // Only verified native appends may advance this snapshot. External edits still invalidate.
176
- if (!sameRevision(progress.revision, statSync(ref, { bigint: true })))
213
+ if (!sameRevision(progress.revision, statSync(ref, { bigint: true }))) {
177
214
  continue;
178
- if (!valid)
215
+ }
216
+ if (!valid) {
179
217
  index.total = total;
218
+ }
180
219
  // Serve the opened prefix; retain verified append checkpoints for the next read.
181
220
  if (cacheable && progress.cacheable) {
182
- const next = { ...index, revision: progress.revision, total: index.total + progress.total,
183
- points: [...index.points], digest: progress.hasher.copy().digest("hex"), hasher: progress.hasher };
184
- for (const point of progress.points)
221
+ const next = {
222
+ ...index,
223
+ revision: progress.revision,
224
+ total: index.total + progress.total,
225
+ points: [...index.points],
226
+ digest: progress.hasher.copy().digest("hex"),
227
+ hasher: progress.hasher,
228
+ };
229
+ for (const point of progress.points) {
185
230
  checkpoint(next, { bytes: point.bytes, characters: index.total + point.characters });
231
+ }
186
232
  remember(ref, next);
187
233
  }
188
234
  return { page, total: index.total };
@@ -191,8 +237,9 @@ export async function readPlainResult(ref, offset, limit, maxBytes, maxLines) {
191
237
  if (progress) {
192
238
  const active = readers.get(ref);
193
239
  active.delete(progress);
194
- if (!active.size)
240
+ if (!active.size) {
195
241
  readers.delete(ref);
242
+ }
196
243
  }
197
244
  await file.close();
198
245
  }