@giovannijecha/jecode 0.2.2 → 0.2.4
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/dist/accounts.js +12 -2
- package/dist/controller.js +12 -0
- package/dist/credentials.js +19 -4
- package/dist/prompt.js +0 -2
- package/dist/providers/ollama-stream.js +8 -4
- package/dist/providers/ollama-wire.js +19 -3
- package/dist/tools/fs.js +32 -9
- package/package.json +1 -1
package/dist/accounts.js
CHANGED
|
@@ -11,8 +11,18 @@ export function openAICodexAccount() {
|
|
|
11
11
|
return account === undefined ? undefined : { ...account };
|
|
12
12
|
}
|
|
13
13
|
export function accountValues() {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Keep both snapshots. Another Jecode process may rotate the refresh token
|
|
15
|
+
// after this process populated its account cache; shell-output redaction
|
|
16
|
+
// must recognize the newly persisted values without forgetting the old ones.
|
|
17
|
+
const values = new Set();
|
|
18
|
+
for (const source of [store(), readStore(accountsPath())]) {
|
|
19
|
+
const account = source.accounts["openai-codex"];
|
|
20
|
+
if (account === undefined)
|
|
21
|
+
continue;
|
|
22
|
+
values.add(account.accessToken);
|
|
23
|
+
values.add(account.refreshToken);
|
|
24
|
+
}
|
|
25
|
+
return [...values];
|
|
16
26
|
}
|
|
17
27
|
export function accountsPath() {
|
|
18
28
|
return userDataPath("accounts.json");
|
package/dist/controller.js
CHANGED
|
@@ -37,6 +37,7 @@ export async function runTurn(history, options, events, signal) {
|
|
|
37
37
|
if (calls.length > MAX_TOOL_CALLS_PER_STEP) {
|
|
38
38
|
throw new Error(`provider returned ${calls.length} tool calls in one step (maximum ${MAX_TOOL_CALLS_PER_STEP})`);
|
|
39
39
|
}
|
|
40
|
+
assertToolCallIds(calls);
|
|
40
41
|
history.push(assistant);
|
|
41
42
|
if (calls.length === 0) {
|
|
42
43
|
if (assistant.usage !== undefined)
|
|
@@ -95,6 +96,17 @@ export async function runTurn(history, options, events, signal) {
|
|
|
95
96
|
}
|
|
96
97
|
throw new Error(`gave up after ${options.maxSteps} steps without finishing (raise --max-steps)`);
|
|
97
98
|
}
|
|
99
|
+
function assertToolCallIds(calls) {
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (const call of calls) {
|
|
102
|
+
if (call.id.trim() === "")
|
|
103
|
+
throw new Error("provider returned a tool call without an id");
|
|
104
|
+
if (seen.has(call.id)) {
|
|
105
|
+
throw new Error("provider returned duplicate tool call ids in one step");
|
|
106
|
+
}
|
|
107
|
+
seen.add(call.id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
98
110
|
async function settle(call, options, events, signal, preview) {
|
|
99
111
|
const tool = findTool(options.tools, call.name);
|
|
100
112
|
if (tool === undefined) {
|
package/dist/credentials.js
CHANGED
|
@@ -31,8 +31,20 @@ export function credentialSource(name) {
|
|
|
31
31
|
/** Values that must never survive in shell output, regardless of their source. */
|
|
32
32
|
export function credentialValues() {
|
|
33
33
|
const stored = fromDisk();
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// A second process can replace the saved store after this session cached it.
|
|
35
|
+
// Redact both snapshots so neither a stale nor a newly persisted key can
|
|
36
|
+
// cross the shell boundary.
|
|
37
|
+
const current = readSavedStore();
|
|
38
|
+
const names = new Set([
|
|
39
|
+
...held.keys(),
|
|
40
|
+
...Object.keys(stored),
|
|
41
|
+
...Object.keys(current),
|
|
42
|
+
]);
|
|
43
|
+
const values = new Set([
|
|
44
|
+
...held.values(),
|
|
45
|
+
...Object.values(stored),
|
|
46
|
+
...Object.values(current),
|
|
47
|
+
]);
|
|
36
48
|
for (const name of names) {
|
|
37
49
|
const environment = use(process.env[name]);
|
|
38
50
|
if (environment !== undefined)
|
|
@@ -94,10 +106,13 @@ export function reload() {
|
|
|
94
106
|
function fromDisk() {
|
|
95
107
|
if (saved !== undefined)
|
|
96
108
|
return saved;
|
|
109
|
+
saved = readSavedStore();
|
|
110
|
+
return saved;
|
|
111
|
+
}
|
|
112
|
+
function readSavedStore() {
|
|
97
113
|
const current = readStore(storePath());
|
|
98
114
|
const legacy = legacyUserDataPath("credentials.json");
|
|
99
|
-
|
|
100
|
-
return saved;
|
|
115
|
+
return current ?? (legacy === undefined ? undefined : readStore(legacy)) ?? {};
|
|
101
116
|
}
|
|
102
117
|
function readStore(file) {
|
|
103
118
|
try {
|
package/dist/prompt.js
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
// gets rewritten far more often than the loop that carries it.
|
|
3
3
|
export function systemPrompt(config) {
|
|
4
4
|
return [
|
|
5
|
-
"You are jecode, a coding agent working in a terminal alongside the user.",
|
|
6
|
-
"",
|
|
7
5
|
`Workspace root: ${config.root}`,
|
|
8
6
|
`Platform: ${process.platform}`,
|
|
9
7
|
"",
|
|
@@ -8,6 +8,7 @@ export async function assembleOllama(events, onStream) {
|
|
|
8
8
|
const calls = new Map();
|
|
9
9
|
let toolArgumentChars = 0;
|
|
10
10
|
let content = "";
|
|
11
|
+
let reasoning = "";
|
|
11
12
|
let finishReason;
|
|
12
13
|
let usage;
|
|
13
14
|
for await (const raw of events) {
|
|
@@ -28,9 +29,12 @@ export async function assembleOllama(events, onStream) {
|
|
|
28
29
|
continue;
|
|
29
30
|
// Reasoning has no standardized field name across the models Ollama
|
|
30
31
|
// serves, so both spellings in circulation are accepted.
|
|
31
|
-
const
|
|
32
|
-
if (typeof
|
|
33
|
-
|
|
32
|
+
const reasoningDelta = delta.reasoning ?? delta.reasoning_content;
|
|
33
|
+
if (typeof reasoningDelta === "string" && reasoningDelta !== "") {
|
|
34
|
+
// Keep the authoritative copy as well as the display event. Ollama
|
|
35
|
+
// expects reasoning to accompany an assistant tool call on continuation.
|
|
36
|
+
reasoning += reasoningDelta;
|
|
37
|
+
onStream?.({ kind: "thinking", text: reasoningDelta });
|
|
34
38
|
}
|
|
35
39
|
if (typeof delta.content === "string" && delta.content !== "") {
|
|
36
40
|
content += delta.content;
|
|
@@ -59,5 +63,5 @@ export async function assembleOllama(events, onStream) {
|
|
|
59
63
|
// Not every server sends an id, and the loop needs one to pair the result
|
|
60
64
|
// back to its call.
|
|
61
65
|
.map(([index, call]) => (call.id === "" ? { ...call, id: `call_${index}` } : call));
|
|
62
|
-
return { content, toolCalls, finishReason, usage };
|
|
66
|
+
return { content, reasoning, toolCalls, finishReason, usage };
|
|
63
67
|
}
|
|
@@ -40,6 +40,9 @@ export function toWireMessages(system, messages) {
|
|
|
40
40
|
if (texts.length === 0 && toolCalls.length === 0)
|
|
41
41
|
continue;
|
|
42
42
|
const turn = { role: "assistant", content: texts.join("\n") };
|
|
43
|
+
const reasoning = ollamaReasoning(message);
|
|
44
|
+
if (reasoning !== undefined)
|
|
45
|
+
turn["reasoning"] = reasoning;
|
|
43
46
|
// An empty tool_calls array is not the same as no tool_calls to every
|
|
44
47
|
// server, so the key is omitted rather than sent empty.
|
|
45
48
|
if (toolCalls.length > 0)
|
|
@@ -59,9 +62,22 @@ export function fromWireReply(reply) {
|
|
|
59
62
|
for (const call of reply.toolCalls) {
|
|
60
63
|
content.push({ kind: "tool_call", id: call.id, name: call.name, input: parseArgs(call.args) });
|
|
61
64
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
const raw = reply.reasoning === "" ? undefined : { reasoning: reply.reasoning };
|
|
66
|
+
return {
|
|
67
|
+
role: "assistant",
|
|
68
|
+
content,
|
|
69
|
+
...(raw === undefined ? {} : { raw, rawFrom: "ollama" }),
|
|
70
|
+
usage: normalizeUsage(reply),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function ollamaReasoning(message) {
|
|
74
|
+
if (message.rawFrom !== "ollama" ||
|
|
75
|
+
typeof message.raw !== "object" ||
|
|
76
|
+
message.raw === null ||
|
|
77
|
+
Array.isArray(message.raw))
|
|
78
|
+
return undefined;
|
|
79
|
+
const reasoning = message.raw["reasoning"];
|
|
80
|
+
return typeof reasoning === "string" && reasoning !== "" ? reasoning : undefined;
|
|
65
81
|
}
|
|
66
82
|
function normalizeUsage(reply) {
|
|
67
83
|
if (reply.usage === undefined)
|
package/dist/tools/fs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Filesystem tools: read, list, write, edit.
|
|
2
|
-
import {
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { optionalBool, optionalInt, requireString } from "./args.js";
|
|
@@ -9,9 +9,10 @@ import { atomicWrite } from "../atomic.js";
|
|
|
9
9
|
const MAX_READ_CHARS = 60_000;
|
|
10
10
|
const MAX_LIST_CHARS = 60_000;
|
|
11
11
|
const MAX_LIST_ENTRIES = 2_000;
|
|
12
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
12
13
|
export const readFile = {
|
|
13
14
|
name: "read_file",
|
|
14
|
-
description: "Read a UTF-8 text file inside the workspace. Optionally start at a line " +
|
|
15
|
+
description: "Read a regular UTF-8 text file inside the workspace. Optionally start at a line " +
|
|
15
16
|
"(1-based) and cap how many lines come back. Large files are truncated.",
|
|
16
17
|
dangerous: false,
|
|
17
18
|
input: {
|
|
@@ -28,7 +29,7 @@ export const readFile = {
|
|
|
28
29
|
const target = await resolveExistingInRoot(root, requireString(args, "path"));
|
|
29
30
|
const offset = optionalInt(args, "offset");
|
|
30
31
|
const limit = optionalInt(args, "limit");
|
|
31
|
-
const { text, truncated } = await readRange(target, offset, limit);
|
|
32
|
+
const { text, truncated } = await readRange(target, offset, limit, ctx.signal);
|
|
32
33
|
if (truncated) {
|
|
33
34
|
return {
|
|
34
35
|
output: `${text}\n\n[truncated at ${MAX_READ_CHARS} characters — read a narrower range]`,
|
|
@@ -169,13 +170,22 @@ export const editFile = {
|
|
|
169
170
|
};
|
|
170
171
|
},
|
|
171
172
|
};
|
|
172
|
-
async function readRange(target, offset, limit) {
|
|
173
|
+
async function readRange(target, offset, limit, signal) {
|
|
174
|
+
throwIfAborted(signal);
|
|
173
175
|
const firstLine = Math.max(1, offset ?? 1);
|
|
174
176
|
const lineCount = limit === undefined ? undefined : Math.max(0, limit);
|
|
175
177
|
const endLine = lineCount === undefined ? Number.POSITIVE_INFINITY : firstLine + lineCount;
|
|
176
178
|
if (lineCount === 0)
|
|
177
179
|
return { text: "", truncated: false };
|
|
178
|
-
|
|
180
|
+
if (!(await fs.lstat(target)).isFile())
|
|
181
|
+
throw new Error("path must be a regular file");
|
|
182
|
+
// O_NONBLOCK prevents a path swapped to a FIFO between lstat and open from
|
|
183
|
+
// waiting forever for a writer. The handle stat closes that race before any
|
|
184
|
+
// content is accepted.
|
|
185
|
+
const flags = process.platform === "win32"
|
|
186
|
+
? "r"
|
|
187
|
+
: constants.O_RDONLY | (constants.O_NONBLOCK ?? 0);
|
|
188
|
+
const handle = await fs.open(target, flags);
|
|
179
189
|
const decoder = new TextDecoder();
|
|
180
190
|
let text = "";
|
|
181
191
|
let line = 1;
|
|
@@ -216,19 +226,32 @@ async function readRange(target, offset, limit) {
|
|
|
216
226
|
}
|
|
217
227
|
};
|
|
218
228
|
try {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
229
|
+
if (!(await handle.stat()).isFile())
|
|
230
|
+
throw new Error("path must be a regular file");
|
|
231
|
+
let position = 0;
|
|
232
|
+
const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
|
|
233
|
+
while (!stopped) {
|
|
234
|
+
throwIfAborted(signal);
|
|
235
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position);
|
|
236
|
+
throwIfAborted(signal);
|
|
237
|
+
if (bytesRead === 0)
|
|
222
238
|
break;
|
|
239
|
+
position += bytesRead;
|
|
240
|
+
consume(decoder.decode(buffer.subarray(0, bytesRead), { stream: true }));
|
|
223
241
|
}
|
|
224
242
|
if (!stopped)
|
|
225
243
|
consume(decoder.decode());
|
|
226
244
|
}
|
|
227
245
|
finally {
|
|
228
|
-
|
|
246
|
+
await handle.close();
|
|
229
247
|
}
|
|
230
248
|
return { text, truncated };
|
|
231
249
|
}
|
|
250
|
+
function throwIfAborted(signal) {
|
|
251
|
+
if (signal?.aborted !== true)
|
|
252
|
+
return;
|
|
253
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("interrupted");
|
|
254
|
+
}
|
|
232
255
|
/**
|
|
233
256
|
* The edit worked out against a given text — the one place its rules live.
|
|
234
257
|
*
|