jaitus 0.3.6 → 0.3.8
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 +82 -6
- package/src/agent/permissions.js +3 -1
- package/src/agent/rules.js +27 -3
- package/src/agent/tools.js +39 -2
- package/src/api.js +3 -1
- package/src/errors.js +2 -1
- package/src/index.js +187 -44
- package/src/sessions.js +22 -1
- package/src/skills/builtin/documents/SKILL.md +91 -0
- package/src/skills/builtin/frontend-design/SKILL.md +102 -0
- package/src/skills.js +6 -1
- package/src/tui/entries.js +36 -5
- package/src/tui/widgets.js +32 -0
- package/src/tui-ink.js +99 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jaitus",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
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/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/continuation.js && node tests/steps.js && node tests/files.js && node tests/processes.js && node tests/security.js && node tests/context-budget.js",
|
|
19
|
+
"test": "node tests/source-sanity.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/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",
|
|
20
20
|
"preview": "node scripts/tui-preview.js",
|
|
21
21
|
"prepublishOnly": "npm test"
|
|
22
22
|
},
|
package/src/agent/loop.js
CHANGED
|
@@ -106,6 +106,9 @@ How to work:
|
|
|
106
106
|
- Run the project's own tests or build after a change when one exists, and report the real result — including when it fails.
|
|
107
107
|
- Use run_command for anything the shell does better: git, tests, package managers, linters.
|
|
108
108
|
- Never invent a file path, a function name, or a test result. If you are unsure, look.
|
|
109
|
+
- Every turn re-sends the whole conversation, so make independent reads, searches and
|
|
110
|
+
listings together in ONE turn rather than one per turn. Read the part of a large file you
|
|
111
|
+
need (offset/limit) rather than all of it.
|
|
109
112
|
- For current news or other web facts, report only details grounded in web_search or
|
|
110
113
|
web_fetch output. Include the supplied source URLs; never fill a missing headline or
|
|
111
114
|
date from memory.
|
|
@@ -229,6 +232,69 @@ export function repairToolPairs(messages) {
|
|
|
229
232
|
return messages;
|
|
230
233
|
}
|
|
231
234
|
|
|
235
|
+
// ── Keeping the conversation small ────────────────────────────────────────────
|
|
236
|
+
//
|
|
237
|
+
// Every request re-sends the whole conversation, and in real use that re-send — cached,
|
|
238
|
+
// but not free — was ~75% of what a session cost: 109 requests averaged 46k tokens of
|
|
239
|
+
// context each, most of it tool output from many steps ago (a file read at step 3 is
|
|
240
|
+
// still paid for at step 60). This is the documented "clear old tool results" strategy
|
|
241
|
+
// (Anthropic's context editing, clear_tool_uses; Claude Code's micro-compaction): once
|
|
242
|
+
// the conversation passes TRIM_AT, the outputs of all but the KEEP_RECENT latest tool
|
|
243
|
+
// calls are replaced with a line saying they were cleared and can be re-run. It clears
|
|
244
|
+
// at least TRIM_AT_LEAST at a time, because each clearing breaks the prompt cache once
|
|
245
|
+
// — clearing a little on every step would cost more than it saves. Small results (a
|
|
246
|
+
// "file written", an exit code) are kept: they carry meaning and cost nothing.
|
|
247
|
+
// 100k was the documented default for 200k-window models. Here the window is up to 1M, so
|
|
248
|
+
// that ceiling was rarely near: production requests averaged 53k tokens and two thirds of
|
|
249
|
+
// all input tokens came from requests over 60k. At 40k a clearing (one cache break, ~17
|
|
250
|
+
// credits on glm-5.3-flash) pays for itself within about ten later requests.
|
|
251
|
+
export const TRIM_AT = 40_000; // estimated tokens of conversation
|
|
252
|
+
export const KEEP_RECENT = 6; // latest tool results always kept whole
|
|
253
|
+
export const TRIM_AT_LEAST = 12_000; // estimated tokens a clearing must free
|
|
254
|
+
const TRIM_MIN_CHARS = 1_500; // results shorter than this are never cleared
|
|
255
|
+
const CLEARED = '[Output cleared';
|
|
256
|
+
|
|
257
|
+
const blockChars = (b) => {
|
|
258
|
+
if (b?.type === 'text') return (b.text || '').length;
|
|
259
|
+
if (b?.type === 'tool_use') return JSON.stringify(b.input || {}).length;
|
|
260
|
+
if (b?.type === 'tool_result') return typeof b.content === 'string' ? b.content.length : JSON.stringify(b.content || '').length;
|
|
261
|
+
return 0;
|
|
262
|
+
};
|
|
263
|
+
export function conversationTokens(messages) {
|
|
264
|
+
let chars = 0;
|
|
265
|
+
for (const m of messages || []) chars += typeof m?.content === 'string' ? m.content.length : (Array.isArray(m?.content) ? m.content.reduce((n, b) => n + blockChars(b), 0) : 0);
|
|
266
|
+
return Math.ceil(chars / 4);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Clear old tool outputs in place when the conversation is large. Returns how many
|
|
271
|
+
* estimated tokens were freed (0 when nothing was worth clearing).
|
|
272
|
+
*/
|
|
273
|
+
export function clearOldToolResults(messages, { at = TRIM_AT, keep = KEEP_RECENT, atLeast = TRIM_AT_LEAST } = {}) {
|
|
274
|
+
if (conversationTokens(messages) < at) return 0;
|
|
275
|
+
const names = new Map();
|
|
276
|
+
for (const m of messages) {
|
|
277
|
+
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);
|
|
278
|
+
}
|
|
279
|
+
const results = [];
|
|
280
|
+
for (const m of messages) {
|
|
281
|
+
if (m?.role !== 'user' || !Array.isArray(m.content)) continue;
|
|
282
|
+
m.content.forEach((b, i) => { if (b?.type === 'tool_result') results.push({ m, i, b }); });
|
|
283
|
+
}
|
|
284
|
+
const candidates = results.slice(0, Math.max(0, results.length - keep)).filter(({ b }) => {
|
|
285
|
+
const text = typeof b.content === 'string' ? b.content : '';
|
|
286
|
+
return text.length >= TRIM_MIN_CHARS && !text.startsWith(CLEARED);
|
|
287
|
+
});
|
|
288
|
+
const freed = Math.ceil(candidates.reduce((n, { b }) => n + blockChars(b), 0) / 4);
|
|
289
|
+
if (freed < atLeast) return 0;
|
|
290
|
+
for (const { m, i, b } of candidates) {
|
|
291
|
+
const chars = blockChars(b);
|
|
292
|
+
const name = names.get(b.tool_use_id) || 'this tool';
|
|
293
|
+
m.content[i] = { ...b, content: `${CLEARED}: ${name} returned ${chars.toLocaleString()} characters here, removed to keep the conversation small. Run it again if you need it.]` };
|
|
294
|
+
}
|
|
295
|
+
return freed;
|
|
296
|
+
}
|
|
297
|
+
|
|
232
298
|
export const IMAGE_BUDGET = 6_000_000;
|
|
233
299
|
export function withinImageBudget(messages, budget = IMAGE_BUDGET) {
|
|
234
300
|
let used = 0;
|
|
@@ -273,14 +339,16 @@ async function streamTurn({ client, messages, model, system, tools, onText, onTo
|
|
|
273
339
|
// — no message_stop, no stop reason — was cut, and used to be taken as a complete,
|
|
274
340
|
// silent answer: the "it stops midway for no reason" bug.
|
|
275
341
|
let sawStop = false;
|
|
276
|
-
|
|
342
|
+
// `credits` is what the gateway counted against the plan for this request — real cost,
|
|
343
|
+
// the one unit the plan, the dashboard and this CLI all show.
|
|
344
|
+
const usage = { input_tokens: 0, output_tokens: 0, cache_read_input_tokens: 0, cache_creation_input_tokens: 0, credits: 0 };
|
|
277
345
|
// Usage can arrive at the START of a stream (Anthropic-format providers) or only at
|
|
278
346
|
// the END, in message_delta (OpenAI-format providers, translated by the gateway —
|
|
279
347
|
// their usage exists only once generation finishes). Reading input from message_start
|
|
280
348
|
// alone recorded every OpenAI-format turn as 0 input tokens.
|
|
281
349
|
const takeUsage = (u) => {
|
|
282
350
|
if (!u) return;
|
|
283
|
-
for (const k of ['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens']) {
|
|
351
|
+
for (const k of ['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens', 'credits']) {
|
|
284
352
|
if (u[k] != null && Number.isFinite(Number(u[k]))) usage[k] = Number(u[k]);
|
|
285
353
|
}
|
|
286
354
|
};
|
|
@@ -395,7 +463,7 @@ export async function runAgent({
|
|
|
395
463
|
client, messages, model, cwd, permissions, signal, tui = null,
|
|
396
464
|
extraSystem = '', maxIterations = MAX_ITERATIONS, silent = false,
|
|
397
465
|
onText = null, onEvent = null, systemOverride = null, journal = null,
|
|
398
|
-
subagents = true, onContext = null,
|
|
466
|
+
subagents = true, onContext = null, trimContext = true,
|
|
399
467
|
}) {
|
|
400
468
|
// Loop detection. A call is identified by its tool, its input, and how many files had
|
|
401
469
|
// been changed when it was made — so edit → test → edit → test is progress (the count
|
|
@@ -404,7 +472,7 @@ export async function runAgent({
|
|
|
404
472
|
const repeats = new Map();
|
|
405
473
|
// input = fresh input, cached = prompt-cache reads (billed far cheaper), and
|
|
406
474
|
// context = the real size of the last prompt sent, as the provider counted it.
|
|
407
|
-
const totals = { input: 0, cached: 0, output: 0, tools: 0, context: 0 };
|
|
475
|
+
const totals = { input: 0, cached: 0, output: 0, tools: 0, context: 0, credits: 0 };
|
|
408
476
|
// Read once per request, not per turn: an agent that rewrites JAITUS.md mid-task
|
|
409
477
|
// should not change the rules it is being judged by halfway through.
|
|
410
478
|
// The model is told when it is in plan mode — it used to find out only by having its
|
|
@@ -438,6 +506,12 @@ export async function runAgent({
|
|
|
438
506
|
let continuing = false;
|
|
439
507
|
|
|
440
508
|
for (let iteration = 0; iteration < maxIterations; iteration++) {
|
|
509
|
+
// Old tool outputs are cleared once the conversation is large (see TRIM_AT) — in
|
|
510
|
+
// place, so the saved session shrinks too and the cache stays warm afterwards.
|
|
511
|
+
if (trimContext) {
|
|
512
|
+
const freed = clearOldToolResults(messages);
|
|
513
|
+
if (freed && tui?.notice) tui.notice(`Cleared old tool output (~${Math.round(freed / 1000)}k tokens) to keep each request small.`, 'info');
|
|
514
|
+
}
|
|
441
515
|
let streamed = false;
|
|
442
516
|
const spin = tui ? { stop: () => {}, update: () => {} } : (silent ? { stop: () => {}, update: () => {} } : spinner(glyph('Working…', 'Working...')));
|
|
443
517
|
if (tui) tui.setStatus(`${c.brand(glyph('•', '*'))} ${c.brand(glyph('Working…', 'Working...'))}`);
|
|
@@ -532,6 +606,7 @@ export async function runAgent({
|
|
|
532
606
|
totals.input += turn.usage.input_tokens || 0;
|
|
533
607
|
totals.cached += (turn.usage.cache_read_input_tokens || 0) + (turn.usage.cache_creation_input_tokens || 0);
|
|
534
608
|
totals.output += turn.usage.output_tokens || 0;
|
|
609
|
+
totals.credits += Number(turn.usage.credits) || 0;
|
|
535
610
|
const promptSize = (turn.usage.input_tokens || 0) + (turn.usage.cache_read_input_tokens || 0) + (turn.usage.cache_creation_input_tokens || 0);
|
|
536
611
|
if (promptSize) {
|
|
537
612
|
totals.context = promptSize + (turn.usage.output_tokens || 0);
|
|
@@ -708,7 +783,8 @@ export async function runAgent({
|
|
|
708
783
|
const done = ` ${toolMark(call.name)} ${c.dim(String(label).slice(0, 100))} ${c.dim(`· ${took}s`)}`;
|
|
709
784
|
if (row) {
|
|
710
785
|
tui.clearStatus();
|
|
711
|
-
|
|
786
|
+
// The whole output travels with the row (capped), so a click can open it.
|
|
787
|
+
row.update({ status: 'ok', took, preview: previewFor(call.name, output), full: String(output ?? '').slice(0, 200_000) });
|
|
712
788
|
} else if (tui) { tui.clearStatus(); tui.print(done, 'tool'); } else if (!silent) out(done);
|
|
713
789
|
tui?.recordActivity?.({ id: activityId, name: call.name, label, status: 'ok', detail: String(output).slice(0, 2000) });
|
|
714
790
|
onEvent?.({ type: 'tool_result', name: call.name, input: call.input, ok: true, output });
|
|
@@ -761,7 +837,7 @@ export async function runAgent({
|
|
|
761
837
|
const bad = ` ${c.red(glyph('✗', 'x'))} ${c.dim(String(label).slice(0, 80))} ${c.dim(`· ${took}s`)} — ${msg.slice(0, 120)}`;
|
|
762
838
|
if (row) {
|
|
763
839
|
tui.clearStatus();
|
|
764
|
-
row.update({ status: 'failed', took, error: msg.split('\n')[0].slice(0, 200), preview: msg.includes('\n') ? msg : '' });
|
|
840
|
+
row.update({ status: 'failed', took, error: msg.split('\n')[0].slice(0, 200), preview: msg.includes('\n') ? msg : '', full: msg.slice(0, 200_000) });
|
|
765
841
|
} else if (tui) { tui.clearStatus(); tui.print(bad, 'tool'); } else if (!silent) out(bad);
|
|
766
842
|
tui?.recordActivity?.({ id: activityId, name: call.name, label, status: 'failed', detail: msg.slice(0, 2000) });
|
|
767
843
|
onEvent?.({ type: 'tool_result', name: call.name, input: call.input, ok: false, error: msg });
|
package/src/agent/permissions.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { c, out, askKey, glyph } from '../ui.js';
|
|
19
|
-
import { decide, ruleForCall, broaderRule, DEFAULT_DENY, subjectOf } from './rules.js';
|
|
19
|
+
import { decide, ruleForCall, broaderRule, DEFAULT_DENY, subjectOf, wideAllows } from './rules.js';
|
|
20
20
|
|
|
21
21
|
export class PermissionDenied extends Error {}
|
|
22
22
|
|
|
@@ -44,6 +44,8 @@ export function createPermissions({
|
|
|
44
44
|
const api = {
|
|
45
45
|
get mode() { return current; },
|
|
46
46
|
get rules() { return { allow: [...savedAllow], deny: [...savedDeny], session: [...sessionAllow] }; },
|
|
47
|
+
/** Saved rules that take a whole tool out of the mode's hands (see wideAllows). */
|
|
48
|
+
get wide() { return wideAllows(savedAllow); },
|
|
47
49
|
|
|
48
50
|
/** Add a rule. `persist` writes it to settings via onRulesChange. */
|
|
49
51
|
addRule(rule, { persist = false, kind = 'allow' } = {}) {
|
package/src/agent/rules.js
CHANGED
|
@@ -206,15 +206,39 @@ export function ruleForCall(toolName, input, cwd) {
|
|
|
206
206
|
return `${toolName}(${relative && !relative.startsWith('..') ? relative : subject})`;
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* A broader rule, offered beside the exact one: the file's directory. Never the whole
|
|
211
|
+
* tool. "Always allow every run command" used to be the second option on every command
|
|
212
|
+
* prompt; one click on it saved a rule that approves every command in that folder in
|
|
213
|
+
* every mode, forever — after which Ask, Auto-edit and Bypass all behaved the same and
|
|
214
|
+
* nothing on screen said why. A tool-wide rule can still be written on purpose with
|
|
215
|
+
* `/permissions allow run_command`; it is just never one click away.
|
|
216
|
+
*/
|
|
210
217
|
export function broaderRule(toolName, input, cwd) {
|
|
211
218
|
const subject = subjectOf(toolName, input);
|
|
212
219
|
if (['write_file', 'edit_file', 'read_file', 'read_image', 'list_files'].includes(toolName) && subject) {
|
|
213
220
|
const relative = cwd ? normalise(path.relative(cwd, path.resolve(cwd, subject))) : subject;
|
|
214
221
|
const dir = relative.includes('/') ? relative.slice(0, relative.lastIndexOf('/')) : '';
|
|
215
|
-
|
|
222
|
+
if (dir && !dir.startsWith('..')) return `${toolName}(${dir}/**)`;
|
|
223
|
+
}
|
|
224
|
+
return ruleForCall(toolName, input, cwd);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Saved allow rules that approve a whole tool — `run_command`, `edit_file(**)` — for the
|
|
229
|
+
* tools a mode is about. While one exists, the mode no longer decides that tool, so the
|
|
230
|
+
* mode's label is only half the truth; callers say so beside it.
|
|
231
|
+
*/
|
|
232
|
+
export function wideAllows(allow = []) {
|
|
233
|
+
const GATED = ['run_command', 'background_run', 'write_file', 'edit_file'];
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const rule of allow) {
|
|
236
|
+
const parsed = typeof rule === 'string' ? parseRule(rule) : rule;
|
|
237
|
+
if (!parsed || !GATED.includes(parsed.tool)) continue;
|
|
238
|
+
const p = parsed.pattern;
|
|
239
|
+
if (p === null || p === '' || p === '*' || p === '**' || p === '**/*') out.push(parsed.source);
|
|
216
240
|
}
|
|
217
|
-
return
|
|
241
|
+
return out;
|
|
218
242
|
}
|
|
219
243
|
|
|
220
244
|
/**
|
package/src/agent/tools.js
CHANGED
|
@@ -26,6 +26,18 @@ const MAX_READ_LINES = 2_000;
|
|
|
26
26
|
|
|
27
27
|
export class ToolError extends Error {}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Keep the start AND the end. For a test run or a build the end is where the failures and
|
|
31
|
+
* the summary are; cutting from the front kept the noise and threw away the answer.
|
|
32
|
+
*/
|
|
33
|
+
function truncateMiddle(text, limit) {
|
|
34
|
+
const s = String(text ?? '');
|
|
35
|
+
if (s.length <= limit) return s;
|
|
36
|
+
const head = Math.floor(limit / 3);
|
|
37
|
+
const tail = limit - head;
|
|
38
|
+
return `${s.slice(0, head)}\n\n[… ${s.length - limit} characters cut from the middle …]\n\n${s.slice(-tail)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
function truncate(text, limit = MAX_OUTPUT) {
|
|
30
42
|
const s = String(text ?? '');
|
|
31
43
|
if (s.length <= limit) return s;
|
|
@@ -560,8 +572,10 @@ export const TOOLS = [
|
|
|
560
572
|
// the joined text cut the exit code off the end of any long output, so the
|
|
561
573
|
// model could not tell whether the command had worked.
|
|
562
574
|
const parts = [];
|
|
563
|
-
|
|
564
|
-
|
|
575
|
+
// 30k in all (the common agent limit for shell output): every later request
|
|
576
|
+
// re-sends it until it is cleared.
|
|
577
|
+
const stdout = truncateMiddle(out.text().trim(), 22_000);
|
|
578
|
+
const stderr = truncateMiddle(err.text().trim(), 8_000);
|
|
565
579
|
if (stdout) parts.push(stdout);
|
|
566
580
|
if (stderr) parts.push(`stderr:\n${stderr}`);
|
|
567
581
|
if (note) parts.push(note);
|
|
@@ -832,6 +846,29 @@ export const TOOLS = [
|
|
|
832
846
|
return `Stopped ${input.id}.`;
|
|
833
847
|
},
|
|
834
848
|
},
|
|
849
|
+
|
|
850
|
+
{
|
|
851
|
+
// The model loads a skill's instructions itself when a task matches one. Only each
|
|
852
|
+
// skill's one-line description rides along with every request; the body (a few
|
|
853
|
+
// thousand tokens) is paid for only in conversations that need it.
|
|
854
|
+
name: 'load_skill',
|
|
855
|
+
description: 'Load the full instructions of a skill listed under "Skills" in the system prompt. Call it BEFORE starting a task that matches a skill\'s description, then follow what it says.',
|
|
856
|
+
needsPermission: false,
|
|
857
|
+
input_schema: {
|
|
858
|
+
type: 'object',
|
|
859
|
+
properties: { name: { type: 'string', description: 'The skill name, e.g. frontend-design.' } },
|
|
860
|
+
required: ['name'],
|
|
861
|
+
},
|
|
862
|
+
summarise: (i) => `skill ${i.name}`,
|
|
863
|
+
async run(input, { cwd }) {
|
|
864
|
+
const { discoverSkills } = await import('../skills.js');
|
|
865
|
+
const skills = discoverSkills(cwd || process.cwd());
|
|
866
|
+
const name = String(input.name || '').replace(/^\//, '').trim();
|
|
867
|
+
const skill = skills[name];
|
|
868
|
+
if (!skill) throw new ToolError(`No skill named ${name}. Available: ${Object.keys(skills).join(', ') || 'none'}.`);
|
|
869
|
+
return `Skill: ${skill.name}\n\n${skill.content}`;
|
|
870
|
+
},
|
|
871
|
+
},
|
|
835
872
|
];
|
|
836
873
|
|
|
837
874
|
/** The live task list todo_write maintains; the session renders it in the panel. */
|
package/src/api.js
CHANGED
|
@@ -18,7 +18,9 @@ export function createClient() {
|
|
|
18
18
|
const { baseUrl } = endpoints();
|
|
19
19
|
if (!signedIn()) throw new NotSignedIn('Not signed in. Run `jaitus login` first.');
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
// x-jaitus-features: what this build understands, so the gateway can use newer error
|
|
22
|
+
// codes (a used-up usage window) without an older CLI reading them as "went wrong".
|
|
23
|
+
const base = { 'Content-Type': 'application/json', 'user-agent': 'jaitus-cli', 'x-jaitus-features': 'usage-window' };
|
|
22
24
|
const headers = (token) => ({ ...base, authorization: `Bearer ${token}` });
|
|
23
25
|
|
|
24
26
|
return {
|
package/src/errors.js
CHANGED
|
@@ -18,7 +18,8 @@ const CATALOG = {
|
|
|
18
18
|
rate_limited: { title: 'Slow down', hint: 'Wait a moment, then retry.' },
|
|
19
19
|
context_too_long: { title: 'Conversation too long', hint: 'Run /compact, or start a new session.' },
|
|
20
20
|
request_too_large: { title: 'Request too large', hint: 'Usually images: remove some, or run /compact to drop old ones. Images over 3.5 MB are shrunk automatically.' },
|
|
21
|
-
daily_limit_reached: { title: '
|
|
21
|
+
daily_limit_reached: { title: 'Usage limit reached', hint: 'It refills automatically. /usage shows where you are.' },
|
|
22
|
+
window_limit_reached: { title: 'This usage window is used up', hint: 'It refills when the window ends — the rest of your month is still there. /usage shows when.' },
|
|
22
23
|
token_expired: { title: 'Signed out', hint: 'Type /login to sign in again (or run `jaitus login`).' },
|
|
23
24
|
refresh_race: { title: 'Sign-in was being renewed', hint: 'Try again.' },
|
|
24
25
|
content_blocked: { title: 'Request refused', hint: 'Rephrase the request and try again.' },
|