@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 +6 -0
- package/README.md +49 -0
- package/dist/client.js +15 -0
- package/dist/commands.js +343 -0
- package/dist/http.js +54 -0
- package/dist/logo.js +33 -0
- package/dist/main.js +170 -0
- package/dist/mobius.js +246 -0
- package/dist/pager/app.js +420 -0
- package/dist/pager/history.js +48 -0
- package/dist/pager/input.js +83 -0
- package/dist/pager/model.js +211 -0
- package/dist/pager/render.js +147 -0
- package/dist/pager/stdio.js +42 -0
- package/dist/pager/theme.js +39 -0
- package/dist/sse.js +81 -0
- package/dist/store.js +144 -0
- package/dist/toolSummary.js +95 -0
- package/dist/transport.js +154 -0
- package/dist/tui.js +287 -0
- package/package.json +47 -0
package/dist/store.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
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.DEFAULT_API_BASE = void 0;
|
|
7
|
+
exports.getSisuHome = getSisuHome;
|
|
8
|
+
exports.readAuth = readAuth;
|
|
9
|
+
exports.writeAuth = writeAuth;
|
|
10
|
+
exports.readSession = readSession;
|
|
11
|
+
exports.writeSession = writeSession;
|
|
12
|
+
exports.clearAuth = clearAuth;
|
|
13
|
+
exports.readWorkspaces = readWorkspaces;
|
|
14
|
+
exports.bindWorkspace = bindWorkspace;
|
|
15
|
+
exports.requireAuth = requireAuth;
|
|
16
|
+
exports.describeStatus = describeStatus;
|
|
17
|
+
const fs_1 = __importDefault(require("fs"));
|
|
18
|
+
const os_1 = __importDefault(require("os"));
|
|
19
|
+
const path_1 = __importDefault(require("path"));
|
|
20
|
+
exports.DEFAULT_API_BASE = 'https://www.sisu.chat';
|
|
21
|
+
function getSisuHome() {
|
|
22
|
+
const override = (process.env.SISU_HOME || '').trim();
|
|
23
|
+
return override || path_1.default.join(os_1.default.homedir(), '.sisu');
|
|
24
|
+
}
|
|
25
|
+
function authPath() {
|
|
26
|
+
return path_1.default.join(getSisuHome(), 'auth.json');
|
|
27
|
+
}
|
|
28
|
+
function workspacePath() {
|
|
29
|
+
return path_1.default.join(getSisuHome(), 'workspace-paths.json');
|
|
30
|
+
}
|
|
31
|
+
function sessionPath() {
|
|
32
|
+
return path_1.default.join(getSisuHome(), 'session.json');
|
|
33
|
+
}
|
|
34
|
+
const HOME_MODE = 0o700;
|
|
35
|
+
const FILE_MODE = 0o600;
|
|
36
|
+
function ensureSisuHome() {
|
|
37
|
+
const home = getSisuHome();
|
|
38
|
+
fs_1.default.mkdirSync(home, { recursive: true, mode: HOME_MODE });
|
|
39
|
+
try {
|
|
40
|
+
fs_1.default.chmodSync(home, HOME_MODE);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// POSIX modes are ignored on some filesystems (e.g. Windows).
|
|
44
|
+
}
|
|
45
|
+
return home;
|
|
46
|
+
}
|
|
47
|
+
function readJson(file, fallback) {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return fallback;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function writeJson(file, value) {
|
|
56
|
+
ensureSisuHome();
|
|
57
|
+
fs_1.default.writeFileSync(file, JSON.stringify(value, null, 2) + '\n', {
|
|
58
|
+
encoding: 'utf8',
|
|
59
|
+
mode: FILE_MODE,
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
fs_1.default.chmodSync(file, FILE_MODE);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// POSIX modes are ignored on some filesystems (e.g. Windows).
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function readAuth() {
|
|
69
|
+
const raw = readJson(authPath(), null);
|
|
70
|
+
if (!raw || typeof raw.token !== 'string' || !raw.token.trim())
|
|
71
|
+
return null;
|
|
72
|
+
return {
|
|
73
|
+
token: raw.token,
|
|
74
|
+
email: String(raw.email || ''),
|
|
75
|
+
user_id: String(raw.user_id || ''),
|
|
76
|
+
api_base: String(raw.api_base || exports.DEFAULT_API_BASE).replace(/\/+$/, ''),
|
|
77
|
+
plan_code: String(raw.plan_code || ''),
|
|
78
|
+
name: String(raw.name || ''),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function writeAuth(record) {
|
|
82
|
+
writeJson(authPath(), {
|
|
83
|
+
token: record.token,
|
|
84
|
+
email: record.email,
|
|
85
|
+
user_id: record.user_id,
|
|
86
|
+
api_base: record.api_base.replace(/\/+$/, '') || exports.DEFAULT_API_BASE,
|
|
87
|
+
plan_code: record.plan_code || '',
|
|
88
|
+
name: record.name || '',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function readSession() {
|
|
92
|
+
return readJson(sessionPath(), {});
|
|
93
|
+
}
|
|
94
|
+
function writeSession(record) {
|
|
95
|
+
writeJson(sessionPath(), record);
|
|
96
|
+
}
|
|
97
|
+
function clearAuth() {
|
|
98
|
+
try {
|
|
99
|
+
fs_1.default.unlinkSync(authPath());
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// already logged out
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function readWorkspaces() {
|
|
106
|
+
const raw = readJson(workspacePath(), {});
|
|
107
|
+
return raw && typeof raw === 'object' ? raw : {};
|
|
108
|
+
}
|
|
109
|
+
function bindWorkspace(projectId, requestedPath) {
|
|
110
|
+
if (!projectId.trim())
|
|
111
|
+
throw new Error('missing project id');
|
|
112
|
+
const trimmed = (requestedPath || '').trim();
|
|
113
|
+
if (!trimmed)
|
|
114
|
+
throw new Error('missing path');
|
|
115
|
+
if (!fs_1.default.existsSync(trimmed))
|
|
116
|
+
throw new Error('directory does not exist');
|
|
117
|
+
const stat = fs_1.default.statSync(trimmed);
|
|
118
|
+
if (!stat.isDirectory())
|
|
119
|
+
throw new Error('path is not a directory');
|
|
120
|
+
fs_1.default.accessSync(trimmed, fs_1.default.constants.R_OK);
|
|
121
|
+
const resolved = fs_1.default.realpathSync.native(trimmed);
|
|
122
|
+
const map = readWorkspaces();
|
|
123
|
+
map[projectId] = resolved;
|
|
124
|
+
writeJson(workspacePath(), map);
|
|
125
|
+
return { projectId, path: resolved };
|
|
126
|
+
}
|
|
127
|
+
function requireAuth() {
|
|
128
|
+
const auth = readAuth();
|
|
129
|
+
if (!auth)
|
|
130
|
+
throw new Error('not logged in — run sisu login');
|
|
131
|
+
return auth;
|
|
132
|
+
}
|
|
133
|
+
function describeStatus() {
|
|
134
|
+
const auth = readAuth();
|
|
135
|
+
return {
|
|
136
|
+
home: getSisuHome(),
|
|
137
|
+
logged_in: Boolean(auth),
|
|
138
|
+
email: auth?.email || '',
|
|
139
|
+
name: auth?.name || '',
|
|
140
|
+
plan_code: auth?.plan_code || '',
|
|
141
|
+
api_base: auth?.api_base || process.env.SISU_API_BASE || exports.DEFAULT_API_BASE,
|
|
142
|
+
workspaces: readWorkspaces(),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TOOL_DETAIL_CAP = exports.TOOL_STATUS_PHASES = void 0;
|
|
4
|
+
exports.boundToolText = boundToolText;
|
|
5
|
+
exports.summarizeToolInput = summarizeToolInput;
|
|
6
|
+
exports.summarizeLiveTool = summarizeLiveTool;
|
|
7
|
+
exports.summarizePersistedTool = summarizePersistedTool;
|
|
8
|
+
exports.TOOL_STATUS_PHASES = new Set(['start', 'executing', 'end']);
|
|
9
|
+
exports.TOOL_DETAIL_CAP = 160;
|
|
10
|
+
function boundToolText(value, cap = exports.TOOL_DETAIL_CAP) {
|
|
11
|
+
const one = value.replace(/\s+/g, ' ').trim();
|
|
12
|
+
if (!one)
|
|
13
|
+
return '';
|
|
14
|
+
if (one.length <= cap)
|
|
15
|
+
return one;
|
|
16
|
+
return `${one.slice(0, Math.max(1, cap - 1))}…`;
|
|
17
|
+
}
|
|
18
|
+
function summarizeToolInput(input) {
|
|
19
|
+
if (typeof input === 'string')
|
|
20
|
+
return boundToolText(input);
|
|
21
|
+
if (!input || typeof input !== 'object')
|
|
22
|
+
return '';
|
|
23
|
+
const rec = input;
|
|
24
|
+
for (const key of ['path', 'file', 'query', 'url', 'name']) {
|
|
25
|
+
if (typeof rec[key] === 'string' && rec[key].trim()) {
|
|
26
|
+
return boundToolText(`${key}=${rec[key]}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const keys = Object.keys(rec);
|
|
30
|
+
if (keys.length === 1 && typeof rec[keys[0]] === 'string') {
|
|
31
|
+
return boundToolText(`${keys[0]}=${rec[keys[0]]}`);
|
|
32
|
+
}
|
|
33
|
+
return boundToolText(keys.join(','));
|
|
34
|
+
}
|
|
35
|
+
function toolName(data) {
|
|
36
|
+
return String(data.tool || data.name || data.type || 'tool');
|
|
37
|
+
}
|
|
38
|
+
function startDetail(data) {
|
|
39
|
+
if (typeof data.description === 'string' && data.description.trim()) {
|
|
40
|
+
return boundToolText(data.description);
|
|
41
|
+
}
|
|
42
|
+
if (typeof data.text === 'string' && data.text.trim())
|
|
43
|
+
return boundToolText(data.text);
|
|
44
|
+
if (typeof data.content === 'string' && data.content.trim())
|
|
45
|
+
return boundToolText(data.content);
|
|
46
|
+
if (data.content && typeof data.content === 'object')
|
|
47
|
+
return summarizeToolInput(data.content);
|
|
48
|
+
if (data.input !== undefined)
|
|
49
|
+
return summarizeToolInput(data.input);
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
function endDetail(data) {
|
|
53
|
+
const ok = data.success === false ? 'fail' : data.success === true ? 'ok' : '';
|
|
54
|
+
const preview = typeof data.result_preview === 'string'
|
|
55
|
+
? data.result_preview
|
|
56
|
+
: typeof data.result_summary === 'string'
|
|
57
|
+
? data.result_summary
|
|
58
|
+
: typeof data.result === 'string'
|
|
59
|
+
? data.result
|
|
60
|
+
: '';
|
|
61
|
+
const bits = [ok, preview ? boundToolText(preview) : ''].filter(Boolean);
|
|
62
|
+
return bits.join(' · ');
|
|
63
|
+
}
|
|
64
|
+
/** Live SSE tool_call / recognized tool_status. Returns null for ignored phases. */
|
|
65
|
+
function summarizeLiveTool(data, eventName) {
|
|
66
|
+
const tool = toolName(data);
|
|
67
|
+
if (eventName === 'tool_status') {
|
|
68
|
+
const phase = String(data.event || '');
|
|
69
|
+
if (!exports.TOOL_STATUS_PHASES.has(phase))
|
|
70
|
+
return null;
|
|
71
|
+
if (phase === 'end') {
|
|
72
|
+
const detail = endDetail(data);
|
|
73
|
+
return detail ? `${tool} · end · ${detail}` : `${tool} · end`;
|
|
74
|
+
}
|
|
75
|
+
const detail = startDetail(data);
|
|
76
|
+
return detail ? `${tool} · ${phase} · ${detail}` : `${tool} · ${phase}`;
|
|
77
|
+
}
|
|
78
|
+
if (eventName === 'tool_call') {
|
|
79
|
+
const detail = startDetail(data);
|
|
80
|
+
return detail ? `${tool} · call · ${detail}` : `${tool} · call`;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
/** Persisted content_blocks snapshot. */
|
|
85
|
+
function summarizePersistedTool(block) {
|
|
86
|
+
const tool = toolName(block);
|
|
87
|
+
const type = String(block.type || '');
|
|
88
|
+
if (type === 'tool_end' || type === 'tool_result') {
|
|
89
|
+
const detail = endDetail(block);
|
|
90
|
+
if (detail)
|
|
91
|
+
return `${tool} · ${detail}`;
|
|
92
|
+
}
|
|
93
|
+
const detail = startDetail(block);
|
|
94
|
+
return detail ? `${tool} · ${detail}` : tool;
|
|
95
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapSseEventToTurn = mapSseEventToTurn;
|
|
4
|
+
exports.createFastApiTransport = createFastApiTransport;
|
|
5
|
+
const client_1 = require("./client");
|
|
6
|
+
const toolSummary_1 = require("./toolSummary");
|
|
7
|
+
const http_1 = require("./http");
|
|
8
|
+
const sse_1 = require("./sse");
|
|
9
|
+
const store_1 = require("./store");
|
|
10
|
+
function errorEventText(event) {
|
|
11
|
+
if (typeof event.data === 'string')
|
|
12
|
+
return event.data;
|
|
13
|
+
if (event.data && typeof event.data === 'object') {
|
|
14
|
+
const message = event.data.message;
|
|
15
|
+
if (typeof message === 'string' && message)
|
|
16
|
+
return message;
|
|
17
|
+
}
|
|
18
|
+
return 'stream error';
|
|
19
|
+
}
|
|
20
|
+
function mapSseEventToTurn(event) {
|
|
21
|
+
if (event.type === 'error')
|
|
22
|
+
return { type: 'error', text: errorEventText(event) };
|
|
23
|
+
if (event.type === 'text') {
|
|
24
|
+
const text = (0, sse_1.sseEventText)(event);
|
|
25
|
+
return text ? { type: 'text', text } : null;
|
|
26
|
+
}
|
|
27
|
+
if (event.name === 'tool_call' || event.name === 'tool_status') {
|
|
28
|
+
const data = event.data && typeof event.data === 'object' ? event.data : {};
|
|
29
|
+
const text = (0, toolSummary_1.summarizeLiveTool)(data, event.name);
|
|
30
|
+
return text ? { type: 'tool', text } : null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
async function* responseChunks(response) {
|
|
35
|
+
if (response.stream) {
|
|
36
|
+
yield* response.stream();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
yield await response.text();
|
|
40
|
+
}
|
|
41
|
+
function turnEventsFrom(buffer) {
|
|
42
|
+
const parsed = (0, sse_1.consumeSse)(buffer);
|
|
43
|
+
const events = [];
|
|
44
|
+
for (const event of parsed.events) {
|
|
45
|
+
const mapped = mapSseEventToTurn(event);
|
|
46
|
+
if (mapped)
|
|
47
|
+
events.push(mapped);
|
|
48
|
+
}
|
|
49
|
+
return { events, rest: parsed.rest };
|
|
50
|
+
}
|
|
51
|
+
function createFastApiTransport(http) {
|
|
52
|
+
return {
|
|
53
|
+
async *send(prompt, options = {}) {
|
|
54
|
+
const auth = (0, store_1.requireAuth)();
|
|
55
|
+
const text = prompt.trim();
|
|
56
|
+
if (!text)
|
|
57
|
+
throw new Error('prompt is required');
|
|
58
|
+
const stamp = (0, client_1.clientStamp)('tui');
|
|
59
|
+
const session = (0, store_1.readSession)();
|
|
60
|
+
let conversationId = options.conversationId || (!options.newConversation ? session.last_conversation_id : '') || '';
|
|
61
|
+
if (!conversationId) {
|
|
62
|
+
const created = await http(`${auth.api_base}/api/chat/conversations`, {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: (0, http_1.authHeaders)(auth.token),
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
title: text.slice(0, 50),
|
|
67
|
+
project_id: session.last_project_id || undefined,
|
|
68
|
+
client: stamp.client,
|
|
69
|
+
client_version: stamp.client_version,
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
const body = await created.json().catch(() => ({}));
|
|
73
|
+
if (!created.ok)
|
|
74
|
+
throw new Error((0, http_1.errorDetail)(body, `create conversation failed (${created.status})`));
|
|
75
|
+
conversationId = String(body.id || '');
|
|
76
|
+
if (!conversationId)
|
|
77
|
+
throw new Error('create conversation missing id');
|
|
78
|
+
}
|
|
79
|
+
(0, store_1.writeSession)({
|
|
80
|
+
...(0, store_1.readSession)(),
|
|
81
|
+
last_conversation_id: conversationId,
|
|
82
|
+
});
|
|
83
|
+
yield { type: 'bound', text: conversationId };
|
|
84
|
+
const sent = await http(`${auth.api_base}/api/chat/send`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: (0, http_1.authHeaders)(auth.token),
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
conversation_id: conversationId,
|
|
89
|
+
message: text,
|
|
90
|
+
task_category: 'coding',
|
|
91
|
+
client: stamp.client,
|
|
92
|
+
client_version: stamp.client_version,
|
|
93
|
+
client_request_id: stamp.client_request_id,
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
if (!sent.ok) {
|
|
97
|
+
const body = await sent.json().catch(() => ({}));
|
|
98
|
+
throw new Error((0, http_1.errorDetail)(body, `exec failed (${sent.status})`));
|
|
99
|
+
}
|
|
100
|
+
let buffer = '';
|
|
101
|
+
for await (const chunk of responseChunks(sent)) {
|
|
102
|
+
const parsed = turnEventsFrom(buffer + chunk);
|
|
103
|
+
buffer = parsed.rest;
|
|
104
|
+
for (const event of parsed.events)
|
|
105
|
+
yield event;
|
|
106
|
+
}
|
|
107
|
+
if (buffer.trim()) {
|
|
108
|
+
const parsed = turnEventsFrom(`${buffer}\n\n`);
|
|
109
|
+
for (const event of parsed.events)
|
|
110
|
+
yield event;
|
|
111
|
+
}
|
|
112
|
+
return { conversationId };
|
|
113
|
+
},
|
|
114
|
+
async listConversations() {
|
|
115
|
+
const auth = (0, store_1.requireAuth)();
|
|
116
|
+
const response = await http(`${auth.api_base}/api/chat/conversations?limit=30`, {
|
|
117
|
+
headers: (0, http_1.authHeaders)(auth.token),
|
|
118
|
+
});
|
|
119
|
+
const body = await response.json().catch(() => []);
|
|
120
|
+
if (!response.ok)
|
|
121
|
+
throw new Error((0, http_1.errorDetail)(body, `history failed (${response.status})`));
|
|
122
|
+
const rows = Array.isArray(body) ? body : [];
|
|
123
|
+
return rows.map((row) => ({
|
|
124
|
+
id: String(row.id || ''),
|
|
125
|
+
title: String(row.title || ''),
|
|
126
|
+
...(row.client ? { client: String(row.client) } : {}),
|
|
127
|
+
}));
|
|
128
|
+
},
|
|
129
|
+
async getConversation(id) {
|
|
130
|
+
const auth = (0, store_1.requireAuth)();
|
|
131
|
+
const trimmed = id.trim();
|
|
132
|
+
if (!trimmed)
|
|
133
|
+
throw new Error('conversation id is required');
|
|
134
|
+
const response = await http(`${auth.api_base}/api/chat/conversations/${encodeURIComponent(trimmed)}`, {
|
|
135
|
+
headers: (0, http_1.authHeaders)(auth.token),
|
|
136
|
+
});
|
|
137
|
+
const body = await response.json().catch(() => ({}));
|
|
138
|
+
if (!response.ok)
|
|
139
|
+
throw new Error((0, http_1.errorDetail)(body, `conversation failed (${response.status})`));
|
|
140
|
+
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
141
|
+
return {
|
|
142
|
+
id: String(body.id || trimmed),
|
|
143
|
+
title: String(body.title || ''),
|
|
144
|
+
messages: messages.map((row) => ({
|
|
145
|
+
id: String(row.id || ''),
|
|
146
|
+
role: String(row.role || ''),
|
|
147
|
+
content: String(row.content || ''),
|
|
148
|
+
...(row.message_type ? { message_type: String(row.message_type) } : {}),
|
|
149
|
+
...(row.content_blocks ? { content_blocks: row.content_blocks } : {}),
|
|
150
|
+
})),
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
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.shouldAnimateSplash = shouldAnimateSplash;
|
|
7
|
+
exports.playMobiusIntro = playMobiusIntro;
|
|
8
|
+
exports.defaultTuiIo = defaultTuiIo;
|
|
9
|
+
exports.tuiHelp = tuiHelp;
|
|
10
|
+
exports.runTui = runTui;
|
|
11
|
+
const readline_1 = __importDefault(require("readline"));
|
|
12
|
+
const commands_1 = require("./commands");
|
|
13
|
+
const http_1 = require("./http");
|
|
14
|
+
const logo_1 = require("./logo");
|
|
15
|
+
const mobius_1 = require("./mobius");
|
|
16
|
+
const app_1 = require("./pager/app");
|
|
17
|
+
const stdio_1 = require("./pager/stdio");
|
|
18
|
+
const store_1 = require("./store");
|
|
19
|
+
const transport_1 = require("./transport");
|
|
20
|
+
function shouldAnimateSplash(env = process.env, tty = Boolean(process.stdout.isTTY)) {
|
|
21
|
+
if (env.SISU_TUI_STATIC === '1')
|
|
22
|
+
return false;
|
|
23
|
+
return tty;
|
|
24
|
+
}
|
|
25
|
+
function shouldUsePager(deps, env = process.env) {
|
|
26
|
+
if (deps.animate === false)
|
|
27
|
+
return false;
|
|
28
|
+
if (env.SISU_TUI_STATIC === '1')
|
|
29
|
+
return false;
|
|
30
|
+
return Boolean(process.stdout.isTTY);
|
|
31
|
+
}
|
|
32
|
+
async function playMobiusIntro(io, options = {}) {
|
|
33
|
+
const columns = options.columns ?? process.stdout.columns ?? 80;
|
|
34
|
+
const frames = options.frames ?? 32;
|
|
35
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
36
|
+
const color = options.color ?? Boolean(process.stdout.isTTY);
|
|
37
|
+
const rows = (0, mobius_1.mobiusFrameHeight)(columns);
|
|
38
|
+
io.write('\x1b[?25l');
|
|
39
|
+
for (let i = 0; i < frames; i += 1) {
|
|
40
|
+
const phase = (i / frames) * Math.PI * 2;
|
|
41
|
+
const art = (0, logo_1.sisuMobiusArt)(columns, phase, color);
|
|
42
|
+
if (i === 0)
|
|
43
|
+
io.write(`${art}\n`);
|
|
44
|
+
else
|
|
45
|
+
io.write(`\x1b[${rows}A${art}\n`);
|
|
46
|
+
await sleep(38);
|
|
47
|
+
}
|
|
48
|
+
io.write('\x1b[?25h');
|
|
49
|
+
io.write(`\n${(0, logo_1.sisuWordmark)()}\n\n`);
|
|
50
|
+
}
|
|
51
|
+
function defaultTuiIo() {
|
|
52
|
+
let rl;
|
|
53
|
+
const ensureRl = () => {
|
|
54
|
+
if (!rl) {
|
|
55
|
+
rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
56
|
+
}
|
|
57
|
+
return rl;
|
|
58
|
+
};
|
|
59
|
+
const io = {
|
|
60
|
+
write(text) {
|
|
61
|
+
process.stdout.write(text);
|
|
62
|
+
},
|
|
63
|
+
question(prompt) {
|
|
64
|
+
return new Promise((resolve) => {
|
|
65
|
+
ensureRl().question(prompt, (answer) => resolve(answer));
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
questionPassword(prompt) {
|
|
69
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
70
|
+
return readHiddenPassword(io, prompt);
|
|
71
|
+
}
|
|
72
|
+
return io.question(prompt);
|
|
73
|
+
},
|
|
74
|
+
close() {
|
|
75
|
+
rl?.close();
|
|
76
|
+
rl = undefined;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
return io;
|
|
80
|
+
}
|
|
81
|
+
async function readHiddenPassword(io, prompt) {
|
|
82
|
+
io.close?.();
|
|
83
|
+
io.write(prompt);
|
|
84
|
+
const stdin = process.stdin;
|
|
85
|
+
const wasRaw = stdin.isRaw;
|
|
86
|
+
if (typeof stdin.setRawMode === 'function')
|
|
87
|
+
stdin.setRawMode(true);
|
|
88
|
+
stdin.resume();
|
|
89
|
+
try {
|
|
90
|
+
return await new Promise((resolve, reject) => {
|
|
91
|
+
let password = '';
|
|
92
|
+
const finish = (value, error) => {
|
|
93
|
+
stdin.off('data', onData);
|
|
94
|
+
stdin.off('error', onError);
|
|
95
|
+
io.write('\n');
|
|
96
|
+
if (error)
|
|
97
|
+
reject(error);
|
|
98
|
+
else
|
|
99
|
+
resolve(value);
|
|
100
|
+
};
|
|
101
|
+
const onError = (error) => finish('', error);
|
|
102
|
+
const onData = (chunk) => {
|
|
103
|
+
const text = String(chunk);
|
|
104
|
+
for (const ch of text) {
|
|
105
|
+
if (ch === '\n' || ch === '\r') {
|
|
106
|
+
finish(password);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (ch === '\u0003') {
|
|
110
|
+
finish('\u0003');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (ch === '\u007f' || ch === '\b') {
|
|
114
|
+
password = password.slice(0, -1);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
password += ch;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
stdin.on('data', onData);
|
|
121
|
+
stdin.on('error', onError);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
if (typeof stdin.setRawMode === 'function')
|
|
126
|
+
stdin.setRawMode(Boolean(wasRaw));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function promptLogin(io, login) {
|
|
130
|
+
const email = (await io.question('Email: ')).trim();
|
|
131
|
+
if (!email) {
|
|
132
|
+
io.write('login cancelled\n');
|
|
133
|
+
return 'cancelled';
|
|
134
|
+
}
|
|
135
|
+
const password = await (io.questionPassword ?? io.question)('Password: ');
|
|
136
|
+
if (password === '\u0003') {
|
|
137
|
+
io.write('login cancelled\n');
|
|
138
|
+
return 'cancelled';
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const loggedIn = await login({ email, password });
|
|
142
|
+
io.write(`logged in as ${loggedIn}\n`);
|
|
143
|
+
return 'ok';
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
io.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
147
|
+
return 'failed';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function tuiHelp() {
|
|
151
|
+
return [
|
|
152
|
+
'/status account and quota',
|
|
153
|
+
'/ls local workspace files',
|
|
154
|
+
'/history saved cloud conversations',
|
|
155
|
+
'/open <id> continue a saved conversation',
|
|
156
|
+
'/new start a new conversation',
|
|
157
|
+
'/training on|off allow or refuse training use of new turns',
|
|
158
|
+
'/help this list',
|
|
159
|
+
'/quit leave',
|
|
160
|
+
'otherwise send a turn (billed to your SiSu account)',
|
|
161
|
+
].join('\n');
|
|
162
|
+
}
|
|
163
|
+
async function runTui(io, deps = {}) {
|
|
164
|
+
const http = deps.http ?? http_1.defaultHttp;
|
|
165
|
+
const status = deps.status ?? commands_1.statusCommand;
|
|
166
|
+
const exec = deps.exec ?? commands_1.execCommand;
|
|
167
|
+
const ls = deps.ls ?? commands_1.listLocalCommand;
|
|
168
|
+
const history = deps.history ?? commands_1.listConversationsCommand;
|
|
169
|
+
const openThread = deps.openThread ?? commands_1.openConversationCommand;
|
|
170
|
+
const training = deps.training ?? commands_1.setTrainingCommand;
|
|
171
|
+
const auth = deps.auth ?? store_1.readAuth;
|
|
172
|
+
const columns = deps.columns ?? process.stdout.columns ?? 80;
|
|
173
|
+
const animate = deps.animate ?? shouldAnimateSplash();
|
|
174
|
+
const color = deps.color ?? Boolean(process.stdout.isTTY);
|
|
175
|
+
try {
|
|
176
|
+
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
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (deps.pager || shouldUsePager(deps)) {
|
|
195
|
+
io.close?.();
|
|
196
|
+
const transport = (0, transport_1.createFastApiTransport)(http);
|
|
197
|
+
return await (deps.pager ?? app_1.runPager)((0, stdio_1.stdioPagerIo)(), transport, {
|
|
198
|
+
columns,
|
|
199
|
+
email: account.email,
|
|
200
|
+
quota: async () => (0, commands_1.formatQuota)(await (0, commands_1.fetchBalance)(http)),
|
|
201
|
+
status: () => status(http),
|
|
202
|
+
ls: () => {
|
|
203
|
+
try {
|
|
204
|
+
return ls();
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return error instanceof Error ? error.message : String(error);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
training: (on) => training(on, http),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
io.write(`${await status(http)}\n`);
|
|
214
|
+
io.write(`${tuiHelp()}\n\n`);
|
|
215
|
+
let newConversation = false;
|
|
216
|
+
while (true) {
|
|
217
|
+
const raw = (await io.question('› ')).trim();
|
|
218
|
+
if (!raw)
|
|
219
|
+
continue;
|
|
220
|
+
if (raw === '/quit' || raw === '/exit') {
|
|
221
|
+
io.write('bye\n');
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
224
|
+
if (raw === '/help') {
|
|
225
|
+
io.write(`${tuiHelp()}\n`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (raw === '/status') {
|
|
229
|
+
io.write(`${await status(http)}\n`);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (raw === '/ls') {
|
|
233
|
+
try {
|
|
234
|
+
io.write(`${ls()}\n`);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
io.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
238
|
+
}
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (raw === '/new') {
|
|
242
|
+
newConversation = true;
|
|
243
|
+
io.write('next turn starts a new conversation\n');
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
if (raw === '/history') {
|
|
247
|
+
try {
|
|
248
|
+
io.write(`${await history(http)}\n`);
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
io.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (raw.startsWith('/open ')) {
|
|
256
|
+
try {
|
|
257
|
+
io.write(`${openThread(raw.slice(6).trim())}\n`);
|
|
258
|
+
newConversation = false;
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
io.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (raw === '/training on' || raw === '/training off') {
|
|
266
|
+
try {
|
|
267
|
+
io.write(`${await training(raw.endsWith('on'), http)}\n`);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
io.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
271
|
+
}
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const result = await exec(raw, { newConversation, client: 'tui' }, http);
|
|
276
|
+
newConversation = false;
|
|
277
|
+
io.write(`${result.text || '(empty reply)'}\n`);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
io.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
io.close?.();
|
|
286
|
+
}
|
|
287
|
+
}
|