jaitus 0.3.18 → 0.3.20

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/brand.js CHANGED
@@ -1,232 +1,233 @@
1
- // ─── Jaitus terminal identity ─────────────────────────────────────────────────
2
- // A coding session should look like a tool, not a launch screen. The product mark is
3
- // intentionally one compact coral token beside the Jaitus name; the web logo remains
4
- // the right place for the detailed mark, while a terminal needs recognition without
5
- // stealing the first screen from the work.
6
-
7
- import { c, columns, colorLevel, unicode, visibleWidth } from './ui.js';
8
-
9
- export function badge() {
10
- // JAITUS_ASCII forces plain text so the mark never renders as tofu.
11
- if (process.env.JAITUS_ASCII) return '[J]';
12
- const level = colorLevel();
13
- if (level >= 3) return '\x1b[48;2;255;107;74m\x1b[97m J \x1b[0m';
14
- if (level >= 2) return '\x1b[48;5;208m\x1b[97m J \x1b[0m';
15
- if (level === 1) return '\x1b[41m\x1b[97m J \x1b[0m';
16
- return '[J]';
17
- }
18
-
19
- export function wordmark(version = '') {
20
- return `${badge()} ${c.brand(c.bold('JAITUS'))}${version ? ` ${c.dim(`v${version}`)}` : ''}`;
21
- }
22
-
23
- // The Jaitus J alone as mascot art — no disc, no block. Sampled once from
24
- // the checked-in logo pixels (white-J bbox) into quadrant cells: hard-edged
25
- // vertices and corners, single brand ink, small and condensed.
26
- const MASCOT_ART = [
27
- "..████▌.",
28
- "......▟█",
29
- "██..▟█▛▀",
30
- "▀█████▌.",
31
- ];
32
- // Full J for the banner showcase: same geometry, full height.
33
- const MARK_LARGE = [
34
- "...▐███████...",
35
- "...▐███████...",
36
- "...▝██████▛...",
37
- "..........▐███",
38
- "..........▐███",
39
- "▄▄▄▖...▄▄▄▟▀▀▀",
40
- "███▌...████...",
41
- "███▌...████...",
42
- "▜██▙▄▄▄████...",
43
- ".▀█████████...",
44
- "..▝████████...",
45
- ];
46
- /**
47
- * The mascot J as raw rows, for the app view to ink itself.
48
- *
49
- * Deliberately used in preference to downsampling the real badge at small sizes. A
50
- * 125×125 photograph reduced to eight cells is a coral blob — the white J inside it
51
- * lands on about two pixels and disappears. These quadrant cells were drawn for this
52
- * size, so the letterform survives. The photographic mark is for somewhere it has room.
53
- */
54
- export function MASCOT_ROWS() {
55
- return MASCOT_ART.map((row) => row.replace(/\./g, ' '));
56
- }
57
-
58
- /** Mascot rows in the brand ink (pad to equal width for text lockups).
59
- * Dots in the grid are transparent cells, never printed. */
60
- export function renderMascot() {
61
- const clean = MASCOT_ART.map((r) => r.replace(/\./g, ' '));
62
- const width = Math.max(...clean.map((r) => [...r].length));
63
- return clean.map((r) => ` ${c.brand(r + ' '.repeat(Math.max(0, width - [...r].length)))}`);
64
- }
65
- /** Full-height mark for `banner`: the complete J geometry, same ink. */
66
- export function renderMarkLarge() {
67
- const clean = MARK_LARGE.map((r) => r.replace(/\./g, ' '));
68
- const width = Math.max(...clean.map((r) => [...r].length));
69
- return clean.map((r) => ` ${c.brand(r + ' '.repeat(Math.max(0, width - [...r].length)))}`);
70
- }
71
- // The JAITUS name as pure-ASCII block letters for terminals that render
72
- // nothing else (classic conhost raster fonts). 7 rows, 45 columns, only `#`
73
- // and spaces — the grand opening where block pixels would be tofu.
74
- const ASCII_GRAND = [
75
- ' ### ### ##### ####### ## ## ######',
76
- ' ### ## ## ##### ####### ## ## ##',
77
- ' ### ## ## ##### ### ## ## ##',
78
- ' ### ####### ##### ### ## ## ######',
79
- ' ## ### ## ## ##### ### ## ## ##',
80
- ' ## ### ## ## ##### ### ## ## ##',
81
- ' ###### ## ## ##### ### ##### ######',
82
- ];
83
- /** ASCII grand logo rows (unstyled plain text — color never reaches these terminals). */
84
- export function renderAsciiGrand() {
85
- return ASCII_GRAND.map((r) => ` ${r.replace(/\s+$/, '')}`);
86
- }
87
- // Fresh-session home mark in the opencode idiom: the name as 4-row block
88
- // letters, left half muted, right half bright, centered — the whole welcome.
89
- // ASCII terminals get the plain wordmark line instead of tofu.
90
- const HOME_LOGO_LEFT = [
91
- ' █ ▀▀ █ ',
92
- ' █ █ █ ',
93
- ' █ █▀▀█ █ ',
94
- '▀▀▀ ▀ █ █ ',
95
- ];
96
- const HOME_LOGO_RIGHT = [
97
- ' █ █ █ ▀▀ ',
98
- '███ █ █ █ ',
99
- ' █ █ █ ▀▀█',
100
- ' █ ▀▀▀▀ ▀▀█',
101
- ];
102
- /**
103
- * The same block letters as data, for the app view.
104
- *
105
- * The app renders them through a real per-character gradient, which cannot be done
106
- * with a pre-coloured string: the escape codes would have to be shredded mid-frame.
107
- * Kept here beside the art so the two surfaces can never drift apart.
108
- */
109
- export function HOME_LOGO_ROWS() {
110
- return HOME_LOGO_LEFT.map((left, i) => ({ left, right: HOME_LOGO_RIGHT[i] }));
111
- }
112
-
113
- /** Centered two-tone JAITUS block rows for a fresh fullscreen session.
114
- * Both halves wear the brand coral (plain left, bold right) — the opencode
115
- * idiom in Jaitus ink. A full reset closes each row so no attribute leaks. */
116
- export function renderHomeLogo({ version = '', width = 80 } = {}) {
117
- if (!unicode() || width < 40) {
118
- const line = `JAITUS${version ? ` v${version}` : ''}`;
119
- const pad = Math.max(0, Math.floor((width - visibleWidth(line)) / 2));
120
- return [`${' '.repeat(pad)}${c.bold(line)}`];
121
- }
122
- const pad = Math.max(0, Math.floor((width - 29) / 2));
123
- const end = colorLevel() > 0 ? '\x1b[0m' : '';
124
- return HOME_LOGO_LEFT.map((l, i) => `${' '.repeat(pad)}${c.brand(l)} ${c.bold(c.brand(HOME_LOGO_RIGHT[i]))}${end}`);
125
- }
126
- /** Rotating startup tips — every one names a real command. */
127
- const STARTUP_TIPS = [
128
- '/research digs up sources with citations',
129
- '/code-review --fix ships cleaner code',
130
- '/rewind un-breaks the timeline',
131
- '/btw asks off the record',
132
- '/branch tries the other way',
133
- '/test --fix repairs failures',
134
- '/explain reads the code for you',
135
- '/commit writes the message',
136
- ];
137
- export function startupTip() {
138
- const day = Math.floor(Date.now() / 86400000);
139
- return STARTUP_TIPS[day % STARTUP_TIPS.length];
140
- }
141
- /** Compact classic open: codex-style session header. Lowercase wordmark,
142
- * model, approval posture, directory, account — one block, pure ASCII,
143
- * instant. Returns plain-text lines. */
144
- export function renderClassicOpen({ version, model, approval, cwd, account, session, narrow }) {
145
- const meta = [
146
- `jaitus v${version}`,
147
- `model ${model}`,
148
- `approval ${approval}`,
149
- `directory ${cwd}`,
150
- ...(account ? [`account ${account} - session ${session}`] : [`session ${session}`]),
151
- ];
152
- if (narrow) return [...meta.map((m) => ` ${m}`), ` Tip: ${startupTip()}`];
153
- return [...meta.map((m) => ` ${m}`), ` Tip: ${startupTip()}`];
154
- }
155
- export function logoCard(version = '') {
156
- if (process.env.JAITUS_ASCII || !unicode()) {
157
- const inner = 40;
158
- const bar = ` +${'-'.repeat(inner)}+`;
159
- const row = (text) => ` |${text}${' '.repeat(Math.max(0, inner - visibleWidth(text)))}|`;
160
- return [
161
- bar,
162
- row(` ${c.brand(c.bold('J A I T U S'))}`),
163
- row(` a coding agent in your terminal ${c.dim(`v${version}`)}`),
164
- bar,
165
- ];
166
- }
167
- return [
168
- ...renderMarkLarge(),
169
- ` ${c.brand(c.bold('JAITUS'))} ${c.dim(`a coding agent in your terminal v${version}`)}`,
170
- ];
171
- }
172
-
173
- export function bannerLines({ version = '', width = columns() } = {}) {
174
- const suffix = width >= 44 ? ` ${c.dim('coding agent')}` : '';
175
- return [` ${wordmark(version)}${suffix}`];
176
- }
177
-
178
- export async function banner(opts = {}) {
179
- if (!process.stdout.isTTY && !opts.force) return;
180
- if (process.env.JAITUS_NO_BANNER && !opts.force) return;
181
- // `jaitus banner --image`: the README block-letter mark is ALWAYS printed,
182
- // because a sixel-only path goes invisible on terminals that misreport the
183
- // DA capability (seen on conhost: probe says yes, pixels never land, and the
184
- // user sees the same quiet line as before). Real pixels go first when the
185
- // probe positively confirms sixel, then the block letters guarantee ink.
186
- if (opts.image) {
187
- let sixelSeq = '';
188
- try {
189
- const { sixelSupported } = await import('./terminal.js');
190
- if (sixelSupported()) {
191
- const { markSixel } = await import('./sixel.js');
192
- sixelSeq = markSixel({}) || '';
193
- }
194
- } catch { sixelSeq = ''; }
195
- if (sixelSeq) process.stdout.write(`\n${sixelSeq}\n`);
196
- for (const line of logoCard(opts.version || '')) process.stdout.write(`${line}\n`);
197
- process.stdout.write('\n');
198
- return;
199
- }
200
- // One-shot wordmark shimmer on interactive start: a light sweep across the
201
- // brand row (~600 ms), once, only when the terminal animates. Skipped for
202
- // --image, non-TTY, reduced motion, and the /banner re-draw (opts.force).
203
- // The sweep paints per-character from the ANSI-stripped text — spreading the
204
- // styled string would shred escape sequences mid-frame.
205
- if (!opts.image && !opts.force && process.stdout.isTTY && !process.env.JAITUS_NO_ANIMATION && colorLevel() > 0 && unicode()) {
206
- const styled = bannerLines(opts);
207
- const plainText = (styled[0] || '').replace(/\x1b\[[0-9;]*m/g, '');
208
- const chars = [...plainText];
209
- const sweep = async () => {
210
- for (let pos = -3; pos <= chars.length + 3; pos++) {
211
- const frame = chars.map((ch, idx) => {
212
- const d = Math.abs(idx - pos);
213
- if (d === 0) return c.bold(`\x1b[97m${ch}\x1b[0m`);
214
- if (d <= 2) return c.shimmer(ch);
215
- return c.brand(ch);
216
- }).join('');
217
- process.stdout.write(`\r${frame}`);
218
- await new Promise((r) => setTimeout(r, 24));
219
- }
220
- process.stdout.write(`\r${styled.join('\n')}\n\n`);
221
- };
222
- try { await sweep(); } catch { process.stdout.write(`${styled.join('\n')}\n\n`); }
223
- } else {
224
- process.stdout.write(`${bannerLines(opts).join('\n')}\n\n`);
225
- }
226
- }
227
-
228
- // Kept as a small pure helper for the classic renderer and external integrations.
229
- // It is deliberately not a welcome card.
230
- export function welcomeLines({ version = '' } = {}) {
231
- return [` ${wordmark(version)}`];
232
- }
1
+ // ─── Jaitus terminal identity ─────────────────────────────────────────────────
2
+ // A coding session should look like a tool, not a launch screen. The product mark is
3
+ // intentionally one compact mint token beside the Jaitus name; the web logo remains
4
+ // the right place for the detailed mark, while a terminal needs recognition without
5
+ // stealing the first screen from the work.
6
+
7
+ import { c, columns, colorLevel, unicode, visibleWidth } from './ui.js';
8
+
9
+ export function badge() {
10
+ // JAITUS_ASCII forces plain text so the mark never renders as tofu.
11
+ if (process.env.JAITUS_ASCII) return '[J]';
12
+ const level = colorLevel();
13
+ // Dark ink on mint, like the logo: white on mint would fail contrast.
14
+ if (level >= 3) return '\x1b[48;2;61;242;176m\x1b[38;2;3;20;13m J \x1b[0m';
15
+ if (level >= 2) return '\x1b[48;5;85m\x1b[38;5;16m J \x1b[0m';
16
+ if (level === 1) return '\x1b[42m\x1b[30m J \x1b[0m';
17
+ return '[J]';
18
+ }
19
+
20
+ export function wordmark(version = '') {
21
+ return `${badge()} ${c.brand(c.bold('JAITUS'))}${version ? ` ${c.dim(`v${version}`)}` : ''}`;
22
+ }
23
+
24
+ // The Jaitus J alone as mascot art — no disc, no block. Sampled once from
25
+ // the checked-in logo pixels (white-J bbox) into quadrant cells: hard-edged
26
+ // vertices and corners, single brand ink, small and condensed.
27
+ const MASCOT_ART = [
28
+ "..████▌.",
29
+ "......▟█",
30
+ "██..▟█▛▀",
31
+ "▀█████▌.",
32
+ ];
33
+ // Full J for the banner showcase: same geometry, full height.
34
+ const MARK_LARGE = [
35
+ "...▐███████...",
36
+ "...▐███████...",
37
+ "...▝██████▛...",
38
+ "..........▐███",
39
+ "..........▐███",
40
+ "▄▄▄▖...▄▄▄▟▀▀▀",
41
+ "███▌...████...",
42
+ "███▌...████...",
43
+ "▜██▙▄▄▄████...",
44
+ ".▀█████████...",
45
+ "..▝████████...",
46
+ ];
47
+ /**
48
+ * The mascot J as raw rows, for the app view to ink itself.
49
+ *
50
+ * Deliberately used in preference to downsampling the real badge at small sizes. A
51
+ * 125×125 photograph reduced to eight cells is a mint blob — the white J inside it
52
+ * lands on about two pixels and disappears. These quadrant cells were drawn for this
53
+ * size, so the letterform survives. The photographic mark is for somewhere it has room.
54
+ */
55
+ export function MASCOT_ROWS() {
56
+ return MASCOT_ART.map((row) => row.replace(/\./g, ' '));
57
+ }
58
+
59
+ /** Mascot rows in the brand ink (pad to equal width for text lockups).
60
+ * Dots in the grid are transparent cells, never printed. */
61
+ export function renderMascot() {
62
+ const clean = MASCOT_ART.map((r) => r.replace(/\./g, ' '));
63
+ const width = Math.max(...clean.map((r) => [...r].length));
64
+ return clean.map((r) => ` ${c.brand(r + ' '.repeat(Math.max(0, width - [...r].length)))}`);
65
+ }
66
+ /** Full-height mark for `banner`: the complete J geometry, same ink. */
67
+ export function renderMarkLarge() {
68
+ const clean = MARK_LARGE.map((r) => r.replace(/\./g, ' '));
69
+ const width = Math.max(...clean.map((r) => [...r].length));
70
+ return clean.map((r) => ` ${c.brand(r + ' '.repeat(Math.max(0, width - [...r].length)))}`);
71
+ }
72
+ // The JAITUS name as pure-ASCII block letters for terminals that render
73
+ // nothing else (classic conhost raster fonts). 7 rows, 45 columns, only `#`
74
+ // and spaces — the grand opening where block pixels would be tofu.
75
+ const ASCII_GRAND = [
76
+ ' ### ### ##### ####### ## ## ######',
77
+ ' ### ## ## ##### ####### ## ## ##',
78
+ ' ### ## ## ##### ### ## ## ##',
79
+ ' ### ####### ##### ### ## ## ######',
80
+ ' ## ### ## ## ##### ### ## ## ##',
81
+ ' ## ### ## ## ##### ### ## ## ##',
82
+ ' ###### ## ## ##### ### ##### ######',
83
+ ];
84
+ /** ASCII grand logo rows (unstyled plain text — color never reaches these terminals). */
85
+ export function renderAsciiGrand() {
86
+ return ASCII_GRAND.map((r) => ` ${r.replace(/\s+$/, '')}`);
87
+ }
88
+ // Fresh-session home mark in the opencode idiom: the name as 4-row block
89
+ // letters, left half muted, right half bright, centered — the whole welcome.
90
+ // ASCII terminals get the plain wordmark line instead of tofu.
91
+ const HOME_LOGO_LEFT = [
92
+ ' █ ▀▀ █ ',
93
+ ' █ █ █ ',
94
+ ' █ █▀▀█ █ ',
95
+ '▀▀▀ ▀ █ █ ',
96
+ ];
97
+ const HOME_LOGO_RIGHT = [
98
+ ' █ █ █ ▀▀ ',
99
+ '███ █ █ █ ',
100
+ ' █ █ █ ▀▀█',
101
+ ' █ ▀▀▀▀ ▀▀█',
102
+ ];
103
+ /**
104
+ * The same block letters as data, for the app view.
105
+ *
106
+ * The app renders them through a real per-character gradient, which cannot be done
107
+ * with a pre-coloured string: the escape codes would have to be shredded mid-frame.
108
+ * Kept here beside the art so the two surfaces can never drift apart.
109
+ */
110
+ export function HOME_LOGO_ROWS() {
111
+ return HOME_LOGO_LEFT.map((left, i) => ({ left, right: HOME_LOGO_RIGHT[i] }));
112
+ }
113
+
114
+ /** Centered two-tone JAITUS block rows for a fresh fullscreen session.
115
+ * Both halves wear the brand mint (plain left, bold right) — the opencode
116
+ * idiom in Jaitus ink. A full reset closes each row so no attribute leaks. */
117
+ export function renderHomeLogo({ version = '', width = 80 } = {}) {
118
+ if (!unicode() || width < 40) {
119
+ const line = `JAITUS${version ? ` v${version}` : ''}`;
120
+ const pad = Math.max(0, Math.floor((width - visibleWidth(line)) / 2));
121
+ return [`${' '.repeat(pad)}${c.bold(line)}`];
122
+ }
123
+ const pad = Math.max(0, Math.floor((width - 29) / 2));
124
+ const end = colorLevel() > 0 ? '\x1b[0m' : '';
125
+ return HOME_LOGO_LEFT.map((l, i) => `${' '.repeat(pad)}${c.brand(l)} ${c.bold(c.brand(HOME_LOGO_RIGHT[i]))}${end}`);
126
+ }
127
+ /** Rotating startup tips — every one names a real command. */
128
+ const STARTUP_TIPS = [
129
+ '/research digs up sources with citations',
130
+ '/code-review --fix ships cleaner code',
131
+ '/rewind un-breaks the timeline',
132
+ '/btw asks off the record',
133
+ '/branch tries the other way',
134
+ '/test --fix repairs failures',
135
+ '/explain reads the code for you',
136
+ '/commit writes the message',
137
+ ];
138
+ export function startupTip() {
139
+ const day = Math.floor(Date.now() / 86400000);
140
+ return STARTUP_TIPS[day % STARTUP_TIPS.length];
141
+ }
142
+ /** Compact classic open: a one-block session header. Lowercase wordmark,
143
+ * model, approval posture, directory, account — one block, pure ASCII,
144
+ * instant. Returns plain-text lines. */
145
+ export function renderClassicOpen({ version, model, approval, cwd, account, session, narrow }) {
146
+ const meta = [
147
+ `jaitus v${version}`,
148
+ `model ${model}`,
149
+ `approval ${approval}`,
150
+ `directory ${cwd}`,
151
+ ...(account ? [`account ${account} - session ${session}`] : [`session ${session}`]),
152
+ ];
153
+ if (narrow) return [...meta.map((m) => ` ${m}`), ` Tip: ${startupTip()}`];
154
+ return [...meta.map((m) => ` ${m}`), ` Tip: ${startupTip()}`];
155
+ }
156
+ export function logoCard(version = '') {
157
+ if (process.env.JAITUS_ASCII || !unicode()) {
158
+ const inner = 40;
159
+ const bar = ` +${'-'.repeat(inner)}+`;
160
+ const row = (text) => ` |${text}${' '.repeat(Math.max(0, inner - visibleWidth(text)))}|`;
161
+ return [
162
+ bar,
163
+ row(` ${c.brand(c.bold('J A I T U S'))}`),
164
+ row(` a coding agent in your terminal ${c.dim(`v${version}`)}`),
165
+ bar,
166
+ ];
167
+ }
168
+ return [
169
+ ...renderMarkLarge(),
170
+ ` ${c.brand(c.bold('JAITUS'))} ${c.dim(`a coding agent in your terminal v${version}`)}`,
171
+ ];
172
+ }
173
+
174
+ export function bannerLines({ version = '', width = columns() } = {}) {
175
+ const suffix = width >= 44 ? ` ${c.dim('coding agent')}` : '';
176
+ return [` ${wordmark(version)}${suffix}`];
177
+ }
178
+
179
+ export async function banner(opts = {}) {
180
+ if (!process.stdout.isTTY && !opts.force) return;
181
+ if (process.env.JAITUS_NO_BANNER && !opts.force) return;
182
+ // `jaitus banner --image`: the README block-letter mark is ALWAYS printed,
183
+ // because a sixel-only path goes invisible on terminals that misreport the
184
+ // DA capability (seen on conhost: probe says yes, pixels never land, and the
185
+ // user sees the same quiet line as before). Real pixels go first when the
186
+ // probe positively confirms sixel, then the block letters guarantee ink.
187
+ if (opts.image) {
188
+ let sixelSeq = '';
189
+ try {
190
+ const { sixelSupported } = await import('./terminal.js');
191
+ if (sixelSupported()) {
192
+ const { markSixel } = await import('./sixel.js');
193
+ sixelSeq = markSixel({}) || '';
194
+ }
195
+ } catch { sixelSeq = ''; }
196
+ if (sixelSeq) process.stdout.write(`\n${sixelSeq}\n`);
197
+ for (const line of logoCard(opts.version || '')) process.stdout.write(`${line}\n`);
198
+ process.stdout.write('\n');
199
+ return;
200
+ }
201
+ // One-shot wordmark shimmer on interactive start: a light sweep across the
202
+ // brand row (~600 ms), once, only when the terminal animates. Skipped for
203
+ // --image, non-TTY, reduced motion, and the /banner re-draw (opts.force).
204
+ // The sweep paints per-character from the ANSI-stripped text — spreading the
205
+ // styled string would shred escape sequences mid-frame.
206
+ if (!opts.image && !opts.force && process.stdout.isTTY && !process.env.JAITUS_NO_ANIMATION && colorLevel() > 0 && unicode()) {
207
+ const styled = bannerLines(opts);
208
+ const plainText = (styled[0] || '').replace(/\x1b\[[0-9;]*m/g, '');
209
+ const chars = [...plainText];
210
+ const sweep = async () => {
211
+ for (let pos = -3; pos <= chars.length + 3; pos++) {
212
+ const frame = chars.map((ch, idx) => {
213
+ const d = Math.abs(idx - pos);
214
+ if (d === 0) return c.bold(`\x1b[97m${ch}\x1b[0m`);
215
+ if (d <= 2) return c.shimmer(ch);
216
+ return c.brand(ch);
217
+ }).join('');
218
+ process.stdout.write(`\r${frame}`);
219
+ await new Promise((r) => setTimeout(r, 24));
220
+ }
221
+ process.stdout.write(`\r${styled.join('\n')}\n\n`);
222
+ };
223
+ try { await sweep(); } catch { process.stdout.write(`${styled.join('\n')}\n\n`); }
224
+ } else {
225
+ process.stdout.write(`${bannerLines(opts).join('\n')}\n\n`);
226
+ }
227
+ }
228
+
229
+ // Kept as a small pure helper for the classic renderer and external integrations.
230
+ // It is deliberately not a welcome card.
231
+ export function welcomeLines({ version = '' } = {}) {
232
+ return [` ${wordmark(version)}`];
233
+ }
@@ -14,6 +14,7 @@ import { c, out, table, heading, present, colorLevel, glyph } from '../ui.js';
14
14
  import { endpoints, loadCredentials, loadSettings, HOME, version } from '../config.js';
15
15
  import { signedIn, accessToken } from '../session.js';
16
16
  import { checkRegistry, isNewer } from './update.js';
17
+ import { freeAllowance } from '../planView.js';
17
18
 
18
19
  const TIMEOUT_MS = 8000;
19
20
 
@@ -77,9 +78,9 @@ export default async function doctor(argv = [], { json = false } = {}) {
77
78
  }
78
79
  }
79
80
 
80
- // ── The gateway ───────────────────────────────────────────────────────────────
81
+ // ── The server ────────────────────────────────────────────────────────────────
81
82
  const { baseUrl, siteUrl } = endpoints();
82
- add('Gateway', PASS, baseUrl);
83
+ add('Server', PASS, baseUrl);
83
84
 
84
85
  let reachable = false;
85
86
  try {
@@ -91,18 +92,18 @@ export default async function doctor(argv = [], { json = false } = {}) {
91
92
  const body = await res.json().catch(() => ({}));
92
93
  const ids = (body?.data || []).map((m) => m.id);
93
94
  add('Reachable', PASS, `HTTP ${res.status}`);
94
- add('Models', ids.length ? PASS : WARN, ids.length ? ids.join(', ') : 'the gateway advertised none',
95
- ids.length ? null : 'Nothing to run against. This is a gateway configuration problem, not a local one.');
95
+ add('Models', ids.length ? PASS : WARN, ids.length ? ids.join(', ') : 'Jaitus sent no model list',
96
+ ids.length ? null : 'Nothing to run against. This is a problem on the Jaitus side, not on your computer.');
96
97
  } else if (res.status === 401 || res.status === 403) {
97
98
  add('Reachable', PASS, `HTTP ${res.status}`);
98
99
  add('Session accepted', FAIL, 'Jaitus did not accept this device\'s session',
99
100
  'Run `jaitus login` again — this device may have been signed out from the website.');
100
101
  } else {
101
- add('Reachable', WARN, `HTTP ${res.status}`, 'The gateway answered but not with a model list.');
102
+ add('Reachable', WARN, `HTTP ${res.status}`, 'Jaitus answered, but not with a model list.');
102
103
  }
103
104
  } catch (e) {
104
105
  add('Reachable', FAIL, describeNetworkError(e, baseUrl),
105
- 'Check your connection, and whether a proxy or firewall is between you and the gateway.');
106
+ 'Check your connection, and whether a proxy or firewall is between you and Jaitus.');
106
107
  }
107
108
 
108
109
  // ── The account ───────────────────────────────────────────────────────────────
@@ -112,11 +113,11 @@ export default async function doctor(argv = [], { json = false } = {}) {
112
113
  const body = await res.json().catch(() => ({}));
113
114
  const u = body.user || body.data || {};
114
115
  if (res.ok && (u.email || u.id)) {
115
- const plan = u.isPaid ? (u.planStatus?.name || 'paid') : `free · ${u.freeRequestsUsed ?? 0} of ${u.freeRequestLimit ?? '?'} prompts used`;
116
+ const plan = u.isPaid ? (u.planStatus?.name || 'paid') : `free · ${freeAllowance(u).text} used`;
116
117
  add('Account', PASS, `${u.email || 'signed in'} (${plan})`);
117
118
  } else {
118
119
  add('Account', WARN, body?.message || `HTTP ${res.status}`,
119
- 'The key works for the gateway but the account endpoint did not answer.');
120
+ 'You are signed in, but your account details did not load. Try again in a minute.');
120
121
  }
121
122
  } catch (e) {
122
123
  add('Account', WARN, e.message);
@@ -126,7 +127,7 @@ export default async function doctor(argv = [], { json = false } = {}) {
126
127
  // ── The terminal ──────────────────────────────────────────────────────────────
127
128
  const settings = loadSettings();
128
129
  add('Model', settings.model ? PASS : PASS,
129
- settings.model || 'not pinned — whatever the gateway lists first');
130
+ settings.model || 'automatic — Jaitus picks the model for each request');
130
131
  add('Website', PASS, siteUrl);
131
132
 
132
133
  // ── Is there a newer one ──────────────────────────────────────────────────────
@@ -155,7 +156,7 @@ export default async function doctor(argv = [], { json = false } = {}) {
155
156
  // Labeled swatches: if brand and error reds are indistinguishable on this
156
157
  // console, this row shows exactly which token renders wrong.
157
158
  out('');
158
- out(` ${c.dim('colors:')} level ${colorLevel()} · TERM=${process.env.TERM || '?'}${process.env.WT_SESSION ? ' · WT' : ''}${process.env.TERM_PROGRAM ? ` · ${process.env.TERM_PROGRAM}` : ''} · theme ${loadSettings().theme || 'auto'}`);
159
+ out(` ${c.dim('colours:')} level ${colorLevel()} · TERM=${process.env.TERM || '?'}${process.env.WT_SESSION ? ' · WT' : ''}${process.env.TERM_PROGRAM ? ` · ${process.env.TERM_PROGRAM}` : ''} · theme ${loadSettings().theme || 'auto'}`);
159
160
  out(` ${c.dim('brand')} ${c.brand('[Ja]')} ${c.dim('accent')} ${c.accent('[Te]')} ${c.dim('ok')} ${c.green('[✓]')} ${c.dim('warn')} ${c.yellow('[!]')} ${c.dim('error')} ${c.red('[✗]')}`);
160
161
  if (failed.length || warned.length) {
161
162
  out('');
@@ -1,7 +1,7 @@
1
1
  // ─── jaitus login ───────────────────────────────────────────────────────────────
2
2
  //
3
3
  // The loopback-redirect flow from RFC 8252 (OAuth 2.0 for Native Apps) with PKCE
4
- // (RFC 7636), which is what `gh auth login`, Codex and the cloud CLIs use:
4
+ // (RFC 7636), which is what `gh auth login` and the cloud CLIs use:
5
5
  //
6
6
  // 1. Start an HTTP server on a random port, bound to 127.0.0.1 only, and make a PKCE
7
7
  // verifier that never leaves this process.
@@ -49,7 +49,7 @@ const PAGE = (title, body, accent) => `<!doctype html><meta charset="utf-8">
49
49
  body{background:#0b0f16;color:#e6edf6} h1{color:#fff} p{color:#94a3b8}
50
50
  }
51
51
  </style>
52
- <div class="card"><div class="dot">${accent === '#ff6b4a' ? '✓' : '✕'}</div>
52
+ <div class="card"><div class="dot">${accent === '#1fc48a' ? '✓' : '✕'}</div>
53
53
  <h1>${title}</h1><p>${body}</p></div>`;
54
54
 
55
55
  /** Wait for the browser to come back with a code. Resolves { code } or rejects. */
@@ -98,7 +98,7 @@ function awaitCallback(server, expectedState) {
98
98
  return reject(new Error('No code returned.'));
99
99
  }
100
100
 
101
- finish(200, PAGE('You‘re signed in', 'You can close this tab and go back to your terminal.', '#ff6b4a'));
101
+ finish(200, PAGE("You're signed in to Jaitus", 'You can close this tab and go back to your terminal.', '#1fc48a'));
102
102
  resolve({ code });
103
103
  });
104
104
  });
@@ -21,7 +21,7 @@ export default async function logout() {
21
21
  ok('Signed out of this device.');
22
22
  } else {
23
23
  ok('Signed out here.');
24
- warn(`Could not reach Jaitus to end the session on the server. It will expire on its own, or sign it out at ${endpoints().siteUrl}/gateway.`);
24
+ warn(`Could not reach Jaitus to end the session on the server. It will expire on its own, or sign it out under Signed-in devices on your dashboard (${endpoints().siteUrl}/gateway).`);
25
25
  }
26
26
  return 0;
27
27
  }
@@ -83,13 +83,22 @@ export async function cachedLatest({ now = Date.now(), file = CHECK_FILE, check
83
83
  return latest || null;
84
84
  }
85
85
 
86
- /** One line for the app to show when a newer version is out, or null. */
86
+ /**
87
+ * One line for the app to show when a newer version is out, or null. Only a global npm
88
+ * install (with auto-update left on) updates itself as it starts; every other copy is
89
+ * told the command that gets the new version, the same advice autoUpdate() prints.
90
+ */
87
91
  export async function updateNotice(opts = {}) {
88
92
  if (!updateCheckEnabled(opts.env)) return null;
89
93
  const current = opts.current || version();
90
94
  const latest = await cachedLatest(opts).catch(() => null);
91
95
  if (!latest || !isNewer(latest, current)) return null;
92
- return `Jaitus v${latest} is out — you have v${current}. Quit and start jaitus again: it updates itself as it starts.`;
96
+ const head = `Jaitus v${latest} is out — you have v${current}.`;
97
+ const how = await (opts.method || (() => installMethod()))().catch(() => 'other');
98
+ const env = opts.env || process.env;
99
+ if (how === 'npm-global' && !env.JAITUS_NO_AUTOUPDATE) return `${head} Quit and start jaitus again: it updates itself as it starts.`;
100
+ if (how === 'npx') return `${head} Start it with npx jaitus@latest.`;
101
+ return `${head} Update it with npm install -g jaitus@latest.`;
93
102
  }
94
103
 
95
104
  function run(cmd, args, { capture = false } = {}) {