@stevezhou/sisu 0.1.9 → 0.1.11

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
@@ -18,6 +18,10 @@ exports.execCommand = execCommand;
18
18
  exports.listConversationsCommand = listConversationsCommand;
19
19
  exports.openConversationCommand = openConversationCommand;
20
20
  exports.setTrainingCommand = setTrainingCommand;
21
+ exports.fetchModelCatalog = fetchModelCatalog;
22
+ exports.resolveCatalogModel = resolveCatalogModel;
23
+ exports.listModelsCommand = listModelsCommand;
24
+ exports.setModelCommand = setModelCommand;
21
25
  const child_process_1 = require("child_process");
22
26
  const fs_1 = __importDefault(require("fs"));
23
27
  const path_1 = __importDefault(require("path"));
@@ -274,7 +278,7 @@ async function execCommand(prompt, options = {}, http = http_1.defaultHttp) {
274
278
  headers: (0, http_1.authHeaders)(auth.token),
275
279
  body: JSON.stringify({
276
280
  title: text.slice(0, 50),
277
- model: options.model || undefined,
281
+ model: options.model || (0, store_1.readSession)().last_model || undefined,
278
282
  project_id: options.projectId || (0, store_1.readSession)().last_project_id || undefined,
279
283
  client: stamp.client,
280
284
  client_version: stamp.client_version,
@@ -293,7 +297,7 @@ async function execCommand(prompt, options = {}, http = http_1.defaultHttp) {
293
297
  body: JSON.stringify({
294
298
  conversation_id: conversationId,
295
299
  message: text,
296
- model: options.model || undefined,
300
+ model: options.model || (0, store_1.readSession)().last_model || undefined,
297
301
  task_category: 'coding',
298
302
  client: stamp.client,
299
303
  client_version: stamp.client_version,
@@ -348,3 +352,58 @@ async function setTrainingCommand(optIn, http = http_1.defaultHttp) {
348
352
  throw new Error((0, http_1.errorDetail)(body, `training update failed (${response.status})`));
349
353
  return optIn ? 'training opt-in on (new turns may be used if eligible)' : 'training opt-in off';
350
354
  }
355
+ function normalizeModelKey(value) {
356
+ return value.toLowerCase().replace(/[-_.\s]/g, '');
357
+ }
358
+ async function fetchModelCatalog(http = http_1.defaultHttp) {
359
+ const auth = (0, store_1.requireAuth)();
360
+ const response = await http(`${auth.api_base}/api/chat/models`, { headers: (0, http_1.authHeaders)(auth.token) });
361
+ const body = await response.json().catch(() => ({}));
362
+ if (!response.ok)
363
+ throw new Error((0, http_1.errorDetail)(body, `models failed (${response.status})`));
364
+ const rows = Array.isArray(body?.models) ? body.models : [];
365
+ const models = rows
366
+ .map((row) => {
367
+ const name = String(row?.name || '').trim();
368
+ if (!name)
369
+ return null;
370
+ return { name, label: String(row.display_name || row.label || name) };
371
+ })
372
+ .filter((row) => Boolean(row));
373
+ return { models, defaultModel: String(body?.default_model || '') };
374
+ }
375
+ function resolveCatalogModel(query, models) {
376
+ const needle = normalizeModelKey(query);
377
+ if (!needle)
378
+ return undefined;
379
+ return (models.find((row) => normalizeModelKey(row.name) === needle) ||
380
+ models.find((row) => normalizeModelKey(row.label) === needle) ||
381
+ models.find((row) => normalizeModelKey(row.name).includes(needle) || normalizeModelKey(row.label).includes(needle)));
382
+ }
383
+ async function listModelsCommand(http = http_1.defaultHttp) {
384
+ const { models, defaultModel } = await fetchModelCatalog(http);
385
+ const current = (0, store_1.readSession)().last_model || defaultModel;
386
+ if (!current && !models.length)
387
+ return 'no models available';
388
+ const lines = models.map((row) => {
389
+ const mark = row.name === current ? '* ' : ' ';
390
+ const extra = row.label !== row.name ? ` ${row.label}` : '';
391
+ return `${mark}${row.name}${extra}`;
392
+ });
393
+ if (current && !models.some((row) => row.name === current)) {
394
+ lines.unshift(`* ${current}`);
395
+ }
396
+ return lines.join('\n') || `* ${current}`;
397
+ }
398
+ async function setModelCommand(query, http = http_1.defaultHttp) {
399
+ const wanted = query.trim();
400
+ if (!wanted)
401
+ return listModelsCommand(http);
402
+ const { models, defaultModel } = await fetchModelCatalog(http);
403
+ const match = resolveCatalogModel(wanted, models);
404
+ const name = match?.name || (normalizeModelKey(wanted) === normalizeModelKey(defaultModel) ? defaultModel : '');
405
+ if (!name)
406
+ throw new Error(`unknown model ${wanted}`);
407
+ (0, store_1.writeSession)({ ...(0, store_1.readSession)(), last_model: name });
408
+ return `model ${name}`;
409
+ }
package/dist/main.js CHANGED
@@ -25,6 +25,8 @@ Usage:
25
25
  sisu open <dir> --project <project-id>
26
26
  sisu ls [--project <project-id>]
27
27
  sisu exec "<prompt>" [--project <id>] [--model <name>] [--new]
28
+ sisu models
29
+ sisu model <name>
28
30
  sisu history
29
31
  sisu thread <conversation-id>
30
32
  sisu training --on|--off
@@ -146,6 +148,15 @@ async function runCli(argv, deps = {}) {
146
148
  process.stdout.write(`${(0, commands_1.openConversationCommand)(id)}\n`);
147
149
  return 0;
148
150
  }
151
+ if (command === 'models') {
152
+ process.stdout.write(`${await (0, commands_1.listModelsCommand)(http)}\n`);
153
+ return 0;
154
+ }
155
+ if (command === 'model') {
156
+ const name = args.find((item) => !item.startsWith('--')) || '';
157
+ process.stdout.write(`${await (0, commands_1.setModelCommand)(name, http)}\n`);
158
+ return 0;
159
+ }
149
160
  if (command === 'training') {
150
161
  if (args.includes('--on')) {
151
162
  process.stdout.write(`${await (0, commands_1.setTrainingCommand)(true)}\n`);
package/dist/pager/app.js CHANGED
@@ -1,28 +1,42 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.formatChromeStatus = formatChromeStatus;
4
7
  exports.chromeShortQuota = chromeShortQuota;
5
8
  exports.runPager = runPager;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
6
11
  const store_1 = require("../store");
7
12
  const input_1 = require("./input");
8
13
  const model_1 = require("./model");
9
14
  const history_1 = require("./history");
10
15
  const logo_1 = require("../logo");
16
+ const text_1 = require("./text");
11
17
  const render_1 = require("./render");
12
18
  const ALT_ENTER = '\x1b[?1049h\x1b[?25l';
13
19
  const ALT_LEAVE = '\x1b[?1049l\x1b[?25h';
14
- function formatChromeStatus(email, quota, conversationId) {
20
+ function shortConversationId(id) {
21
+ const conv = (id || '').trim();
22
+ if (!conv || conv === 'new')
23
+ return '';
24
+ return conv.length > 12 ? conv.slice(0, 8) : conv;
25
+ }
26
+ function formatChromeStatus(email, quota, conversationId = '', model = '') {
15
27
  const who = (email || '').trim();
28
+ const conv = shortConversationId(conversationId);
29
+ const modelName = (model || '').trim();
16
30
  if (!who) {
17
- const conv = (conversationId || '').trim();
18
- return conv && conv !== 'new' ? `sisu · not signed in · ${conv}` : 'sisu · not signed in';
31
+ return conv ? `sisu · not signed in · ${conv}` : 'sisu · not signed in';
19
32
  }
20
33
  const parts = [who];
34
+ if (modelName)
35
+ parts.push(modelName);
21
36
  const quotaText = (quota || '').trim();
22
37
  if (quotaText && quotaText !== 'quota unavailable')
23
38
  parts.push(quotaText);
24
- const conv = (conversationId || '').trim();
25
- if (conv && conv !== 'new')
39
+ if (conv)
26
40
  parts.push(conv);
27
41
  return parts.join(' · ');
28
42
  }
@@ -69,6 +83,8 @@ function commandHead(text) {
69
83
  return '/new';
70
84
  if (token === '/history')
71
85
  return '/resume';
86
+ if (token === '/m')
87
+ return '/model';
72
88
  return token;
73
89
  }
74
90
  async function resolveText(value) {
@@ -80,9 +96,10 @@ async function runPager(io, transport, options = {}) {
80
96
  let theme = options.theme ?? 'dark';
81
97
  let quotaLine = 'quota unavailable';
82
98
  let chromeEmail = (options.email || '').trim();
99
+ let chromeModel = ((0, store_1.readSession)().last_model || '').trim();
83
100
  const withChrome = (next) => ({
84
101
  ...next,
85
- statusLine: formatChromeStatus(chromeEmail, quotaLine, next.conversationId),
102
+ statusLine: formatChromeStatus(chromeEmail, quotaLine, next.conversationId, chromeModel),
86
103
  });
87
104
  const refreshQuota = async () => {
88
105
  if (!options.quota)
@@ -211,24 +228,24 @@ async function runPager(io, transport, options = {}) {
211
228
  paint();
212
229
  return;
213
230
  }
214
- state = pushEntry(state, 'status', 'Opening browser to sign in…');
231
+ state = { ...state, statusLine: 'Opening browser to sign in…' };
215
232
  paint();
216
233
  try {
217
234
  const email = await options.login((line) => {
218
235
  if (!running)
219
236
  return;
220
- state = pushEntry(state, 'status', line);
237
+ state = { ...state, statusLine: line };
221
238
  paint();
222
239
  });
223
240
  if (!running)
224
241
  return;
225
242
  chromeEmail = email;
226
243
  state = withChrome(state);
227
- state = pushEntry(state, 'status', `logged in as ${email}`);
228
244
  }
229
245
  catch (err) {
230
246
  if (!running)
231
247
  return;
248
+ state = withChrome(state);
232
249
  state = pushEntry(state, 'status', err instanceof Error ? err.message : String(err));
233
250
  }
234
251
  paint();
@@ -290,6 +307,79 @@ async function runPager(io, transport, options = {}) {
290
307
  paint();
291
308
  return;
292
309
  }
310
+ if (head === '/logout') {
311
+ options.logout?.();
312
+ chromeEmail = '';
313
+ chromeModel = '';
314
+ state = withChrome(state);
315
+ state = pushEntry(state, 'status', 'signed out');
316
+ paint();
317
+ return;
318
+ }
319
+ if (head === '/models' || (head === '/model' && !text.replace(/^\S+\s*/, '').trim())) {
320
+ const body = options.models ? await resolveText(options.models()) : 'not wired';
321
+ if (!running)
322
+ return;
323
+ state = pushEntry(state, 'status', body);
324
+ paint();
325
+ return;
326
+ }
327
+ if (head === '/model') {
328
+ const wanted = text.replace(/^\S+\s*/, '').trim();
329
+ if (!options.setModel) {
330
+ state = pushEntry(state, 'status', 'not wired');
331
+ }
332
+ else {
333
+ try {
334
+ const body = await resolveText(options.setModel(wanted));
335
+ if (!running)
336
+ return;
337
+ chromeModel = ((0, store_1.readSession)().last_model || wanted).trim();
338
+ state = withChrome(state);
339
+ state = pushEntry(state, 'status', body);
340
+ }
341
+ catch (err) {
342
+ if (!running)
343
+ return;
344
+ state = pushEntry(state, 'status', err instanceof Error ? err.message : String(err));
345
+ }
346
+ }
347
+ paint();
348
+ return;
349
+ }
350
+ if (head === '/copy') {
351
+ const last = [...state.entries].reverse().find((entry) => entry.kind === 'assistant');
352
+ const body = last ? (0, text_1.visibleAssistantText)(last.text).trim() : '';
353
+ if (!body) {
354
+ state = pushEntry(state, 'status', 'nothing to copy');
355
+ }
356
+ else {
357
+ const dest = path_1.default.join((0, store_1.getSisuHome)(), 'last-copy.txt');
358
+ fs_1.default.mkdirSync((0, store_1.getSisuHome)(), { recursive: true });
359
+ fs_1.default.writeFileSync(dest, `${body}\n`, 'utf8');
360
+ state = pushEntry(state, 'status', `copied to ${dest}`);
361
+ }
362
+ paint();
363
+ return;
364
+ }
365
+ if (head === '/export') {
366
+ const dest = text.replace(/^\S+\s*/, '').trim() || path_1.default.join((0, store_1.getSisuHome)(), 'export.md');
367
+ const body = state.entries
368
+ .map((entry) => {
369
+ if (entry.kind === 'user')
370
+ return `## You\n\n${entry.text}`;
371
+ if (entry.kind === 'assistant')
372
+ return `## SiSu\n\n${(0, text_1.visibleAssistantText)(entry.text)}`;
373
+ return '';
374
+ })
375
+ .filter(Boolean)
376
+ .join('\n\n');
377
+ fs_1.default.mkdirSync(path_1.default.dirname(path_1.default.resolve(dest)), { recursive: true });
378
+ fs_1.default.writeFileSync(path_1.default.resolve(dest), `${body}\n`, 'utf8');
379
+ state = pushEntry(state, 'status', `exported ${path_1.default.resolve(dest)}`);
380
+ paint();
381
+ return;
382
+ }
293
383
  if (head === '/status') {
294
384
  const body = options.status ? await resolveText(options.status()) : 'not wired';
295
385
  if (!running)
@@ -9,8 +9,13 @@ exports.appendText = appendText;
9
9
  exports.applyKey = applyKey;
10
10
  exports.SLASH_COMMANDS = [
11
11
  { name: '/login', hint: 'Sign in with the browser' },
12
+ { name: '/logout', hint: 'Sign out of this terminal' },
12
13
  { name: '/new', hint: 'Start a new conversation (alias: /clear)' },
13
14
  { name: '/resume', hint: 'Resume a conversation (alias: /history)' },
15
+ { name: '/model', hint: 'Switch model (alias: /m)' },
16
+ { name: '/models', hint: 'List models available to your account' },
17
+ { name: '/copy', hint: 'Copy the last assistant reply' },
18
+ { name: '/export', hint: 'Write this conversation to a markdown file' },
14
19
  { name: '/status', hint: 'Show session status' },
15
20
  { name: '/ls', hint: 'List local workspace files' },
16
21
  { name: '/training', hint: 'Training mode' },
@@ -23,6 +28,7 @@ const SLASH_ALIASES = {
23
28
  '/clear': '/new',
24
29
  '/history': '/resume',
25
30
  '/exit': '/quit',
31
+ '/m': '/model',
26
32
  };
27
33
  let nextEntryId = 1;
28
34
  function entryId() {
@@ -7,25 +7,13 @@ const mobius_1 = require("../mobius");
7
7
  const model_1 = require("./model");
8
8
  const theme_1 = require("./theme");
9
9
  Object.defineProperty(exports, "stripAnsi", { enumerable: true, get: function () { return theme_1.stripAnsi; } });
10
+ const text_1 = require("./text");
10
11
  const PROMPT_PREFIX = '› ';
11
12
  const PROMPT_BOX_ROWS = 2;
12
13
  const STATUS_ROWS = 1;
13
14
  const MARK_WIDTH = 2;
14
15
  function wrapPlain(text, width) {
15
- const max = Math.max(1, width);
16
- if (!text)
17
- return [''];
18
- const out = [];
19
- for (const paragraph of text.split('\n')) {
20
- if (!paragraph) {
21
- out.push('');
22
- continue;
23
- }
24
- for (let i = 0; i < paragraph.length; i += max) {
25
- out.push(paragraph.slice(i, i + max));
26
- }
27
- }
28
- return out;
16
+ return (0, text_1.wrapCells)(text, width);
29
17
  }
30
18
  function lineCount(text) {
31
19
  if (!text)
@@ -43,22 +31,25 @@ function paintKind(kind, body, theme) {
43
31
  return theme.dim(body);
44
32
  return theme.text(body);
45
33
  }
46
- function entryPrefix(kind) {
47
- if (kind === 'user')
48
- return 'you ';
49
- if (kind === 'tool')
50
- return 'tool ';
51
- return '';
34
+ function shownText(entry) {
35
+ if (entry.kind === 'assistant')
36
+ return (0, text_1.visibleAssistantText)(entry.text);
37
+ return entry.text;
52
38
  }
53
39
  function entryBodyLines(entry, wrapWidth) {
40
+ const text = shownText(entry);
54
41
  if (entry.folded) {
55
- const n = lineCount(entry.text);
42
+ const n = lineCount(text);
56
43
  const unit = n === 1 ? 'line' : 'lines';
57
44
  return wrapPlain(`${entry.kind} · ${n} ${unit}`, wrapWidth);
58
45
  }
59
- if (!entry.text)
46
+ if (entry.kind === 'user')
47
+ return ['You', ...wrapPlain(text || ' ', wrapWidth)];
48
+ if (entry.kind === 'tool')
49
+ return wrapPlain(text ? `tool ${text}` : 'tool', wrapWidth);
50
+ if (!text)
60
51
  return [''];
61
- return wrapPlain(`${entryPrefix(entry.kind)}${entry.text}`, wrapWidth);
52
+ return wrapPlain(text, wrapWidth);
62
53
  }
63
54
  function layoutScrollback(state, cols, theme) {
64
55
  const wrapWidth = Math.max(1, cols - MARK_WIDTH);
@@ -68,6 +59,10 @@ function layoutScrollback(state, cols, theme) {
68
59
  const entry = state.entries[i];
69
60
  const body = entryBodyLines(entry, wrapWidth);
70
61
  const selected = i === state.selected && state.entries.length > 0;
62
+ if (i > 0) {
63
+ lines.push('');
64
+ entryOf.push(i);
65
+ }
71
66
  for (let j = 0; j < body.length; j += 1) {
72
67
  const mark = selected && j === 0 ? theme.accent('▸ ') : ' ';
73
68
  lines.push(`${mark}${paintKind(entry.kind, body[j], theme)}`);
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /** Display-side text: hide model scratch and wrap by terminal cells. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.visibleAssistantText = visibleAssistantText;
5
+ exports.wrapCells = wrapCells;
6
+ const theme_1 = require("./theme");
7
+ const THINK_BLOCK = /<think\b[^>]*>[\s\S]*?<\/think>/gi;
8
+ const THINK_OPEN = /<think\b[^>]*>[\s\S]*$/i;
9
+ function visibleAssistantText(raw) {
10
+ let text = String(raw || '');
11
+ text = text.replace(THINK_BLOCK, '');
12
+ text = text.replace(THINK_OPEN, '');
13
+ return text.replace(/^\n+/, '').replace(/\n+$/, '');
14
+ }
15
+ function wrapCells(text, width) {
16
+ const max = Math.max(1, width);
17
+ if (!text)
18
+ return [''];
19
+ const out = [];
20
+ for (const paragraph of text.split('\n')) {
21
+ if (!paragraph) {
22
+ out.push('');
23
+ continue;
24
+ }
25
+ let line = '';
26
+ for (const ch of paragraph) {
27
+ if ((0, theme_1.visibleWidth)(line + ch) > max) {
28
+ if (line)
29
+ out.push(line);
30
+ line = ch;
31
+ }
32
+ else {
33
+ line += ch;
34
+ }
35
+ }
36
+ if (line)
37
+ out.push(line);
38
+ }
39
+ return out.length ? out : [''];
40
+ }
package/dist/store.js CHANGED
@@ -140,5 +140,6 @@ function describeStatus() {
140
140
  plan_code: auth?.plan_code || '',
141
141
  api_base: auth?.api_base || process.env.SISU_API_BASE || exports.DEFAULT_API_BASE,
142
142
  workspaces: readWorkspaces(),
143
+ model: readSession().last_model || '',
143
144
  };
144
145
  }
package/dist/transport.js CHANGED
@@ -64,6 +64,7 @@ function createFastApiTransport(http) {
64
64
  headers: (0, http_1.authHeaders)(auth.token),
65
65
  body: JSON.stringify({
66
66
  title: text.slice(0, 50),
67
+ model: session.last_model || undefined,
67
68
  project_id: session.last_project_id || undefined,
68
69
  client: stamp.client,
69
70
  client_version: stamp.client_version,
@@ -87,6 +88,7 @@ function createFastApiTransport(http) {
87
88
  body: JSON.stringify({
88
89
  conversation_id: conversationId,
89
90
  message: text,
91
+ model: session.last_model || undefined,
90
92
  task_category: 'coding',
91
93
  client: stamp.client,
92
94
  client_version: stamp.client_version,
package/dist/tui.js CHANGED
@@ -171,6 +171,11 @@ async function promptLogin(io, login) {
171
171
  function tuiHelp() {
172
172
  return [
173
173
  '/login sign in with the browser',
174
+ '/logout sign out',
175
+ '/model switch model (alias /m)',
176
+ '/models list available models',
177
+ '/copy copy last reply to ~/.sisu/last-copy.txt',
178
+ '/export write the thread to a markdown file',
174
179
  '/status account and quota',
175
180
  '/ls local workspace files',
176
181
  '/history saved cloud conversations',
@@ -224,6 +229,9 @@ async function runTui(io, deps = {}) {
224
229
  columns,
225
230
  email: account?.email,
226
231
  login: startWebLogin,
232
+ logout: commands_1.logoutCommand,
233
+ models: () => (0, commands_1.listModelsCommand)(http),
234
+ setModel: (name) => (0, commands_1.setModelCommand)(name, http),
227
235
  intro: animate,
228
236
  sleep: deps.sleep,
229
237
  quota: async () => (0, commands_1.formatQuota)(await (0, commands_1.fetchBalance)(http)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevezhou/sisu",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "SiSu CLI — one login, cloud quota, local workspace",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://www.sisu.chat",