@giovannijecha/jecode 0.8.2 → 0.8.3
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 +19 -278
- package/dist/accounts.js +17 -13
- package/dist/batch.js +54 -6
- package/dist/context/budget.js +13 -1
- package/dist/context/compactor.js +5 -4
- package/dist/context/estimate.js +43 -1
- package/dist/context/manual.js +8 -4
- package/dist/context/policy.js +68 -18
- package/dist/controller-request.js +8 -8
- package/dist/controller.js +6 -1
- package/dist/conversation.js +94 -33
- package/dist/credential-safety.js +56 -9
- package/dist/credentials.js +32 -4
- package/dist/input-boundary.js +80 -0
- package/dist/main.js +4 -1
- package/dist/openai-oauth-callback.js +1 -1
- package/dist/process-shutdown.js +52 -0
- package/dist/providers/anthropic-stream.js +24 -20
- package/dist/providers/anthropic-wire.js +7 -2
- package/dist/providers/ollama-wire.js +7 -15
- package/dist/providers/ollama.js +1 -0
- package/dist/providers/openai-wire.js +2 -16
- package/dist/providers/tool-input.js +17 -0
- package/dist/sessions/codec.js +2 -1
- package/dist/sessions/lease.js +7 -0
- package/dist/sessions/runtime.js +11 -3
- package/dist/sessions/store.js +70 -21
- package/dist/settings.js +10 -5
- package/dist/start.js +12 -2
- package/dist/text-boundary.js +2 -0
- package/dist/tui/app-input.js +40 -5
- package/dist/tui/app-state.js +1 -0
- package/dist/tui/app-workflows.js +1 -0
- package/dist/tui/app.js +11 -3
- package/dist/tui/editor.js +2 -0
- package/dist/tui/keys.js +64 -5
- package/dist/tui/overlay.js +12 -4
- package/dist/tui/picker.js +2 -0
- package/dist/tui/screen.js +5 -17
- package/dist/user-store.js +54 -0
- package/package.json +6 -3
- /package/{docs/assets/brand → assets}/jeco-256.png +0 -0
package/dist/start.js
CHANGED
|
@@ -76,14 +76,24 @@ export async function start(args = process.argv.slice(2), environment = {}) {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
try {
|
|
79
|
-
|
|
79
|
+
if (environment.runInteractive === undefined) {
|
|
80
|
+
await runApp(session, transcriptRoot, { shutdownSignal: environment.signal });
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
await environment.runInteractive(session, transcriptRoot, environment.signal);
|
|
84
|
+
}
|
|
80
85
|
}
|
|
81
86
|
finally {
|
|
82
87
|
await session.persistence?.close();
|
|
83
88
|
}
|
|
84
89
|
}
|
|
85
90
|
else {
|
|
86
|
-
|
|
91
|
+
if (environment.runNonInteractive === undefined) {
|
|
92
|
+
await runBatch(session, { signal: environment.signal });
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
await environment.runNonInteractive(session, environment.signal);
|
|
96
|
+
}
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
99
|
function applyResumedSession(session, conversation, persistence) {
|
package/dist/text-boundary.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Shared grapheme boundaries for every projection of user-visible text.
|
|
2
2
|
const SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
3
|
+
/** Shared persisted-text and user-prompt boundary, measured like String.length. */
|
|
4
|
+
export const MAX_TEXT_CODE_UNITS = 1_048_576;
|
|
3
5
|
export function segmentGraphemes(text) {
|
|
4
6
|
return SEGMENTER.segment(text);
|
|
5
7
|
}
|
package/dist/tui/app-input.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Keyboard and pointer intent for the TUI shell.
|
|
2
|
+
import { PROMPT_LIMIT_MESSAGE, PromptLimitError } from "../input-boundary.js";
|
|
2
3
|
import { activate as activateCompletion, move as moveCompletion, selected as selectedCompletion, } from "./complete.js";
|
|
3
4
|
import * as edit from "./editor.js";
|
|
4
5
|
import { turnBlocker } from "./feedback.js";
|
|
@@ -21,6 +22,13 @@ export function appInput(options) {
|
|
|
21
22
|
}
|
|
22
23
|
if (state.feedback !== undefined)
|
|
23
24
|
feedback.dismiss();
|
|
25
|
+
if (key.name === "input_limit") {
|
|
26
|
+
if (state.open === undefined)
|
|
27
|
+
rejectPrompt();
|
|
28
|
+
else
|
|
29
|
+
showInputLimit();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
24
32
|
// Detail expansion remains available while an approval is open. A large
|
|
25
33
|
// diff may be compacted, but the user must be able to inspect it before
|
|
26
34
|
// answering the permission prompt.
|
|
@@ -33,6 +41,8 @@ export function appInput(options) {
|
|
|
33
41
|
if (state.open !== undefined) {
|
|
34
42
|
const outcome = overlay.handle(state.open, key);
|
|
35
43
|
state.open = outcome.open;
|
|
44
|
+
if (outcome.inputLimit === true)
|
|
45
|
+
showInputLimit();
|
|
36
46
|
if (outcome.abort === true)
|
|
37
47
|
state.activity?.control.abort(new Error("interrupted"));
|
|
38
48
|
if (outcome.quit === true)
|
|
@@ -61,8 +71,10 @@ export function appInput(options) {
|
|
|
61
71
|
case "enter": {
|
|
62
72
|
if (state.completing !== undefined) {
|
|
63
73
|
const completed = selectedCompletion(state.completing);
|
|
64
|
-
if (completed !== undefined)
|
|
74
|
+
if (completed !== undefined) {
|
|
65
75
|
state.editor = edit.of(completed);
|
|
76
|
+
state.promptRejected = false;
|
|
77
|
+
}
|
|
66
78
|
state.completing = undefined;
|
|
67
79
|
}
|
|
68
80
|
submit();
|
|
@@ -75,6 +87,7 @@ export function appInput(options) {
|
|
|
75
87
|
const completed = completion === undefined ? undefined : selectedCompletion(completion);
|
|
76
88
|
if (completed !== undefined) {
|
|
77
89
|
state.editor = edit.of(completed);
|
|
90
|
+
state.promptRejected = false;
|
|
78
91
|
state.completing = undefined;
|
|
79
92
|
}
|
|
80
93
|
return;
|
|
@@ -106,16 +119,26 @@ export function appInput(options) {
|
|
|
106
119
|
options.invalidate();
|
|
107
120
|
return;
|
|
108
121
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
122
|
+
try {
|
|
123
|
+
const edited = applyKey(state.editor, key);
|
|
124
|
+
if (edited !== undefined) {
|
|
125
|
+
if (edited.text !== state.editor.text)
|
|
126
|
+
state.promptRejected = false;
|
|
127
|
+
state.editor = edited;
|
|
128
|
+
state.completing = state.activity === undefined ? activateCompletion(edited.text) : undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (!(error instanceof PromptLimitError))
|
|
133
|
+
throw error;
|
|
134
|
+
rejectPrompt();
|
|
113
135
|
}
|
|
114
136
|
}
|
|
115
137
|
function recall(step) {
|
|
116
138
|
if (state.past.length === 0)
|
|
117
139
|
return;
|
|
118
140
|
state.completing = undefined;
|
|
141
|
+
state.promptRejected = false;
|
|
119
142
|
if (state.recall === -1) {
|
|
120
143
|
if (step > 0)
|
|
121
144
|
return;
|
|
@@ -132,6 +155,10 @@ export function appInput(options) {
|
|
|
132
155
|
state.editor = edit.of(state.past[state.recall]);
|
|
133
156
|
}
|
|
134
157
|
function submit() {
|
|
158
|
+
if (state.promptRejected) {
|
|
159
|
+
keep(PROMPT_LIMIT_MESSAGE);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
135
162
|
const text = state.editor.text.trim();
|
|
136
163
|
if (text === "")
|
|
137
164
|
return;
|
|
@@ -171,6 +198,7 @@ export function appInput(options) {
|
|
|
171
198
|
state.recall = -1;
|
|
172
199
|
state.draft = "";
|
|
173
200
|
state.completing = undefined;
|
|
201
|
+
state.promptRejected = false;
|
|
174
202
|
state.past.push(text);
|
|
175
203
|
state.scroll = 0;
|
|
176
204
|
state.follow = true;
|
|
@@ -179,5 +207,12 @@ export function appInput(options) {
|
|
|
179
207
|
function keep(text) {
|
|
180
208
|
feedback.show({ text: `${text} · prompt kept`, tone: "warn", timeoutMs: 4_000 });
|
|
181
209
|
}
|
|
210
|
+
function rejectPrompt() {
|
|
211
|
+
state.promptRejected = true;
|
|
212
|
+
keep(PROMPT_LIMIT_MESSAGE);
|
|
213
|
+
}
|
|
214
|
+
function showInputLimit() {
|
|
215
|
+
feedback.show({ text: PROMPT_LIMIT_MESSAGE, tone: "warn", timeoutMs: 4_000 });
|
|
216
|
+
}
|
|
182
217
|
return { handle };
|
|
183
218
|
}
|
package/dist/tui/app-state.js
CHANGED
|
@@ -212,6 +212,7 @@ export function appWorkflows(options) {
|
|
|
212
212
|
nodeId: nodeId ?? prospectiveNodeId,
|
|
213
213
|
coveredMessages: context?.messageCount ?? 0,
|
|
214
214
|
lastInputTokens: Math.max(session.usage.lastInputTokens, request.inputTokens),
|
|
215
|
+
estimatedInputTokens: request.inputTokens,
|
|
215
216
|
signal: activity.control.signal,
|
|
216
217
|
force,
|
|
217
218
|
policy: request.policy,
|
package/dist/tui/app.js
CHANGED
|
@@ -42,6 +42,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
42
42
|
let escapeTimer;
|
|
43
43
|
let stopResize = () => { };
|
|
44
44
|
let stopInput = () => { };
|
|
45
|
+
let stopShutdown = () => { };
|
|
45
46
|
let failure;
|
|
46
47
|
let activeWorkflow;
|
|
47
48
|
// Timers outlive the teardown they were scheduled before. Painting after the
|
|
@@ -158,6 +159,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
158
159
|
live = false;
|
|
159
160
|
safely(stopInput);
|
|
160
161
|
safely(stopResize);
|
|
162
|
+
safely(stopShutdown);
|
|
161
163
|
if (activityTimer !== undefined)
|
|
162
164
|
clearInterval(activityTimer);
|
|
163
165
|
if (frameTimer !== undefined)
|
|
@@ -194,15 +196,15 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
194
196
|
activeWorkflow = tracked;
|
|
195
197
|
return tracked;
|
|
196
198
|
}
|
|
197
|
-
function requestQuit() {
|
|
199
|
+
function requestQuit(reason = new Error("interrupted")) {
|
|
200
|
+
state.open = overlay.cancel(state.open);
|
|
198
201
|
const activity = state.activity;
|
|
199
202
|
if (activity === undefined) {
|
|
200
203
|
quit();
|
|
201
204
|
return;
|
|
202
205
|
}
|
|
203
206
|
state.closeWhenIdle = true;
|
|
204
|
-
|
|
205
|
-
activity.control.abort(new Error("interrupted"));
|
|
207
|
+
activity.control.abort(reason);
|
|
206
208
|
}
|
|
207
209
|
function startActivity(kind, label) {
|
|
208
210
|
if (state.activity !== undefined)
|
|
@@ -305,6 +307,12 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
305
307
|
}
|
|
306
308
|
}
|
|
307
309
|
try {
|
|
310
|
+
const shutdownSignal = environment.shutdownSignal;
|
|
311
|
+
const onShutdown = () => requestQuit(shutdownSignal?.reason);
|
|
312
|
+
if (shutdownSignal?.aborted === true)
|
|
313
|
+
return;
|
|
314
|
+
shutdownSignal?.addEventListener("abort", onShutdown, { once: true });
|
|
315
|
+
stopShutdown = () => shutdownSignal?.removeEventListener("abort", onShutdown);
|
|
308
316
|
terminal.enter(session.config.reducedMotion);
|
|
309
317
|
stopResize = terminal.onResize(() => guard(() => {
|
|
310
318
|
paint.invalidate();
|
package/dist/tui/editor.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Pure functions over an immutable state — no rendering, no I/O — so the whole
|
|
4
4
|
// line editor is testable without a terminal.
|
|
5
|
+
import { assertPromptAppend } from "../input-boundary.js";
|
|
5
6
|
import { graphemes } from "../ui/width.js";
|
|
6
7
|
export const EMPTY = { text: "", cursor: 0 };
|
|
7
8
|
export function of(text) {
|
|
8
9
|
return { text, cursor: text.length };
|
|
9
10
|
}
|
|
10
11
|
export function insert(state, chunk) {
|
|
12
|
+
assertPromptAppend(state.text.length, chunk.length);
|
|
11
13
|
return {
|
|
12
14
|
text: state.text.slice(0, state.cursor) + chunk + state.text.slice(state.cursor),
|
|
13
15
|
cursor: state.cursor + chunk.length,
|
package/dist/tui/keys.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// sequence can be split across reads, so an incomplete one is held rather than
|
|
5
5
|
// guessed at; and a bracketed paste arrives as a delimited run that must not be
|
|
6
6
|
// interpreted key by key, or a pasted newline submits half the paste.
|
|
7
|
+
import { MAX_PROMPT_CODE_UNITS } from "../input-boundary.js";
|
|
7
8
|
const ESC = String.fromCharCode(27);
|
|
8
9
|
const BS = String.fromCharCode(8);
|
|
9
10
|
const DEL = String.fromCharCode(127);
|
|
@@ -64,6 +65,17 @@ export function decoder(options = {}) {
|
|
|
64
65
|
let held = "";
|
|
65
66
|
let pasting = false;
|
|
66
67
|
let pasted = "";
|
|
68
|
+
let pasteTooLong = false;
|
|
69
|
+
const appendPaste = (text) => {
|
|
70
|
+
if (pasteTooLong || text === "")
|
|
71
|
+
return;
|
|
72
|
+
if (text.length > MAX_PROMPT_CODE_UNITS - pasted.length) {
|
|
73
|
+
pasted = "";
|
|
74
|
+
pasteTooLong = true;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
pasted += text;
|
|
78
|
+
};
|
|
67
79
|
const drain = (final) => {
|
|
68
80
|
const keys = [];
|
|
69
81
|
while (held !== "") {
|
|
@@ -77,22 +89,26 @@ export function decoder(options = {}) {
|
|
|
77
89
|
// paste, then let the normal control-key path handle the byte.
|
|
78
90
|
held = held.slice(interrupt);
|
|
79
91
|
pasted = "";
|
|
92
|
+
pasteTooLong = false;
|
|
80
93
|
pasting = false;
|
|
81
94
|
continue;
|
|
82
95
|
}
|
|
83
96
|
// Hold back a possible partial terminator rather than pasting it.
|
|
84
97
|
const safe = held.length - PASTE_END.length - 1;
|
|
85
98
|
if (safe > 0) {
|
|
86
|
-
|
|
99
|
+
appendPaste(held.slice(0, safe));
|
|
87
100
|
held = held.slice(safe);
|
|
88
101
|
}
|
|
89
102
|
break;
|
|
90
103
|
}
|
|
91
|
-
|
|
104
|
+
appendPaste(held.slice(0, end));
|
|
92
105
|
held = held.slice(end + PASTE_END.length + 1);
|
|
93
106
|
pasting = false;
|
|
94
|
-
keys.push(
|
|
107
|
+
keys.push(pasteTooLong
|
|
108
|
+
? { name: "input_limit", text: "", ctrl: false }
|
|
109
|
+
: { name: "paste", text: pasted, ctrl: false });
|
|
95
110
|
pasted = "";
|
|
111
|
+
pasteTooLong = false;
|
|
96
112
|
continue;
|
|
97
113
|
}
|
|
98
114
|
const ch = held[0];
|
|
@@ -103,6 +119,7 @@ export function decoder(options = {}) {
|
|
|
103
119
|
if (rest.startsWith(PASTE_START)) {
|
|
104
120
|
held = rest.slice(PASTE_START.length);
|
|
105
121
|
pasting = true;
|
|
122
|
+
pasteTooLong = false;
|
|
106
123
|
continue;
|
|
107
124
|
}
|
|
108
125
|
const mouse = MOUSE.exec(rest);
|
|
@@ -161,14 +178,56 @@ export function decoder(options = {}) {
|
|
|
161
178
|
};
|
|
162
179
|
return {
|
|
163
180
|
push(chunk) {
|
|
164
|
-
|
|
165
|
-
|
|
181
|
+
// The usual printable run needs no protocol buffering. This also rejects
|
|
182
|
+
// one unbracketed paste atomically before copying it into `held`.
|
|
183
|
+
if (held === "" && !pasting && printable(chunk)) {
|
|
184
|
+
return [chunk.length > MAX_PROMPT_CODE_UNITS
|
|
185
|
+
? { name: "input_limit", text: "", ctrl: false }
|
|
186
|
+
: { name: "char", text: chunk, ctrl: false }];
|
|
187
|
+
}
|
|
188
|
+
const keys = [];
|
|
189
|
+
const chunkSize = 64 * 1_024;
|
|
190
|
+
for (let from = 0; from < chunk.length;) {
|
|
191
|
+
let to = Math.min(from + chunkSize, chunk.length);
|
|
192
|
+
if (to < chunk.length &&
|
|
193
|
+
isHighSurrogate(chunk.charCodeAt(to - 1)) &&
|
|
194
|
+
isLowSurrogate(chunk.charCodeAt(to))) {
|
|
195
|
+
to++;
|
|
196
|
+
}
|
|
197
|
+
const part = chunk.slice(from, to);
|
|
198
|
+
from = to;
|
|
199
|
+
if (part.length > MAX_PROMPT_CODE_UNITS - held.length) {
|
|
200
|
+
held = "";
|
|
201
|
+
pasted = "";
|
|
202
|
+
pasting = false;
|
|
203
|
+
pasteTooLong = false;
|
|
204
|
+
keys.push({ name: "input_limit", text: "", ctrl: false });
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
held += part;
|
|
208
|
+
keys.push(...drain(false));
|
|
209
|
+
}
|
|
210
|
+
return keys;
|
|
166
211
|
},
|
|
167
212
|
flush() {
|
|
168
213
|
return drain(true);
|
|
169
214
|
},
|
|
170
215
|
};
|
|
171
216
|
}
|
|
217
|
+
function isHighSurrogate(code) {
|
|
218
|
+
return code >= 0xd800 && code <= 0xdbff;
|
|
219
|
+
}
|
|
220
|
+
function isLowSurrogate(code) {
|
|
221
|
+
return code >= 0xdc00 && code <= 0xdfff;
|
|
222
|
+
}
|
|
223
|
+
function printable(text) {
|
|
224
|
+
for (let index = 0; index < text.length; index++) {
|
|
225
|
+
const char = text[index];
|
|
226
|
+
if ((char.codePointAt(0) ?? 0) < 0x20 || char === ESC || char === DEL)
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
return text !== "";
|
|
230
|
+
}
|
|
172
231
|
function firstInterrupt(text) {
|
|
173
232
|
const ctrlC = text.indexOf(String.fromCharCode(3));
|
|
174
233
|
const ctrlD = text.indexOf(String.fromCharCode(4));
|
package/dist/tui/overlay.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as picker from "./picker.js";
|
|
3
3
|
import { oneLine } from "./field.js";
|
|
4
4
|
import { applyKey } from "./input.js";
|
|
5
|
+
import { PromptLimitError } from "../input-boundary.js";
|
|
5
6
|
export function shown(open) {
|
|
6
7
|
if (open === undefined)
|
|
7
8
|
return undefined;
|
|
@@ -33,10 +34,17 @@ export function handle(open, key) {
|
|
|
33
34
|
cancel(open);
|
|
34
35
|
return {};
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
try {
|
|
38
|
+
if ("picker" in open)
|
|
39
|
+
return handlePicker(open, key);
|
|
40
|
+
if ("field" in open)
|
|
41
|
+
return handleField(open, key);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error instanceof PromptLimitError)
|
|
45
|
+
return { open, inputLimit: true };
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
40
48
|
return { open };
|
|
41
49
|
}
|
|
42
50
|
function handlePicker(open, key) {
|
package/dist/tui/picker.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// One interaction model for every terminal selector.
|
|
2
2
|
import { row } from "../ui/render.js";
|
|
3
3
|
import { elide, graphemes } from "../ui/width.js";
|
|
4
|
+
import { assertPromptAppend } from "../input-boundary.js";
|
|
4
5
|
import { menuWindow, renderMenuRows } from "./components/menu.js";
|
|
5
6
|
import { promptCursor, promptLine } from "./components/prompt.js";
|
|
6
7
|
const WINDOW = 6;
|
|
@@ -28,6 +29,7 @@ export function adjust(picker, step) {
|
|
|
28
29
|
export function type(picker, text) {
|
|
29
30
|
if (picker.searchable !== true || text === "")
|
|
30
31
|
return picker;
|
|
32
|
+
assertPromptAppend((picker.query ?? "").length, text.length);
|
|
31
33
|
return withQuery(picker, `${picker.query ?? ""}${text}`);
|
|
32
34
|
}
|
|
33
35
|
export function backspace(picker) {
|
package/dist/tui/screen.js
CHANGED
|
@@ -37,7 +37,7 @@ const CURSOR_RESET = `${CSI}0 q`;
|
|
|
37
37
|
const SYNC_BEGIN = `${CSI}?2026h`;
|
|
38
38
|
const SYNC_END = `${CSI}?2026l`;
|
|
39
39
|
let active = false;
|
|
40
|
-
let
|
|
40
|
+
let restoreHandlersRegistered = false;
|
|
41
41
|
export function interactive() {
|
|
42
42
|
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
43
43
|
}
|
|
@@ -54,7 +54,7 @@ export function enter(reducedMotion = false) {
|
|
|
54
54
|
if (active)
|
|
55
55
|
return;
|
|
56
56
|
active = true;
|
|
57
|
-
|
|
57
|
+
registerRestoreHandlers();
|
|
58
58
|
write(ALT_ON + WRAP_OFF + CURSOR_HIDE + (reducedMotion ? CURSOR_STEADY : CURSOR_BLOCK) + PASTE_ON + MOUSE_ON);
|
|
59
59
|
process.stdin.setRawMode(true);
|
|
60
60
|
process.stdin.setEncoding("utf8");
|
|
@@ -73,24 +73,12 @@ export function setReducedMotion(reducedMotion) {
|
|
|
73
73
|
if (active)
|
|
74
74
|
write(reducedMotion ? CURSOR_STEADY : CURSOR_BLOCK);
|
|
75
75
|
}
|
|
76
|
-
function
|
|
77
|
-
if (
|
|
76
|
+
function registerRestoreHandlers() {
|
|
77
|
+
if (restoreHandlersRegistered)
|
|
78
78
|
return;
|
|
79
|
-
|
|
79
|
+
restoreHandlersRegistered = true;
|
|
80
80
|
process.on("exit", leave);
|
|
81
81
|
process.on("uncaughtExceptionMonitor", leave);
|
|
82
|
-
process.on("SIGTERM", onSigterm);
|
|
83
|
-
process.on("SIGHUP", onSighup);
|
|
84
|
-
}
|
|
85
|
-
function onSigterm() {
|
|
86
|
-
onFatalSignal(15);
|
|
87
|
-
}
|
|
88
|
-
function onSighup() {
|
|
89
|
-
onFatalSignal(1);
|
|
90
|
-
}
|
|
91
|
-
function onFatalSignal(number) {
|
|
92
|
-
leave();
|
|
93
|
-
process.exit(128 + number);
|
|
94
82
|
}
|
|
95
83
|
export function onResize(handler) {
|
|
96
84
|
process.stdout.on("resize", handler);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Bounded synchronous reads for the tiny JSON stores under ~/.jecode.
|
|
2
|
+
import { Buffer } from "node:buffer";
|
|
3
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
4
|
+
export const USER_STORE_LIMITS = Object.freeze({
|
|
5
|
+
settingsBytes: 64 * 1_024,
|
|
6
|
+
credentialsBytes: 256 * 1_024,
|
|
7
|
+
accountsBytes: 128 * 1_024,
|
|
8
|
+
credentialEntries: 64,
|
|
9
|
+
credentialName: 256,
|
|
10
|
+
credentialValue: 16_384,
|
|
11
|
+
model: 512,
|
|
12
|
+
endpoint: 2_048,
|
|
13
|
+
accountToken: 32_768,
|
|
14
|
+
accountLabel: 1_024,
|
|
15
|
+
});
|
|
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);
|
|
23
|
+
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));
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
closeSync(descriptor);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function assertStoreText(text, maxBytes) {
|
|
51
|
+
if (Buffer.byteLength(text, "utf8") > maxBytes) {
|
|
52
|
+
throw new Error(`user store exceeds ${maxBytes} bytes`);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@giovannijecha/jecode",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "An owned coding agent with zero external runtime dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"bin/",
|
|
27
27
|
"dist/",
|
|
28
|
-
"
|
|
28
|
+
"assets/jeco-256.png",
|
|
29
29
|
"LICENSE",
|
|
30
30
|
"README.md"
|
|
31
31
|
],
|
|
@@ -44,8 +44,11 @@
|
|
|
44
44
|
"pack:release": "npm run build:release && npm pack --ignore-scripts",
|
|
45
45
|
"start": "node src/main.ts",
|
|
46
46
|
"tui:lab": "node dev/tui-lab.ts",
|
|
47
|
-
"bench:
|
|
47
|
+
"bench:context": "node dev/benchmark-context.ts",
|
|
48
|
+
"bench:redaction": "node dev/benchmark-redaction.ts",
|
|
48
49
|
"bench:search": "node dev/benchmark-search.ts",
|
|
50
|
+
"bench:session": "node dev/benchmark-session.ts",
|
|
51
|
+
"bench:transcript": "node dev/benchmark-transcript.ts",
|
|
49
52
|
"typecheck": "tsc --noEmit",
|
|
50
53
|
"test": "npm run build:release && node --test",
|
|
51
54
|
"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",
|
|
File without changes
|