@giovannijecha/jecode 0.8.4 → 0.8.6
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 +28 -9
- package/dist/accounts.js +47 -10
- package/dist/atomic.js +24 -14
- package/dist/batch-view.js +27 -2
- package/dist/batch.js +39 -4
- package/dist/bounded-file.js +212 -0
- package/dist/cli-info.js +0 -1
- package/dist/commands.js +2 -0
- package/dist/config.js +16 -7
- package/dist/context/automatic.js +35 -0
- package/dist/context/compactor.js +32 -2
- package/dist/context/manual.js +20 -3
- package/dist/context/request-projection.js +130 -0
- package/dist/controller-request.js +33 -11
- package/dist/credential-commands.js +25 -9
- package/dist/credentials.js +56 -18
- package/dist/directory-anchor.js +91 -0
- package/dist/file-identity.js +12 -0
- package/dist/model-command.js +5 -4
- package/dist/openai-account-command.js +23 -10
- package/dist/openai-account.js +7 -5
- package/dist/openai-oauth-callback.js +13 -4
- package/dist/openai-oauth-tokens.js +9 -7
- package/dist/openai-oauth.js +8 -6
- package/dist/permission-command.js +1 -1
- package/dist/process-lease.js +329 -0
- package/dist/provider-commands.js +11 -37
- package/dist/provider-errors.js +31 -7
- package/dist/provider-label.js +9 -3
- package/dist/providers/anthropic-stream.js +4 -1
- package/dist/providers/anthropic-wire.js +8 -3
- package/dist/providers/anthropic.js +39 -20
- package/dist/providers/catalog.js +4 -4
- package/dist/providers/failure.js +181 -0
- package/dist/providers/http.js +82 -23
- package/dist/providers/index.js +1 -5
- package/dist/providers/ollama-context.js +42 -0
- package/dist/providers/ollama-endpoint.js +7 -34
- package/dist/providers/ollama-stream.js +5 -1
- package/dist/providers/ollama.js +36 -158
- package/dist/providers/openai-codex.js +71 -45
- package/dist/providers/openai-stream.js +26 -2
- package/dist/providers/openai.js +51 -25
- package/dist/providers/sse.js +52 -8
- package/dist/request-identity.js +32 -0
- package/dist/sessions/bucket.js +55 -0
- package/dist/sessions/catalog-io.js +162 -0
- package/dist/sessions/catalog.js +3 -1
- package/dist/sessions/codec-messages.js +122 -0
- package/dist/sessions/codec-transcript.js +93 -0
- package/dist/sessions/codec-values.js +52 -0
- package/dist/sessions/codec.js +4 -257
- package/dist/sessions/files.js +158 -0
- package/dist/sessions/lease.js +132 -49
- package/dist/sessions/load.js +90 -0
- package/dist/sessions/runtime.js +15 -8
- package/dist/sessions/snapshot.js +33 -0
- package/dist/sessions/store.js +183 -378
- package/dist/settings-command.js +10 -5
- package/dist/settings.js +75 -22
- package/dist/stable-directory.js +148 -0
- package/dist/start.js +1 -2
- package/dist/store-lock.js +68 -84
- package/dist/tools/args.js +2 -2
- package/dist/tools/file-read.js +192 -0
- package/dist/tools/file-summary.js +9 -0
- package/dist/tools/{fs.js → file-write.js} +5 -171
- package/dist/tools/glob.js +107 -0
- package/dist/tools/index.js +2 -1
- package/dist/tools/search.js +66 -205
- package/dist/tools/text-boundary.js +7 -33
- package/dist/tui/app-workflows.js +9 -334
- package/dist/tui/approve.js +5 -3
- package/dist/tui/blocks.js +8 -7
- package/dist/tui/command-workflow.js +106 -0
- package/dist/tui/components/command-menu.js +7 -10
- package/dist/tui/components/footer.js +1 -1
- package/dist/tui/components/menu.js +74 -43
- package/dist/tui/components/messages.js +16 -9
- package/dist/tui/components/tool-evidence.js +107 -0
- package/dist/tui/components/tool-motion.js +32 -0
- package/dist/tui/components/tool.js +48 -202
- package/dist/tui/feedback.js +4 -0
- package/dist/tui/help.js +1 -1
- package/dist/tui/picker-layout.js +40 -0
- package/dist/tui/picker.js +7 -71
- package/dist/tui/session-view.js +7 -2
- package/dist/tui/tool-details.js +135 -0
- package/dist/tui/transcript-grammar.js +8 -1
- package/dist/tui/transcript-view.js +26 -112
- package/dist/tui/turn-workflow.js +264 -0
- package/dist/tui/turn.js +7 -140
- package/dist/tui/workflow-types.js +2 -0
- package/dist/tui/workspace.js +21 -7
- package/dist/user-store.js +23 -31
- package/package.json +14 -15
- package/dist/ollama-settings-command.js +0 -74
- package/dist/tools/ripgrep.js +0 -230
- package/dist/tui/motion.js +0 -32
package/dist/tui/turn.js
CHANGED
|
@@ -3,17 +3,14 @@
|
|
|
3
3
|
// The controller speaks in stream events and tool results; the screen speaks in
|
|
4
4
|
// blocks. This is the whole of the translation, kept out of the shell so that
|
|
5
5
|
// neither has to know how the other is built.
|
|
6
|
-
import { graphemes } from "../text-boundary.js";
|
|
7
|
-
import { condense, diff } from "../ui/diff.js";
|
|
8
6
|
import { promptFor } from "./approve.js";
|
|
7
|
+
import { toolTarget, previewDetails, producedDetails, outputDetails } from "./tool-details.js";
|
|
9
8
|
// Semantic activity labels feed the footer's compact state and timer while
|
|
10
9
|
// reasoning and tools keep the detailed work visible in the transcript.
|
|
11
10
|
const WAITING = "Waiting";
|
|
12
11
|
const THINKING = "Thinking";
|
|
13
12
|
const RESPONDING = "Responding";
|
|
14
13
|
const ASKING = "Waiting for you";
|
|
15
|
-
/** Unchanged rows kept either side of a change. */
|
|
16
|
-
const CONTEXT = 2;
|
|
17
14
|
export function transcribe(stage) {
|
|
18
15
|
// The block the stream is currently filling. A change of kind starts a new
|
|
19
16
|
// one, which is what keeps reasoning and answer from running together.
|
|
@@ -104,10 +101,10 @@ export function transcribe(stage) {
|
|
|
104
101
|
const block = {
|
|
105
102
|
kind: "tool",
|
|
106
103
|
name: call.name,
|
|
107
|
-
target:
|
|
104
|
+
target: toolTarget(call.input),
|
|
108
105
|
right: "ready",
|
|
109
106
|
tone: "pending",
|
|
110
|
-
body:
|
|
107
|
+
body: previewDetails(call, look),
|
|
111
108
|
};
|
|
112
109
|
tools.set(call.id, block);
|
|
113
110
|
stage.emit(block);
|
|
@@ -117,7 +114,7 @@ export function transcribe(stage) {
|
|
|
117
114
|
const block = tools.get(call.id);
|
|
118
115
|
if (block === undefined || block.kind !== "tool" || block.tone !== "pending")
|
|
119
116
|
return;
|
|
120
|
-
block.body =
|
|
117
|
+
block.body = outputDetails(output, "out");
|
|
121
118
|
stage.render(block);
|
|
122
119
|
},
|
|
123
120
|
onToolResult(call, result, summary) {
|
|
@@ -138,8 +135,8 @@ export function transcribe(stage) {
|
|
|
138
135
|
// A failure replaces the preview: what the call was going to do stops
|
|
139
136
|
// being the interesting part the moment it did not do it.
|
|
140
137
|
const outcome = result.isError
|
|
141
|
-
?
|
|
142
|
-
:
|
|
138
|
+
? outputDetails(result.output, "out")
|
|
139
|
+
: producedDetails(call, result.output);
|
|
143
140
|
if (outcome !== undefined)
|
|
144
141
|
block.body = outcome;
|
|
145
142
|
stage.render(block);
|
|
@@ -158,7 +155,7 @@ export function transcribe(stage) {
|
|
|
158
155
|
stage.render(block);
|
|
159
156
|
}
|
|
160
157
|
return new Promise((resolve) => {
|
|
161
|
-
stage.ask(promptFor(call,
|
|
158
|
+
stage.ask(promptFor(call, toolTarget(call.input), stage.palette), (answer) => {
|
|
162
159
|
if (answer === "always")
|
|
163
160
|
stage.remember(call);
|
|
164
161
|
const approved = answer !== "no";
|
|
@@ -204,133 +201,3 @@ function settleDuration(block) {
|
|
|
204
201
|
}
|
|
205
202
|
block.startedAt = undefined;
|
|
206
203
|
}
|
|
207
|
-
/** The argument worth showing: the thing the call acts on. */
|
|
208
|
-
function target(input) {
|
|
209
|
-
const { path, command } = input;
|
|
210
|
-
if (typeof command === "string")
|
|
211
|
-
return command;
|
|
212
|
-
if (typeof path === "string")
|
|
213
|
-
return path;
|
|
214
|
-
const rest = JSON.stringify(input);
|
|
215
|
-
return rest === "{}" ? "" : rest;
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* What the call is about to do, drawn before it is allowed to do it.
|
|
219
|
-
*
|
|
220
|
-
* The tool is asked first, because only it knows what is already on disk: a
|
|
221
|
-
* write against an existing file is a replacement, and showing it as a page of
|
|
222
|
-
* additions hides exactly the part worth approving. The fallback diffs what is
|
|
223
|
-
* in the arguments, which is all there is when a tool has nothing to say.
|
|
224
|
-
*/
|
|
225
|
-
function preview(call, look) {
|
|
226
|
-
if (look !== undefined)
|
|
227
|
-
return changes(look.before, look.after);
|
|
228
|
-
const input = call.input;
|
|
229
|
-
if (call.name === "write_file" && typeof input.content === "string") {
|
|
230
|
-
return changes("", input.content);
|
|
231
|
-
}
|
|
232
|
-
if (call.name === "edit_file" && typeof input.old_text === "string") {
|
|
233
|
-
return changes(input.old_text, typeof input.new_text === "string" ? input.new_text : "");
|
|
234
|
-
}
|
|
235
|
-
return undefined;
|
|
236
|
-
}
|
|
237
|
-
/** Two texts as the rows of their difference, unchanged runs summed up. */
|
|
238
|
-
function changes(before, after) {
|
|
239
|
-
let oldLine = 1;
|
|
240
|
-
let newLine = 1;
|
|
241
|
-
const rows = [];
|
|
242
|
-
for (const changed of condense(diff(before, after), CONTEXT)) {
|
|
243
|
-
if (changed.kind === "gap") {
|
|
244
|
-
rows.push({ kind: "gap", text: `… ${changed.skipped} unchanged` });
|
|
245
|
-
oldLine += changed.skipped;
|
|
246
|
-
newLine += changed.skipped;
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
if (changed.kind === "keep") {
|
|
250
|
-
rows.push({ kind: "keep", text: tabs(changed.text), oldLine, newLine });
|
|
251
|
-
oldLine++;
|
|
252
|
-
newLine++;
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
if (changed.kind === "del") {
|
|
256
|
-
rows.push({ kind: "del", text: tabs(changed.text), oldLine });
|
|
257
|
-
oldLine++;
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
rows.push({ kind: "add", text: tabs(changed.text), newLine });
|
|
261
|
-
newLine++;
|
|
262
|
-
}
|
|
263
|
-
emphasizePairs(rows);
|
|
264
|
-
return rows.length === 0 ? undefined : rows;
|
|
265
|
-
}
|
|
266
|
-
function emphasizePairs(rows) {
|
|
267
|
-
for (let index = 0; index < rows.length - 1; index++) {
|
|
268
|
-
const removed = rows[index];
|
|
269
|
-
const added = rows[index + 1];
|
|
270
|
-
if (removed?.kind !== "del" || added?.kind !== "add")
|
|
271
|
-
continue;
|
|
272
|
-
if (rows[index - 1]?.kind === "del" || rows[index + 2]?.kind === "add")
|
|
273
|
-
continue;
|
|
274
|
-
const removedClusters = graphemes(removed.text);
|
|
275
|
-
const addedClusters = graphemes(added.text);
|
|
276
|
-
let prefix = 0;
|
|
277
|
-
let start = 0;
|
|
278
|
-
while (prefix < removedClusters.length &&
|
|
279
|
-
prefix < addedClusters.length &&
|
|
280
|
-
removedClusters[prefix] === addedClusters[prefix]) {
|
|
281
|
-
start += removedClusters[prefix].length;
|
|
282
|
-
prefix++;
|
|
283
|
-
}
|
|
284
|
-
let suffix = 0;
|
|
285
|
-
let removedSuffix = 0;
|
|
286
|
-
let addedSuffix = 0;
|
|
287
|
-
while (suffix < removedClusters.length - prefix &&
|
|
288
|
-
suffix < addedClusters.length - prefix &&
|
|
289
|
-
removedClusters[removedClusters.length - 1 - suffix] ===
|
|
290
|
-
addedClusters[addedClusters.length - 1 - suffix]) {
|
|
291
|
-
removedSuffix += removedClusters[removedClusters.length - 1 - suffix].length;
|
|
292
|
-
addedSuffix += addedClusters[addedClusters.length - 1 - suffix].length;
|
|
293
|
-
suffix++;
|
|
294
|
-
}
|
|
295
|
-
while (suffix > 0 &&
|
|
296
|
-
(!wordBoundary(removed.text, removedSuffix) || !wordBoundary(added.text, addedSuffix))) {
|
|
297
|
-
removedSuffix -= removedClusters[removedClusters.length - suffix].length;
|
|
298
|
-
addedSuffix -= addedClusters[addedClusters.length - suffix].length;
|
|
299
|
-
suffix--;
|
|
300
|
-
}
|
|
301
|
-
const removedLength = removed.text.length - start - removedSuffix;
|
|
302
|
-
const addedLength = added.text.length - start - addedSuffix;
|
|
303
|
-
if (removedLength > 0)
|
|
304
|
-
removed.emphasis = { start, length: removedLength };
|
|
305
|
-
if (addedLength > 0)
|
|
306
|
-
added.emphasis = { start, length: addedLength };
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
function wordBoundary(text, suffix) {
|
|
310
|
-
const at = text.length - suffix;
|
|
311
|
-
if (at <= 0 || at >= text.length)
|
|
312
|
-
return true;
|
|
313
|
-
return /\w/.test(text[at - 1]) !== /\w/.test(text[at]);
|
|
314
|
-
}
|
|
315
|
-
/** What the call left behind, for the calls whose output is worth a look. */
|
|
316
|
-
function produced(call, output) {
|
|
317
|
-
if (call.name === "run_command")
|
|
318
|
-
return details(output, "out");
|
|
319
|
-
// A read or a listing is already summed up on the right of its own row, and
|
|
320
|
-
// the model is about to say what was in it. Printing it twice helps nobody.
|
|
321
|
-
return undefined;
|
|
322
|
-
}
|
|
323
|
-
function details(text, kind) {
|
|
324
|
-
const rows = split(text).map((line) => ({ kind, text: tabs(line) }));
|
|
325
|
-
return rows.length === 0 ? undefined : rows;
|
|
326
|
-
}
|
|
327
|
-
function split(text) {
|
|
328
|
-
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
329
|
-
while (lines.length > 0 && lines[lines.length - 1] === "")
|
|
330
|
-
lines.pop();
|
|
331
|
-
return lines.map(tabs);
|
|
332
|
-
}
|
|
333
|
-
// A tab is a width the terminal decides and the row measurement cannot see.
|
|
334
|
-
function tabs(line) {
|
|
335
|
-
return line.replace(/\t/g, " ");
|
|
336
|
-
}
|
package/dist/tui/workspace.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Compact workspace identity for the persistent footer.
|
|
2
|
-
import {
|
|
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
|
|
34
|
-
if (info.
|
|
35
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
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);
|
package/dist/user-store.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
:
|
|
22
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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.
|
|
3
|
+
"version": "0.8.6",
|
|
4
4
|
"description": "An owned coding agent with zero external runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,27 +41,26 @@
|
|
|
41
41
|
"registry": "https://registry.npmjs.org/"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
-
"build:release": "node
|
|
44
|
+
"build:release": "node scripts/build-release.ts",
|
|
45
45
|
"pack:release": "npm run build:release && npm pack --ignore-scripts",
|
|
46
46
|
"start": "node src/main.ts",
|
|
47
|
-
"tui:lab": "node dev/tui
|
|
48
|
-
"bench:context": "node dev/
|
|
49
|
-
"bench:redaction": "node dev/
|
|
50
|
-
"bench:search": "node dev/
|
|
51
|
-
"bench:session": "node dev/
|
|
52
|
-
"bench:transcript": "node dev/
|
|
47
|
+
"tui:lab": "node dev/tui/main.ts",
|
|
48
|
+
"bench:context": "node dev/benchmarks/context.ts",
|
|
49
|
+
"bench:redaction": "node dev/benchmarks/redaction.ts",
|
|
50
|
+
"bench:search": "node dev/benchmarks/search.ts",
|
|
51
|
+
"bench:session": "node dev/benchmarks/session.ts",
|
|
52
|
+
"bench:transcript": "node dev/benchmarks/transcript.ts",
|
|
53
53
|
"typecheck": "tsc --noEmit",
|
|
54
54
|
"test": "npm run build:release && node --test",
|
|
55
55
|
"coverage": "npm run build:release && node --test --experimental-test-coverage --test-coverage-include=\"src/**/*.ts\" --test-coverage-lines=80 --test-coverage-branches=75 --test-coverage-functions=75",
|
|
56
|
-
"check:source-tree": "node
|
|
57
|
-
"check:release-tag": "node
|
|
58
|
-
"check:package": "npm run build:release -- --quiet && node
|
|
59
|
-
"check:install": "npm run build:release -- --quiet && node
|
|
56
|
+
"check:source-tree": "node scripts/check-source-tree.ts",
|
|
57
|
+
"check:release-tag": "node scripts/check-release-tag.ts",
|
|
58
|
+
"check:package": "npm run build:release -- --quiet && node scripts/check-package.ts",
|
|
59
|
+
"check:install": "npm run build:release -- --quiet && node scripts/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.
|
|
65
|
-
"typescript": "^
|
|
63
|
+
"@types/node": "^26.4.1",
|
|
64
|
+
"typescript": "^7.0.2"
|
|
66
65
|
}
|
|
67
66
|
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// Ollama connection choices inside the persistent settings hub.
|
|
2
|
-
import { askForKey } from "./credential-commands.js";
|
|
3
|
-
import { keyFor } from "./credentials.js";
|
|
4
|
-
import { of } from "./tui/editor.js";
|
|
5
|
-
import { heading } from "./tui/picker.js";
|
|
6
|
-
import { OLLAMA_CLOUD_HOST, OLLAMA_LOCAL_HOST, parseOllamaEndpoint, } from "./providers/ollama-endpoint.js";
|
|
7
|
-
import { configureOllama, ollamaConnection } from "./providers/ollama.js";
|
|
8
|
-
const KEY = "OLLAMA_API_KEY";
|
|
9
|
-
export function ollamaConnectionHint() {
|
|
10
|
-
const connection = ollamaConnection();
|
|
11
|
-
const origin = new URL(connection.baseUrl).host;
|
|
12
|
-
const automatic = connection.inferred ? " · automatic" : "";
|
|
13
|
-
if (connection.kind === "local")
|
|
14
|
-
return `local · this computer${automatic}`;
|
|
15
|
-
return `${connection.kind} · ${origin}${automatic}`;
|
|
16
|
-
}
|
|
17
|
-
export async function ollamaConnectionSetting(session, host, persist) {
|
|
18
|
-
if (host.choose === undefined)
|
|
19
|
-
return;
|
|
20
|
-
const current = ollamaConnection();
|
|
21
|
-
const index = await host.choose({
|
|
22
|
-
title: heading("Ollama connection", "where Ollama requests run", session.palette),
|
|
23
|
-
options: [
|
|
24
|
-
{ label: "cloud", hint: "ollama.com · API key" },
|
|
25
|
-
{ label: "local", hint: "this computer · no API key" },
|
|
26
|
-
{ label: "custom", hint: "HTTPS or loopback endpoint" },
|
|
27
|
-
],
|
|
28
|
-
index: current.kind === "cloud" ? 0 : current.kind === "local" ? 1 : 2,
|
|
29
|
-
});
|
|
30
|
-
if (index === undefined)
|
|
31
|
-
return;
|
|
32
|
-
let nextHost;
|
|
33
|
-
if (index === 0)
|
|
34
|
-
nextHost = OLLAMA_CLOUD_HOST;
|
|
35
|
-
else if (index === 1)
|
|
36
|
-
nextHost = OLLAMA_LOCAL_HOST;
|
|
37
|
-
else {
|
|
38
|
-
const custom = await customEndpoint(session, host, current.kind === "custom" ? current.baseUrl : "https://");
|
|
39
|
-
if (custom === undefined)
|
|
40
|
-
return;
|
|
41
|
-
nextHost = custom;
|
|
42
|
-
}
|
|
43
|
-
const endpoint = parseOllamaEndpoint(nextHost);
|
|
44
|
-
if (!endpoint.loopback && keyFor(KEY) === undefined) {
|
|
45
|
-
const accepted = await askForKey(KEY, host, session.palette);
|
|
46
|
-
if (!accepted)
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (!(await persist({ ollamaHost: endpoint.baseUrl })))
|
|
50
|
-
return;
|
|
51
|
-
configureOllama(endpoint.baseUrl);
|
|
52
|
-
session.config.ollamaHost = endpoint.baseUrl;
|
|
53
|
-
}
|
|
54
|
-
async function customEndpoint(session, host, initial) {
|
|
55
|
-
if (host.type === undefined)
|
|
56
|
-
return undefined;
|
|
57
|
-
const field = {
|
|
58
|
-
title: heading("Ollama endpoint", "HTTPS or exact loopback URL", session.palette),
|
|
59
|
-
right: "enter save · esc back",
|
|
60
|
-
editor: of(initial),
|
|
61
|
-
secret: false,
|
|
62
|
-
note: "Remote endpoints must use HTTPS. API keys are stored separately.",
|
|
63
|
-
};
|
|
64
|
-
const value = await host.type(field);
|
|
65
|
-
if (value === undefined)
|
|
66
|
-
return undefined;
|
|
67
|
-
try {
|
|
68
|
-
return parseOllamaEndpoint(value).baseUrl;
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
host.emit({ kind: "notice", text: error.message, tone: "error" });
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
}
|
package/dist/tools/ripgrep.js
DELETED
|
@@ -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
|
-
}
|