jaitus 0.3.14 → 0.3.17
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/package.json +2 -2
- package/src/agent/loop.js +40 -4
- package/src/api.js +177 -174
- package/src/commands/update.js +8 -3
- package/src/compact.js +224 -0
- package/src/errors.js +2 -0
- package/src/index.js +220 -119
- package/src/machine.js +227 -0
- package/src/tui/ansi.js +65 -1
- package/src/tui/entries.js +23 -11
- package/src/tui/markdown.js +40 -15
- package/src/tui-ink.js +2922 -2841
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jaitus",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.17",
|
|
4
4
|
"description": "Jaitus — a coding agent in your terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"node": ">=20"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "node tests/source-sanity.js && node tests/react-build.js && node tests/stalls.js && node tests/clipboard-write.js && node tests/rewind-images.js && node tests/rules.js && node tests/run.js && node tests/commands.js && node tests/login.js && node tests/session.js && node tests/openUrl.js && node tests/ink-tui.js && node tests/tui-render.js && node tests/mouse-e2e.js && node tests/environment.js && node tests/update.js && node tests/service-notices.js && node tests/continuation.js && node tests/steps.js && node tests/files.js && node tests/processes.js && node tests/security.js && node tests/context-budget.js && node tests/context-trim.js",
|
|
19
|
+
"test": "node tests/source-sanity.js && node tests/react-build.js && node tests/stalls.js && node tests/clipboard-write.js && node tests/rewind-images.js && node tests/rules.js && node tests/run.js && node tests/commands.js && node tests/login.js && node tests/session.js && node tests/openUrl.js && node tests/ink-tui.js && node tests/tui-render.js && node tests/mouse-e2e.js && node tests/environment.js && node tests/update.js && node tests/service-notices.js && node tests/continuation.js && node tests/steps.js && node tests/files.js && node tests/processes.js && node tests/security.js && node tests/context-budget.js && node tests/context-trim.js && node tests/compaction.js && node tests/copy-clean.js && node tests/parallel-tools.js",
|
|
20
20
|
"preview": "node scripts/tui-preview.js",
|
|
21
21
|
"prepublishOnly": "npm test"
|
|
22
22
|
},
|
package/src/agent/loop.js
CHANGED
|
@@ -262,6 +262,12 @@ export function repairToolPairs(messages) {
|
|
|
262
262
|
export const TRIM_AT = 40_000; // estimated tokens of conversation
|
|
263
263
|
export const KEEP_RECENT = 6; // latest tool results always kept whole
|
|
264
264
|
export const TRIM_AT_LEAST = 12_000; // estimated tokens a clearing must free
|
|
265
|
+
// Past DEEP_AT only the newest KEEP_RECENT_DEEP results stay whole. Production, 7 days to
|
|
266
|
+
// 2 Oct: requests carrying 150k+ tokens were a quarter of all requests and 62% of all
|
|
267
|
+
// credits — every step of a long task re-sent the outputs of its last six tools whole,
|
|
268
|
+
// up to 60,000 characters each.
|
|
269
|
+
export const DEEP_AT = 100_000;
|
|
270
|
+
export const KEEP_RECENT_DEEP = 3;
|
|
265
271
|
const TRIM_MIN_CHARS = 1_500; // results shorter than this are never cleared
|
|
266
272
|
const CLEARED = '[Output cleared';
|
|
267
273
|
|
|
@@ -281,8 +287,10 @@ export function conversationTokens(messages) {
|
|
|
281
287
|
* Clear old tool outputs in place when the conversation is large. Returns how many
|
|
282
288
|
* estimated tokens were freed (0 when nothing was worth clearing).
|
|
283
289
|
*/
|
|
284
|
-
export function clearOldToolResults(messages, { at = TRIM_AT, keep
|
|
285
|
-
|
|
290
|
+
export function clearOldToolResults(messages, { at = TRIM_AT, keep, atLeast = TRIM_AT_LEAST } = {}) {
|
|
291
|
+
const size = conversationTokens(messages);
|
|
292
|
+
if (size < at) return 0;
|
|
293
|
+
if (keep === undefined) keep = size >= DEEP_AT ? KEEP_RECENT_DEEP : KEEP_RECENT;
|
|
286
294
|
const names = new Map();
|
|
287
295
|
for (const m of messages) {
|
|
288
296
|
if (m?.role === 'assistant' && Array.isArray(m.content)) for (const b of m.content) if (b?.type === 'tool_use') names.set(b.id, b.name);
|
|
@@ -306,6 +314,9 @@ export function clearOldToolResults(messages, { at = TRIM_AT, keep = KEEP_RECENT
|
|
|
306
314
|
return freed;
|
|
307
315
|
}
|
|
308
316
|
|
|
317
|
+
// Tools that change nothing and ask nothing: several in one batch run at the same time.
|
|
318
|
+
const PARALLEL_SAFE = new Set(['read_file', 'glob', 'list_files', 'search', 'web_search', 'web_fetch', 'background_list', 'background_read', 'load_skill']);
|
|
319
|
+
|
|
309
320
|
export const IMAGE_BUDGET = 6_000_000;
|
|
310
321
|
export function withinImageBudget(messages, budget = IMAGE_BUDGET) {
|
|
311
322
|
let used = 0;
|
|
@@ -495,7 +506,7 @@ export async function runAgent({
|
|
|
495
506
|
client, messages, model, cwd, permissions, signal, tui = null,
|
|
496
507
|
extraSystem = '', maxIterations = MAX_ITERATIONS, silent = false,
|
|
497
508
|
onText = null, onEvent = null, systemOverride = null, journal = null,
|
|
498
|
-
subagents = true, onContext = null, trimContext = true,
|
|
509
|
+
subagents = true, onContext = null, trimContext = true, beforeStep = null,
|
|
499
510
|
}) {
|
|
500
511
|
// Loop detection. A call is identified by its tool, its input, and how many files had
|
|
501
512
|
// been changed when it was made — so edit → test → edit → test is progress (the count
|
|
@@ -544,6 +555,11 @@ export async function runAgent({
|
|
|
544
555
|
const freed = clearOldToolResults(messages);
|
|
545
556
|
if (freed && tui?.notice) tui.notice(`Cleared old tool output (~${Math.round(freed / 1000)}k tokens) to keep each request small.`, 'info');
|
|
546
557
|
}
|
|
558
|
+
// The caller may compact the history between steps (index.js: when the context is
|
|
559
|
+
// nearly full). Never before the first step — the caller already checked then.
|
|
560
|
+
if (beforeStep && iteration > 0 && !signal?.aborted) {
|
|
561
|
+
try { await beforeStep(messages); } catch { /* a failed compaction is reported by the caller; the step still runs */ }
|
|
562
|
+
}
|
|
547
563
|
let streamed = false;
|
|
548
564
|
const spin = tui ? { stop: () => {}, update: () => {} } : (silent ? { stop: () => {}, update: () => {} } : spinner(glyph('Working…', 'Working...')));
|
|
549
565
|
if (tui) tui.setStatus(`${c.brand(glyph('•', '*'))} ${c.brand(glyph('Working…', 'Working...'))}`);
|
|
@@ -728,6 +744,26 @@ export async function runAgent({
|
|
|
728
744
|
return { ...totals, stopReason: 'stuck' };
|
|
729
745
|
}
|
|
730
746
|
|
|
747
|
+
// Read-only calls in one batch run AT THE SAME TIME. The model is told to batch
|
|
748
|
+
// independent reads, and then they ran one after another anyway: five file reads or
|
|
749
|
+
// three searches cost five or three round trips of wall time. Only tools that change
|
|
750
|
+
// nothing and need no permission start early; their rows, results and order are
|
|
751
|
+
// exactly as before — the loop below just finds the work already done. Anything
|
|
752
|
+
// that writes, runs a command or asks the user still runs one by one, in order.
|
|
753
|
+
const early = new Map();
|
|
754
|
+
const parallel = calls.filter((call) => {
|
|
755
|
+
const t = toolMap[call.name];
|
|
756
|
+
return t && PARALLEL_SAFE.has(call.name) && !t.needsPermission && call.input?.__parse_error === undefined
|
|
757
|
+
&& !planBlocked.has(call.id) && permissions?.verdict?.(call.name, call.input, cwd) !== 'deny';
|
|
758
|
+
});
|
|
759
|
+
if (parallel.length > 1 && !signal?.aborted) {
|
|
760
|
+
for (const call of parallel) {
|
|
761
|
+
const started = Promise.resolve().then(() => toolMap[call.name].run(call.input, { cwd, signal }));
|
|
762
|
+
started.catch(() => { /* reported when the loop reaches the call */ });
|
|
763
|
+
early.set(call.id, started);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
731
767
|
// Every call gets a result, whatever happened.
|
|
732
768
|
const results = [];
|
|
733
769
|
for (const call of calls) {
|
|
@@ -811,7 +847,7 @@ export async function runAgent({
|
|
|
811
847
|
} else if (journal && call.name === 'run_command') {
|
|
812
848
|
journal.snapshotTracked(cwd);
|
|
813
849
|
}
|
|
814
|
-
const output = await tool.run(call.input, { cwd, signal });
|
|
850
|
+
const output = await (early.get(call.id) ?? tool.run(call.input, { cwd, signal }));
|
|
815
851
|
spin2.stop(); stopElapsed();
|
|
816
852
|
const took = ((Date.now() - toolStarted) / 1000).toFixed(1);
|
|
817
853
|
// opencode tool icons: $ shell, ← edits, → reads, ✱ search,
|
package/src/api.js
CHANGED
|
@@ -1,174 +1,177 @@
|
|
|
1
|
-
// ─── Talking to Jaitus ──────────────────────────────────────────────────────────
|
|
2
|
-
//
|
|
3
|
-
// The CLI is the only client of the gateway. Every failure it gets back is turned into
|
|
4
|
-
// a JaitusError here, at the edge, so no caller ever has an HTTP body in its hands to
|
|
5
|
-
// print — see errors.js for why that matters.
|
|
6
|
-
|
|
7
|
-
import { endpoints, version } from './config.js';
|
|
8
|
-
import { signedIn, withAuth } from './session.js';
|
|
9
|
-
import { fromGatewayBody, fromNetwork, JaitusError } from './errors.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
let
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
timer
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*
|
|
69
|
-
* and
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
t.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
1
|
+
// ─── Talking to Jaitus ──────────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// The CLI is the only client of the gateway. Every failure it gets back is turned into
|
|
4
|
+
// a JaitusError here, at the edge, so no caller ever has an HTTP body in its hands to
|
|
5
|
+
// print — see errors.js for why that matters.
|
|
6
|
+
|
|
7
|
+
import { endpoints, version } from './config.js';
|
|
8
|
+
import { signedIn, withAuth } from './session.js';
|
|
9
|
+
import { fromGatewayBody, fromNetwork, JaitusError } from './errors.js';
|
|
10
|
+
import { machineHeader } from './machine.js';
|
|
11
|
+
|
|
12
|
+
export class NotSignedIn extends Error {}
|
|
13
|
+
|
|
14
|
+
// fetch() has no timeout of its own, so a request the gateway never answered used to
|
|
15
|
+
// pin the CLI on "Working..." forever. Every call below carries a deadline: slow
|
|
16
|
+
// becomes a `timeout` JaitusError (retryable by the agent loop, shown with a request
|
|
17
|
+
// id) instead of eternity. The caller's signal (Esc) still wins: it aborts through the
|
|
18
|
+
// same controller, and only a genuine user abort reads as one.
|
|
19
|
+
//
|
|
20
|
+
// A turn is bounded by SILENCE, not by its length. Until the reply starts, the gateway
|
|
21
|
+
// may be failing over between provider keys (each gets a 20-45 s budget), so the first
|
|
22
|
+
// byte gets a few minutes. Once it starts, the gateway writes a heartbeat at least every
|
|
23
|
+
// 10 s for as long as the provider is working, so 90 s without a single byte means the
|
|
24
|
+
// connection is dead — while a long, healthy answer streams for as long as it needs.
|
|
25
|
+
// (A fixed 10-minute ceiling on the whole request used to cut long answers off and
|
|
26
|
+
// start them again from nothing.)
|
|
27
|
+
// (Overridable only so the tests can drive a stall in under a second.)
|
|
28
|
+
const FIRST_BYTE_TIMEOUT_MS = Number(process.env.JAITUS_FIRST_BYTE_MS) || 3 * 60 * 1000;
|
|
29
|
+
const STREAM_IDLE_TIMEOUT_MS = Number(process.env.JAITUS_STREAM_IDLE_MS) || 90 * 1000;
|
|
30
|
+
const API_TIMEOUT_MS = 30 * 1000; // dashboard calls are small JSON
|
|
31
|
+
|
|
32
|
+
// Combine an optional caller signal with a restartable deadline into one signal.
|
|
33
|
+
// Manual combine (not AbortSignal.any): .any needs Node 20.3+, this works everywhere.
|
|
34
|
+
function withTimeout(signal, ms) {
|
|
35
|
+
const ctrl = new AbortController();
|
|
36
|
+
let timedOut = false;
|
|
37
|
+
let timer = null;
|
|
38
|
+
const arm = (after) => {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
timer = setTimeout(() => {
|
|
41
|
+
timedOut = true;
|
|
42
|
+
try { ctrl.abort(new DOMException('Request timed out', 'TimeoutError')); }
|
|
43
|
+
catch { try { ctrl.abort(); } catch { /* already gone */ } }
|
|
44
|
+
}, after);
|
|
45
|
+
if (timer.unref) timer.unref();
|
|
46
|
+
};
|
|
47
|
+
arm(ms);
|
|
48
|
+
const onAbort = () => {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
try { ctrl.abort(signal.reason); } catch { try { ctrl.abort(); } catch { /* already gone */ } }
|
|
51
|
+
};
|
|
52
|
+
if (signal) {
|
|
53
|
+
if (signal.aborted) onAbort();
|
|
54
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
signal: ctrl.signal,
|
|
58
|
+
// True only when OUR timer fired. A user abort also lands here as an AbortError,
|
|
59
|
+
// but signal.aborted is set then — never mistake Esc for a timeout.
|
|
60
|
+
timedOut: () => timedOut && !signal?.aborted,
|
|
61
|
+
/** Start the deadline again from now (with a new length). */
|
|
62
|
+
restart: (after) => { if (!ctrl.signal.aborted) arm(after); },
|
|
63
|
+
cleanup: () => { clearTimeout(timer); signal?.removeEventListener?.('abort', onAbort); },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The response with its body watched for silence: every chunk restarts the idle
|
|
69
|
+
* deadline, and the end of the body (or the reader letting go of it) stops the timer
|
|
70
|
+
* and drops the caller's abort listener, so nothing outlives the request.
|
|
71
|
+
*/
|
|
72
|
+
function watchedResponse(res, t, idleMs) {
|
|
73
|
+
if (!res.body) { t.cleanup(); return res; }
|
|
74
|
+
t.restart(idleMs);
|
|
75
|
+
const body = res.body.pipeThrough(new TransformStream({
|
|
76
|
+
transform(chunk, controller) { t.restart(idleMs); controller.enqueue(chunk); },
|
|
77
|
+
flush() { t.cleanup(); },
|
|
78
|
+
cancel() { t.cleanup(); },
|
|
79
|
+
}));
|
|
80
|
+
return new Response(body, { status: res.status, statusText: res.statusText, headers: res.headers });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function timeoutError() {
|
|
84
|
+
return new JaitusError({ code: 'timeout', status: 504, message: 'Jaitus took too long to respond. Try again.' });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The context window the gateway last advertised, or null before it has been asked.
|
|
88
|
+
let contextWindowSeen = null;
|
|
89
|
+
export const advertisedContextWindow = () => contextWindowSeen;
|
|
90
|
+
|
|
91
|
+
export function createClient() {
|
|
92
|
+
const { baseUrl } = endpoints();
|
|
93
|
+
if (!signedIn()) throw new NotSignedIn('Not signed in. Run `jaitus login` first.');
|
|
94
|
+
|
|
95
|
+
// x-jaitus-features: what this build understands, so the gateway can use newer error
|
|
96
|
+
// codes (a used-up usage window) without an older CLI reading them as "went wrong".
|
|
97
|
+
const base = { 'Content-Type': 'application/json', 'user-agent': `jaitus-cli/${version()}`, 'x-jaitus-features': 'usage-window' };
|
|
98
|
+
// x-jaitus-device: this computer's hashed identifiers (machine.js), which the free
|
|
99
|
+
// tier is limited by. Built once per process.
|
|
100
|
+
const headers = (token) => ({ ...base, 'x-jaitus-device': machineHeader(), authorization: `Bearer ${token}` });
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
baseUrl,
|
|
104
|
+
|
|
105
|
+
/** POST /v1/messages. Returns the raw Response so the caller can stream it. */
|
|
106
|
+
async messages(body, { signal } = {}) {
|
|
107
|
+
const t = withTimeout(signal, FIRST_BYTE_TIMEOUT_MS);
|
|
108
|
+
let res;
|
|
109
|
+
try {
|
|
110
|
+
res = await withAuth((token) => fetch(`${baseUrl}/v1/messages`, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: headers(token),
|
|
113
|
+
body: JSON.stringify(body),
|
|
114
|
+
signal: t.signal,
|
|
115
|
+
}));
|
|
116
|
+
} catch (cause) {
|
|
117
|
+
t.cleanup();
|
|
118
|
+
if (cause?.name === 'JaitusError') throw cause;
|
|
119
|
+
if (t.timedOut()) throw timeoutError();
|
|
120
|
+
if (cause?.name === 'AbortError') throw cause;
|
|
121
|
+
throw fromNetwork(cause);
|
|
122
|
+
}
|
|
123
|
+
// A mid-body stall (headers fast, then nothing) aborts the body; the reader sees
|
|
124
|
+
// an AbortError while the caller's signal is NOT aborted, and turns it into a
|
|
125
|
+
// timeout — keeping whatever already arrived (agent/loop.js).
|
|
126
|
+
return watchedResponse(res, t, STREAM_IDLE_TIMEOUT_MS);
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/** A dashboard API call, returning parsed JSON. */
|
|
130
|
+
async api(pathname, init = {}) {
|
|
131
|
+
const t = withTimeout(init.signal, API_TIMEOUT_MS);
|
|
132
|
+
let res;
|
|
133
|
+
try {
|
|
134
|
+
res = await withAuth((token) => fetch(`${baseUrl}/api${pathname}`, {
|
|
135
|
+
...init,
|
|
136
|
+
headers: { ...headers(token), ...(init.headers || {}) },
|
|
137
|
+
signal: t.signal,
|
|
138
|
+
}));
|
|
139
|
+
const body = await res.json().catch(() => ({}));
|
|
140
|
+
return { ok: res.ok, status: res.status, body };
|
|
141
|
+
} catch (cause) {
|
|
142
|
+
if (cause?.name === 'JaitusError') throw cause;
|
|
143
|
+
if (t.timedOut()) throw timeoutError();
|
|
144
|
+
if (cause?.name === 'AbortError') throw cause;
|
|
145
|
+
throw fromNetwork(cause);
|
|
146
|
+
} finally {
|
|
147
|
+
t.cleanup();
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/** Model ids the gateway advertises. */
|
|
152
|
+
async models() {
|
|
153
|
+
const t = withTimeout(undefined, API_TIMEOUT_MS);
|
|
154
|
+
let res;
|
|
155
|
+
try {
|
|
156
|
+
res = await withAuth((token) => fetch(`${baseUrl}/v1/models`, { headers: headers(token), signal: t.signal }));
|
|
157
|
+
} catch (cause) {
|
|
158
|
+
t.cleanup();
|
|
159
|
+
if (cause?.name === 'JaitusError') throw cause;
|
|
160
|
+
if (t.timedOut()) throw timeoutError();
|
|
161
|
+
if (cause?.name === 'AbortError') throw cause;
|
|
162
|
+
throw fromNetwork(cause);
|
|
163
|
+
}
|
|
164
|
+
const body = await res.json().catch(() => ({}));
|
|
165
|
+
t.cleanup();
|
|
166
|
+
if (!res.ok) throw fromGatewayBody(res.status, body, { requestId: res.headers.get('x-request-id') || '' });
|
|
167
|
+
const models = (body?.data || []).map((m) => m?.id).filter(Boolean);
|
|
168
|
+
// How much conversation fits, as the gateway computed it for this account.
|
|
169
|
+
const ctx = (body?.data || []).map((m) => Number(m?.context_window)).find((n) => n > 0);
|
|
170
|
+
if (ctx) contextWindowSeen = ctx;
|
|
171
|
+
// An empty list is a service fault, not something the user can fix — and naming
|
|
172
|
+
// the gateway's model map to them would be naming our internals.
|
|
173
|
+
if (!models.length) throw new JaitusError({ code: 'unavailable', status: 502 });
|
|
174
|
+
return models;
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
package/src/commands/update.js
CHANGED
|
@@ -121,10 +121,15 @@ function run(cmd, args, { capture = false } = {}) {
|
|
|
121
121
|
export function installGlobal(v, { onTick = () => {}, onStart = () => {} } = {}) {
|
|
122
122
|
return new Promise((resolve) => {
|
|
123
123
|
const args = ['install', '-g', `jaitus@${v}`, '--no-audit', '--no-fund', '--loglevel=error'];
|
|
124
|
-
|
|
124
|
+
// Windows: `detached` gives the child a console of its OWN — the update opened a
|
|
125
|
+
// second terminal window. Instead npm runs in this window through `start /b`, which
|
|
126
|
+
// makes it ignore Ctrl+C (cmd's documented behaviour), so it still cannot be cut off
|
|
127
|
+
// halfway; delayed expansion hands npm's exit code back through `start /wait`.
|
|
128
|
+
// Elsewhere a detached process group does the same without any window.
|
|
125
129
|
const child = process.platform === 'win32'
|
|
126
|
-
? spawn(['
|
|
127
|
-
|
|
130
|
+
? spawn('cmd.exe', ['/d', '/v:on', '/s', '/c', `"start "" /b /wait npm ${args.join(' ')} & exit !errorlevel!"`],
|
|
131
|
+
{ stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true, windowsVerbatimArguments: true })
|
|
132
|
+
: spawn('npm', args, { stdio: ['ignore', 'ignore', 'pipe'], detached: true });
|
|
128
133
|
if (child.pid) onStart(child.pid);
|
|
129
134
|
let stderr = '';
|
|
130
135
|
child.stderr?.on('data', (d) => { stderr = (stderr + d).slice(-4000); });
|