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/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 gateway counts usage in credits (a fixed slice of real model cost) — that is its
4
- // internal unit and an admin's, not a customer's. Everything the CLI shows about a plan
5
- // is a percentage of the 5-hour session or of the week, with when each resets, the way
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 session and of
56
- * the week — null for either the plan does not have.
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 two percentages, and the two budgets
75
- * a turn's spend is measured against. null for an account with no plan limits.
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
- /** What one turn has used so far, for the work bar: "0.6% of session". '' when unknown. */
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 session`;
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 plan as meters for /usage (popup) and `jaitus usage` (text): the 5-hour session
119
- * and the week fill at their own rates from the same requests, so each gets its own bar
120
- * and its own reset time. Extra usage and the free tier follow.
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 its free accounts', tone: 'bad' });
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
- out.push({ meter: 'Free prompts', used: u?.freeRequestsUsed ?? 0, total: u?.freeRequestLimit || 0, detail: `${u?.freeRequestsUsed ?? 0} of ${u?.freeRequestLimit ?? '?'} prompts used` });
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 session`, used: p.window, total: 100, note: p.windowResetsAt ? `resets at ${clock(p.windowResetsAt)}` : 'starts with your next request' });
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: 'Renews by', value: longDate(u.planExpiresAt) });
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 session · 0.5% of your week". */
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 session`);
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 (counted at the lower cached price)` },
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) return [['Plan', 'Free'], ['Free prompts', `${u?.freeRequestsUsed ?? 0} of ${u?.freeRequestLimit ?? '?'} used`]];
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 session`, `${p.window}% used · ${p.windowResetsAt ? `resets at ${clock(p.windowResetsAt)}` : 'starts with your next request'}`]);
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(['Renews by', new Date(u.planExpiresAt).toLocaleDateString()]);
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
- /** A copy of a response for --json with every credit field taken out, at any depth. */
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 (in .claude/skills) are picked up too; they are
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';
@@ -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: '#ffffff', backgroundColor: p.brand, bold: true };
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 coral
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 two-tone identity is the site's: coral leads, teal answers. Red and amber are
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: '#0b0f14',
17
- surface: '#11161e',
18
- raised: '#18202b',
19
- sunken: '#080c11',
20
- line: '#212c3a',
21
- lineSoft: '#161f2a',
22
- text: '#e6edf3',
23
- muted: '#93a4b8',
24
- dim: '#5d6e82',
25
- faint: '#3a4757',
26
- brand: '#ff6b4a',
27
- brandSoft: '#ffa285',
28
- brandDeep: '#c94a2e',
29
- teal: '#20a898',
30
- tealSoft: '#63dccc',
31
- tealDeep: '#0f7a6e',
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: '#c8431f',
69
- brandSoft: '#e4724f',
70
- brandDeep: '#8f2c11',
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 coral: deuteranopia and protanopia both keep red/orange and
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,
@@ -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 coral to teal across the word and dims the whole thing during the
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) {