@stevezhou/sisu 0.1.10 → 0.2.0

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/NOTICE ADDED
@@ -0,0 +1,12 @@
1
+ SiSu CLI
2
+ Copyright 2026 SiSu / 思溯
3
+
4
+ This product includes grok-build first-party source (Apache-2.0)
5
+ from https://github.com/xai-org/grok-build, Copyright 2023-2026 SpaceXAI.
6
+
7
+ The grok-build tree lives at vendor/grok-build/ together with its
8
+ LICENSE and THIRD-PARTY-NOTICES. SiSu二次开发 replaces xAI login,
9
+ telemetry defaults, branding, and the model/quota endpoint so the
10
+ user-facing binary is SiSu (`sisu`), not the official `grok` client.
11
+
12
+ See vendor/grok-build/NOTICE for the grok-build-specific attribution.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Official publish channel for the SiSu CLI npm package.
4
4
 
5
- One login. Cloud quota. Local workspace. Auth lives in `~/.sisu`, shared with SiSu Desktop. Product development of the CLI still happens in the main SiSu repo; this repository is what `npm publish` and `npm install -g @stevezhou/sisu` use.
5
+ One login. Cloud quota. Local runtime. Auth lives in `~/.sisu`, shared with SiSu Desktop. The CLI is a grok-build-derived local coding agent (read / edit / search / shell + the grok-build extension surface). SiSu cloud plays the Claude/Grok role: device login, model list, billed completions (`POST /api/runtime/complete`). Sessions are local under `~/.sisu/sessions`.
6
6
 
7
7
  ## Install
8
8
 
@@ -29,9 +29,10 @@ Default API is `https://www.sisu.chat`. Override with `--api` or `SISU_API_BASE`
29
29
  ## Commands
30
30
 
31
31
  ```
32
- sisu interactive TUI
32
+ sisu interactive TUI (local runtime; Grok Build if the pager binary is present)
33
33
  sisu open <dir> --project <id>
34
34
  sisu exec "<prompt>"
35
+ sisu -p "<prompt>"
35
36
  sisu history
36
37
  sisu logout
37
38
  ```
package/dist/commands.js CHANGED
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveRuntimeModel = exports.resolveCatalogModel = exports.fetchModelCatalog = void 0;
6
7
  exports.loginCommand = loginCommand;
7
8
  exports.logoutCommand = logoutCommand;
8
9
  exports.resolveVerificationUrl = resolveVerificationUrl;
@@ -18,12 +19,19 @@ exports.execCommand = execCommand;
18
19
  exports.listConversationsCommand = listConversationsCommand;
19
20
  exports.openConversationCommand = openConversationCommand;
20
21
  exports.setTrainingCommand = setTrainingCommand;
22
+ exports.listModelsCommand = listModelsCommand;
23
+ exports.setModelCommand = setModelCommand;
21
24
  const child_process_1 = require("child_process");
22
25
  const fs_1 = __importDefault(require("fs"));
23
26
  const path_1 = __importDefault(require("path"));
24
27
  const http_1 = require("./http");
25
- const sse_1 = require("./sse");
26
- const client_1 = require("./client");
28
+ const adapter_1 = require("./runtime/adapter");
29
+ const loop_1 = require("./runtime/loop");
30
+ const models_1 = require("./runtime/models");
31
+ Object.defineProperty(exports, "fetchModelCatalog", { enumerable: true, get: function () { return models_1.fetchModelCatalog; } });
32
+ Object.defineProperty(exports, "resolveCatalogModel", { enumerable: true, get: function () { return models_1.resolveCatalogModel; } });
33
+ Object.defineProperty(exports, "resolveRuntimeModel", { enumerable: true, get: function () { return models_1.resolveRuntimeModel; } });
34
+ const transport_1 = require("./runtime/transport");
27
35
  const store_1 = require("./store");
28
36
  async function loginCommand(input, http = http_1.defaultHttp) {
29
37
  const apiBase = (input.apiBase || process.env.SISU_API_BASE || store_1.DEFAULT_API_BASE).replace(/\/+$/, '');
@@ -262,56 +270,37 @@ function listLocalCommand(projectId) {
262
270
  }).join('\n');
263
271
  }
264
272
  async function execCommand(prompt, options = {}, http = http_1.defaultHttp) {
265
- const auth = (0, store_1.requireAuth)();
266
273
  const text = prompt.trim();
267
274
  if (!text)
268
275
  throw new Error('prompt is required');
269
- const stamp = (0, client_1.clientStamp)(options.client || 'cli');
270
- let conversationId = options.conversationId || (!options.newConversation ? (0, store_1.readSession)().last_conversation_id : '');
271
- if (!conversationId) {
272
- const created = await http(`${auth.api_base}/api/chat/conversations`, {
273
- method: 'POST',
274
- headers: (0, http_1.authHeaders)(auth.token),
275
- body: JSON.stringify({
276
- title: text.slice(0, 50),
277
- model: options.model || undefined,
278
- project_id: options.projectId || (0, store_1.readSession)().last_project_id || undefined,
279
- client: stamp.client,
280
- client_version: stamp.client_version,
281
- }),
282
- });
283
- const body = await created.json().catch(() => ({}));
284
- if (!created.ok)
285
- throw new Error((0, http_1.errorDetail)(body, `create conversation failed (${created.status})`));
286
- conversationId = String(body.id || '');
287
- if (!conversationId)
288
- throw new Error('create conversation missing id');
276
+ const stub = Boolean(options.stub || process.env.SISU_RUNTIME_STUB === '1');
277
+ const cwd = options.cwd || process.cwd();
278
+ const modelClient = options.modelClient || (stub
279
+ ? (0, loop_1.createLaunchStubModel)()
280
+ : (() => {
281
+ const auth = (0, store_1.requireAuth)();
282
+ return (0, adapter_1.createSisuCloudModel)(http, {
283
+ apiBase: auth.api_base,
284
+ token: auth.token,
285
+ client: options.client || 'cli',
286
+ });
287
+ })());
288
+ if (!stub)
289
+ (0, store_1.requireAuth)();
290
+ if (options.projectId) {
291
+ (0, store_1.writeSession)({ ...(0, store_1.readSession)(), last_project_id: options.projectId });
289
292
  }
290
- const sent = await http(`${auth.api_base}/api/chat/send`, {
291
- method: 'POST',
292
- headers: (0, http_1.authHeaders)(auth.token),
293
- body: JSON.stringify({
294
- conversation_id: conversationId,
295
- message: text,
296
- model: options.model || undefined,
297
- task_category: 'coding',
298
- client: stamp.client,
299
- client_version: stamp.client_version,
300
- client_request_id: stamp.client_request_id,
301
- }),
293
+ const model = await (0, models_1.resolveRuntimeModel)(http, { explicit: options.model, stub });
294
+ const result = await (0, transport_1.execLocalTurn)(text, {
295
+ cwd,
296
+ model,
297
+ conversationId: options.conversationId,
298
+ newConversation: options.newConversation,
299
+ modelClient,
300
+ http,
301
+ client: options.client || 'cli',
302
302
  });
303
- if (!sent.ok) {
304
- const body = await sent.json().catch(() => ({}));
305
- throw new Error((0, http_1.errorDetail)(body, `exec failed (${sent.status})`));
306
- }
307
- const stream = await sent.text();
308
- const output = (0, sse_1.extractSseText)(stream);
309
- (0, store_1.writeSession)({
310
- ...(0, store_1.readSession)(),
311
- last_conversation_id: conversationId,
312
- last_project_id: options.projectId || (0, store_1.readSession)().last_project_id,
313
- });
314
- return { conversationId, text: output };
303
+ return { conversationId: result.conversationId, text: result.text };
315
304
  }
316
305
  async function listConversationsCommand(http = http_1.defaultHttp) {
317
306
  const auth = (0, store_1.requireAuth)();
@@ -348,3 +337,25 @@ async function setTrainingCommand(optIn, http = http_1.defaultHttp) {
348
337
  throw new Error((0, http_1.errorDetail)(body, `training update failed (${response.status})`));
349
338
  return optIn ? 'training opt-in on (new turns may be used if eligible)' : 'training opt-in off';
350
339
  }
340
+ async function listModelsCommand(http = http_1.defaultHttp) {
341
+ const { models, defaultModel } = await (0, models_1.fetchModelCatalog)(http);
342
+ const current = (0, store_1.readSession)().last_model || defaultModel;
343
+ if (!current && !models.length)
344
+ return 'no models available';
345
+ const lines = models.map((row) => {
346
+ const mark = row.name === current ? '* ' : ' ';
347
+ const extra = row.label !== row.name ? ` ${row.label}` : '';
348
+ return `${mark}${row.name}${extra}`;
349
+ });
350
+ if (current && !models.some((row) => row.name === current)) {
351
+ lines.unshift(`* ${current}`);
352
+ }
353
+ return lines.join('\n') || `* ${current}`;
354
+ }
355
+ async function setModelCommand(query, http = http_1.defaultHttp) {
356
+ const wanted = query.trim();
357
+ if (!wanted)
358
+ return listModelsCommand(http);
359
+ const name = await (0, models_1.resolveRuntimeModel)(http, { explicit: wanted });
360
+ return `model ${name}`;
361
+ }
package/dist/main.js CHANGED
@@ -24,11 +24,14 @@ Usage:
24
24
  sisu status
25
25
  sisu open <dir> --project <project-id>
26
26
  sisu ls [--project <project-id>]
27
- sisu exec "<prompt>" [--project <id>] [--model <name>] [--new]
27
+ sisu exec "<prompt>" [--project <id>] [--model <name>] [--new] [--stub]
28
+ sisu -p "<prompt>" headless local-agent turn (alias of exec)
29
+ sisu models
30
+ sisu model <name>
28
31
  sisu history
29
32
  sisu thread <conversation-id>
30
33
  sisu training --on|--off
31
- sisu interactive TUI (Möbius splash)
34
+ sisu Grok Build TUI (SiSu account, models, quota)
32
35
  sisu help
33
36
 
34
37
  Auth and workspaces live in $SISU_HOME (default ~/.sisu), shared with Desktop.
@@ -49,8 +52,8 @@ function parseArgs(args) {
49
52
  const switches = new Set();
50
53
  for (let i = 0; i < args.length; i += 1) {
51
54
  const item = args[i];
52
- if (item === '--new') {
53
- switches.add('new');
55
+ if (item === '--new' || item === '--stub' || item === '-p' || item === '--print') {
56
+ switches.add(item.replace(/^-+/, ''));
54
57
  continue;
55
58
  }
56
59
  if (item.startsWith('--')) {
@@ -121,7 +124,7 @@ async function runCli(argv, deps = {}) {
121
124
  process.stdout.write(`${(0, commands_1.listLocalCommand)(flag(args, '--project'))}\n`);
122
125
  return 0;
123
126
  }
124
- if (command === 'exec') {
127
+ if (command === 'exec' || command === '-p' || command === '--print') {
125
128
  const parsed = parseArgs(args);
126
129
  const prompt = parsed.rest.join(' ').trim();
127
130
  if (!prompt) {
@@ -132,6 +135,7 @@ async function runCli(argv, deps = {}) {
132
135
  projectId: parsed.flags['--project'],
133
136
  model: parsed.flags['--model'],
134
137
  newConversation: parsed.switches.has('new'),
138
+ stub: parsed.switches.has('stub') || process.env.SISU_RUNTIME_STUB === '1',
135
139
  });
136
140
  if (result.text)
137
141
  process.stdout.write(`${result.text}\n`);
@@ -146,6 +150,15 @@ async function runCli(argv, deps = {}) {
146
150
  process.stdout.write(`${(0, commands_1.openConversationCommand)(id)}\n`);
147
151
  return 0;
148
152
  }
153
+ if (command === 'models') {
154
+ process.stdout.write(`${await (0, commands_1.listModelsCommand)(http)}\n`);
155
+ return 0;
156
+ }
157
+ if (command === 'model') {
158
+ const name = args.find((item) => !item.startsWith('--')) || '';
159
+ process.stdout.write(`${await (0, commands_1.setModelCommand)(name, http)}\n`);
160
+ return 0;
161
+ }
149
162
  if (command === 'training') {
150
163
  if (args.includes('--on')) {
151
164
  process.stdout.write(`${await (0, commands_1.setTrainingCommand)(true)}\n`);
package/dist/pager/app.js CHANGED
@@ -1,13 +1,19 @@
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';
@@ -17,13 +23,16 @@ function shortConversationId(id) {
17
23
  return '';
18
24
  return conv.length > 12 ? conv.slice(0, 8) : conv;
19
25
  }
20
- function formatChromeStatus(email, quota, conversationId = '') {
26
+ function formatChromeStatus(email, quota, conversationId = '', model = '') {
21
27
  const who = (email || '').trim();
22
28
  const conv = shortConversationId(conversationId);
29
+ const modelName = (model || '').trim();
23
30
  if (!who) {
24
- return conv ? `sisu · not signed in · ${conv}` : 'sisu · not signed in';
31
+ return conv ? `sisu local · not signed in · ${conv}` : 'sisu local · not signed in';
25
32
  }
26
- const parts = [who];
33
+ const parts = [who, 'local'];
34
+ if (modelName)
35
+ parts.push(modelName);
27
36
  const quotaText = (quota || '').trim();
28
37
  if (quotaText && quotaText !== 'quota unavailable')
29
38
  parts.push(quotaText);
@@ -54,7 +63,7 @@ function pushEntry(state, kind, text) {
54
63
  return { ...state, entries, selected: entries.length - 1 };
55
64
  }
56
65
  function clearDraft(state) {
57
- return { ...state, draft: '', slashOpen: false, slashIndex: 0 };
66
+ return { ...state, draft: '', slashOpen: false, slashIndex: 0, draftIndex: 0, historyIndex: -1, stashDraft: '' };
58
67
  }
59
68
  function submittedText(state) {
60
69
  const raw = state.draft.trim();
@@ -74,6 +83,8 @@ function commandHead(text) {
74
83
  return '/new';
75
84
  if (token === '/history')
76
85
  return '/resume';
86
+ if (token === '/m')
87
+ return '/model';
77
88
  return token;
78
89
  }
79
90
  async function resolveText(value) {
@@ -85,9 +96,10 @@ async function runPager(io, transport, options = {}) {
85
96
  let theme = options.theme ?? 'dark';
86
97
  let quotaLine = 'quota unavailable';
87
98
  let chromeEmail = (options.email || '').trim();
99
+ let chromeModel = ((0, store_1.readSession)().last_model || '').trim();
88
100
  const withChrome = (next) => ({
89
101
  ...next,
90
- statusLine: formatChromeStatus(chromeEmail, quotaLine, next.conversationId),
102
+ statusLine: formatChromeStatus(chromeEmail, quotaLine, next.conversationId, chromeModel),
91
103
  });
92
104
  const refreshQuota = async () => {
93
105
  if (!options.quota)
@@ -295,6 +307,79 @@ async function runPager(io, transport, options = {}) {
295
307
  paint();
296
308
  return;
297
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
+ }
298
383
  if (head === '/status') {
299
384
  const body = options.status ? await resolveText(options.status()) : 'not wired';
300
385
  if (!running)
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.decodeKeys = decodeKeys;
4
4
  /**
5
5
  * Decode a raw terminal input chunk into pager keys.
6
- * Incomplete CSI sequences are returned in `rest` so the next read can finish them.
6
+ * Incomplete CSI/SS3 sequences are returned in `rest` so the next read can finish them.
7
7
  * `\x03` (Ctrl+C) maps to escape; the app treats escape at empty draft as quit.
8
8
  */
9
9
  function decodeKeys(chunk) {
@@ -12,37 +12,52 @@ function decodeKeys(chunk) {
12
12
  while (i < chunk.length) {
13
13
  const ch = chunk[i];
14
14
  if (ch === '\x1b') {
15
- // Incomplete CSI prefix — hold for the next chunk.
16
15
  if (i + 1 === chunk.length) {
17
16
  keys.push({ type: 'escape' });
18
17
  i += 1;
19
18
  continue;
20
19
  }
21
- if (chunk[i + 1] === '[') {
22
- if (i + 2 >= chunk.length) {
20
+ if (chunk[i + 1] === 'O') {
21
+ if (i + 2 >= chunk.length)
23
22
  return { keys, rest: chunk.slice(i) };
24
- }
25
- const code = chunk[i + 2];
26
- const arrow = code === 'A'
27
- ? 'up'
28
- : code === 'B'
29
- ? 'down'
30
- : code === 'C'
31
- ? 'right'
32
- : code === 'D'
33
- ? 'left'
34
- : null;
23
+ const arrow = arrowFrom(chunk[i + 2]);
35
24
  if (arrow) {
36
25
  keys.push({ type: arrow });
37
26
  i += 3;
38
27
  continue;
39
28
  }
40
- // Unknown completed CSI: treat ESC as escape and rescan from '['.
41
29
  keys.push({ type: 'escape' });
42
30
  i += 1;
43
31
  continue;
44
32
  }
45
- // ESC not introducing CSI escape
33
+ if (chunk[i + 1] === '[') {
34
+ let j = i + 2;
35
+ while (j < chunk.length && /[0-9;]/.test(chunk[j]))
36
+ j += 1;
37
+ if (j >= chunk.length)
38
+ return { keys, rest: chunk.slice(i) };
39
+ const body = chunk.slice(i + 2, j);
40
+ const term = chunk[j];
41
+ const arrow = arrowFrom(term);
42
+ if (arrow) {
43
+ keys.push({ type: arrow });
44
+ i = j + 1;
45
+ continue;
46
+ }
47
+ if (term === '~' && body === '5') {
48
+ keys.push({ type: 'pageup' });
49
+ i = j + 1;
50
+ continue;
51
+ }
52
+ if (term === '~' && body === '6') {
53
+ keys.push({ type: 'pagedown' });
54
+ i = j + 1;
55
+ continue;
56
+ }
57
+ keys.push({ type: 'escape' });
58
+ i += 1;
59
+ continue;
60
+ }
46
61
  keys.push({ type: 'escape' });
47
62
  i += 1;
48
63
  continue;
@@ -57,19 +72,16 @@ function decodeKeys(chunk) {
57
72
  i += 1;
58
73
  continue;
59
74
  }
60
- // Ctrl+C → escape (app treats escape at empty draft as quit)
61
75
  if (ch === '\x03') {
62
76
  keys.push({ type: 'escape' });
63
77
  i += 1;
64
78
  continue;
65
79
  }
66
- // Skip other C0 controls (except those handled above)
67
80
  const code = ch.charCodeAt(0);
68
81
  if (code < 0x20 || code === 0x7f) {
69
82
  i += 1;
70
83
  continue;
71
84
  }
72
- // Printable UTF-16 code unit / surrogate pair as one char
73
85
  const cp = chunk.codePointAt(i);
74
86
  if (cp === undefined) {
75
87
  i += 1;
@@ -81,3 +93,14 @@ function decodeKeys(chunk) {
81
93
  }
82
94
  return { keys, rest: '' };
83
95
  }
96
+ function arrowFrom(code) {
97
+ if (code === 'A')
98
+ return 'up';
99
+ if (code === 'B')
100
+ return 'down';
101
+ if (code === 'C')
102
+ return 'right';
103
+ if (code === 'D')
104
+ return 'left';
105
+ return null;
106
+ }