@stevezhou/sisu 0.1.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/LICENSE ADDED
@@ -0,0 +1,6 @@
1
+ UNLICENSED
2
+
3
+ Copyright (c) 2026 SiSu (思溯). All rights reserved.
4
+
5
+ This software is proprietary. See the SiSu product license.
6
+ Use, copying, and redistribution require permission from SiSu.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # sisu
2
+
3
+ Official publish channel for the SiSu CLI npm package.
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.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @stevezhou/sisu
11
+ sisu --help
12
+ sisu login
13
+ ```
14
+
15
+ Requires Node.js 20 or newer. `npx sisu` works without a global install.
16
+
17
+ ## Login
18
+
19
+ ```bash
20
+ sisu login
21
+ sisu login --code <grant>
22
+ sisu login --email you@example.com --password '…'
23
+ sisu login --token <jwt>
24
+ sisu status
25
+ ```
26
+
27
+ Default API is `https://www.sisu.chat`. Override with `--api` or `SISU_API_BASE`.
28
+
29
+ ## Commands
30
+
31
+ ```
32
+ sisu interactive TUI
33
+ sisu open <dir> --project <id>
34
+ sisu exec "<prompt>"
35
+ sisu history
36
+ sisu logout
37
+ ```
38
+
39
+ ## Publish
40
+
41
+ 1. Add repository secret `NPM_TOKEN` (npm automation token with publish rights to `sisu`).
42
+ 2. Tag a release and push:
43
+
44
+ ```bash
45
+ git tag v0.1.0
46
+ git push origin v0.1.0
47
+ ```
48
+
49
+ The `Publish` workflow runs tests, packs, and `npm publish --access public`.
package/dist/client.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SISU_CLIENT_VERSION = void 0;
4
+ exports.clientStamp = clientStamp;
5
+ const module_1 = require("module");
6
+ const crypto_1 = require("crypto");
7
+ const req = (0, module_1.createRequire)(__filename);
8
+ exports.SISU_CLIENT_VERSION = String(req('../package.json').version || '0.1.0');
9
+ function clientStamp(kind) {
10
+ return {
11
+ client: kind,
12
+ client_version: exports.SISU_CLIENT_VERSION,
13
+ client_request_id: (0, crypto_1.randomUUID)(),
14
+ };
15
+ }
@@ -0,0 +1,343 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loginCommand = loginCommand;
7
+ exports.logoutCommand = logoutCommand;
8
+ exports.resolveVerificationUrl = resolveVerificationUrl;
9
+ exports.openBrowserSafely = openBrowserSafely;
10
+ exports.webLoginCommand = webLoginCommand;
11
+ exports.formatQuota = formatQuota;
12
+ exports.fetchBalance = fetchBalance;
13
+ exports.statusCommand = statusCommand;
14
+ exports.openCommand = openCommand;
15
+ exports.resolveBoundWorkspace = resolveBoundWorkspace;
16
+ exports.listLocalCommand = listLocalCommand;
17
+ exports.execCommand = execCommand;
18
+ exports.listConversationsCommand = listConversationsCommand;
19
+ exports.openConversationCommand = openConversationCommand;
20
+ exports.setTrainingCommand = setTrainingCommand;
21
+ const child_process_1 = require("child_process");
22
+ const fs_1 = __importDefault(require("fs"));
23
+ const path_1 = __importDefault(require("path"));
24
+ const http_1 = require("./http");
25
+ const sse_1 = require("./sse");
26
+ const client_1 = require("./client");
27
+ const store_1 = require("./store");
28
+ async function loginCommand(input, http = http_1.defaultHttp) {
29
+ const apiBase = (input.apiBase || process.env.SISU_API_BASE || store_1.DEFAULT_API_BASE).replace(/\/+$/, '');
30
+ if (input.token) {
31
+ const me = await http(`${apiBase}/api/auth/me`, { headers: (0, http_1.authHeaders)(input.token) });
32
+ const user = await me.json().catch(() => ({}));
33
+ if (!me.ok)
34
+ throw new Error((0, http_1.errorDetail)(user, `login failed (${me.status})`));
35
+ (0, store_1.writeAuth)({
36
+ token: input.token,
37
+ email: user.email || '',
38
+ user_id: String(user.id || ''),
39
+ api_base: apiBase,
40
+ plan_code: user.plan_code || '',
41
+ name: user.name || '',
42
+ });
43
+ return user.email || 'authenticated';
44
+ }
45
+ if (!input.email || !input.password) {
46
+ throw new Error('login requires --email/--password, --token, or a web login');
47
+ }
48
+ const response = await http(`${apiBase}/api/auth/login`, {
49
+ method: 'POST',
50
+ headers: { 'Content-Type': 'application/json' },
51
+ body: JSON.stringify({ email: input.email, password: input.password }),
52
+ });
53
+ const body = await response.json().catch(() => ({}));
54
+ if (!response.ok) {
55
+ throw new Error((0, http_1.errorDetail)(body, `login failed (${response.status})`));
56
+ }
57
+ const token = String(body.access_token || '');
58
+ if (!token)
59
+ throw new Error('login response missing access_token');
60
+ (0, store_1.writeAuth)({
61
+ token,
62
+ email: body.user?.email || input.email,
63
+ user_id: String(body.user?.id || ''),
64
+ api_base: apiBase,
65
+ plan_code: body.user?.plan_code || '',
66
+ name: body.user?.name || '',
67
+ });
68
+ return body.user?.email || input.email;
69
+ }
70
+ function logoutCommand() {
71
+ (0, store_1.clearAuth)();
72
+ }
73
+ function resolveVerificationUrl(raw, apiBase) {
74
+ const base = new URL(apiBase.endsWith('/') ? apiBase : `${apiBase}/`);
75
+ const parsed = new URL(raw, base);
76
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
77
+ throw new Error('verification URL must be http(s)');
78
+ }
79
+ parsed.protocol = base.protocol;
80
+ parsed.host = base.host;
81
+ return parsed.toString();
82
+ }
83
+ function openBrowserSafely(url, spawnFn = child_process_1.spawn, platform = process.platform) {
84
+ let parsed;
85
+ try {
86
+ parsed = new URL(url);
87
+ }
88
+ catch {
89
+ return;
90
+ }
91
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
92
+ return;
93
+ try {
94
+ const child = platform === 'darwin'
95
+ ? spawnFn('open', [parsed.toString()], { detached: true, stdio: 'ignore' })
96
+ : platform === 'win32'
97
+ ? spawnFn('explorer.exe', [parsed.toString()], { detached: true, stdio: 'ignore' })
98
+ : spawnFn('xdg-open', [parsed.toString()], { detached: true, stdio: 'ignore' });
99
+ child.on('error', () => undefined);
100
+ child.unref();
101
+ }
102
+ catch {
103
+ // printed URL is enough when no browser can open
104
+ }
105
+ }
106
+ function pendingPoll(status, body) {
107
+ if (status === 428)
108
+ return true;
109
+ return String(body.detail || '') === 'authorization_pending';
110
+ }
111
+ async function webLoginCommand(input = {}, http = http_1.defaultHttp) {
112
+ const apiBase = (input.apiBase || process.env.SISU_API_BASE || store_1.DEFAULT_API_BASE).replace(/\/+$/, '');
113
+ if (input.grantCode) {
114
+ const exchanged = await http(`${apiBase}/api/auth/cli/device/exchange`, {
115
+ method: 'POST',
116
+ headers: { 'Content-Type': 'application/json' },
117
+ body: JSON.stringify({ grant_code: input.grantCode }),
118
+ });
119
+ const body = await exchanged.json().catch(() => ({}));
120
+ if (!exchanged.ok)
121
+ throw new Error((0, http_1.errorDetail)(body, `grant exchange failed (${exchanged.status})`));
122
+ const token = String(body.access_token || '');
123
+ if (!token)
124
+ throw new Error('grant exchange missing access_token');
125
+ return loginCommand({ token, apiBase }, http);
126
+ }
127
+ const started = await http(`${apiBase}/api/auth/cli/device`, { method: 'POST' });
128
+ const startBody = await started.json().catch(() => ({}));
129
+ if (!started.ok)
130
+ throw new Error((0, http_1.errorDetail)(startBody, `device start failed (${started.status})`));
131
+ const deviceCode = String(startBody.device_code || '');
132
+ const userCode = String(startBody.user_code || '');
133
+ const rawVerify = String(startBody.verification_uri_complete || startBody.verification_uri || '');
134
+ if (!deviceCode || !userCode || !rawVerify)
135
+ throw new Error('device start missing fields');
136
+ const complete = resolveVerificationUrl(rawVerify, apiBase);
137
+ input.onStart?.({
138
+ verification_uri: String(startBody.verification_uri || complete),
139
+ verification_uri_complete: complete,
140
+ user_code: userCode,
141
+ });
142
+ try {
143
+ (input.openBrowser ?? openBrowserSafely)(complete);
144
+ }
145
+ catch {
146
+ // printed URL is enough when no browser can open
147
+ }
148
+ const sleep = input.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
149
+ const intervalSec = Math.max(1, Number(startBody.interval || 1));
150
+ const expiresIn = Math.max(intervalSec, Number(startBody.expires_in || 600));
151
+ const maxAttempts = input.maxAttempts ?? Math.max(1, Math.ceil(expiresIn / intervalSec));
152
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
153
+ const polled = await http(`${apiBase}/api/auth/cli/device/token`, {
154
+ method: 'POST',
155
+ headers: { 'Content-Type': 'application/json' },
156
+ body: JSON.stringify({ device_code: deviceCode }),
157
+ });
158
+ const body = await polled.json().catch(() => ({}));
159
+ if (polled.ok) {
160
+ const token = String(body.access_token || '');
161
+ if (!token)
162
+ throw new Error('device token missing access_token');
163
+ return loginCommand({ token, apiBase }, http);
164
+ }
165
+ if (pendingPoll(polled.status, body)) {
166
+ await sleep(Number(startBody.interval || 1) * 1000);
167
+ continue;
168
+ }
169
+ if (String(body.detail || '') === 'access_denied')
170
+ throw new Error('login cancelled');
171
+ throw new Error((0, http_1.errorDetail)(body, `device poll failed (${polled.status})`));
172
+ }
173
+ throw new Error('login timed out');
174
+ }
175
+ function formatQuota(balance) {
176
+ if (balance.allowance?.unlimited)
177
+ return 'quota unlimited';
178
+ const total = Number(balance.total ?? 0);
179
+ const plan = Number(balance.plan?.balance ?? 0);
180
+ const wallet = Number(balance.wallet?.balance ?? 0);
181
+ const bonus = Number(balance.bonus?.balance ?? 0);
182
+ const name = balance.plan?.plan_name || balance.plan?.plan_code || 'plan';
183
+ const allowance = balance.allowance;
184
+ const ration = allowance
185
+ ? `allowance ${allowance.used ?? 0}/${allowance.limit ?? 0}`
186
+ : '';
187
+ return [`quota ${total} pts`, `${name} ${plan}`, `wallet ${wallet}`, `bonus ${bonus}`, ration].filter(Boolean).join(' · ');
188
+ }
189
+ async function fetchBalance(http = http_1.defaultHttp) {
190
+ const auth = (0, store_1.requireAuth)();
191
+ const response = await http(`${auth.api_base}/api/points/balance`, { headers: (0, http_1.authHeaders)(auth.token) });
192
+ const body = await response.json().catch(() => ({}));
193
+ if (!response.ok)
194
+ throw new Error((0, http_1.errorDetail)(body, `balance failed (${response.status})`));
195
+ return body;
196
+ }
197
+ async function statusCommand(http) {
198
+ const status = (0, store_1.describeStatus)();
199
+ const lines = [
200
+ `home ${status.home}`,
201
+ status.logged_in ? `user ${status.email}${status.plan_code ? ` (${status.plan_code})` : ''}` : 'user logged out',
202
+ `api ${status.api_base}`,
203
+ ];
204
+ if (status.logged_in && http) {
205
+ try {
206
+ lines.push(formatQuota(await fetchBalance(http)));
207
+ }
208
+ catch (error) {
209
+ lines.push(`quota unavailable (${error instanceof Error ? error.message : String(error)})`);
210
+ }
211
+ }
212
+ const entries = Object.entries(status.workspaces);
213
+ if (!entries.length) {
214
+ lines.push('workspaces none');
215
+ }
216
+ else {
217
+ for (const [projectId, workspacePath] of entries) {
218
+ lines.push(`workspace ${projectId} ${workspacePath}`);
219
+ }
220
+ }
221
+ return lines.join('\n');
222
+ }
223
+ function openCommand(projectId, dir) {
224
+ const resolved = dir === '.' ? process.cwd() : dir;
225
+ if (!fs_1.default.existsSync(resolved))
226
+ throw new Error('directory does not exist');
227
+ const bound = (0, store_1.bindWorkspace)(projectId, resolved);
228
+ const session = (0, store_1.readSession)();
229
+ (0, store_1.writeSession)({ ...session, last_project_id: bound.projectId });
230
+ return `opened ${bound.path} for ${bound.projectId}`;
231
+ }
232
+ function resolveBoundWorkspace(projectId) {
233
+ const workspaces = (0, store_1.readWorkspaces)();
234
+ const requested = (projectId || (0, store_1.readSession)().last_project_id || '').trim();
235
+ if (requested && workspaces[requested]) {
236
+ return { projectId: requested, path: workspaces[requested] };
237
+ }
238
+ const entries = Object.entries(workspaces);
239
+ if (entries.length === 1)
240
+ return { projectId: entries[0][0], path: entries[0][1] };
241
+ if (!entries.length)
242
+ throw new Error('no local workspace — run sisu open <dir> --project <id>');
243
+ throw new Error('multiple workspaces — pass --project');
244
+ }
245
+ function listLocalCommand(projectId) {
246
+ (0, store_1.requireAuth)();
247
+ const bound = resolveBoundWorkspace(projectId);
248
+ const names = fs_1.default.readdirSync(bound.path).filter((name) => !name.startsWith('.'));
249
+ if (!names.length)
250
+ return `${bound.path} (empty)`;
251
+ return names.map((name) => {
252
+ const full = path_1.default.join(bound.path, name);
253
+ const suffix = fs_1.default.statSync(full).isDirectory() ? '/' : '';
254
+ return `${name}${suffix}`;
255
+ }).join('\n');
256
+ }
257
+ async function execCommand(prompt, options = {}, http = http_1.defaultHttp) {
258
+ const auth = (0, store_1.requireAuth)();
259
+ const text = prompt.trim();
260
+ if (!text)
261
+ throw new Error('prompt is required');
262
+ const stamp = (0, client_1.clientStamp)(options.client || 'cli');
263
+ let conversationId = options.conversationId || (!options.newConversation ? (0, store_1.readSession)().last_conversation_id : '');
264
+ if (!conversationId) {
265
+ const created = await http(`${auth.api_base}/api/chat/conversations`, {
266
+ method: 'POST',
267
+ headers: (0, http_1.authHeaders)(auth.token),
268
+ body: JSON.stringify({
269
+ title: text.slice(0, 50),
270
+ model: options.model || undefined,
271
+ project_id: options.projectId || (0, store_1.readSession)().last_project_id || undefined,
272
+ client: stamp.client,
273
+ client_version: stamp.client_version,
274
+ }),
275
+ });
276
+ const body = await created.json().catch(() => ({}));
277
+ if (!created.ok)
278
+ throw new Error((0, http_1.errorDetail)(body, `create conversation failed (${created.status})`));
279
+ conversationId = String(body.id || '');
280
+ if (!conversationId)
281
+ throw new Error('create conversation missing id');
282
+ }
283
+ const sent = await http(`${auth.api_base}/api/chat/send`, {
284
+ method: 'POST',
285
+ headers: (0, http_1.authHeaders)(auth.token),
286
+ body: JSON.stringify({
287
+ conversation_id: conversationId,
288
+ message: text,
289
+ model: options.model || undefined,
290
+ task_category: 'coding',
291
+ client: stamp.client,
292
+ client_version: stamp.client_version,
293
+ client_request_id: stamp.client_request_id,
294
+ }),
295
+ });
296
+ if (!sent.ok) {
297
+ const body = await sent.json().catch(() => ({}));
298
+ throw new Error((0, http_1.errorDetail)(body, `exec failed (${sent.status})`));
299
+ }
300
+ const stream = await sent.text();
301
+ const output = (0, sse_1.extractSseText)(stream);
302
+ (0, store_1.writeSession)({
303
+ ...(0, store_1.readSession)(),
304
+ last_conversation_id: conversationId,
305
+ last_project_id: options.projectId || (0, store_1.readSession)().last_project_id,
306
+ });
307
+ return { conversationId, text: output };
308
+ }
309
+ async function listConversationsCommand(http = http_1.defaultHttp) {
310
+ const auth = (0, store_1.requireAuth)();
311
+ const response = await http(`${auth.api_base}/api/chat/conversations?limit=30`, {
312
+ headers: (0, http_1.authHeaders)(auth.token),
313
+ });
314
+ const body = await response.json().catch(() => []);
315
+ if (!response.ok)
316
+ throw new Error((0, http_1.errorDetail)(body, `history failed (${response.status})`));
317
+ const rows = Array.isArray(body) ? body : [];
318
+ if (!rows.length)
319
+ return 'no saved conversations';
320
+ return rows.map((row) => {
321
+ const client = row.client ? ` [${row.client}]` : '';
322
+ return `${row.id} ${row.title || '(untitled)'}${client}`;
323
+ }).join('\n');
324
+ }
325
+ function openConversationCommand(conversationId) {
326
+ const id = conversationId.trim();
327
+ if (!id)
328
+ throw new Error('conversation id is required');
329
+ (0, store_1.writeSession)({ ...(0, store_1.readSession)(), last_conversation_id: id });
330
+ return `opened ${id}`;
331
+ }
332
+ async function setTrainingCommand(optIn, http = http_1.defaultHttp) {
333
+ const auth = (0, store_1.requireAuth)();
334
+ const response = await http(`${auth.api_base}/api/auth/profile`, {
335
+ method: 'PATCH',
336
+ headers: (0, http_1.authHeaders)(auth.token),
337
+ body: JSON.stringify({ training_opt_in: optIn }),
338
+ });
339
+ const body = await response.json().catch(() => ({}));
340
+ if (!response.ok)
341
+ throw new Error((0, http_1.errorDetail)(body, `training update failed (${response.status})`));
342
+ return optIn ? 'training opt-in on (new turns may be used if eligible)' : 'training opt-in off';
343
+ }
package/dist/http.js ADDED
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authHeaders = authHeaders;
4
+ exports.defaultHttp = defaultHttp;
5
+ exports.errorDetail = errorDetail;
6
+ function authHeaders(token) {
7
+ return {
8
+ Authorization: `Bearer ${token}`,
9
+ 'Content-Type': 'application/json',
10
+ };
11
+ }
12
+ async function* streamResponse(response) {
13
+ const body = response.body;
14
+ if (!body) {
15
+ yield await response.text();
16
+ return;
17
+ }
18
+ const decoder = new TextDecoder();
19
+ const reader = body.getReader();
20
+ try {
21
+ while (true) {
22
+ const { done, value } = await reader.read();
23
+ if (done)
24
+ break;
25
+ if (value)
26
+ yield decoder.decode(value, { stream: true });
27
+ }
28
+ const tail = decoder.decode();
29
+ if (tail)
30
+ yield tail;
31
+ }
32
+ finally {
33
+ reader.releaseLock();
34
+ }
35
+ }
36
+ async function defaultHttp(url, init) {
37
+ const response = await fetch(url, init);
38
+ return {
39
+ ok: response.ok,
40
+ status: response.status,
41
+ json: () => response.json(),
42
+ text: () => response.text(),
43
+ stream: () => streamResponse(response),
44
+ };
45
+ }
46
+ function errorDetail(body, fallback) {
47
+ if (typeof body?.detail === 'string')
48
+ return body.detail;
49
+ if (typeof body?.error === 'string')
50
+ return body.error;
51
+ if (typeof body?.message === 'string')
52
+ return body.message;
53
+ return fallback;
54
+ }
package/dist/logo.js ADDED
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sisuMobiusArt = sisuMobiusArt;
4
+ exports.sisuWordmark = sisuWordmark;
5
+ exports.sisuBanner = sisuBanner;
6
+ exports.stripAnsi = stripAnsi;
7
+ const mobius_1 = require("./mobius");
8
+ function sisuMobiusArt(columns = 80, phase = 0, color = false) {
9
+ return (0, mobius_1.renderMobiusFrame)({
10
+ cols: (0, mobius_1.mobiusFrameWidth)(columns),
11
+ rows: (0, mobius_1.mobiusFrameHeight)(columns),
12
+ phase,
13
+ color,
14
+ });
15
+ }
16
+ function sisuWordmark() {
17
+ return [
18
+ ' 思 溯',
19
+ ' S I S U',
20
+ ].join('\n');
21
+ }
22
+ function sisuBanner(columns = 80, phase = 0, color = false) {
23
+ return [
24
+ '',
25
+ sisuMobiusArt(columns, phase, color),
26
+ '',
27
+ sisuWordmark(),
28
+ '',
29
+ ].join('\n');
30
+ }
31
+ function stripAnsi(text) {
32
+ return text.replace(/\x1b\[[0-9;]*m/g, '');
33
+ }
package/dist/main.js ADDED
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.helpText = helpText;
5
+ exports.runCli = runCli;
6
+ const commands_1 = require("./commands");
7
+ const http_1 = require("./http");
8
+ const store_1 = require("./store");
9
+ const tui_1 = require("./tui");
10
+ function helpText() {
11
+ return `sisu — SiSu local client
12
+
13
+ One login. Cloud quota. Local workspace.
14
+
15
+ Install:
16
+ npm install -g @stevezhou/sisu
17
+
18
+ Usage:
19
+ sisu login open a browser (or print a URL) to sign in
20
+ sisu login --code <grant> paste the grant code from the approve page
21
+ sisu login --email <email> --password <password> [--api <url>]
22
+ sisu login --token <jwt> [--api <url>]
23
+ sisu logout
24
+ sisu status
25
+ sisu open <dir> --project <project-id>
26
+ sisu ls [--project <project-id>]
27
+ sisu exec "<prompt>" [--project <id>] [--model <name>] [--new]
28
+ sisu history
29
+ sisu thread <conversation-id>
30
+ sisu training --on|--off
31
+ sisu interactive TUI (Möbius splash)
32
+ sisu help
33
+
34
+ Auth and workspaces live in $SISU_HOME (default ~/.sisu), shared with Desktop.
35
+ `;
36
+ }
37
+ function printHelp() {
38
+ process.stdout.write(helpText());
39
+ }
40
+ function flag(args, name) {
41
+ const index = args.indexOf(name);
42
+ if (index === -1)
43
+ return undefined;
44
+ return args[index + 1];
45
+ }
46
+ function parseArgs(args) {
47
+ const flags = {};
48
+ const rest = [];
49
+ const switches = new Set();
50
+ for (let i = 0; i < args.length; i += 1) {
51
+ const item = args[i];
52
+ if (item === '--new') {
53
+ switches.add('new');
54
+ continue;
55
+ }
56
+ if (item.startsWith('--')) {
57
+ flags[item] = args[i + 1] || '';
58
+ i += 1;
59
+ continue;
60
+ }
61
+ rest.push(item);
62
+ }
63
+ return { flags, rest, switches };
64
+ }
65
+ async function runCli(argv, deps = {}) {
66
+ const http = deps.http ?? http_1.defaultHttp;
67
+ const [command, ...args] = argv;
68
+ if (command === 'help' || command === '--help' || command === '-h') {
69
+ printHelp();
70
+ return 0;
71
+ }
72
+ if (!command || command === 'tui') {
73
+ return (0, tui_1.runTui)((0, tui_1.defaultTuiIo)());
74
+ }
75
+ if (command === 'status') {
76
+ process.stdout.write(`${await (0, commands_1.statusCommand)(http_1.defaultHttp)}\n`);
77
+ return 0;
78
+ }
79
+ if (command === 'logout') {
80
+ (0, commands_1.logoutCommand)();
81
+ process.stdout.write('logged out\n');
82
+ return 0;
83
+ }
84
+ if (command === 'login') {
85
+ const email = flag(args, '--email') || process.env.SISU_EMAIL || '';
86
+ const password = flag(args, '--password') || process.env.SISU_PASSWORD || '';
87
+ const token = flag(args, '--token') || process.env.SISU_TOKEN || '';
88
+ const grantCode = flag(args, '--code') || '';
89
+ const apiBase = flag(args, '--api') || process.env.SISU_API_BASE || store_1.DEFAULT_API_BASE;
90
+ const loggedIn = grantCode
91
+ ? await (0, commands_1.webLoginCommand)({
92
+ apiBase,
93
+ grantCode,
94
+ openBrowser: deps.http ? () => undefined : undefined,
95
+ }, http)
96
+ : (email && password) || token
97
+ ? await (0, commands_1.loginCommand)({ email, password, token, apiBase }, http)
98
+ : await (0, commands_1.webLoginCommand)({
99
+ apiBase,
100
+ openBrowser: deps.http ? () => undefined : undefined,
101
+ onStart: (info) => {
102
+ process.stdout.write(`Open ${info.verification_uri_complete}\n`);
103
+ process.stdout.write(`Confirm code ${info.user_code} (or paste --code from the page)\n`);
104
+ },
105
+ }, http);
106
+ process.stdout.write(`logged in as ${loggedIn}\n`);
107
+ process.stdout.write(`${await (0, commands_1.statusCommand)(http)}\n`);
108
+ return 0;
109
+ }
110
+ if (command === 'open') {
111
+ const dir = args.find((item) => !item.startsWith('--')) || '.';
112
+ const projectId = flag(args, '--project') || '';
113
+ if (!projectId) {
114
+ process.stderr.write('sisu open requires --project <project-id>\n');
115
+ return 2;
116
+ }
117
+ process.stdout.write(`${(0, commands_1.openCommand)(projectId, dir)}\n`);
118
+ return 0;
119
+ }
120
+ if (command === 'ls') {
121
+ process.stdout.write(`${(0, commands_1.listLocalCommand)(flag(args, '--project'))}\n`);
122
+ return 0;
123
+ }
124
+ if (command === 'exec') {
125
+ const parsed = parseArgs(args);
126
+ const prompt = parsed.rest.join(' ').trim();
127
+ if (!prompt) {
128
+ process.stderr.write('sisu exec requires a prompt\n');
129
+ return 2;
130
+ }
131
+ const result = await (0, commands_1.execCommand)(prompt, {
132
+ projectId: parsed.flags['--project'],
133
+ model: parsed.flags['--model'],
134
+ newConversation: parsed.switches.has('new'),
135
+ });
136
+ if (result.text)
137
+ process.stdout.write(`${result.text}\n`);
138
+ return 0;
139
+ }
140
+ if (command === 'history') {
141
+ process.stdout.write(`${await (0, commands_1.listConversationsCommand)(http_1.defaultHttp)}\n`);
142
+ return 0;
143
+ }
144
+ if (command === 'thread') {
145
+ const id = args.find((item) => !item.startsWith('--')) || '';
146
+ process.stdout.write(`${(0, commands_1.openConversationCommand)(id)}\n`);
147
+ return 0;
148
+ }
149
+ if (command === 'training') {
150
+ if (args.includes('--on')) {
151
+ process.stdout.write(`${await (0, commands_1.setTrainingCommand)(true)}\n`);
152
+ return 0;
153
+ }
154
+ if (args.includes('--off')) {
155
+ process.stdout.write(`${await (0, commands_1.setTrainingCommand)(false)}\n`);
156
+ return 0;
157
+ }
158
+ process.stderr.write('sisu training requires --on or --off\n');
159
+ return 2;
160
+ }
161
+ process.stderr.write(`unknown command: ${command}\n`);
162
+ printHelp();
163
+ return 2;
164
+ }
165
+ if (require.main === module) {
166
+ runCli(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
167
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
168
+ process.exit(1);
169
+ });
170
+ }