@giovannijecha/jecode 0.8.4 → 0.8.5

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 (53) hide show
  1. package/README.md +8 -6
  2. package/dist/accounts.js +47 -10
  3. package/dist/atomic.js +24 -14
  4. package/dist/batch.js +37 -4
  5. package/dist/bounded-file.js +212 -0
  6. package/dist/commands.js +2 -0
  7. package/dist/config.js +2 -1
  8. package/dist/context/automatic.js +35 -0
  9. package/dist/context/compactor.js +32 -2
  10. package/dist/context/manual.js +20 -3
  11. package/dist/context/request-projection.js +130 -0
  12. package/dist/controller-request.js +33 -11
  13. package/dist/credential-commands.js +22 -6
  14. package/dist/credentials.js +56 -18
  15. package/dist/directory-anchor.js +91 -0
  16. package/dist/file-identity.js +12 -0
  17. package/dist/model-command.js +5 -4
  18. package/dist/openai-account-command.js +13 -2
  19. package/dist/process-lease.js +329 -0
  20. package/dist/provider-commands.js +11 -5
  21. package/dist/provider-errors.js +37 -4
  22. package/dist/provider-label.js +13 -0
  23. package/dist/providers/anthropic-stream.js +4 -1
  24. package/dist/providers/anthropic-wire.js +8 -3
  25. package/dist/providers/anthropic.js +39 -19
  26. package/dist/providers/catalog.js +4 -4
  27. package/dist/providers/failure.js +181 -0
  28. package/dist/providers/http.js +82 -23
  29. package/dist/providers/ollama-stream.js +5 -1
  30. package/dist/providers/ollama.js +31 -19
  31. package/dist/providers/openai-codex.js +67 -41
  32. package/dist/providers/openai-stream.js +26 -2
  33. package/dist/providers/openai.js +51 -24
  34. package/dist/providers/sse.js +52 -8
  35. package/dist/request-identity.js +32 -0
  36. package/dist/sessions/lease.js +132 -49
  37. package/dist/sessions/runtime.js +15 -8
  38. package/dist/sessions/store.js +366 -142
  39. package/dist/settings.js +62 -10
  40. package/dist/stable-directory.js +148 -0
  41. package/dist/store-lock.js +68 -84
  42. package/dist/tools/args.js +2 -2
  43. package/dist/tools/fs.js +124 -102
  44. package/dist/tools/search.js +65 -100
  45. package/dist/tools/text-boundary.js +7 -33
  46. package/dist/tui/app-workflows.js +32 -4
  47. package/dist/tui/components/footer.js +1 -1
  48. package/dist/tui/feedback.js +4 -0
  49. package/dist/tui/session-view.js +7 -2
  50. package/dist/tui/workspace.js +21 -7
  51. package/dist/user-store.js +23 -31
  52. package/package.json +3 -4
  53. package/dist/tools/ripgrep.js +0 -230
@@ -2,6 +2,7 @@
2
2
  import { handleCommand } from "../commands.js";
3
3
  import { runTurn } from "../controller.js";
4
4
  import { resolveContextPolicy } from "../context/capacity.js";
5
+ import { automaticCompactionGate, automaticCompactionKey, } from "../context/automatic.js";
5
6
  import { compactContext } from "../context/compactor.js";
6
7
  import { compactSession } from "../context/manual.js";
7
8
  import { isContextOverflow } from "../context/policy.js";
@@ -10,6 +11,8 @@ import { steeringInbox } from "../steering.js";
10
11
  import { saveTranscript } from "../transcript-export.js";
11
12
  import { recordAuxiliaryUsage, recordRequestInput, recordUsage } from "../usage.js";
12
13
  import { selectTimeline } from "../timeline.js";
14
+ import { toolSpecs } from "../tools/index.js";
15
+ import { resetRequestIdentity, requestIdentityForSession } from "../request-identity.js";
13
16
  import { transition } from "./activity.js";
14
17
  import { answerAt } from "./approve.js";
15
18
  import * as edit from "./editor.js";
@@ -20,6 +23,7 @@ const WAITING = "Waiting";
20
23
  export function appWorkflows(options) {
21
24
  const { session, state, permissions, feedback } = options;
22
25
  let activeSteering;
26
+ const automaticCompaction = automaticCompactionGate();
23
27
  const choose = (picker) => new Promise((resolve) => {
24
28
  state.open = { picker, settle: resolve };
25
29
  options.render();
@@ -52,6 +56,8 @@ export function appWorkflows(options) {
52
56
  },
53
57
  reset: async () => {
54
58
  await session.persistence?.reset();
59
+ resetRequestIdentity(session);
60
+ automaticCompaction.reset();
55
61
  state.blocks.splice(0);
56
62
  state.past.length = 0;
57
63
  permissions.reset();
@@ -80,13 +86,16 @@ export function appWorkflows(options) {
80
86
  if (session.conversation.activeNodeId !== state.committedNodeId) {
81
87
  return "branch-pending";
82
88
  }
83
- return compactSession(session, {
89
+ const result = await compactSession(session, {
84
90
  signal: activity.control.signal,
85
91
  onStatus: (said) => {
86
92
  status(said ?? activity.label);
87
93
  options.render();
88
94
  },
89
95
  });
96
+ if (result === "compacted")
97
+ automaticCompaction.reset();
98
+ return result;
90
99
  },
91
100
  });
92
101
  if (outcome === "exit")
@@ -131,6 +140,8 @@ export function appWorkflows(options) {
131
140
  let nodeId;
132
141
  let context;
133
142
  const unpersistedSteering = [];
143
+ const turnTools = permissions.availableTools();
144
+ const specs = toolSpecs(turnTools);
134
145
  let firstPolicy = true;
135
146
  const policy = () => {
136
147
  let visible = firstPolicy;
@@ -202,7 +213,12 @@ export function appWorkflows(options) {
202
213
  (request.error === undefined || !isContextOverflow(request.error))) {
203
214
  return undefined;
204
215
  }
205
- const force = request.reason === "overflow";
216
+ const force = request.reason === "overflow" || request.projectionSaturated;
217
+ const key = automaticCompactionKey(session.provider.id, session.model, nodeId ?? prospectiveNodeId, checkpoint.length);
218
+ const attempt = { key, reason: request.reason };
219
+ if (!automaticCompaction.allows(attempt))
220
+ return undefined;
221
+ let attempted = false;
206
222
  const result = await compactContext({
207
223
  provider: session.provider,
208
224
  model: session.model,
@@ -216,7 +232,14 @@ export function appWorkflows(options) {
216
232
  signal: activity.control.signal,
217
233
  force,
218
234
  policy: request.policy,
235
+ requestEnvelope: {
236
+ system: session.system,
237
+ tools: specs,
238
+ maxOutputTokens: session.config.maxTokens,
239
+ },
240
+ requestIdentity: requestIdentityForSession(session),
219
241
  onBegin: () => {
242
+ attempted = true;
220
243
  status("Compacting");
221
244
  options.render();
222
245
  },
@@ -225,8 +248,12 @@ export function appWorkflows(options) {
225
248
  options.render();
226
249
  },
227
250
  });
228
- if (result === undefined)
251
+ if (result === undefined) {
252
+ if (attempted && !activity.control.signal.aborted)
253
+ automaticCompaction.failed(attempt);
229
254
  return undefined;
255
+ }
256
+ automaticCompaction.succeeded(attempt);
230
257
  context = result.anchor;
231
258
  if (result.usage !== undefined)
232
259
  recordAuxiliaryUsage(session.usage, result.usage);
@@ -244,6 +271,7 @@ export function appWorkflows(options) {
244
271
  reason: "budget",
245
272
  policy: await policy(),
246
273
  inputTokens: session.usage.lastInputTokens,
274
+ projectionSaturated: false,
247
275
  });
248
276
  if (compacted !== undefined)
249
277
  await persist(checkpoint, settlement);
@@ -252,7 +280,7 @@ export function appWorkflows(options) {
252
280
  let finishReason;
253
281
  let failed;
254
282
  try {
255
- await runTurn(history, controllerOptions(session, policy, permissions.availableTools(), inbox), events, activity.control.signal, modelHistory);
283
+ await runTurn(history, controllerOptions(session, policy, turnTools, inbox), events, activity.control.signal, modelHistory);
256
284
  }
257
285
  catch (error) {
258
286
  const interrupted = activity.control.signal.aborted;
@@ -10,7 +10,7 @@ export function renderFooter(info, status, width, pal) {
10
10
  function identity(info, cols) {
11
11
  if (cols <= 0)
12
12
  return "";
13
- const core = `${info.model || "no model"} · ${info.effort}`;
13
+ const core = `${info.provider || "Provider"} · ${info.model || "no model"} · ${info.effort}`;
14
14
  if (textWidth(core) >= cols || info.workspace === "")
15
15
  return elide(core, cols);
16
16
  const divider = " · ";
@@ -1,4 +1,5 @@
1
1
  // Operational feedback belongs in the footer, not in the conversation.
2
+ import { CONVERSATION_LIMITS } from "../conversation.js";
2
3
  import { providerLabel } from "../provider-label.js";
3
4
  const INFO_MS = 2_200;
4
5
  const WARN_MS = 4_200;
@@ -53,6 +54,9 @@ export function turnBlocker(session) {
53
54
  if (session.persistence?.failure !== undefined) {
54
55
  return { text: "session could not be saved · /new to retry", tone: "error" };
55
56
  }
57
+ if (session.conversation.nodes.length >= CONVERSATION_LIMITS.nodes) {
58
+ return { text: "conversation reached its session limit · /new to continue", tone: "warn" };
59
+ }
56
60
  const blocked = session.provider.blocked();
57
61
  const auth = session.provider.auth;
58
62
  const expected = auth.kind === "api-key"
@@ -1,6 +1,9 @@
1
1
  // Small projections of the live session used by the shell and footer.
2
2
  import { credentialSource } from "../credentials.js";
3
3
  import { providerFailure } from "../provider-errors.js";
4
+ import { providerRouteLabel } from "../provider-label.js";
5
+ import { providerFailureDetails } from "../providers/failure.js";
6
+ import { requestIdentityForSession } from "../request-identity.js";
4
7
  export function controllerOptions(session, contextPolicy, tools = session.tools, steering) {
5
8
  return {
6
9
  provider: session.provider,
@@ -10,6 +13,7 @@ export function controllerOptions(session, contextPolicy, tools = session.tools,
10
13
  maxTokens: session.config.maxTokens,
11
14
  contextPolicy,
12
15
  effort: session.config.effort,
16
+ requestIdentity: requestIdentityForSession(session),
13
17
  maxModelRequests: session.config.maxModelRequests,
14
18
  toolContext: { root: session.config.root },
15
19
  ...(steering === undefined ? {} : { steering }),
@@ -18,6 +22,7 @@ export function controllerOptions(session, contextPolicy, tools = session.tools,
18
22
  export function footerInfo(session, workspace = session.config.root) {
19
23
  return {
20
24
  workspace,
25
+ provider: providerRouteLabel(session.provider),
21
26
  model: session.model || "no model",
22
27
  effort: session.config.effort,
23
28
  };
@@ -26,8 +31,8 @@ export function footerInfo(session, workspace = session.config.root) {
26
31
  export function turnFailure(session, error, aborted) {
27
32
  if (aborted)
28
33
  return { kind: "notice", text: "[interrupted]", tone: "warn" };
29
- let text = providerFailure(session.provider, error);
30
- if (/\b401\b/.test(text)) {
34
+ let text = providerFailure(session.provider, error, true);
35
+ if (providerFailureDetails(session.provider.id, error).kind === "authentication") {
31
36
  const auth = session.provider.auth;
32
37
  if (auth.kind === "oauth") {
33
38
  text += ` · reconnect ${auth.label} in /providers`;
@@ -1,7 +1,10 @@
1
1
  // Compact workspace identity for the persistent footer.
2
- import { readFile, stat } from "node:fs/promises";
2
+ import { lstat } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import * as path from "node:path";
5
+ import { readBoundedText, stableFileExpectation } from "../bounded-file.js";
6
+ import { assertDirectoryAnchor, captureDirectDirectory, } from "../directory-anchor.js";
7
+ const MAX_GIT_POINTER_BYTES = 4_096;
5
8
  export async function workspaceLabel(root) {
6
9
  const shown = displayRoot(root);
7
10
  const branch = await findBranch(root);
@@ -30,16 +33,24 @@ async function findBranch(root) {
30
33
  async function branchAt(directory) {
31
34
  const marker = path.join(directory, ".git");
32
35
  try {
33
- const info = await stat(marker);
34
- if (info.isDirectory())
35
- return readHead(marker);
36
+ const info = await lstat(marker, { bigint: true });
37
+ if (info.isSymbolicLink())
38
+ return undefined;
39
+ if (info.isDirectory()) {
40
+ const gitDirectory = await captureDirectDirectory(marker, "Git directory");
41
+ return readHead(gitDirectory);
42
+ }
36
43
  if (!info.isFile())
37
44
  return undefined;
38
- const pointer = await readFile(marker, "utf8");
45
+ const pointer = await readBoundedText(marker, MAX_GIT_POINTER_BYTES, {
46
+ label: "Git directory pointer",
47
+ expected: stableFileExpectation(info),
48
+ });
39
49
  const match = /^gitdir:\s*(.+)\s*$/im.exec(pointer);
40
50
  if (match === null)
41
51
  return undefined;
42
- return readHead(path.resolve(directory, match[1]));
52
+ const gitDirectory = await captureDirectDirectory(path.resolve(directory, match[1]), "Git directory");
53
+ return readHead(gitDirectory);
43
54
  }
44
55
  catch {
45
56
  return undefined;
@@ -47,7 +58,10 @@ async function branchAt(directory) {
47
58
  }
48
59
  async function readHead(gitDirectory) {
49
60
  try {
50
- const head = (await readFile(path.join(gitDirectory, "HEAD"), "utf8")).trim();
61
+ const head = (await readBoundedText(path.join(gitDirectory.path, "HEAD"), MAX_GIT_POINTER_BYTES, {
62
+ label: "Git HEAD",
63
+ validate: async () => assertDirectoryAnchor(gitDirectory),
64
+ })).trim();
51
65
  const prefix = "ref: refs/heads/";
52
66
  if (head.startsWith(prefix))
53
67
  return head.slice(prefix.length);
@@ -1,6 +1,7 @@
1
1
  // Bounded synchronous reads for the tiny JSON stores under ~/.jecode.
2
2
  import { Buffer } from "node:buffer";
3
- import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
3
+ import { readBoundedFileSync } from "./bounded-file.js";
4
+ import { assertDirectoryAnchorSync } from "./directory-anchor.js";
4
5
  export const USER_STORE_LIMITS = Object.freeze({
5
6
  settingsBytes: 64 * 1_024,
6
7
  credentialsBytes: 256 * 1_024,
@@ -13,38 +14,29 @@ export const USER_STORE_LIMITS = Object.freeze({
13
14
  accountToken: 32_768,
14
15
  accountLabel: 1_024,
15
16
  });
16
- export function readBoundedJsonSync(file, maxBytes) {
17
- // Non-blocking open keeps a replaced FIFO or device from stalling startup;
18
- // fstat below then accepts only regular files before any content is read.
19
- const flags = process.platform === "win32"
20
- ? "r"
21
- : constants.O_RDONLY | (constants.O_NONBLOCK ?? 0);
22
- const descriptor = openSync(file, flags);
17
+ export function readBoundedJsonSync(file, maxBytes, directory) {
18
+ const validate = directory === undefined
19
+ ? undefined
20
+ : () => assertDirectoryAnchorSync(directory);
21
+ return JSON.parse(readBoundedFileSync(file, maxBytes, {
22
+ label: "user store",
23
+ validate,
24
+ }).toString("utf8"));
25
+ }
26
+ export function readBoundedJsonForMutationSync(file, maxBytes, label, directory) {
23
27
  try {
24
- const details = fstatSync(descriptor);
25
- if (!details.isFile())
26
- throw new Error("user store must be a regular file");
27
- if (!Number.isSafeInteger(details.size) || details.size > maxBytes) {
28
- throw new Error(`user store exceeds ${maxBytes} bytes`);
29
- }
30
- // One extra byte detects an in-place growth after fstat without allowing
31
- // the read allocation to follow the file's new size.
32
- const capacity = Math.min(maxBytes + 1, Math.max(1, details.size + 1));
33
- const bytes = Buffer.allocUnsafe(capacity);
34
- let offset = 0;
35
- while (offset < capacity) {
36
- const count = readSync(descriptor, bytes, offset, capacity - offset, null);
37
- if (count === 0)
38
- break;
39
- offset += count;
40
- }
41
- if (offset > maxBytes || offset > details.size) {
42
- throw new Error("user store changed while it was being read");
43
- }
44
- return JSON.parse(bytes.toString("utf8", 0, offset));
28
+ const validate = directory === undefined
29
+ ? undefined
30
+ : () => assertDirectoryAnchorSync(directory);
31
+ return JSON.parse(readBoundedFileSync(file, maxBytes, {
32
+ label: "user store",
33
+ validate,
34
+ }).toString("utf8"));
45
35
  }
46
- finally {
47
- closeSync(descriptor);
36
+ catch (error) {
37
+ if (error.code === "ENOENT")
38
+ return undefined;
39
+ throw new Error(`${label} is invalid, unsafe, or too large`, { cause: error });
48
40
  }
49
41
  }
50
42
  export function assertStoreText(text, maxBytes) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giovannijecha/jecode",
3
- "version": "0.8.4",
3
+ "version": "0.8.5",
4
4
  "description": "An owned coding agent with zero external runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -59,9 +59,8 @@
59
59
  "check:install": "npm run build:release -- --quiet && node dev/check-installed-cli.ts",
60
60
  "check": "npm run check:source-tree && npm run typecheck && npm run coverage && npm run check:package && npm run check:install"
61
61
  },
62
- "dependencies": {},
63
62
  "devDependencies": {
64
- "@types/node": "^26.4.0",
65
- "typescript": "^5.9.3"
63
+ "@types/node": "^26.4.1",
64
+ "typescript": "^7.0.2"
66
65
  }
67
66
  }
@@ -1,230 +0,0 @@
1
- // Optional native acceleration for bounded literal search.
2
- import { spawn } from "node:child_process";
3
- import * as path from "node:path";
4
- import { shellEnvironment } from "../credential-safety.js";
5
- import { resolveExecutable } from "../executable.js";
6
- const MAX_BATCH_FILES = 256;
7
- const MAX_BATCH_BYTES = 16_000_000;
8
- const MAX_COMMAND_CHARS = 12_000;
9
- const MAX_QUERY_CHARS = 4_000;
10
- const MAX_EVENT_CHARS = 8_000_000;
11
- const MAX_STDERR_CHARS = 16_000;
12
- /** Return undefined when ripgrep is unavailable or cannot preserve the contract. */
13
- export async function trySearchWithRipgrep(options) {
14
- if (options.files.length === 0)
15
- return { matches: [], binaryPaths: [] };
16
- if (options.query.length > MAX_QUERY_CHARS || options.query.includes("\0"))
17
- return undefined;
18
- const executable = resolveExecutable("rg", { rejectUnder: options.root });
19
- if (executable === undefined)
20
- return undefined;
21
- const matches = [];
22
- const binaryPaths = new Set();
23
- try {
24
- for (const batch of batches(options.files, options.query.length)) {
25
- throwIfAborted(options.signal);
26
- const searched = await searchBatch(executable, batch, options);
27
- if (searched === undefined)
28
- return undefined;
29
- for (const match of searched.matches) {
30
- if (matches.length >= options.limit)
31
- break;
32
- matches.push(match);
33
- }
34
- for (const file of searched.binaryPaths)
35
- binaryPaths.add(file);
36
- if (matches.length >= options.limit)
37
- break;
38
- }
39
- }
40
- catch (error) {
41
- if (options.signal?.aborted === true)
42
- throw abortReason(options.signal);
43
- return undefined;
44
- }
45
- return { matches, binaryPaths: [...binaryPaths] };
46
- }
47
- function batches(files, queryChars) {
48
- const groups = [];
49
- let group = [];
50
- let bytes = 0;
51
- let chars = queryChars;
52
- for (const file of files) {
53
- const nextChars = chars + file.path.length + 1;
54
- if (group.length > 0 &&
55
- (group.length >= MAX_BATCH_FILES ||
56
- bytes + file.bytes > MAX_BATCH_BYTES ||
57
- nextChars > MAX_COMMAND_CHARS)) {
58
- groups.push(group);
59
- group = [];
60
- bytes = 0;
61
- chars = queryChars;
62
- }
63
- group.push(file);
64
- bytes += file.bytes;
65
- chars += file.path.length + 1;
66
- }
67
- if (group.length > 0)
68
- groups.push(group);
69
- return groups;
70
- }
71
- function searchBatch(executable, files, options) {
72
- throwIfAborted(options.signal);
73
- const args = [
74
- "--json",
75
- "--no-config",
76
- "--fixed-strings",
77
- "--max-filesize",
78
- "1000000",
79
- "--max-count",
80
- String(options.limit),
81
- ...(options.caseSensitive ? [] : ["--ignore-case"]),
82
- "--",
83
- options.query,
84
- ...files.map((file) => file.path),
85
- ];
86
- return new Promise((resolve, reject) => {
87
- let child;
88
- try {
89
- child = spawn(executable, args, {
90
- cwd: path.dirname(executable),
91
- env: shellEnvironment(),
92
- windowsHide: true,
93
- stdio: ["ignore", "pipe", "pipe"],
94
- });
95
- }
96
- catch {
97
- resolve(undefined);
98
- return;
99
- }
100
- const matches = [];
101
- const binaryPaths = new Set();
102
- let buffered = "";
103
- let stderr = "";
104
- let invalid = false;
105
- let overLimit = false;
106
- let settled = false;
107
- const onAbort = () => child.kill();
108
- options.signal?.addEventListener("abort", onAbort, { once: true });
109
- if (options.signal?.aborted === true)
110
- onAbort();
111
- const finish = (value, error) => {
112
- if (settled)
113
- return;
114
- settled = true;
115
- options.signal?.removeEventListener("abort", onAbort);
116
- if (error !== undefined)
117
- reject(error);
118
- else
119
- resolve(value);
120
- };
121
- const consume = (line) => {
122
- if (line === "" || invalid || overLimit)
123
- return;
124
- try {
125
- const event = JSON.parse(line);
126
- const parsed = ripgrepEvent(event);
127
- if (parsed?.kind === "match") {
128
- // `rg --max-count` is per file, not global. Once the raw stream
129
- // exceeds the requested result count, stop the accelerator and let
130
- // the portable scanner produce the exact bounded answer. Returning
131
- // early here would lose the later binary-file end marker.
132
- if (matches.length >= options.limit) {
133
- overLimit = true;
134
- child.kill();
135
- return;
136
- }
137
- matches.push(parsed.match);
138
- }
139
- if (parsed?.kind === "binary")
140
- binaryPaths.add(parsed.path);
141
- }
142
- catch {
143
- invalid = true;
144
- child.kill();
145
- }
146
- };
147
- child.stdout.setEncoding("utf8");
148
- child.stderr.setEncoding("utf8");
149
- child.stdout.on("data", (chunk) => {
150
- buffered += chunk;
151
- if (buffered.length > MAX_EVENT_CHARS) {
152
- invalid = true;
153
- child.kill();
154
- return;
155
- }
156
- let newline = buffered.indexOf("\n");
157
- while (newline !== -1) {
158
- consume(buffered.slice(0, newline));
159
- buffered = buffered.slice(newline + 1);
160
- newline = buffered.indexOf("\n");
161
- }
162
- });
163
- child.stderr.on("data", (chunk) => {
164
- stderr = `${stderr}${chunk}`.slice(-MAX_STDERR_CHARS);
165
- });
166
- child.on("error", (error) => {
167
- if (options.signal?.aborted === true)
168
- finish(undefined, abortReason(options.signal));
169
- else if (error.code === "ENOENT")
170
- finish(undefined);
171
- else
172
- finish(undefined);
173
- });
174
- child.on("close", (code) => {
175
- if (options.signal?.aborted === true) {
176
- finish(undefined, abortReason(options.signal));
177
- return;
178
- }
179
- consume(buffered);
180
- if (invalid || overLimit || (code !== 0 && code !== 1) || stderr.trim() !== "") {
181
- finish(undefined);
182
- return;
183
- }
184
- const order = new Map(files.map((file, index) => [file.path, index]));
185
- finish({
186
- matches: matches
187
- .filter((match) => !binaryPaths.has(match.path))
188
- .sort((a, b) => ((order.get(a.path) ?? Number.MAX_SAFE_INTEGER) -
189
- (order.get(b.path) ?? Number.MAX_SAFE_INTEGER) ||
190
- a.line - b.line)),
191
- binaryPaths: [...binaryPaths],
192
- });
193
- });
194
- });
195
- }
196
- function ripgrepEvent(value) {
197
- if (!record(value) || !record(value["data"]))
198
- return undefined;
199
- const data = value["data"];
200
- const file = textField(data["path"]);
201
- if (file === undefined)
202
- return undefined;
203
- if (value["type"] === "match") {
204
- const line = data["line_number"];
205
- const text = textField(data["lines"]);
206
- if (typeof line !== "number" || !Number.isInteger(line) || text === undefined)
207
- return undefined;
208
- return {
209
- kind: "match",
210
- match: { path: file, line, text: text.replace(/\r?\n$/, "") },
211
- };
212
- }
213
- if (value["type"] === "end" && typeof data["binary_offset"] === "number") {
214
- return { kind: "binary", path: file };
215
- }
216
- return undefined;
217
- }
218
- function textField(value) {
219
- return record(value) && typeof value["text"] === "string" ? value["text"] : undefined;
220
- }
221
- function record(value) {
222
- return typeof value === "object" && value !== null && !Array.isArray(value);
223
- }
224
- function throwIfAborted(signal) {
225
- if (signal?.aborted === true)
226
- throw abortReason(signal);
227
- }
228
- function abortReason(signal) {
229
- return signal.reason instanceof Error ? signal.reason : new Error("interrupted");
230
- }