@stevezhou/sisu 0.1.0 → 0.1.2

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/dist/commands.js CHANGED
@@ -126,8 +126,15 @@ async function webLoginCommand(input = {}, http = http_1.defaultHttp) {
126
126
  }
127
127
  const started = await http(`${apiBase}/api/auth/cli/device`, { method: 'POST' });
128
128
  const startBody = await started.json().catch(() => ({}));
129
- if (!started.ok)
129
+ if (!started.ok) {
130
+ if (started.status === 404 || started.status === 405) {
131
+ throw new Error('Browser login is not on this server yet. Use: sisu login --email <email> --password <password>');
132
+ }
133
+ if (started.status === 503) {
134
+ throw new Error('Sign-in is temporarily unavailable. Try again in a moment.');
135
+ }
130
136
  throw new Error((0, http_1.errorDetail)(startBody, `device start failed (${started.status})`));
137
+ }
131
138
  const deviceCode = String(startBody.device_code || '');
132
139
  const userCode = String(startBody.user_code || '');
133
140
  const rawVerify = String(startBody.verification_uri_complete || startBody.verification_uri || '');
package/dist/logo.js CHANGED
@@ -14,10 +14,7 @@ function sisuMobiusArt(columns = 80, phase = 0, color = false) {
14
14
  });
15
15
  }
16
16
  function sisuWordmark() {
17
- return [
18
- ' 思 溯',
19
- ' S I S U',
20
- ].join('\n');
17
+ return ['思溯', 'SISU'].join('\n');
21
18
  }
22
19
  function sisuBanner(columns = 80, phase = 0, color = false) {
23
20
  return [
package/dist/mobius.js CHANGED
@@ -232,9 +232,9 @@ function renderMobiusFrame(options = {}) {
232
232
  return ch;
233
233
  let [r, g, b] = mobiusRgb(cell.t);
234
234
  const lift = 0.18 + cell.shade * 0.95 + cell.spec * 0.7;
235
- r = clamp(Math.round(r * lift), 0, 255);
236
- g = clamp(Math.round(g * lift), 0, 255);
237
- b = clamp(Math.round(b * lift), 0, 255);
235
+ r = clamp(Number.isFinite(r * lift) ? Math.round(r * lift) : 180, 0, 255);
236
+ g = clamp(Number.isFinite(g * lift) ? Math.round(g * lift) : 180, 0, 255);
237
+ b = clamp(Number.isFinite(b * lift) ? Math.round(b * lift) : 180, 0, 255);
238
238
  return `${ansiRgb(r, g, b)}${ch}${RESET}`;
239
239
  }).join('')).join('\n');
240
240
  }
package/dist/pager/app.js CHANGED
@@ -12,12 +12,18 @@ const ALT_ENTER = '\x1b[?1049h\x1b[?25l';
12
12
  const ALT_LEAVE = '\x1b[?1049l\x1b[?25h';
13
13
  const CLEAR_HOME = '\x1b[2J\x1b[H';
14
14
  function formatChromeStatus(email, quota, conversationId) {
15
- const parts = [
16
- (email || '').trim(),
17
- (quota || '').trim() || 'quota unavailable',
18
- conversationId || 'new',
19
- 'client=tui',
20
- ].filter((part) => part.length > 0);
15
+ const who = (email || '').trim();
16
+ if (!who) {
17
+ const conv = (conversationId || '').trim();
18
+ return conv && conv !== 'new' ? `sisu · not signed in · ${conv}` : 'sisu · not signed in';
19
+ }
20
+ const parts = [who];
21
+ const quotaText = (quota || '').trim();
22
+ if (quotaText && quotaText !== 'quota unavailable')
23
+ parts.push(quotaText);
24
+ const conv = (conversationId || '').trim();
25
+ if (conv && conv !== 'new')
26
+ parts.push(conv);
21
27
  return parts.join(' · ');
22
28
  }
23
29
  /** Short chrome fragment: `quota unlimited`, first `quota N pts`, or `quota unavailable`. */
@@ -73,9 +79,10 @@ async function runPager(io, transport, options = {}) {
73
79
  const rows = options.rows ?? io.rows;
74
80
  let theme = options.theme ?? 'dark';
75
81
  let quotaLine = 'quota unavailable';
82
+ let chromeEmail = (options.email || '').trim();
76
83
  const withChrome = (next) => ({
77
84
  ...next,
78
- statusLine: formatChromeStatus(options.email, quotaLine, next.conversationId),
85
+ statusLine: formatChromeStatus(chromeEmail, quotaLine, next.conversationId),
79
86
  });
80
87
  const refreshQuota = async () => {
81
88
  if (!options.quota)
@@ -185,6 +192,35 @@ async function runPager(io, transport, options = {}) {
185
192
  finish(0);
186
193
  return;
187
194
  }
195
+ if (head === '/login') {
196
+ if (!options.login) {
197
+ state = pushEntry(state, 'status', 'not wired');
198
+ paint();
199
+ return;
200
+ }
201
+ state = pushEntry(state, 'status', 'Opening browser to sign in…');
202
+ paint();
203
+ try {
204
+ const email = await options.login((line) => {
205
+ if (!running)
206
+ return;
207
+ state = pushEntry(state, 'status', line);
208
+ paint();
209
+ });
210
+ if (!running)
211
+ return;
212
+ chromeEmail = email;
213
+ state = withChrome(state);
214
+ state = pushEntry(state, 'status', `logged in as ${email}`);
215
+ }
216
+ catch (err) {
217
+ if (!running)
218
+ return;
219
+ state = pushEntry(state, 'status', err instanceof Error ? err.message : String(err));
220
+ }
221
+ paint();
222
+ return;
223
+ }
188
224
  if (head === '/new') {
189
225
  pickMode = false;
190
226
  pickableIds.clear();
@@ -289,6 +325,12 @@ async function runPager(io, transport, options = {}) {
289
325
  paint();
290
326
  };
291
327
  const sendTurn = async (prompt) => {
328
+ if (!chromeEmail && options.login) {
329
+ state = pushEntry(state, 'status', 'Not logged in. Type /login to sign in.');
330
+ state = clearDraft(state);
331
+ paint();
332
+ return;
333
+ }
292
334
  state = pushEntry(state, 'user', prompt);
293
335
  state = (0, model_1.startAssistant)(state);
294
336
  state = clearDraft(state);
@@ -8,6 +8,7 @@ exports.insertToolBeforeLiveAssistant = insertToolBeforeLiveAssistant;
8
8
  exports.appendText = appendText;
9
9
  exports.applyKey = applyKey;
10
10
  exports.SLASH_COMMANDS = [
11
+ { name: '/login', hint: 'Sign in with the browser' },
11
12
  { name: '/new', hint: 'Start a new conversation (alias: /clear)' },
12
13
  { name: '/resume', hint: 'Resume a conversation (alias: /history)' },
13
14
  { name: '/status', hint: 'Show session status' },
@@ -88,6 +88,25 @@ function windowAroundSelected(lines, entryOf, selected, budget, lastEntry) {
88
88
  const windowStart = Math.max(0, Math.min(start, end + 1 - budget));
89
89
  return lines.slice(windowStart, windowStart + budget);
90
90
  }
91
+ function isIdleWelcome(state) {
92
+ return !state.conversationId && state.entries.length === 0;
93
+ }
94
+ function welcomeLines(guest) {
95
+ if (guest) {
96
+ return [
97
+ 'SISU',
98
+ '',
99
+ 'Sign in to start a conversation.',
100
+ '/login open the browser',
101
+ '/help commands',
102
+ ];
103
+ }
104
+ return [
105
+ 'SISU',
106
+ '',
107
+ 'Ask anything, or type /help.',
108
+ ];
109
+ }
91
110
  function slashMenuLines(state) {
92
111
  if (!state.slashOpen)
93
112
  return [];
@@ -116,13 +135,17 @@ function renderPager(state, cols, rows, theme = 'dark') {
116
135
  const bodyBudget = Math.max(0, height - chrome);
117
136
  const slash = slashMenuLines(state);
118
137
  const slashTake = Math.min(slash.length, bodyBudget);
119
- const scrollBudget = bodyBudget - slashTake;
138
+ const guest = !(state.statusLine || '').trim() || (state.statusLine || '').includes('not signed in');
139
+ const welcome = isIdleWelcome(state) ? welcomeLines(guest).map((line) => (line ? ` ${line}` : '')) : [];
140
+ const welcomeTake = Math.min(welcome.length, Math.max(0, bodyBudget - slashTake));
141
+ const scrollBudget = bodyBudget - slashTake - welcomeTake;
120
142
  const laid = layoutScrollback(state, width);
121
143
  const lastEntry = Math.max(0, state.entries.length - 1);
122
144
  const visibleScroll = windowAroundSelected(laid.lines, laid.entryOf, state.selected, scrollBudget, lastEntry);
123
145
  const body = [];
124
- // Top-pad scrollback so newest content sits just above slash/status/prompt.
125
- while (body.length + visibleScroll.length < scrollBudget) {
146
+ body.push(...welcome.slice(0, welcomeTake));
147
+ // Top-pad remaining scrollback so live status sits just above slash/prompt.
148
+ while (body.length + visibleScroll.length < welcomeTake + scrollBudget) {
126
149
  body.push('');
127
150
  }
128
151
  body.push(...visibleScroll);
package/dist/tui.js CHANGED
@@ -149,6 +149,7 @@ async function promptLogin(io, login) {
149
149
  }
150
150
  function tuiHelp() {
151
151
  return [
152
+ '/login sign in with the browser',
152
153
  '/status account and quota',
153
154
  '/ls local workspace files',
154
155
  '/history saved cloud conversations',
@@ -169,34 +170,30 @@ async function runTui(io, deps = {}) {
169
170
  const openThread = deps.openThread ?? commands_1.openConversationCommand;
170
171
  const training = deps.training ?? commands_1.setTrainingCommand;
171
172
  const auth = deps.auth ?? store_1.readAuth;
173
+ const webLogin = deps.webLogin ?? commands_1.webLoginCommand;
172
174
  const columns = deps.columns ?? process.stdout.columns ?? 80;
173
175
  const animate = deps.animate ?? shouldAnimateSplash();
174
- const color = deps.color ?? Boolean(process.stdout.isTTY);
175
176
  try {
177
+ io.write(`\n${(0, logo_1.sisuWordmark)()}\n\n`);
176
178
  if (animate) {
177
- await playMobiusIntro(io, { columns, sleep: deps.sleep, color });
178
- }
179
- else {
180
- io.write(`${(0, logo_1.sisuBanner)(columns, 0.35, color)}\n`);
181
- }
182
- let account = auth();
183
- if (!account) {
184
- const login = deps.login ?? commands_1.loginCommand;
185
- const result = await promptLogin(io, login);
186
- if (result !== 'ok')
187
- return 2;
188
- account = auth();
189
- if (!account) {
190
- io.write('login did not persist auth\n');
191
- return 2;
192
- }
179
+ await (deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))))(80);
193
180
  }
181
+ const account = auth();
182
+ const startWebLogin = async (notify) => {
183
+ return webLogin({
184
+ onStart: (info) => {
185
+ notify(`Open ${info.verification_uri_complete}`);
186
+ notify(`Confirm code ${info.user_code}`);
187
+ },
188
+ }, http);
189
+ };
194
190
  if (deps.pager || shouldUsePager(deps)) {
195
191
  io.close?.();
196
192
  const transport = (0, transport_1.createFastApiTransport)(http);
197
193
  return await (deps.pager ?? app_1.runPager)((0, stdio_1.stdioPagerIo)(), transport, {
198
194
  columns,
199
- email: account.email,
195
+ email: account?.email,
196
+ login: startWebLogin,
200
197
  quota: async () => (0, commands_1.formatQuota)(await (0, commands_1.fetchBalance)(http)),
201
198
  status: () => status(http),
202
199
  ls: () => {
@@ -211,12 +208,24 @@ async function runTui(io, deps = {}) {
211
208
  });
212
209
  }
213
210
  io.write(`${await status(http)}\n`);
211
+ if (!account)
212
+ io.write('Not logged in. Type /login to sign in with your browser.\n');
214
213
  io.write(`${tuiHelp()}\n\n`);
215
214
  let newConversation = false;
216
215
  while (true) {
217
216
  const raw = (await io.question('› ')).trim();
218
217
  if (!raw)
219
218
  continue;
219
+ if (raw === '/login') {
220
+ try {
221
+ const email = await startWebLogin((line) => io.write(`${line}\n`));
222
+ io.write(`logged in as ${email}\n`);
223
+ }
224
+ catch (error) {
225
+ io.write(`${error instanceof Error ? error.message : String(error)}\n`);
226
+ }
227
+ continue;
228
+ }
220
229
  if (raw === '/quit' || raw === '/exit') {
221
230
  io.write('bye\n');
222
231
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevezhou/sisu",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "SiSu CLI — one login, cloud quota, local workspace",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://www.sisu.chat",