@giovannijecha/jecode 0.8.5 → 0.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -9
- package/assets/tokenizers/LICENSE +21 -0
- package/assets/tokenizers/o200k-base.tiktoken.gz +0 -0
- package/dist/cli-info.js +8 -14
- package/dist/commands.js +7 -3
- package/dist/config.js +47 -53
- package/dist/context/automatic.js +16 -1
- package/dist/context/budget.js +5 -2
- package/dist/context/compactor.js +91 -37
- package/dist/context/diagnostics.js +108 -0
- package/dist/context/lifetime.js +18 -0
- package/dist/context/manager.js +67 -0
- package/dist/context/manual.js +11 -10
- package/dist/context/measurement.js +75 -0
- package/dist/context/policy.js +13 -10
- package/dist/context/request-observation.js +46 -0
- package/dist/context/request-projection.js +8 -34
- package/dist/context/request.js +22 -0
- package/dist/context/tokenizer/bpe.js +68 -0
- package/dist/context/tokenizer/o200k.js +54 -0
- package/dist/context/tokenizer/vocabulary.js +34 -0
- package/dist/controller-request.js +47 -45
- package/dist/controller.js +13 -2
- package/dist/credential-commands.js +3 -3
- package/dist/input-boundary.js +0 -62
- package/dist/launch.js +13 -9
- package/dist/model-command.js +7 -17
- package/dist/oauth-result-page.js +68 -0
- package/dist/openai-account-command.js +14 -12
- package/dist/openai-account.js +7 -5
- package/dist/openai-oauth-callback.js +15 -54
- package/dist/openai-oauth-tokens.js +9 -7
- package/dist/openai-oauth.js +8 -6
- package/dist/permission-command.js +9 -24
- package/dist/permissions.js +10 -8
- package/dist/provider-commands.js +1 -33
- package/dist/provider-errors.js +1 -10
- package/dist/provider-label.js +3 -10
- package/dist/providers/anthropic-wire.js +1 -1
- package/dist/providers/anthropic.js +3 -2
- package/dist/providers/index.js +1 -5
- package/dist/providers/input-measurement.js +85 -0
- package/dist/providers/ollama-context.js +42 -0
- package/dist/providers/ollama-endpoint.js +7 -34
- package/dist/providers/ollama-wire.js +1 -1
- package/dist/providers/ollama.js +15 -147
- package/dist/providers/openai-codex.js +7 -4
- package/dist/providers/openai-stream.js +20 -8
- package/dist/providers/openai-summary.js +37 -0
- package/dist/providers/openai-wire.js +1 -1
- package/dist/providers/openai.js +3 -1
- 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/load.js +90 -0
- package/dist/sessions/snapshot.js +33 -0
- package/dist/sessions/store.js +42 -461
- package/dist/settings.js +17 -16
- package/dist/start.js +36 -53
- package/dist/timeline.js +2 -0
- 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 -193
- package/dist/tools/glob.js +107 -0
- package/dist/tools/index.js +2 -1
- package/dist/tools/search.js +1 -105
- package/dist/tui/activity.js +1 -1
- package/dist/tui/app-input.js +34 -17
- package/dist/tui/app-workflows.js +13 -361
- package/dist/tui/app.js +55 -28
- package/dist/tui/approve.js +3 -2
- package/dist/tui/blocks.js +1 -2
- package/dist/tui/command-workflow.js +112 -0
- package/dist/tui/components/command-menu.js +7 -10
- package/dist/tui/components/menu.js +82 -43
- package/dist/tui/components/messages.js +16 -9
- package/dist/tui/components/status.js +1 -1
- 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/frame.js +8 -3
- package/dist/tui/help.js +3 -2
- package/dist/tui/picker-layout.js +44 -0
- package/dist/tui/picker.js +7 -71
- package/dist/tui/screen.js +7 -0
- package/dist/tui/session-view.js +0 -1
- package/dist/tui/tool-details.js +135 -0
- package/dist/tui/transcript-grammar.js +1 -1
- package/dist/tui/transcript-view.js +30 -112
- package/dist/tui/turn-workflow.js +235 -0
- package/dist/tui/turn.js +7 -140
- package/dist/tui/view.js +4 -4
- package/dist/tui/workflow-types.js +2 -0
- package/dist/ui/render.js +0 -4
- package/package.json +16 -12
- package/dist/batch-view.js +0 -17
- package/dist/batch.js +0 -267
- package/dist/ollama-settings-command.js +0 -74
- package/dist/tui/motion.js +0 -32
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Owned o200k_base ordinary-text counter. Special-looking user text stays literal.
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { countPiece } from "./bpe.js";
|
|
4
|
+
import { vocabulary } from "./vocabulary.js";
|
|
5
|
+
// OpenAI tiktoken's o200k_base split, with scoped ASCII case-insensitivity
|
|
6
|
+
// expanded for JavaScript. Vocabulary and expression attribution: assets/tokenizers/LICENSE.
|
|
7
|
+
const pattern = /[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]*[\p{Ll}\p{Lm}\p{Lo}\p{M}]+(?:'[sStTmMdD]|'[rR][eE]|'[vV][eE]|'[lL][lL])?|[^\r\n\p{L}\p{N}]?[\p{Lu}\p{Lt}\p{Lm}\p{Lo}\p{M}]+[\p{Ll}\p{Lm}\p{Lo}\p{M}]*(?:'[sStTmMdD]|'[rR][eE]|'[vV][eE]|'[lL][lL])?|\p{N}{1,3}| ?[^\p{White_Space}\p{L}\p{N}]+[\r\n/]*|\p{White_Space}*[\r\n]+|\p{White_Space}+(?!\P{White_Space})|\p{White_Space}+/gu;
|
|
8
|
+
const cache = new Map();
|
|
9
|
+
const CHUNK = 8_192;
|
|
10
|
+
export async function countO200k(text, signal) {
|
|
11
|
+
signal?.throwIfAborted();
|
|
12
|
+
const key = createHash("sha256").update(text).digest("hex");
|
|
13
|
+
const cached = cache.get(key);
|
|
14
|
+
if (cached !== undefined)
|
|
15
|
+
return cached;
|
|
16
|
+
const ranks = await vocabulary();
|
|
17
|
+
signal?.throwIfAborted();
|
|
18
|
+
let count = 0;
|
|
19
|
+
// Repetitive logs or minified data must not repeat the same merge work.
|
|
20
|
+
// Keep text only within this call, bounded independently of input size.
|
|
21
|
+
const chunks = new Map();
|
|
22
|
+
let chunkUnits = 0;
|
|
23
|
+
for (let start = 0; start < text.length;) {
|
|
24
|
+
let end = Math.min(start + CHUNK, text.length);
|
|
25
|
+
if (end < text.length && /[\uD800-\uDBFF]/u.test(text[end - 1]))
|
|
26
|
+
end--;
|
|
27
|
+
const chunk = text.slice(start, end);
|
|
28
|
+
let tokens = chunks.get(chunk);
|
|
29
|
+
if (tokens === undefined) {
|
|
30
|
+
tokens = 0;
|
|
31
|
+
for (const match of chunk.matchAll(pattern))
|
|
32
|
+
tokens += countPiece(Buffer.from(match[0], "utf8"), ranks);
|
|
33
|
+
while (chunkUnits + chunk.length > 131_072) {
|
|
34
|
+
const oldest = chunks.keys().next().value;
|
|
35
|
+
chunks.delete(oldest);
|
|
36
|
+
chunkUnits -= oldest.length;
|
|
37
|
+
}
|
|
38
|
+
chunks.set(chunk, tokens);
|
|
39
|
+
chunkUnits += chunk.length;
|
|
40
|
+
}
|
|
41
|
+
count += tokens;
|
|
42
|
+
// Bounded slices may split a token/pre-token. Reserve boundary room; this
|
|
43
|
+
// is an input estimate, not a claim of exact whole-request tokenization.
|
|
44
|
+
if (end < text.length)
|
|
45
|
+
count += 8;
|
|
46
|
+
start = end;
|
|
47
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
48
|
+
signal?.throwIfAborted();
|
|
49
|
+
}
|
|
50
|
+
if (cache.size >= 2_048)
|
|
51
|
+
cache.delete(cache.keys().next().value);
|
|
52
|
+
cache.set(key, count);
|
|
53
|
+
return count;
|
|
54
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Pinned data, loaded once on demand. No network, executable data, or user cache.
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { gunzip } from "node:zlib";
|
|
6
|
+
let loading;
|
|
7
|
+
const unpack = promisify(gunzip);
|
|
8
|
+
const digest = "446a9538cb6c348e3516120d7c08b09f57c36495e2acfffe59a5bf8b0cfb1a2d";
|
|
9
|
+
export function vocabulary() {
|
|
10
|
+
loading ??= load();
|
|
11
|
+
return loading;
|
|
12
|
+
}
|
|
13
|
+
async function load() {
|
|
14
|
+
const compressed = await readFile(new URL("../../../assets/tokenizers/o200k-base.tiktoken.gz", import.meta.url));
|
|
15
|
+
if (compressed.length > 2_000_000)
|
|
16
|
+
throw new Error("token vocabulary exceeds its size limit");
|
|
17
|
+
const data = await unpack(compressed, { maxOutputLength: 4_000_000 });
|
|
18
|
+
if (createHash("sha256").update(data).digest("hex") !== digest) {
|
|
19
|
+
throw new Error("token vocabulary checksum mismatch");
|
|
20
|
+
}
|
|
21
|
+
const ranks = new Map();
|
|
22
|
+
const lines = data.toString("ascii").trimEnd().split("\n");
|
|
23
|
+
for (let index = 0; index < lines.length; index++) {
|
|
24
|
+
const [encoded, rank] = lines[index].split(" ");
|
|
25
|
+
if (encoded === undefined || Number(rank) !== index)
|
|
26
|
+
throw new Error("invalid token vocabulary");
|
|
27
|
+
ranks.set(Buffer.from(encoded, "base64").toString("latin1"), index);
|
|
28
|
+
if (index % 2_048 === 0)
|
|
29
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
30
|
+
}
|
|
31
|
+
if (ranks.size !== 199_998)
|
|
32
|
+
throw new Error("incomplete token vocabulary");
|
|
33
|
+
return ranks;
|
|
34
|
+
}
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
// One streamed provider request with a single safe context-overflow recovery.
|
|
2
|
-
import { budgetRequestFromInputTokens,
|
|
2
|
+
import { budgetRequestFromInputTokens, } from "./context/budget.js";
|
|
3
3
|
import { isContextOverflow } from "./context/policy.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import { fitRequestInput } from "./context/request.js";
|
|
5
|
+
import { observePreparation, sendObserved } from "./context/request-observation.js";
|
|
6
|
+
export async function requestAssistant(history, current, specs, options, events, meter, signal) {
|
|
7
|
+
let preparing = performance.now();
|
|
6
8
|
let policy = await options.contextPolicy();
|
|
7
|
-
const prepared = await prepareContext(history, current, specs, options, events, policy, "budget", signal);
|
|
9
|
+
const prepared = await prepareContext(history, current, specs, options, events, meter, policy, "budget", signal);
|
|
8
10
|
let context = prepared.context;
|
|
9
11
|
let requestMessages = prepared.requestMessages;
|
|
10
12
|
let inputTokens = prepared.inputTokens;
|
|
13
|
+
let measurement = prepared.measurement;
|
|
14
|
+
let clippedResults = prepared.clippedResults;
|
|
11
15
|
let recovered = false;
|
|
12
16
|
for (;;) {
|
|
13
17
|
const budget = budgetRequestFromInputTokens(inputTokens, options.maxTokens, policy);
|
|
14
18
|
try {
|
|
15
|
-
const message = await options.provider
|
|
19
|
+
const message = await sendObserved(options.provider, {
|
|
16
20
|
model: options.model,
|
|
17
21
|
system: options.system,
|
|
18
22
|
messages: requestMessages,
|
|
@@ -25,59 +29,57 @@ export async function requestAssistant(history, current, specs, options, events,
|
|
|
25
29
|
signal,
|
|
26
30
|
onStream: (event) => events.onStream(event),
|
|
27
31
|
onStatus: (status) => events.onStatus?.(status),
|
|
28
|
-
});
|
|
29
|
-
return { message, context, inputTokens: budget.inputTokens };
|
|
32
|
+
}, measurement, policy, performance.now() - preparing, clippedResults);
|
|
33
|
+
return { message, context, inputTokens: budget.inputTokens, measurement };
|
|
30
34
|
}
|
|
31
35
|
catch (error) {
|
|
32
|
-
if (recovered)
|
|
36
|
+
if (recovered || !isContextOverflow(error))
|
|
33
37
|
throw error;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const next = await prepareContext(history, context, specs, options, events, policy, "overflow", signal, error);
|
|
38
|
+
preparing = performance.now();
|
|
39
|
+
policy = await options.contextPolicy();
|
|
40
|
+
const next = await prepareContext(history, context, specs, options, events, meter, policy, "overflow", signal, error);
|
|
37
41
|
if (sameContext(next.context, context))
|
|
38
42
|
throw error;
|
|
39
43
|
context = next.context;
|
|
40
44
|
requestMessages = next.requestMessages;
|
|
41
45
|
inputTokens = next.inputTokens;
|
|
46
|
+
measurement = next.measurement;
|
|
47
|
+
clippedResults = next.clippedResults;
|
|
42
48
|
recovered = true;
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
|
-
async function prepareContext(history, context, specs, options, events, policy, reason, signal, error) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
async function prepareContext(history, context, specs, options, events, meter, policy, reason, signal, error) {
|
|
53
|
+
return observePreparation(policy, reason, signal, async () => {
|
|
54
|
+
const input = {
|
|
55
|
+
model: options.model,
|
|
56
|
+
effort: options.effort,
|
|
57
|
+
system: options.system,
|
|
58
|
+
messages: [...context],
|
|
59
|
+
tools: specs,
|
|
60
|
+
};
|
|
61
|
+
const initial = await meter.measure(input, signal);
|
|
62
|
+
const projected = await events.onContext?.(history, context, {
|
|
63
|
+
reason,
|
|
64
|
+
policy,
|
|
65
|
+
inputTokens: initial.inputTokens,
|
|
66
|
+
...(error === undefined ? {} : { error }),
|
|
67
|
+
});
|
|
68
|
+
const semantic = projected === undefined ? clone(context) : clone(projected);
|
|
69
|
+
if (projected !== undefined)
|
|
70
|
+
meter.reset();
|
|
71
|
+
const semanticInput = { ...input, messages: semantic };
|
|
72
|
+
const measurement = projected === undefined ? initial : await meter.measure(semanticInput, signal);
|
|
73
|
+
const fitted = await fitRequestInput(semanticInput, meter, policy, measurement, signal);
|
|
74
|
+
budgetRequestFromInputTokens(fitted.measurement.inputTokens, options.maxTokens, policy);
|
|
75
|
+
return {
|
|
76
|
+
context: semantic,
|
|
77
|
+
requestMessages: fitted.messages,
|
|
78
|
+
inputTokens: fitted.measurement.inputTokens,
|
|
79
|
+
measurement: fitted.measurement,
|
|
80
|
+
clippedResults: fitted.clippedResults,
|
|
81
|
+
};
|
|
61
82
|
});
|
|
62
|
-
const semantic = projected === undefined ? clone(context) : clone(projected);
|
|
63
|
-
const stableProjection = projected === undefined
|
|
64
|
-
? initialProjection
|
|
65
|
-
: projectToolResults(semantic, projectionBudget);
|
|
66
|
-
const requestMessages = stableProjection.saturated
|
|
67
|
-
? projectToolResultsNewest(semantic, projectionBudget).messages
|
|
68
|
-
: stableProjection.messages;
|
|
69
|
-
const canReuseEstimate = projected === undefined && !stableProjection.saturated;
|
|
70
|
-
return {
|
|
71
|
-
context: semantic,
|
|
72
|
-
requestMessages,
|
|
73
|
-
inputTokens: canReuseEstimate
|
|
74
|
-
? inputTokens
|
|
75
|
-
: await estimateRequestInputTokensResponsive({
|
|
76
|
-
system: options.system,
|
|
77
|
-
messages: requestMessages,
|
|
78
|
-
tools: specs,
|
|
79
|
-
}, signal),
|
|
80
|
-
};
|
|
81
83
|
}
|
|
82
84
|
function clone(messages) {
|
|
83
85
|
return structuredClone([...messages]);
|
package/dist/controller.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { isToolCall } from "./types.js";
|
|
7
7
|
import { findTool, runTool, toolSpecs } from "./tools/index.js";
|
|
8
8
|
import { requestAssistant } from "./controller-request.js";
|
|
9
|
+
import { inputMeter } from "./context/measurement.js";
|
|
9
10
|
export const MAX_TOOL_CALLS_PER_RESPONSE = 32;
|
|
10
11
|
/** Independent read calls share one bounded execution wave. */
|
|
11
12
|
export const MAX_CONCURRENT_TOOL_CALLS = 4;
|
|
@@ -16,6 +17,7 @@ export const MAX_CONCURRENT_TOOL_CALLS = 4;
|
|
|
16
17
|
*/
|
|
17
18
|
export async function runTurn(history, options, events, signal, modelHistory = history) {
|
|
18
19
|
const specs = toolSpecs(options.tools);
|
|
20
|
+
const meter = options.inputMeter ?? inputMeter(options.provider);
|
|
19
21
|
let context = modelHistory;
|
|
20
22
|
throwIfAborted(signal);
|
|
21
23
|
const append = (message) => {
|
|
@@ -43,13 +45,13 @@ export async function runTurn(history, options, events, signal, modelHistory = h
|
|
|
43
45
|
if (options.maxModelRequests !== undefined &&
|
|
44
46
|
requests >= options.maxModelRequests) {
|
|
45
47
|
const requestLabel = options.maxModelRequests === 1 ? "request" : "requests";
|
|
46
|
-
throw new Error(`stopped after ${options.maxModelRequests} model ${requestLabel} (
|
|
48
|
+
throw new Error(`stopped after ${options.maxModelRequests} model ${requestLabel} (request budget reached)`);
|
|
47
49
|
}
|
|
48
50
|
appendSteering(options.steering?.drain() ?? []);
|
|
49
51
|
requests++;
|
|
50
52
|
// The message is displayed as it streams; what comes back here is the
|
|
51
53
|
// assembled version, which exists to be appended to the history.
|
|
52
|
-
const response = await requestAssistant(history, context, specs, options, events, signal);
|
|
54
|
+
const response = await requestAssistant(history, context, specs, options, events, meter, signal);
|
|
53
55
|
const assistant = response.message;
|
|
54
56
|
context = response.context;
|
|
55
57
|
throwIfAborted(signal);
|
|
@@ -63,6 +65,7 @@ export async function runTurn(history, options, events, signal, modelHistory = h
|
|
|
63
65
|
assertToolCallIds(calls);
|
|
64
66
|
if (assistant.usage !== undefined)
|
|
65
67
|
events.onUsage?.(assistant.usage);
|
|
68
|
+
meter.observe(response.measurement, assistant.usage?.inputTokens);
|
|
66
69
|
events.onRequestInput?.(assistant.usage !== undefined && assistant.usage.inputTokens > 0
|
|
67
70
|
? assistant.usage.inputTokens
|
|
68
71
|
: response.inputTokens);
|
|
@@ -193,11 +196,17 @@ async function settle(call, current, total, options, events, signal, preview) {
|
|
|
193
196
|
if (tool === undefined) {
|
|
194
197
|
return refuse(call, `no such tool: ${call.name}`, "unknown tool");
|
|
195
198
|
}
|
|
199
|
+
if (options.toolAllowed?.(call) === false) {
|
|
200
|
+
return refuse(call, "this tool is denied by the current session permissions", "denied");
|
|
201
|
+
}
|
|
196
202
|
const approved = !tool.dangerous || await events.approve(call);
|
|
197
203
|
throwIfAborted(signal);
|
|
198
204
|
if (!approved) {
|
|
199
205
|
return refuse(call, "the user declined this call — ask them how to proceed", "declined");
|
|
200
206
|
}
|
|
207
|
+
if (options.toolAllowed?.(call) === false) {
|
|
208
|
+
return refuse(call, "this tool is denied by the current session permissions", "denied");
|
|
209
|
+
}
|
|
201
210
|
throwIfAborted(signal);
|
|
202
211
|
events.onToolStart?.(call, current, total);
|
|
203
212
|
return runTool(tool, call, {
|
|
@@ -223,6 +232,8 @@ function refuse(call, reason, summary) {
|
|
|
223
232
|
async function look(call, options, signal) {
|
|
224
233
|
if (call.inputError !== undefined)
|
|
225
234
|
return undefined;
|
|
235
|
+
if (options.toolAllowed?.(call) === false)
|
|
236
|
+
return undefined;
|
|
226
237
|
const tool = findTool(options.tools, call.name);
|
|
227
238
|
if (tool?.preview === undefined)
|
|
228
239
|
return undefined;
|
|
@@ -64,13 +64,13 @@ export async function apiKeyCommand(name, label, session, host, providerId) {
|
|
|
64
64
|
if (!hasSaved(name)) {
|
|
65
65
|
host.emit({
|
|
66
66
|
kind: "notice",
|
|
67
|
-
text: `${label}
|
|
67
|
+
text: `${label} key comes from the environment · restart after changing it`,
|
|
68
68
|
tone: "info",
|
|
69
69
|
});
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
const action = await choose({
|
|
73
|
-
title: heading(`${label}
|
|
73
|
+
title: heading(`${label} key`, `${name} · environment`, session.palette),
|
|
74
74
|
description: "The environment value is read-only. A saved copy is currently shadowed.",
|
|
75
75
|
options: [{ label: "forget saved copy", hint: storeLabel(), key: "f" }],
|
|
76
76
|
index: 0,
|
|
@@ -93,7 +93,7 @@ export async function apiKeyCommand(name, label, session, host, providerId) {
|
|
|
93
93
|
: []),
|
|
94
94
|
];
|
|
95
95
|
const index = await choose({
|
|
96
|
-
title: heading(`${label}
|
|
96
|
+
title: heading(`${label} key`, `${name} · ${source ?? "missing"}`, session.palette),
|
|
97
97
|
options: actions,
|
|
98
98
|
index: 0,
|
|
99
99
|
});
|
package/dist/input-boundary.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
// One bounded ingress for text that can become a user prompt.
|
|
2
|
-
import { Buffer } from "node:buffer";
|
|
3
|
-
import { StringDecoder } from "node:string_decoder";
|
|
4
2
|
import { MAX_TEXT_CODE_UNITS } from "./text-boundary.js";
|
|
5
3
|
export const MAX_PROMPT_CODE_UNITS = MAX_TEXT_CODE_UNITS;
|
|
6
4
|
export const PROMPT_LIMIT_MESSAGE = `Prompt cannot exceed ${MAX_PROMPT_CODE_UNITS.toLocaleString("en-US")} UTF-16 code units`;
|
|
@@ -18,63 +16,3 @@ export function assertPromptAppend(current, added) {
|
|
|
18
16
|
if (added > MAX_PROMPT_CODE_UNITS - current)
|
|
19
17
|
throw new PromptLimitError();
|
|
20
18
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Split raw UTF-8 input without letting one unterminated line grow past the
|
|
23
|
-
* prompt boundary. Newline, CRLF, and a final line without a newline match the
|
|
24
|
-
* line semantics used by batch mode.
|
|
25
|
-
*/
|
|
26
|
-
export async function* boundedInputLines(source) {
|
|
27
|
-
const decoder = new StringDecoder("utf8");
|
|
28
|
-
let line = "";
|
|
29
|
-
let pendingCr = false;
|
|
30
|
-
const append = (text, from, to) => {
|
|
31
|
-
const length = to - from;
|
|
32
|
-
assertPromptAppend(line.length, length);
|
|
33
|
-
if (length > 0)
|
|
34
|
-
line += text.slice(from, to);
|
|
35
|
-
};
|
|
36
|
-
const consume = function* (text) {
|
|
37
|
-
if (text === "")
|
|
38
|
-
return;
|
|
39
|
-
let from = 0;
|
|
40
|
-
if (pendingCr) {
|
|
41
|
-
if (text.startsWith("\n"))
|
|
42
|
-
from = 1;
|
|
43
|
-
pendingCr = false;
|
|
44
|
-
yield line;
|
|
45
|
-
line = "";
|
|
46
|
-
}
|
|
47
|
-
while (from < text.length) {
|
|
48
|
-
const cr = text.indexOf("\r", from);
|
|
49
|
-
const lf = text.indexOf("\n", from);
|
|
50
|
-
const newline = cr === -1 ? lf : lf === -1 ? cr : Math.min(cr, lf);
|
|
51
|
-
if (newline === -1) {
|
|
52
|
-
append(text, from, text.length);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
append(text, from, newline);
|
|
56
|
-
if (text[newline] === "\r" && newline + 1 === text.length) {
|
|
57
|
-
pendingCr = true;
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
yield line;
|
|
61
|
-
line = "";
|
|
62
|
-
from = text[newline] === "\r" && text[newline + 1] === "\n"
|
|
63
|
-
? newline + 2
|
|
64
|
-
: newline + 1;
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
for await (const chunk of source) {
|
|
68
|
-
const text = typeof chunk === "string" ? chunk : decoder.write(Buffer.from(chunk));
|
|
69
|
-
yield* consume(text);
|
|
70
|
-
}
|
|
71
|
-
const tail = decoder.end();
|
|
72
|
-
if (tail !== "")
|
|
73
|
-
yield* consume(tail);
|
|
74
|
-
if (pendingCr) {
|
|
75
|
-
yield line;
|
|
76
|
-
line = "";
|
|
77
|
-
}
|
|
78
|
-
if (line !== "")
|
|
79
|
-
yield line;
|
|
80
|
-
}
|
package/dist/launch.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
export function parseLaunch(argv) {
|
|
2
|
+
if (argv.includes("--latest")) {
|
|
3
|
+
throw new Error("--latest has been renamed to --last; use `jecode -c` or `jecode resume --last`");
|
|
4
|
+
}
|
|
2
5
|
const first = argv[0];
|
|
3
|
-
if (first
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const rest = argv.slice(1);
|
|
7
|
-
const latest = rest.filter((value) => value === "--latest").length;
|
|
6
|
+
if (first === "resume" || first === "-c") {
|
|
7
|
+
const rest = first === "-c" ? ["--last", ...argv.slice(1)] : argv.slice(1);
|
|
8
|
+
const latest = rest.filter((value) => value === "--last").length;
|
|
8
9
|
if (latest > 1)
|
|
9
|
-
throw new Error("--
|
|
10
|
+
throw new Error("--last may be passed only once; -c already selects the last session");
|
|
10
11
|
return {
|
|
11
12
|
kind: "resume",
|
|
12
13
|
latest: latest === 1,
|
|
13
|
-
configArgs: rest.filter((value) => value !== "--
|
|
14
|
+
configArgs: rest.filter((value) => value !== "--last"),
|
|
14
15
|
};
|
|
15
16
|
}
|
|
16
|
-
if (
|
|
17
|
-
throw new Error(
|
|
17
|
+
if (first !== undefined && !first.startsWith("--") && first !== "-h" && first !== "-v") {
|
|
18
|
+
throw new Error(`unknown command ${first}`);
|
|
19
|
+
}
|
|
20
|
+
if (argv.includes("--last"))
|
|
21
|
+
throw new Error("--last requires `jecode resume`; use `jecode -c` to continue directly");
|
|
18
22
|
return { kind: "new", latest: false, configArgs: [...argv] };
|
|
19
23
|
}
|
package/dist/model-command.js
CHANGED
|
@@ -17,9 +17,6 @@ export async function modelsCommand(session, host, behavior = {}, providers = PR
|
|
|
17
17
|
const connected = availability
|
|
18
18
|
.filter((entry) => entry.blocked === undefined)
|
|
19
19
|
.map((entry) => entry.provider);
|
|
20
|
-
const disconnected = availability
|
|
21
|
-
.filter((entry) => entry.blocked !== undefined)
|
|
22
|
-
.map((entry) => entry.provider);
|
|
23
20
|
if (connected.length === 0) {
|
|
24
21
|
host.emit({
|
|
25
22
|
kind: "notice",
|
|
@@ -68,10 +65,15 @@ export async function modelsCommand(session, host, behavior = {}, providers = PR
|
|
|
68
65
|
});
|
|
69
66
|
return false;
|
|
70
67
|
}
|
|
71
|
-
|
|
68
|
+
if (failed.length > 0) {
|
|
69
|
+
host.emit({
|
|
70
|
+
kind: "notice",
|
|
71
|
+
text: `model catalogs unavailable: ${failed.map((entry) => providerLabel(entry.provider.id)).join(", ")}`,
|
|
72
|
+
tone: "warn",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
72
75
|
const index = await choose({
|
|
73
76
|
title: [],
|
|
74
|
-
...(description === undefined ? {} : { description }),
|
|
75
77
|
options: choices.map((choice) => ({
|
|
76
78
|
label: choice.model,
|
|
77
79
|
// Provider identity is part of the choice, not optional help: keep it
|
|
@@ -136,18 +138,6 @@ async function alignEffort(provider, model, effort, host) {
|
|
|
136
138
|
return { ok: false };
|
|
137
139
|
}
|
|
138
140
|
}
|
|
139
|
-
function catalogDescription(disconnected, failed) {
|
|
140
|
-
const parts = [
|
|
141
|
-
"The connection on the right will run this model; API and account usage stay separate",
|
|
142
|
-
disconnected.length === 0
|
|
143
|
-
? undefined
|
|
144
|
-
: `Not connected: ${disconnected.map((provider) => providerLabel(provider.id)).join(", ")}`,
|
|
145
|
-
failed.length === 0
|
|
146
|
-
? undefined
|
|
147
|
-
: `Unavailable: ${failed.map((entry) => providerLabel(entry.provider.id)).join(", ")}`,
|
|
148
|
-
].filter((part) => part !== undefined);
|
|
149
|
-
return `${parts.join(" · ")} · manage access in /providers`;
|
|
150
|
-
}
|
|
151
141
|
function emptyCatalogMessage(connected, failed) {
|
|
152
142
|
if (failed.length === 1 && connected.length === 1) {
|
|
153
143
|
const one = failed[0];
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The browser handoff back to the terminal after account sign-in.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
export function oauthResultPage(success) {
|
|
4
|
+
const title = success ? "You're signed in" : "Sign-in failed";
|
|
5
|
+
const detail = success
|
|
6
|
+
? "Return to your terminal. You can close this tab."
|
|
7
|
+
: "Return to your terminal to try again.";
|
|
8
|
+
return `<!doctype html>
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="utf-8">
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
13
|
+
<meta name="color-scheme" content="dark">
|
|
14
|
+
<title>${title} · Jecode</title>
|
|
15
|
+
<style>
|
|
16
|
+
:root {
|
|
17
|
+
color-scheme: dark;
|
|
18
|
+
--background: #1c2026;
|
|
19
|
+
--text: #ebeff4;
|
|
20
|
+
--muted: #9ca9b7;
|
|
21
|
+
--accent: #669bd2;
|
|
22
|
+
--error: #e87070;
|
|
23
|
+
}
|
|
24
|
+
* { box-sizing: border-box; }
|
|
25
|
+
body {
|
|
26
|
+
margin: 0;
|
|
27
|
+
min-height: 100vh;
|
|
28
|
+
min-height: 100svh;
|
|
29
|
+
display: grid;
|
|
30
|
+
place-items: center;
|
|
31
|
+
background: var(--background);
|
|
32
|
+
color: var(--text);
|
|
33
|
+
font-family: "Segoe UI", system-ui, sans-serif;
|
|
34
|
+
}
|
|
35
|
+
main { width: min(28rem, 100%); padding: 2rem; }
|
|
36
|
+
.identity { display: flex; align-items: center; gap: .75rem; margin-bottom: 2rem; }
|
|
37
|
+
.identity img { display: block; width: 3rem; height: 3rem; }
|
|
38
|
+
.identity span {
|
|
39
|
+
color: var(--accent);
|
|
40
|
+
font: 600 1.25rem/1 ui-monospace, "Cascadia Mono", Consolas, monospace;
|
|
41
|
+
letter-spacing: -.04em;
|
|
42
|
+
}
|
|
43
|
+
h1 { margin: 0; font-size: 1.75rem; font-weight: 600; line-height: 1.25; letter-spacing: -.025em; }
|
|
44
|
+
p { margin: .75rem 0 0; color: var(--muted); font-size: 1rem; line-height: 1.6; }
|
|
45
|
+
.failure h1 { color: var(--error); }
|
|
46
|
+
</style>
|
|
47
|
+
</head>
|
|
48
|
+
<body>
|
|
49
|
+
<main${success ? "" : ' class="failure"'}>
|
|
50
|
+
<div class="identity">
|
|
51
|
+
<img src="${mascotDataUri()}" width="48" height="48" alt="">
|
|
52
|
+
<span>jecode</span>
|
|
53
|
+
</div>
|
|
54
|
+
<h1>${title}</h1>
|
|
55
|
+
<p>${detail}</p>
|
|
56
|
+
</main>
|
|
57
|
+
<script>history.replaceState(null,"","/auth/complete")</script>
|
|
58
|
+
</body>
|
|
59
|
+
</html>`;
|
|
60
|
+
}
|
|
61
|
+
let mascot;
|
|
62
|
+
function mascotDataUri() {
|
|
63
|
+
if (mascot === undefined) {
|
|
64
|
+
const file = new URL("../assets/jeco-256.png", import.meta.url);
|
|
65
|
+
mascot = `data:image/png;base64,${readFileSync(file).toString("base64")}`;
|
|
66
|
+
}
|
|
67
|
+
return mascot;
|
|
68
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
// Interactive
|
|
1
|
+
// Interactive account management for the OpenAI Codex provider.
|
|
2
2
|
import { openAICodexAccount } from "./accounts.js";
|
|
3
3
|
import { headlessEnvironment, openExternal } from "./external-browser.js";
|
|
4
4
|
import { openAIAccountHint, removeOpenAIAccount, saveOpenAIAccount, } from "./openai-account.js";
|
|
5
5
|
import { beginBrowserLogin, beginDeviceLogin, } from "./openai-oauth.js";
|
|
6
|
+
import { providerLabel } from "./provider-label.js";
|
|
6
7
|
import { heading } from "./tui/picker.js";
|
|
8
|
+
const ACCOUNT_LABEL = providerLabel("openai-codex");
|
|
7
9
|
const DEFAULT_DEPENDENCIES = {
|
|
8
10
|
beginBrowser: beginBrowserLogin,
|
|
9
11
|
beginDevice: beginDeviceLogin,
|
|
@@ -16,12 +18,12 @@ export async function openAIAccountCommand(session, host, dependencies = DEFAULT
|
|
|
16
18
|
const connected = openAICodexAccount() !== undefined;
|
|
17
19
|
const actions = connected
|
|
18
20
|
? [
|
|
19
|
-
{ label:
|
|
21
|
+
{ label: `reconnect ${ACCOUNT_LABEL}`, hint: "replace the current sign-in", key: "r" },
|
|
20
22
|
{ label: "sign out", hint: "remove the saved account", key: "s" },
|
|
21
23
|
]
|
|
22
|
-
: [{ label:
|
|
24
|
+
: [{ label: `connect ${ACCOUNT_LABEL}`, hint: "sign in with OpenAI", key: "c" }];
|
|
23
25
|
const action = await host.choose({
|
|
24
|
-
title: heading(
|
|
26
|
+
title: heading(ACCOUNT_LABEL, openAIAccountHint(), session.palette),
|
|
25
27
|
options: actions,
|
|
26
28
|
index: 0,
|
|
27
29
|
});
|
|
@@ -35,8 +37,8 @@ export async function openAIAccountCommand(session, host, dependencies = DEFAULT
|
|
|
35
37
|
host.emit({
|
|
36
38
|
kind: "notice",
|
|
37
39
|
text: result.revokeFailed
|
|
38
|
-
?
|
|
39
|
-
:
|
|
40
|
+
? `${ACCOUNT_LABEL} disconnected · remote sign-out could not be confirmed${route}`
|
|
41
|
+
: `${ACCOUNT_LABEL} disconnected${route}`,
|
|
40
42
|
tone: result.revokeFailed ? "warn" : "info",
|
|
41
43
|
});
|
|
42
44
|
return result.removed;
|
|
@@ -56,14 +58,14 @@ async function connectOpenAIAccount(session, host, dependencies) {
|
|
|
56
58
|
{ label: "sign in with device code", hint: "WSL, SSH, or headless", key: "d" },
|
|
57
59
|
];
|
|
58
60
|
const choice = await host.choose({
|
|
59
|
-
title: heading(
|
|
61
|
+
title: heading(`connect ${ACCOUNT_LABEL}`, "OpenAI OAuth", session.palette),
|
|
60
62
|
description: "Choose the flow for this terminal. Your password stays on OpenAI's website.",
|
|
61
63
|
options: methods,
|
|
62
64
|
index: dependencies.headless() ? 1 : 0,
|
|
63
65
|
});
|
|
64
66
|
if (choice === undefined)
|
|
65
67
|
return false;
|
|
66
|
-
host.status?.(
|
|
68
|
+
host.status?.(`Starting ${ACCOUNT_LABEL} sign-in`);
|
|
67
69
|
let login;
|
|
68
70
|
try {
|
|
69
71
|
login = choice === 1
|
|
@@ -97,8 +99,8 @@ async function waitForLogin(login, session, host, openUrl) {
|
|
|
97
99
|
host.emit({
|
|
98
100
|
kind: "notice",
|
|
99
101
|
text: session.provider.id === "openai-codex"
|
|
100
|
-
?
|
|
101
|
-
:
|
|
102
|
+
? `${ACCOUNT_LABEL} connected · current provider route`
|
|
103
|
+
: `${ACCOUNT_LABEL} connected · choose an ${ACCOUNT_LABEL} model in /models to use this account`,
|
|
102
104
|
tone: "info",
|
|
103
105
|
});
|
|
104
106
|
return true;
|
|
@@ -111,7 +113,7 @@ async function waitForLogin(login, session, host, openUrl) {
|
|
|
111
113
|
await openUrl(login.url);
|
|
112
114
|
continue;
|
|
113
115
|
}
|
|
114
|
-
local.abort(new Error(
|
|
116
|
+
local.abort(new Error(`${ACCOUNT_LABEL} sign-in cancelled`));
|
|
115
117
|
return false;
|
|
116
118
|
}
|
|
117
119
|
}
|
|
@@ -124,7 +126,7 @@ function waitingPicker(login, session) {
|
|
|
124
126
|
? "Finish sign-in in your browser. Jecode will continue automatically."
|
|
125
127
|
: `Enter ${login.code} on OpenAI's device page. Jecode will continue automatically.`;
|
|
126
128
|
return {
|
|
127
|
-
title: heading(
|
|
129
|
+
title: heading(`${ACCOUNT_LABEL} sign-in`, login.code ?? "waiting for browser", session.palette),
|
|
128
130
|
description: detail,
|
|
129
131
|
options: [
|
|
130
132
|
{ label: "open browser again", key: "o" },
|
package/dist/openai-account.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
// The live
|
|
1
|
+
// The live OpenAI account: persistence, proactive refresh, and logout.
|
|
2
2
|
import { openAICodexAccount, updateOpenAICodexAccount } from "./accounts.js";
|
|
3
3
|
import { refreshOpenAITokens, revokeOpenAITokens } from "./openai-oauth.js";
|
|
4
|
+
import { providerLabel } from "./provider-label.js";
|
|
5
|
+
const ACCOUNT_LABEL = providerLabel("openai-codex");
|
|
4
6
|
const REFRESH_EARLY_MS = 5 * 60_000;
|
|
5
7
|
let refreshing;
|
|
6
8
|
export function openAIAccountHint() {
|
|
@@ -33,7 +35,7 @@ export async function removeOpenAIAccount(signal) {
|
|
|
33
35
|
export async function openAIAuthorization(rejectedToken, signal, onStatus) {
|
|
34
36
|
const account = openAICodexAccount();
|
|
35
37
|
if (account === undefined)
|
|
36
|
-
throw new Error(
|
|
38
|
+
throw new Error(`${ACCOUNT_LABEL} is not connected`);
|
|
37
39
|
const mustRefresh = rejectedToken !== undefined || expiresSoon(account);
|
|
38
40
|
const ready = mustRefresh
|
|
39
41
|
? await refreshAccount(rejectedToken, signal, onStatus)
|
|
@@ -47,14 +49,14 @@ async function refreshAccount(rejectedToken, signal, onStatus) {
|
|
|
47
49
|
throw abortReason(signal);
|
|
48
50
|
const task = updateOpenAICodexAccount(async (current) => {
|
|
49
51
|
if (current === undefined)
|
|
50
|
-
throw new Error(
|
|
52
|
+
throw new Error(`${ACCOUNT_LABEL} is not connected`);
|
|
51
53
|
if (!requiresAuthorityRefresh(current, rejectedToken))
|
|
52
54
|
return current;
|
|
53
|
-
onStatus?.(
|
|
55
|
+
onStatus?.(`Refreshing ${ACCOUNT_LABEL} sign-in`);
|
|
54
56
|
return refreshOpenAITokens(current);
|
|
55
57
|
}).then((account) => {
|
|
56
58
|
if (account === undefined)
|
|
57
|
-
throw new Error(
|
|
59
|
+
throw new Error(`${ACCOUNT_LABEL} is not connected`);
|
|
58
60
|
return account;
|
|
59
61
|
});
|
|
60
62
|
refreshing = task;
|