jaitus 0.3.19 → 0.3.21
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/LICENSE +1 -1
- package/README.md +66 -30
- package/package.json +1 -1
- package/src/agent/loop.js +10 -11
- package/src/api.js +3 -3
- package/src/brand.js +233 -232
- package/src/commands/doctor.js +11 -10
- package/src/commands/login.js +3 -3
- package/src/commands/logout.js +1 -1
- package/src/commands/update.js +11 -2
- package/src/errors.js +10 -9
- package/src/index.js +44 -44
- package/src/logo-data.js +2 -2
- package/src/planView.js +89 -29
- package/src/skills.js +1 -1
- package/src/tui/entries.js +2 -2
- package/src/tui/theme.js +21 -21
- package/src/tui/wordmark.js +1 -1
- package/src/tui-ink.js +2941 -2921
- package/src/tui.js +2556 -2560
- package/src/ui.js +11 -11
package/src/planView.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// ─── How a plan reads to the person using it: percentages, never credits ───────
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// Claude's /usage reads. /usage, /cost, /status, `jaitus whoami`, `jaitus usage`, the
|
|
3
|
+
// The server counts usage in its own internal unit, which is never shown to a customer.
|
|
4
|
+
// Everything the CLI shows about a plan is a percentage of the 5-hour window, the week
|
|
5
|
+
// or the month, with when each resets, in the usual /usage layout. /usage, /cost, /status, `jaitus whoami`, `jaitus usage`, the
|
|
7
6
|
// status bar and the work bar all come from here, so no screen can say it differently.
|
|
8
7
|
|
|
9
8
|
const pctOf = (used, total) => (Number(total) > 0
|
|
@@ -52,15 +51,16 @@ export function planPercents(st) {
|
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
/**
|
|
55
|
-
* A spend in credits (one turn, one session) as a share of the 5-hour
|
|
56
|
-
* the
|
|
54
|
+
* A spend in credits (one turn, one session) as a share of the 5-hour window, the week
|
|
55
|
+
* and the month (while that limit is on) — null for any the plan does not have.
|
|
57
56
|
*/
|
|
58
57
|
export function spendShare(credits, st) {
|
|
59
58
|
const c = Number(credits) || 0;
|
|
60
|
-
if (!st || c <= 0) return { window: null, week: null };
|
|
59
|
+
if (!st || c <= 0) return { window: null, week: null, month: null };
|
|
61
60
|
return {
|
|
62
61
|
window: Number(st.windowCredits) > 0 ? (100 * c) / st.windowCredits : null,
|
|
63
62
|
week: Number(st.weeklyCredits) > 0 ? (100 * c) / st.weeklyCredits : null,
|
|
63
|
+
month: monthShown(st) && Number(st.monthlyCredits) > 0 ? (100 * c) / st.monthlyCredits : null,
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -71,32 +71,40 @@ export const rememberPlan = (st) => { if (st && typeof st === 'object') lastPlan
|
|
|
71
71
|
export const knownPlan = () => lastPlan;
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
* What the status bar and the work bar need: the
|
|
75
|
-
*
|
|
74
|
+
* What the status bar and the work bar need: the percentages (5-hour window, week, and
|
|
75
|
+
* the month when that limit is on), and the two budgets a turn's spend is measured
|
|
76
|
+
* against. null for an account with no plan limits.
|
|
76
77
|
*/
|
|
77
78
|
export function planBadge(st) {
|
|
78
79
|
const p = planPercents(st);
|
|
79
|
-
if (!p || (p.window == null && p.week == null)) return null;
|
|
80
|
+
if (!p || (p.window == null && p.week == null && p.month == null)) return null;
|
|
80
81
|
return {
|
|
81
|
-
window: p.window, week: p.week, windowHours: p.windowHours,
|
|
82
|
+
window: p.window, week: p.week, month: p.month, windowHours: p.windowHours,
|
|
82
83
|
windowCredits: Number(st.windowCredits) || 0, weeklyCredits: Number(st.weeklyCredits) || 0,
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
/** "5h 12% · week 3%" for the status bar. */
|
|
87
|
+
/** "5h 12% · week 3% · month 18%" for the status bar (month only while that limit is on). */
|
|
87
88
|
export function badgeText(b) {
|
|
88
89
|
if (!b) return '';
|
|
89
90
|
const parts = [];
|
|
90
91
|
if (b.window != null) parts.push(`${b.windowHours || 5}h ${b.window}%`);
|
|
91
92
|
if (b.week != null) parts.push(`week ${b.week}%`);
|
|
93
|
+
if (b.month != null) parts.push(`month ${b.month}%`);
|
|
92
94
|
return parts.join(' · ');
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
/**
|
|
97
|
+
/** The highest of the badge's percentages — what its colour follows. */
|
|
98
|
+
export function badgeTop(b) {
|
|
99
|
+
if (!b) return 0;
|
|
100
|
+
return Math.max(b.window ?? 0, b.week ?? 0, b.month ?? 0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** What one turn has used so far, for the work bar: "0.6% of 5h window". '' when unknown. */
|
|
96
104
|
export function turnShareText(credits, b) {
|
|
97
105
|
const c = Number(credits) || 0;
|
|
98
106
|
if (!b || c <= 0) return '';
|
|
99
|
-
if (b.windowCredits > 0) return `${fmtShare((100 * c) / b.windowCredits)} of
|
|
107
|
+
if (b.windowCredits > 0) return `${fmtShare((100 * c) / b.windowCredits)} of ${b.windowHours || 5}h window`;
|
|
100
108
|
if (b.weeklyCredits > 0) return `${fmtShare((100 * c) / b.weeklyCredits)} of week`;
|
|
101
109
|
return '';
|
|
102
110
|
}
|
|
@@ -114,10 +122,48 @@ function extraLine(p) {
|
|
|
114
122
|
: `available — used once your free prompts run out${until}`;
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
/** 600000 → "600k", 1500000 → "1.5M", for the free allowance. */
|
|
126
|
+
function shortTokens(n) {
|
|
127
|
+
const v = Math.max(0, Number(n) || 0);
|
|
128
|
+
if (v >= 1e6) return `${+(v / 1e6).toFixed(1)}M`;
|
|
129
|
+
if (v >= 1e3) return `${Math.round(v / 1e3)}k`;
|
|
130
|
+
return String(Math.round(v));
|
|
131
|
+
}
|
|
132
|
+
|
|
117
133
|
/**
|
|
118
|
-
* The
|
|
119
|
-
*
|
|
120
|
-
*
|
|
134
|
+
* The free allowance as the server counts it: prompts AND tokens, whichever runs out
|
|
135
|
+
* first. `percent` is the larger of the two shares. The token half is left out when the
|
|
136
|
+
* server does not send a token cap (an older server).
|
|
137
|
+
*/
|
|
138
|
+
export function freeAllowance(u) {
|
|
139
|
+
const usedPrompts = Number(u?.freeRequestsUsed) || 0;
|
|
140
|
+
const limitPrompts = Number(u?.freeRequestLimit) || 0;
|
|
141
|
+
const limitTokens = Number(u?.freeTokenLimit) || 0;
|
|
142
|
+
const usedTokens = Number(u?.totalTokensConsumed) || 0;
|
|
143
|
+
const promptPart = `${usedPrompts} of ${limitPrompts || '?'} prompts`;
|
|
144
|
+
const tokenPart = limitTokens > 0 ? `${shortTokens(Math.min(usedTokens, limitTokens))} of ${shortTokens(limitTokens)} tokens` : '';
|
|
145
|
+
const shares = [limitPrompts > 0 ? usedPrompts / limitPrompts : 0, limitTokens > 0 ? usedTokens / limitTokens : 0];
|
|
146
|
+
return {
|
|
147
|
+
text: tokenPart ? `${promptPart} · ${tokenPart}` : promptPart,
|
|
148
|
+
percent: Math.min(100, Math.floor(100 * Math.max(...shares))),
|
|
149
|
+
both: Boolean(tokenPart),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The GitHub bonus offer, or null once GitHub is linked (or the server sends no bonus). */
|
|
154
|
+
function githubOffer(u) {
|
|
155
|
+
if (u?.github !== null) return null; // undefined = an older server that does not say
|
|
156
|
+
const prompts = Number(u?.githubBonusPrompts) || 0;
|
|
157
|
+
const tokens = Number(u?.githubBonusTokens) || 0;
|
|
158
|
+
if (!prompts && !tokens) return null;
|
|
159
|
+
const gets = [prompts ? `+${prompts} prompts` : '', tokens ? `+${shortTokens(tokens)} tokens` : ''].filter(Boolean).join(' and ');
|
|
160
|
+
return `Link GitHub at jaitus.com for ${gets}.`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* The plan as meters for /usage (popup) and `jaitus usage` (text): the 5-hour window,
|
|
165
|
+
* the week and the month fill at their own rates from the same requests, so each gets
|
|
166
|
+
* its own bar and its own reset time. Extra usage and the free tier follow.
|
|
121
167
|
*/
|
|
122
168
|
export function planSections(u) {
|
|
123
169
|
const st = u?.planStatus || null;
|
|
@@ -125,15 +171,18 @@ export function planSections(u) {
|
|
|
125
171
|
if (!u?.isPaid) {
|
|
126
172
|
out.push({ heading: 'Free plan' });
|
|
127
173
|
if (st?.freeTierStatus === 'locked') {
|
|
128
|
-
out.push({ label: 'Free prompts', value: 'not available — this computer already has
|
|
174
|
+
out.push({ label: 'Free prompts', value: 'not available — another account on this computer already has them. Subscribe at jaitus.com/billing.', tone: 'bad' });
|
|
129
175
|
} else {
|
|
130
|
-
|
|
176
|
+
const free = freeAllowance(u);
|
|
177
|
+
out.push({ meter: 'Free allowance', used: free.percent, total: 100, detail: free.both ? `${free.text} — whichever runs out first` : `${free.text} used` });
|
|
178
|
+
const offer = githubOffer(u);
|
|
179
|
+
if (offer) out.push({ label: 'More free use', value: offer });
|
|
131
180
|
}
|
|
132
181
|
} else {
|
|
133
182
|
out.push({ heading: st?.name || 'Your plan' });
|
|
134
183
|
const p = planPercents(st);
|
|
135
184
|
if (p?.window != null) {
|
|
136
|
-
out.push({ meter: `${p.windowHours}-hour
|
|
185
|
+
out.push({ meter: `${p.windowHours}-hour window`, used: p.window, total: 100, note: p.windowResetsAt ? `resets at ${clock(p.windowResetsAt)}` : 'starts with your next request' });
|
|
137
186
|
}
|
|
138
187
|
if (p?.week != null) {
|
|
139
188
|
out.push({ meter: 'Week', used: p.week, total: 100, note: p.weekResetsAt ? `resets ${dayAndClock(p.weekResetsAt)}` : 'starts with your next request' });
|
|
@@ -141,19 +190,20 @@ export function planSections(u) {
|
|
|
141
190
|
if (p?.month != null) {
|
|
142
191
|
out.push({ meter: 'Month', used: p.month, total: 100, note: p.monthResetsAt ? `resets ${shortDate(p.monthResetsAt)}` : '' });
|
|
143
192
|
}
|
|
144
|
-
if (u.planExpiresAt) out.push({ label: '
|
|
193
|
+
if (u.planExpiresAt) out.push({ label: 'Plan ends', value: longDate(u.planExpiresAt) });
|
|
145
194
|
}
|
|
146
195
|
const extra = extraLine(planPercents(st));
|
|
147
196
|
if (extra) out.push({ label: 'Extra usage', value: extra, tone: 'good' });
|
|
148
197
|
return out;
|
|
149
198
|
}
|
|
150
199
|
|
|
151
|
-
/** A session's spend as plan shares: "2.1% of a 5-hour
|
|
200
|
+
/** A session's spend as plan shares: "2.1% of a 5-hour window · 0.5% of your week". */
|
|
152
201
|
export function spendText(credits, st = lastPlan) {
|
|
153
202
|
const s = spendShare(credits, st);
|
|
154
203
|
const parts = [];
|
|
155
|
-
if (s.window != null) parts.push(`${fmtShare(s.window)} of a 5-hour
|
|
204
|
+
if (s.window != null) parts.push(`${fmtShare(s.window)} of a ${st?.windowHours || 5}-hour window`);
|
|
156
205
|
if (s.week != null) parts.push(`${fmtShare(s.week)} of your week`);
|
|
206
|
+
if (s.month != null) parts.push(`${fmtShare(s.month)} of your month`);
|
|
157
207
|
return parts.join(' · ');
|
|
158
208
|
}
|
|
159
209
|
|
|
@@ -166,7 +216,7 @@ export function costSections(spent, st = lastPlan) {
|
|
|
166
216
|
{ label: 'Tool calls', value: String(spent?.tools || 0) },
|
|
167
217
|
{ gap: true },
|
|
168
218
|
{ label: 'Fresh input', value: `${(spent?.input || 0).toLocaleString()} tokens` },
|
|
169
|
-
{ label: 'Cached input', value: `${(spent?.cached || 0).toLocaleString()} tokens (
|
|
219
|
+
{ label: 'Cached input', value: `${(spent?.cached || 0).toLocaleString()} tokens (uses less of your plan than fresh input)` },
|
|
170
220
|
{ label: 'Output', value: `${(spent?.output || 0).toLocaleString()} tokens` },
|
|
171
221
|
{ gap: true },
|
|
172
222
|
{ label: '', value: '/usage for your plan' },
|
|
@@ -176,15 +226,20 @@ export function costSections(spent, st = lastPlan) {
|
|
|
176
226
|
/** The plan as rows for `jaitus whoami`. */
|
|
177
227
|
export function planRows(u) {
|
|
178
228
|
const st = u?.planStatus || null;
|
|
179
|
-
if (!u?.isPaid)
|
|
229
|
+
if (!u?.isPaid) {
|
|
230
|
+
if (st?.freeTierStatus === 'locked') return [['Plan', 'Free'], ['Free prompts', 'not available — another account on this computer already has them']];
|
|
231
|
+
const free = freeAllowance(u);
|
|
232
|
+
const offer = githubOffer(u);
|
|
233
|
+
return [['Plan', 'Free'], ['Free allowance', `${free.text} used${free.both ? ' (whichever runs out first)' : ''}`], ...(offer ? [['More free use', offer]] : [])];
|
|
234
|
+
}
|
|
180
235
|
const rows = [['Plan', st?.name || 'Paid']];
|
|
181
236
|
const p = planPercents(st);
|
|
182
|
-
if (p?.window != null) rows.push([`${p.windowHours}-hour
|
|
237
|
+
if (p?.window != null) rows.push([`${p.windowHours}-hour window`, `${p.window}% used · ${p.windowResetsAt ? `resets at ${clock(p.windowResetsAt)}` : 'starts with your next request'}`]);
|
|
183
238
|
if (p?.week != null) rows.push(['Week', `${p.week}% used · ${p.weekResetsAt ? `resets ${dayAndClock(p.weekResetsAt)}` : 'starts with your next request'}`]);
|
|
184
239
|
if (p?.month != null) rows.push(['Month', `${p.month}% used${p.monthResetsAt ? ` · resets ${shortDate(p.monthResetsAt)}` : ''}`]);
|
|
185
240
|
const extra = extraLine(p);
|
|
186
241
|
if (extra) rows.push(['Extra usage', extra]);
|
|
187
|
-
if (u.planExpiresAt) rows.push(['
|
|
242
|
+
if (u.planExpiresAt) rows.push(['Plan ends', new Date(u.planExpiresAt).toLocaleDateString()]);
|
|
188
243
|
return rows;
|
|
189
244
|
}
|
|
190
245
|
|
|
@@ -202,13 +257,18 @@ export function publicPlan(st) {
|
|
|
202
257
|
};
|
|
203
258
|
}
|
|
204
259
|
|
|
205
|
-
|
|
260
|
+
// Fields that are the server's internal accounting, not something a customer pays or
|
|
261
|
+
// reads: `cost` and `estimatedCost` are in that internal unit, and the per-model
|
|
262
|
+
// breakdown names what served each request.
|
|
263
|
+
const INTERNAL_KEYS = new Set(['cost', 'estimatedcost', 'modelbreakdown']);
|
|
264
|
+
|
|
265
|
+
/** A copy of a response for --json with every credit (and internal cost) field taken out, at any depth. */
|
|
206
266
|
export function withoutCredits(value) {
|
|
207
267
|
if (Array.isArray(value)) return value.map(withoutCredits);
|
|
208
268
|
if (!value || typeof value !== 'object' || value instanceof Date) return value;
|
|
209
269
|
const out = {};
|
|
210
270
|
for (const [k, v] of Object.entries(value)) {
|
|
211
|
-
if (/credit/i.test(k)) continue;
|
|
271
|
+
if (/credit/i.test(k) || INTERNAL_KEYS.has(k.toLowerCase())) continue;
|
|
212
272
|
out[k] = k === 'planStatus' ? publicPlan(v) : withoutCredits(v);
|
|
213
273
|
}
|
|
214
274
|
return out;
|
package/src/skills.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// A skill is a small, discoverable workflow stored as SKILL.md — the open Agent Skills
|
|
4
4
|
// format: a short YAML frontmatter description plus markdown instructions. Skills
|
|
5
|
-
// already written for other agents
|
|
5
|
+
// already written for other coding agents in the same format are picked up too; they are
|
|
6
6
|
// instructions only and never run anything by themselves.
|
|
7
7
|
|
|
8
8
|
import fs from 'node:fs';
|
package/src/tui/entries.js
CHANGED
|
@@ -89,7 +89,7 @@ const BUILDERS = {
|
|
|
89
89
|
const p = palette();
|
|
90
90
|
const version = data.version ? `v${data.version}` : '';
|
|
91
91
|
const tagline = `a coding agent in your terminal${version ? ` ${pick(['·', '-'])} ${version}` : ''}`;
|
|
92
|
-
const chip = { t: ' J ', color: '#
|
|
92
|
+
const chip = { t: ' J ', color: '#03140d', backgroundColor: p.brand, bold: true };
|
|
93
93
|
// One row: the chip, the name, and the tagline only if it fits whole. A tagline
|
|
94
94
|
// truncated mid-word reads as a rendering bug, not as a small terminal.
|
|
95
95
|
const oneLine = () => {
|
|
@@ -98,7 +98,7 @@ const BUILDERS = {
|
|
|
98
98
|
return [row];
|
|
99
99
|
};
|
|
100
100
|
|
|
101
|
-
// A raster console draws block elements as tofu, so there the brand is the
|
|
101
|
+
// A raster console draws block elements as tofu, so there the brand is the mint
|
|
102
102
|
// chip and the name in caps — still the identity, just not drawn.
|
|
103
103
|
if (useAscii()) return oneLine();
|
|
104
104
|
|
package/src/tui/theme.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// ROLE rather than by hue is what makes a second theme a data change instead of a
|
|
6
6
|
// rewrite: nothing downstream asks for "orange", it asks for `brand`.
|
|
7
7
|
//
|
|
8
|
-
// The
|
|
8
|
+
// The identity is the site's: mint leads, a sibling teal answers. Red and amber are
|
|
9
9
|
// kept strictly semantic — a failure and a warning — so a screen with no red on it is
|
|
10
10
|
// a screen with nothing wrong, which is a property worth having.
|
|
11
11
|
|
|
@@ -13,22 +13,22 @@ import { themeName } from '../ui.js';
|
|
|
13
13
|
|
|
14
14
|
const DARK = {
|
|
15
15
|
name: 'dark',
|
|
16
|
-
bg: '#
|
|
17
|
-
surface: '#
|
|
18
|
-
raised: '#
|
|
19
|
-
sunken: '#
|
|
20
|
-
line: '#
|
|
21
|
-
lineSoft: '#
|
|
22
|
-
text: '#
|
|
23
|
-
muted: '#
|
|
24
|
-
dim: '#
|
|
25
|
-
faint: '#
|
|
26
|
-
brand: '#
|
|
27
|
-
brandSoft: '#
|
|
28
|
-
brandDeep: '#
|
|
29
|
-
teal: '#
|
|
30
|
-
tealSoft: '#
|
|
31
|
-
tealDeep: '#
|
|
16
|
+
bg: '#05070a',
|
|
17
|
+
surface: '#0e1312',
|
|
18
|
+
raised: '#151c1a',
|
|
19
|
+
sunken: '#030405',
|
|
20
|
+
line: '#1f2a27',
|
|
21
|
+
lineSoft: '#141b19',
|
|
22
|
+
text: '#eef3f1',
|
|
23
|
+
muted: '#9aa6a0',
|
|
24
|
+
dim: '#66726c',
|
|
25
|
+
faint: '#36413d',
|
|
26
|
+
brand: '#3df2b0',
|
|
27
|
+
brandSoft: '#7af7ca',
|
|
28
|
+
brandDeep: '#1fc48a',
|
|
29
|
+
teal: '#2dd4bf',
|
|
30
|
+
tealSoft: '#7eeadb',
|
|
31
|
+
tealDeep: '#14a58f',
|
|
32
32
|
green: '#3fb950',
|
|
33
33
|
yellow: '#d9a441',
|
|
34
34
|
red: '#ff6b6b',
|
|
@@ -65,9 +65,9 @@ const LIGHT = {
|
|
|
65
65
|
muted: '#5a6673',
|
|
66
66
|
dim: '#83909e',
|
|
67
67
|
faint: '#aab4bf',
|
|
68
|
-
brand: '#
|
|
69
|
-
brandSoft: '#
|
|
70
|
-
brandDeep: '#
|
|
68
|
+
brand: '#0f8a5f',
|
|
69
|
+
brandSoft: '#1fb37c',
|
|
70
|
+
brandDeep: '#0a5f42',
|
|
71
71
|
teal: '#0f7a6e',
|
|
72
72
|
tealSoft: '#2ba394',
|
|
73
73
|
tealDeep: '#0a574e',
|
|
@@ -110,7 +110,7 @@ const OCEAN = {
|
|
|
110
110
|
tealSoft: '#7df0e0',
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
-
// Blue leads instead of
|
|
113
|
+
// Blue leads instead of mint: deuteranopia and protanopia both keep red/orange and
|
|
114
114
|
// green apart poorly, and a UI whose primary accent is orange loses its whole voice.
|
|
115
115
|
const DALTONIZED = {
|
|
116
116
|
...DARK,
|
package/src/tui/wordmark.js
CHANGED
|
@@ -106,7 +106,7 @@ export function wordSize(word = 'JAITUS') {
|
|
|
106
106
|
* The word as rows of spans, using half-blocks.
|
|
107
107
|
*
|
|
108
108
|
* `ink(column, total)` returns the colour for a column, so the caller owns the gradient
|
|
109
|
-
* — the app runs
|
|
109
|
+
* — the app runs mint to teal across the word and dims the whole thing during the
|
|
110
110
|
* intro, and neither of those belongs in the letterforms.
|
|
111
111
|
*/
|
|
112
112
|
export function wordRows(word = 'JAITUS', ink = () => undefined) {
|