@ruska/cli 0.1.2 → 0.1.4
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/README.md +48 -11
- package/dist/cli.js +38 -18
- package/dist/commands/chat.d.ts +2 -0
- package/dist/commands/chat.js +42 -9
- package/dist/commands/health.d.ts +1 -0
- package/dist/commands/health.js +78 -0
- package/dist/commands/version.d.ts +1 -0
- package/dist/commands/version.js +68 -0
- package/dist/lib/api.d.ts +5 -1
- package/dist/lib/api.js +39 -0
- package/dist/lib/output/truncate.d.ts +16 -0
- package/dist/lib/output/truncate.js +53 -0
- package/dist/types/index.d.ts +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,18 +35,23 @@ $ ruska --help
|
|
|
35
35
|
auth Configure API authentication
|
|
36
36
|
assistants List your assistants
|
|
37
37
|
assistant <id> Get assistant by ID
|
|
38
|
-
chat <message> Chat with
|
|
38
|
+
chat <message> Chat with the LLM (optionally with an assistant)
|
|
39
39
|
create Create a new assistant
|
|
40
40
|
models List available models
|
|
41
|
+
version Show CLI and API version
|
|
42
|
+
health Check API health status
|
|
41
43
|
|
|
42
44
|
Options
|
|
43
45
|
--ui Launch interactive TUI mode
|
|
44
46
|
|
|
45
47
|
Chat Options
|
|
46
|
-
-a, --assistant Assistant ID
|
|
48
|
+
-a, --assistant Assistant ID (optional, uses default chat if omitted)
|
|
47
49
|
-t, --thread Thread ID to continue a conversation
|
|
48
50
|
-m, --message Message (alternative to positional arg)
|
|
49
51
|
--json Output as newline-delimited JSON (auto-enabled when piped)
|
|
52
|
+
--truncate <n> Max characters for tool output (default: 500)
|
|
53
|
+
--truncate-lines Max lines for tool output (default: 10)
|
|
54
|
+
--full-output Disable truncation (show full output)
|
|
50
55
|
|
|
51
56
|
Create Options
|
|
52
57
|
--name Assistant name (required)
|
|
@@ -59,8 +64,9 @@ $ ruska --help
|
|
|
59
64
|
Examples
|
|
60
65
|
$ ruska auth # Configure API key and host
|
|
61
66
|
$ ruska assistants # List your assistants
|
|
62
|
-
$ ruska assistant
|
|
63
|
-
$ ruska chat "Hello"
|
|
67
|
+
$ ruska assistant abc-123 # Get assistant details
|
|
68
|
+
$ ruska chat "Hello" # Direct chat with default LLM
|
|
69
|
+
$ ruska chat "Hello" -a <assistant-id> # Chat with specific assistant
|
|
64
70
|
$ ruska chat "Follow up" -t <thread-id> # Continue existing thread
|
|
65
71
|
$ ruska chat "Hello" -a <id> --json # Output as NDJSON
|
|
66
72
|
$ ruska chat "Query" -a <id> | jq '.type' # Pipe to jq
|
|
@@ -128,7 +134,13 @@ Tools: get_exchange_rate, convert_currency
|
|
|
128
134
|
|
|
129
135
|
### `ruska chat <message>`
|
|
130
136
|
|
|
131
|
-
Chat with
|
|
137
|
+
Chat with the LLM (optionally with an assistant) using streaming. Requires authentication.
|
|
138
|
+
|
|
139
|
+
**Direct chat with default LLM:**
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
$ ruska chat "Hello, how are you?"
|
|
143
|
+
```
|
|
132
144
|
|
|
133
145
|
**Start a new conversation with an assistant:**
|
|
134
146
|
|
|
@@ -159,12 +171,15 @@ $ ruska chat "Hello" -a <assistant-id> | jq '.type'
|
|
|
159
171
|
|
|
160
172
|
**Options:**
|
|
161
173
|
|
|
162
|
-
| Option
|
|
163
|
-
|
|
|
164
|
-
| `-a, --assistant`
|
|
165
|
-
| `-t, --thread`
|
|
166
|
-
| `-m, --message`
|
|
167
|
-
| `--json`
|
|
174
|
+
| Option | Description |
|
|
175
|
+
| ------------------ | ----------------------------------------------------- |
|
|
176
|
+
| `-a, --assistant` | Assistant ID (optional, uses default chat if omitted) |
|
|
177
|
+
| `-t, --thread` | Thread ID to continue an existing conversation |
|
|
178
|
+
| `-m, --message` | Message to send (alternative to positional arg) |
|
|
179
|
+
| `--json` | Output as newline-delimited JSON (NDJSON) |
|
|
180
|
+
| `--truncate <n>` | Max characters for tool output (default: 500) |
|
|
181
|
+
| `--truncate-lines` | Max lines for tool output (default: 10) |
|
|
182
|
+
| `--full-output` | Disable truncation (show full output) |
|
|
168
183
|
|
|
169
184
|
**Exit codes:**
|
|
170
185
|
|
|
@@ -228,6 +243,28 @@ All Models (15):
|
|
|
228
243
|
...
|
|
229
244
|
```
|
|
230
245
|
|
|
246
|
+
### `ruska version`
|
|
247
|
+
|
|
248
|
+
Show CLI and API version information.
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
$ ruska version
|
|
252
|
+
|
|
253
|
+
Ruska CLI v0.1.3
|
|
254
|
+
API: https://chat.ruska.ai
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### `ruska health`
|
|
258
|
+
|
|
259
|
+
Check API health status.
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
$ ruska health
|
|
263
|
+
|
|
264
|
+
API Health Check
|
|
265
|
+
Status: healthy
|
|
266
|
+
```
|
|
267
|
+
|
|
231
268
|
### `ruska --ui`
|
|
232
269
|
|
|
233
270
|
Launch the interactive TUI (Terminal User Interface) mode with a full-screen interface.
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,8 @@ import { runAssistantCommand } from './commands/assistant.js';
|
|
|
10
10
|
import { runModelsCommand } from './commands/models.js';
|
|
11
11
|
import { runCreateAssistantCommand } from './commands/create-assistant.js';
|
|
12
12
|
import { runChatCommand } from './commands/chat.js';
|
|
13
|
+
import { runVersionCommand } from './commands/version.js';
|
|
14
|
+
import { runHealthCommand } from './commands/health.js';
|
|
13
15
|
const cli = meow(`
|
|
14
16
|
Usage
|
|
15
17
|
$ ruska <command> [options]
|
|
@@ -18,18 +20,23 @@ const cli = meow(`
|
|
|
18
20
|
auth Configure API authentication
|
|
19
21
|
assistants List your assistants
|
|
20
22
|
assistant <id> Get assistant by ID
|
|
21
|
-
chat <message> Chat with
|
|
23
|
+
chat <message> Chat with the LLM (optionally with an assistant)
|
|
22
24
|
create Create a new assistant
|
|
23
25
|
models List available models
|
|
26
|
+
version Show CLI and API version
|
|
27
|
+
health Check API health status
|
|
24
28
|
|
|
25
29
|
Options
|
|
26
30
|
--ui Launch interactive TUI mode
|
|
27
31
|
|
|
28
32
|
Chat Options
|
|
29
|
-
-a, --assistant Assistant ID
|
|
33
|
+
-a, --assistant Assistant ID (optional, uses default chat if omitted)
|
|
30
34
|
-t, --thread Thread ID to continue a conversation
|
|
31
35
|
-m, --message Message (alternative to positional arg)
|
|
32
36
|
--json Output as newline-delimited JSON (auto-enabled when piped)
|
|
37
|
+
--truncate <n> Max characters for tool output (default: 500)
|
|
38
|
+
--truncate-lines Max lines for tool output (default: 10)
|
|
39
|
+
--full-output Disable truncation (show full output)
|
|
33
40
|
|
|
34
41
|
Create Options
|
|
35
42
|
--name Assistant name (required)
|
|
@@ -43,7 +50,8 @@ const cli = meow(`
|
|
|
43
50
|
$ ruska auth # Configure API key and host
|
|
44
51
|
$ ruska assistants # List your assistants
|
|
45
52
|
$ ruska assistant abc-123 # Get assistant details
|
|
46
|
-
$ ruska chat "Hello"
|
|
53
|
+
$ ruska chat "Hello" # Direct chat with default LLM
|
|
54
|
+
$ ruska chat "Hello" -a <assistant-id> # Chat with specific assistant
|
|
47
55
|
$ ruska chat "Follow up" -t <thread-id> # Continue existing thread
|
|
48
56
|
$ ruska chat "Hello" -a <id> --json # Output as NDJSON
|
|
49
57
|
$ ruska chat "Query" -a <id> | jq '.type' # Pipe to jq
|
|
@@ -74,6 +82,18 @@ const cli = meow(`
|
|
|
74
82
|
type: 'string',
|
|
75
83
|
shortFlag: 't',
|
|
76
84
|
},
|
|
85
|
+
truncate: {
|
|
86
|
+
type: 'number',
|
|
87
|
+
default: 500,
|
|
88
|
+
},
|
|
89
|
+
truncateLines: {
|
|
90
|
+
type: 'number',
|
|
91
|
+
default: 10,
|
|
92
|
+
},
|
|
93
|
+
fullOutput: {
|
|
94
|
+
type: 'boolean',
|
|
95
|
+
default: false,
|
|
96
|
+
},
|
|
77
97
|
interactive: {
|
|
78
98
|
type: 'boolean',
|
|
79
99
|
shortFlag: 'i',
|
|
@@ -134,29 +154,21 @@ async function main() {
|
|
|
134
154
|
const threadId = cli.flags.thread ??
|
|
135
155
|
cli.flags['t']?.toString();
|
|
136
156
|
const message = args.join(' ') || cli.flags.message;
|
|
137
|
-
// Require either assistant or thread
|
|
138
|
-
if (!assistantId && !threadId) {
|
|
139
|
-
console.error('Usage: ruska chat "<message>" -a <assistant-id>');
|
|
140
|
-
console.error(' ruska chat "<message>" -t <thread-id>');
|
|
141
|
-
console.log('');
|
|
142
|
-
console.log('Options:');
|
|
143
|
-
console.log(' -a, --assistant Assistant ID for new conversations');
|
|
144
|
-
console.log(' -t, --thread Thread ID to continue a conversation');
|
|
145
|
-
console.log('');
|
|
146
|
-
console.log('Examples:');
|
|
147
|
-
console.log(' ruska chat "Hello" -a abc-123');
|
|
148
|
-
console.log(' ruska chat "Follow up" -t thread-456');
|
|
149
|
-
process.exit(1);
|
|
150
|
-
}
|
|
151
157
|
if (!message) {
|
|
152
158
|
console.error('Error: Message is required');
|
|
153
|
-
console.error('Usage: ruska chat "<message>" -a <assistant-id>');
|
|
159
|
+
console.error('Usage: ruska chat "<message>" [-a <assistant-id>]');
|
|
154
160
|
process.exit(1);
|
|
155
161
|
}
|
|
156
162
|
await runChatCommand(message, {
|
|
157
163
|
json: cli.flags.json,
|
|
158
164
|
assistantId,
|
|
159
165
|
threadId,
|
|
166
|
+
truncateOptions: cli.flags.fullOutput
|
|
167
|
+
? undefined
|
|
168
|
+
: {
|
|
169
|
+
maxLength: cli.flags.truncate,
|
|
170
|
+
maxLines: cli.flags.truncateLines,
|
|
171
|
+
},
|
|
160
172
|
});
|
|
161
173
|
break;
|
|
162
174
|
}
|
|
@@ -174,6 +186,14 @@ async function main() {
|
|
|
174
186
|
});
|
|
175
187
|
break;
|
|
176
188
|
}
|
|
189
|
+
case 'version': {
|
|
190
|
+
await runVersionCommand();
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case 'health': {
|
|
194
|
+
await runHealthCommand();
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
177
197
|
case 'help':
|
|
178
198
|
case undefined: {
|
|
179
199
|
cli.showHelp();
|
package/dist/commands/chat.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Chat command for streaming LLM responses
|
|
3
3
|
* Implements Golden Path: Beta architecture + Gamma output + Alpha timeout
|
|
4
4
|
*/
|
|
5
|
+
import { type TruncateOptions } from '../lib/output/truncate.js';
|
|
5
6
|
/**
|
|
6
7
|
* Run the chat command
|
|
7
8
|
*/
|
|
@@ -9,4 +10,5 @@ export declare function runChatCommand(message: string, options?: {
|
|
|
9
10
|
json?: boolean;
|
|
10
11
|
assistantId?: string;
|
|
11
12
|
threadId?: string;
|
|
13
|
+
truncateOptions?: TruncateOptions;
|
|
12
14
|
}): Promise<void>;
|
package/dist/commands/chat.js
CHANGED
|
@@ -13,6 +13,7 @@ import { OutputFormatter } from '../lib/output/formatter.js';
|
|
|
13
13
|
import { classifyError, exitCodes } from '../lib/output/error-handler.js';
|
|
14
14
|
import { writeJson, checkIsTty } from '../lib/output/writers.js';
|
|
15
15
|
import { StreamService, StreamConnectionError, } from '../lib/services/stream-service.js';
|
|
16
|
+
import { truncate } from '../lib/output/truncate.js';
|
|
16
17
|
/**
|
|
17
18
|
* Group messages into blocks by type + name boundaries
|
|
18
19
|
*/
|
|
@@ -42,6 +43,21 @@ function groupMessagesIntoBlocks(messages) {
|
|
|
42
43
|
}
|
|
43
44
|
return blocks;
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Extract model name from stream events
|
|
48
|
+
*/
|
|
49
|
+
function extractModelFromEvents(events) {
|
|
50
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
51
|
+
const event = events[i];
|
|
52
|
+
if (event?.type === 'messages') {
|
|
53
|
+
const message = event.payload[0];
|
|
54
|
+
const modelName = message?.response_metadata?.['model_name'];
|
|
55
|
+
if (modelName)
|
|
56
|
+
return String(modelName);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
45
61
|
/**
|
|
46
62
|
* Status indicator component for TUI mode
|
|
47
63
|
*/
|
|
@@ -67,7 +83,7 @@ function StatusIndicator({ status }) {
|
|
|
67
83
|
/**
|
|
68
84
|
* TUI mode chat command using React hook
|
|
69
85
|
*/
|
|
70
|
-
function ChatCommandTui({ message, assistantId, threadId, }) {
|
|
86
|
+
function ChatCommandTui({ message, assistantId, threadId, truncateOptions, }) {
|
|
71
87
|
const { exit } = useApp();
|
|
72
88
|
const [config, setConfig] = useState();
|
|
73
89
|
const [authError, setAuthError] = useState(false);
|
|
@@ -98,7 +114,7 @@ function ChatCommandTui({ message, assistantId, threadId, }) {
|
|
|
98
114
|
: undefined, [config, assistantId, message, threadId]);
|
|
99
115
|
/* eslint-enable @typescript-eslint/naming-convention */
|
|
100
116
|
// Stream
|
|
101
|
-
const { status, messages, error } = useStream(config, request);
|
|
117
|
+
const { status, messages, events, error } = useStream(config, request);
|
|
102
118
|
// Group messages into blocks by type + name boundaries
|
|
103
119
|
const messageBlocks = useMemo(() => groupMessagesIntoBlocks(messages), [messages]);
|
|
104
120
|
// Exit on completion
|
|
@@ -132,10 +148,27 @@ function ChatCommandTui({ message, assistantId, threadId, }) {
|
|
|
132
148
|
React.createElement(Text, { dimColor: true, color: "cyan" },
|
|
133
149
|
"Tool Output",
|
|
134
150
|
block.name ? `: ${block.name}` : ''),
|
|
135
|
-
React.createElement(Box, { marginLeft: 2 },
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
React.createElement(Box, { marginLeft: 2, flexDirection: "column" }, (() => {
|
|
152
|
+
if (!truncateOptions) {
|
|
153
|
+
return React.createElement(Text, { dimColor: true }, block.content);
|
|
154
|
+
}
|
|
155
|
+
const result = truncate(block.content, truncateOptions);
|
|
156
|
+
return (React.createElement(React.Fragment, null,
|
|
157
|
+
React.createElement(Text, { dimColor: true }, result.text),
|
|
158
|
+
result.wasTruncated && (React.createElement(Text, { dimColor: true, color: "yellow" }, "(use --full-output for full output)"))));
|
|
159
|
+
})()))) : (React.createElement(Text, null, block.content))))),
|
|
160
|
+
status === 'done' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
161
|
+
React.createElement(Text, { color: "green" }, "Done"),
|
|
162
|
+
request?.metadata?.thread_id && (React.createElement(Text, { dimColor: true },
|
|
163
|
+
"Thread: ",
|
|
164
|
+
request.metadata.thread_id)),
|
|
165
|
+
extractModelFromEvents(events) && (React.createElement(Text, { dimColor: true },
|
|
166
|
+
"Model: ",
|
|
167
|
+
extractModelFromEvents(events))),
|
|
168
|
+
request?.metadata?.thread_id && (React.createElement(Text, { dimColor: true },
|
|
169
|
+
"Continue: ruska chat \"message\" -t",
|
|
170
|
+
' ',
|
|
171
|
+
request.metadata.thread_id))))));
|
|
139
172
|
}
|
|
140
173
|
/**
|
|
141
174
|
* JSON mode chat command - direct streaming without React hooks
|
|
@@ -211,7 +244,7 @@ async function runJsonMode(message, assistantId, threadId) {
|
|
|
211
244
|
/**
|
|
212
245
|
* Main chat command component - handles JSON mode branching
|
|
213
246
|
*/
|
|
214
|
-
function ChatCommand({ message, isJsonMode, assistantId, threadId, }) {
|
|
247
|
+
function ChatCommand({ message, isJsonMode, assistantId, threadId, truncateOptions, }) {
|
|
215
248
|
const { exit } = useApp();
|
|
216
249
|
useEffect(() => {
|
|
217
250
|
if (isJsonMode) {
|
|
@@ -226,7 +259,7 @@ function ChatCommand({ message, isJsonMode, assistantId, threadId, }) {
|
|
|
226
259
|
return null;
|
|
227
260
|
}
|
|
228
261
|
// TUI mode
|
|
229
|
-
return (React.createElement(ChatCommandTui, { message: message, assistantId: assistantId, threadId: threadId }));
|
|
262
|
+
return (React.createElement(ChatCommandTui, { message: message, assistantId: assistantId, threadId: threadId, truncateOptions: truncateOptions }));
|
|
230
263
|
}
|
|
231
264
|
/**
|
|
232
265
|
* Run the chat command
|
|
@@ -234,6 +267,6 @@ function ChatCommand({ message, isJsonMode, assistantId, threadId, }) {
|
|
|
234
267
|
export async function runChatCommand(message, options = {}) {
|
|
235
268
|
// Auto-detect: use JSON mode if not TTY (piped) or explicitly requested
|
|
236
269
|
const isJsonMode = options.json ?? !checkIsTty();
|
|
237
|
-
const { waitUntilExit } = render(React.createElement(ChatCommand, { message: message, isJsonMode: isJsonMode, assistantId: options.assistantId, threadId: options.threadId }));
|
|
270
|
+
const { waitUntilExit } = render(React.createElement(ChatCommand, { message: message, isJsonMode: isJsonMode, assistantId: options.assistantId, threadId: options.threadId, truncateOptions: options.truncateOptions }));
|
|
238
271
|
await waitUntilExit();
|
|
239
272
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runHealthCommand(): Promise<void>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { render, Text, Box, useApp } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
import { hostPresets } from '../types/index.js';
|
|
5
|
+
import { loadConfig } from '../lib/config.js';
|
|
6
|
+
import { fetchHealth } from '../lib/api.js';
|
|
7
|
+
function HealthCommand() {
|
|
8
|
+
const { exit } = useApp();
|
|
9
|
+
const [status, setStatus] = useState('loading');
|
|
10
|
+
const [health, setHealth] = useState(undefined);
|
|
11
|
+
const [error, setError] = useState(undefined);
|
|
12
|
+
const [host, setHost] = useState('');
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const checkHealth = async () => {
|
|
15
|
+
// Try to load config for host, fall back to production
|
|
16
|
+
const config = await loadConfig();
|
|
17
|
+
const targetHost = config?.host ?? hostPresets.production;
|
|
18
|
+
setHost(targetHost);
|
|
19
|
+
// Fetch health status
|
|
20
|
+
const result = await fetchHealth(targetHost);
|
|
21
|
+
if (result.success && result.data) {
|
|
22
|
+
setHealth(result.data);
|
|
23
|
+
setStatus('success');
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
setError(result.error ?? 'Failed to fetch health status');
|
|
27
|
+
setStatus('error');
|
|
28
|
+
}
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
exit();
|
|
31
|
+
}, 100);
|
|
32
|
+
};
|
|
33
|
+
void checkHealth();
|
|
34
|
+
}, [exit]);
|
|
35
|
+
if (status === 'loading') {
|
|
36
|
+
return (React.createElement(Box, null,
|
|
37
|
+
React.createElement(Text, { color: "cyan" },
|
|
38
|
+
React.createElement(Spinner, { type: "dots" })),
|
|
39
|
+
React.createElement(Text, null, " Checking health...")));
|
|
40
|
+
}
|
|
41
|
+
if (status === 'error') {
|
|
42
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
43
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
44
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "Health Check"),
|
|
45
|
+
React.createElement(Text, { dimColor: true },
|
|
46
|
+
" (",
|
|
47
|
+
host,
|
|
48
|
+
")")),
|
|
49
|
+
React.createElement(Text, null,
|
|
50
|
+
React.createElement(Text, { dimColor: true }, "Status: "),
|
|
51
|
+
React.createElement(Text, { bold: true, color: "red" }, "unhealthy")),
|
|
52
|
+
React.createElement(Text, null,
|
|
53
|
+
React.createElement(Text, { dimColor: true }, "Error: "),
|
|
54
|
+
React.createElement(Text, { color: "red" }, error))));
|
|
55
|
+
}
|
|
56
|
+
// Success - display health info
|
|
57
|
+
const statusColor = health?.status === 'healthy' ? 'green' : 'yellow';
|
|
58
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
59
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
60
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "Health Check"),
|
|
61
|
+
React.createElement(Text, { dimColor: true },
|
|
62
|
+
" (",
|
|
63
|
+
host,
|
|
64
|
+
")")),
|
|
65
|
+
React.createElement(Text, null,
|
|
66
|
+
React.createElement(Text, { dimColor: true }, "Status: "),
|
|
67
|
+
React.createElement(Text, { bold: true, color: statusColor }, health?.status)),
|
|
68
|
+
React.createElement(Text, null,
|
|
69
|
+
React.createElement(Text, { dimColor: true }, "Message: "),
|
|
70
|
+
React.createElement(Text, null, health?.message)),
|
|
71
|
+
React.createElement(Text, null,
|
|
72
|
+
React.createElement(Text, { dimColor: true }, "Version: "),
|
|
73
|
+
React.createElement(Text, null, health?.version))));
|
|
74
|
+
}
|
|
75
|
+
export async function runHealthCommand() {
|
|
76
|
+
const { waitUntilExit } = render(React.createElement(HealthCommand, null));
|
|
77
|
+
await waitUntilExit();
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runVersionCommand(): Promise<void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { render, Text, Box, useApp } from 'ink';
|
|
3
|
+
import Spinner from 'ink-spinner';
|
|
4
|
+
import { hostPresets } from '../types/index.js';
|
|
5
|
+
import { loadConfig } from '../lib/config.js';
|
|
6
|
+
import { fetchHealth } from '../lib/api.js';
|
|
7
|
+
const cliVersion = '0.1.3';
|
|
8
|
+
function VersionCommand() {
|
|
9
|
+
const { exit } = useApp();
|
|
10
|
+
const [status, setStatus] = useState('loading');
|
|
11
|
+
const [health, setHealth] = useState(undefined);
|
|
12
|
+
const [error, setError] = useState(undefined);
|
|
13
|
+
const [host, setHost] = useState('');
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const getVersion = async () => {
|
|
16
|
+
// Try to load config for host, fall back to production
|
|
17
|
+
const config = await loadConfig();
|
|
18
|
+
const targetHost = config?.host ?? hostPresets.production;
|
|
19
|
+
setHost(targetHost);
|
|
20
|
+
// Fetch health to get API version
|
|
21
|
+
const result = await fetchHealth(targetHost);
|
|
22
|
+
if (result.success && result.data) {
|
|
23
|
+
setHealth(result.data);
|
|
24
|
+
setStatus('success');
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
setError(result.error ?? 'Failed to fetch API version');
|
|
28
|
+
setStatus('error');
|
|
29
|
+
}
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
exit();
|
|
32
|
+
}, 100);
|
|
33
|
+
};
|
|
34
|
+
void getVersion();
|
|
35
|
+
}, [exit]);
|
|
36
|
+
if (status === 'loading') {
|
|
37
|
+
return (React.createElement(Box, null,
|
|
38
|
+
React.createElement(Text, { color: "cyan" },
|
|
39
|
+
React.createElement(Spinner, { type: "dots" })),
|
|
40
|
+
React.createElement(Text, null, " Loading version info...")));
|
|
41
|
+
}
|
|
42
|
+
// Always show CLI version
|
|
43
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
44
|
+
React.createElement(Text, null,
|
|
45
|
+
React.createElement(Text, { bold: true, color: "cyan" }, "@ruska/cli"),
|
|
46
|
+
React.createElement(Text, null,
|
|
47
|
+
" v",
|
|
48
|
+
cliVersion)),
|
|
49
|
+
status === 'error' ? (React.createElement(Text, null,
|
|
50
|
+
React.createElement(Text, { bold: true }, "API:"),
|
|
51
|
+
React.createElement(Text, { color: "red" }, " unavailable"),
|
|
52
|
+
React.createElement(Text, { dimColor: true },
|
|
53
|
+
" (",
|
|
54
|
+
error,
|
|
55
|
+
")"))) : (React.createElement(Text, null,
|
|
56
|
+
React.createElement(Text, { bold: true }, "API:"),
|
|
57
|
+
React.createElement(Text, { color: "green" },
|
|
58
|
+
" v",
|
|
59
|
+
health?.version),
|
|
60
|
+
React.createElement(Text, { dimColor: true },
|
|
61
|
+
" (",
|
|
62
|
+
host,
|
|
63
|
+
")")))));
|
|
64
|
+
}
|
|
65
|
+
export async function runVersionCommand() {
|
|
66
|
+
const { waitUntilExit } = render(React.createElement(VersionCommand, null));
|
|
67
|
+
await waitUntilExit();
|
|
68
|
+
}
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Config, type ApiResponse, type UserInfo, type AssistantsSearchResponse, type ModelsResponse, type CreateAssistantRequest, type CreateAssistantResponse } from '../types/index.js';
|
|
1
|
+
import { type Config, type ApiResponse, type UserInfo, type AssistantsSearchResponse, type ModelsResponse, type HealthResponse, type CreateAssistantRequest, type CreateAssistantResponse } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* API client for making authenticated requests to the Orchestra backend
|
|
4
4
|
*/
|
|
@@ -39,3 +39,7 @@ export declare function validateApiKey(host: string, apiKey: string): Promise<Ap
|
|
|
39
39
|
* Fetch available models (no auth required)
|
|
40
40
|
*/
|
|
41
41
|
export declare function fetchModels(host: string, apiKey?: string): Promise<ApiResponse<ModelsResponse>>;
|
|
42
|
+
/**
|
|
43
|
+
* Fetch health status (no auth required)
|
|
44
|
+
*/
|
|
45
|
+
export declare function fetchHealth(host: string): Promise<ApiResponse<HealthResponse>>;
|
package/dist/lib/api.js
CHANGED
|
@@ -149,3 +149,42 @@ export async function fetchModels(host, apiKey) {
|
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Fetch health status (no auth required)
|
|
154
|
+
*/
|
|
155
|
+
export async function fetchHealth(host) {
|
|
156
|
+
const url = `${host.replace(/\/$/, '')}/api/info/health`;
|
|
157
|
+
try {
|
|
158
|
+
const response = await fetch(url, {
|
|
159
|
+
headers: {
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
const errorText = await response.text();
|
|
165
|
+
let errorMessage;
|
|
166
|
+
try {
|
|
167
|
+
const errorJson = JSON.parse(errorText);
|
|
168
|
+
errorMessage = errorJson.detail ?? `HTTP ${response.status}`;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
errorMessage = errorText || `HTTP ${response.status}`;
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
error: errorMessage,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const data = (await response.json());
|
|
179
|
+
return {
|
|
180
|
+
success: true,
|
|
181
|
+
data,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncation utilities for CLI output
|
|
3
|
+
*/
|
|
4
|
+
export type TruncateOptions = {
|
|
5
|
+
maxLength?: number;
|
|
6
|
+
maxLines?: number;
|
|
7
|
+
indicator?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Truncate text by both line count and character count
|
|
11
|
+
* Applies line truncation first, then character truncation
|
|
12
|
+
*/
|
|
13
|
+
export declare function truncate(input: string, options?: TruncateOptions): {
|
|
14
|
+
text: string;
|
|
15
|
+
wasTruncated: boolean;
|
|
16
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncation utilities for CLI output
|
|
3
|
+
*/
|
|
4
|
+
const defaults = {
|
|
5
|
+
maxLength: 500,
|
|
6
|
+
maxLines: 10,
|
|
7
|
+
indicator: '... [truncated]',
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Truncate text by line count
|
|
11
|
+
*/
|
|
12
|
+
function truncateByLines(input, maxLines, indicator) {
|
|
13
|
+
const lines = input.split('\n');
|
|
14
|
+
if (lines.length <= maxLines) {
|
|
15
|
+
return { text: input, wasTruncated: false };
|
|
16
|
+
}
|
|
17
|
+
const truncated = lines.slice(0, maxLines).join('\n') + '\n' + indicator;
|
|
18
|
+
return { text: truncated, wasTruncated: true };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Truncate text by character count
|
|
22
|
+
*/
|
|
23
|
+
function truncateByLength(input, maxLength, indicator) {
|
|
24
|
+
if (input.length <= maxLength) {
|
|
25
|
+
return { text: input, wasTruncated: false };
|
|
26
|
+
}
|
|
27
|
+
const allowedLength = maxLength - indicator.length;
|
|
28
|
+
if (allowedLength <= 0) {
|
|
29
|
+
return { text: indicator, wasTruncated: true };
|
|
30
|
+
}
|
|
31
|
+
const truncated = input.slice(0, allowedLength) + indicator;
|
|
32
|
+
return { text: truncated, wasTruncated: true };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Truncate text by both line count and character count
|
|
36
|
+
* Applies line truncation first, then character truncation
|
|
37
|
+
*/
|
|
38
|
+
export function truncate(input, options = {}) {
|
|
39
|
+
const maxLength = options.maxLength ?? defaults.maxLength;
|
|
40
|
+
const maxLines = options.maxLines ?? defaults.maxLines;
|
|
41
|
+
const indicator = options.indicator ?? defaults.indicator;
|
|
42
|
+
// Apply line truncation first
|
|
43
|
+
const result = truncateByLines(input, maxLines, indicator);
|
|
44
|
+
// Then apply character truncation
|
|
45
|
+
if (result.text.length > maxLength) {
|
|
46
|
+
const charResult = truncateByLength(result.text, maxLength, indicator);
|
|
47
|
+
return {
|
|
48
|
+
text: charResult.text,
|
|
49
|
+
wasTruncated: result.wasTruncated || charResult.wasTruncated,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -79,6 +79,14 @@ export type ModelsResponse = {
|
|
|
79
79
|
free: string[];
|
|
80
80
|
models: string[];
|
|
81
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* Response from GET /api/health
|
|
84
|
+
*/
|
|
85
|
+
export type HealthResponse = {
|
|
86
|
+
status: string;
|
|
87
|
+
message: string;
|
|
88
|
+
version: string;
|
|
89
|
+
};
|
|
82
90
|
/**
|
|
83
91
|
* Host presets for environment selection
|
|
84
92
|
*/
|