@ruska/cli 0.1.3 → 0.1.5

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 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 an assistant or continue a thread
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 for new conversations
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 eed8d8b3-3dcd-4396-afba-... # Get assistant details
63
- $ ruska chat "Hello" -a <assistant-id> # New conversation with assistant
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 an assistant or continue a thread using streaming. Requires authentication.
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,38 @@ $ ruska chat "Hello" -a <assistant-id> | jq '.type'
159
171
 
160
172
  **Options:**
161
173
 
162
- | Option | Description |
163
- | ----------------- | ----------------------------------------------- |
164
- | `-a, --assistant` | Assistant ID for new conversations |
165
- | `-t, --thread` | Thread ID to continue an existing conversation |
166
- | `-m, --message` | Message to send (alternative to positional arg) |
167
- | `--json` | Output as newline-delimited JSON (NDJSON) |
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
+ | `--tools` | Tools for the chat session (see below for modes) |
180
+ | `--json` | Output as newline-delimited JSON (NDJSON) |
181
+ | `--truncate <n>` | Max characters for tool output (default: 500) |
182
+ | `--truncate-lines` | Max lines for tool output (default: 10) |
183
+ | `--full-output` | Disable truncation (show full output) |
184
+
185
+ **Tool options:**
186
+
187
+ | Value | Behavior |
188
+ | --------------------- | --------------------------------------------------------------------------------------- |
189
+ | (not provided) | Uses default tools: web_search, web_scrape, math_calculator, think_tool, python_sandbox |
190
+ | `--tools=disabled` | Disables all tools |
191
+ | `--tools=tool1,tool2` | Uses only the specified tools |
192
+
193
+ **Examples with tools:**
194
+
195
+ ```bash
196
+ # Chat with default tools (web search, scrape, calculator, think, python)
197
+ $ ruska chat "What's the weather in Dallas?"
198
+
199
+ # Chat without any tools
200
+ $ ruska chat "Tell me a joke" --tools=disabled
201
+
202
+ # Chat with specific tools only
203
+ $ ruska chat "Calculate 2+2" --tools=math_calculator
204
+ $ ruska chat "Search and analyze" --tools=web_search,think_tool
205
+ ```
168
206
 
169
207
  **Exit codes:**
170
208
 
@@ -228,6 +266,28 @@ All Models (15):
228
266
  ...
229
267
  ```
230
268
 
269
+ ### `ruska version`
270
+
271
+ Show CLI and API version information.
272
+
273
+ ```bash
274
+ $ ruska version
275
+
276
+ Ruska CLI v0.1.3
277
+ API: https://chat.ruska.ai
278
+ ```
279
+
280
+ ### `ruska health`
281
+
282
+ Check API health status.
283
+
284
+ ```bash
285
+ $ ruska health
286
+
287
+ API Health Check
288
+ Status: healthy
289
+ ```
290
+
231
291
  ### `ruska --ui`
232
292
 
233
293
  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,26 @@ 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 an assistant or continue a thread
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 for new conversations
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)
36
+ --tools Tools to enable (default: web_search,web_scrape,math_calculator,think_tool,python_sandbox)
37
+ Use --tools=disabled to disable all tools
38
+ Use --tools=tool1,tool2 for specific tools
32
39
  --json Output as newline-delimited JSON (auto-enabled when piped)
40
+ --truncate <n> Max characters for tool output (default: 500)
41
+ --truncate-lines Max lines for tool output (default: 10)
42
+ --full-output Disable truncation (show full output)
33
43
 
34
44
  Create Options
35
45
  --name Assistant name (required)
@@ -43,7 +53,10 @@ const cli = meow(`
43
53
  $ ruska auth # Configure API key and host
44
54
  $ ruska assistants # List your assistants
45
55
  $ ruska assistant abc-123 # Get assistant details
46
- $ ruska chat "Hello" -a <assistant-id> # New conversation with assistant
56
+ $ ruska chat "Hello" # Chat with default tools enabled
57
+ $ ruska chat "Hello" --tools=disabled # Chat without any tools
58
+ $ ruska chat "Search X" --tools=web_search # Chat with specific tools
59
+ $ ruska chat "Hello" -a <assistant-id> # Chat with specific assistant
47
60
  $ ruska chat "Follow up" -t <thread-id> # Continue existing thread
48
61
  $ ruska chat "Hello" -a <id> --json # Output as NDJSON
49
62
  $ ruska chat "Query" -a <id> | jq '.type' # Pipe to jq
@@ -74,6 +87,18 @@ const cli = meow(`
74
87
  type: 'string',
75
88
  shortFlag: 't',
76
89
  },
90
+ truncate: {
91
+ type: 'number',
92
+ default: 500,
93
+ },
94
+ truncateLines: {
95
+ type: 'number',
96
+ default: 10,
97
+ },
98
+ fullOutput: {
99
+ type: 'boolean',
100
+ default: false,
101
+ },
77
102
  interactive: {
78
103
  type: 'boolean',
79
104
  shortFlag: 'i',
@@ -96,7 +121,7 @@ const cli = meow(`
96
121
  },
97
122
  },
98
123
  });
99
- const [command, ...args] = cli.input;
124
+ const [command, ...arguments_] = cli.input;
100
125
  // Route to appropriate command
101
126
  async function main() {
102
127
  if (cli.flags.ui) {
@@ -114,7 +139,7 @@ async function main() {
114
139
  break;
115
140
  }
116
141
  case 'assistant': {
117
- const assistantId = args[0];
142
+ const assistantId = arguments_[0];
118
143
  if (!assistantId) {
119
144
  console.error('Usage: ruska assistant <id>');
120
145
  console.log('Run `ruska assistants` to list available assistants');
@@ -133,30 +158,23 @@ async function main() {
133
158
  cli.flags['a']?.toString();
134
159
  const threadId = cli.flags.thread ??
135
160
  cli.flags['t']?.toString();
136
- 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
- }
161
+ const message = arguments_.join(' ') || cli.flags.message;
151
162
  if (!message) {
152
163
  console.error('Error: Message is required');
153
- console.error('Usage: ruska chat "<message>" -a <assistant-id>');
164
+ console.error('Usage: ruska chat "<message>" [-a <assistant-id>]');
154
165
  process.exit(1);
155
166
  }
156
167
  await runChatCommand(message, {
157
168
  json: cli.flags.json,
158
169
  assistantId,
159
170
  threadId,
171
+ tools: cli.flags.tools,
172
+ truncateOptions: cli.flags.fullOutput
173
+ ? undefined
174
+ : {
175
+ maxLength: cli.flags.truncate,
176
+ maxLines: cli.flags.truncateLines,
177
+ },
160
178
  });
161
179
  break;
162
180
  }
@@ -174,6 +192,14 @@ async function main() {
174
192
  });
175
193
  break;
176
194
  }
195
+ case 'version': {
196
+ await runVersionCommand();
197
+ break;
198
+ }
199
+ case 'health': {
200
+ await runHealthCommand();
201
+ break;
202
+ }
177
203
  case 'help':
178
204
  case undefined: {
179
205
  cli.showHelp();
@@ -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,6 @@ export declare function runChatCommand(message: string, options?: {
9
10
  json?: boolean;
10
11
  assistantId?: string;
11
12
  threadId?: string;
13
+ tools?: string;
14
+ truncateOptions?: TruncateOptions;
12
15
  }): Promise<void>;
@@ -13,6 +13,8 @@ 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';
17
+ import { parseToolsFlag } from '../lib/tools.js';
16
18
  /**
17
19
  * Group messages into blocks by type + name boundaries
18
20
  */
@@ -42,6 +44,21 @@ function groupMessagesIntoBlocks(messages) {
42
44
  }
43
45
  return blocks;
44
46
  }
47
+ /**
48
+ * Extract model name from stream events
49
+ */
50
+ function extractModelFromEvents(events) {
51
+ for (let i = events.length - 1; i >= 0; i--) {
52
+ const event = events[i];
53
+ if (event?.type === 'messages') {
54
+ const message = event.payload[0];
55
+ const modelName = message?.response_metadata?.['model_name'];
56
+ if (modelName)
57
+ return String(modelName);
58
+ }
59
+ }
60
+ return undefined;
61
+ }
45
62
  /**
46
63
  * Status indicator component for TUI mode
47
64
  */
@@ -67,7 +84,7 @@ function StatusIndicator({ status }) {
67
84
  /**
68
85
  * TUI mode chat command using React hook
69
86
  */
70
- function ChatCommandTui({ message, assistantId, threadId, }) {
87
+ function ChatCommandTui({ message, assistantId, threadId, tools, truncateOptions, }) {
71
88
  const { exit } = useApp();
72
89
  const [config, setConfig] = useState();
73
90
  const [authError, setAuthError] = useState(false);
@@ -90,15 +107,16 @@ function ChatCommandTui({ message, assistantId, threadId, }) {
90
107
  const request = useMemo(() => config
91
108
  ? {
92
109
  input: { messages: [{ role: 'user', content: message }] },
110
+ tools,
93
111
  metadata: {
94
112
  ...(assistantId && { assistant_id: assistantId }),
95
113
  ...(threadId && { thread_id: threadId }),
96
114
  },
97
115
  }
98
- : undefined, [config, assistantId, message, threadId]);
116
+ : undefined, [config, assistantId, message, threadId, tools]);
99
117
  /* eslint-enable @typescript-eslint/naming-convention */
100
118
  // Stream
101
- const { status, messages, error } = useStream(config, request);
119
+ const { status, messages, events, error } = useStream(config, request);
102
120
  // Group messages into blocks by type + name boundaries
103
121
  const messageBlocks = useMemo(() => groupMessagesIntoBlocks(messages), [messages]);
104
122
  // Exit on completion
@@ -132,16 +150,33 @@ function ChatCommandTui({ message, assistantId, threadId, }) {
132
150
  React.createElement(Text, { dimColor: true, color: "cyan" },
133
151
  "Tool Output",
134
152
  block.name ? `: ${block.name}` : ''),
135
- React.createElement(Box, { marginLeft: 2 },
136
- React.createElement(Text, { dimColor: true }, block.content)))) : (React.createElement(Text, null, block.content))))),
137
- status === 'done' && (React.createElement(Box, { marginTop: 1 },
138
- React.createElement(Text, { color: "green" }, "Done")))));
153
+ React.createElement(Box, { marginLeft: 2, flexDirection: "column" }, (() => {
154
+ if (!truncateOptions) {
155
+ return React.createElement(Text, { dimColor: true }, block.content);
156
+ }
157
+ const result = truncate(block.content, truncateOptions);
158
+ return (React.createElement(React.Fragment, null,
159
+ React.createElement(Text, { dimColor: true }, result.text),
160
+ result.wasTruncated && (React.createElement(Text, { dimColor: true, color: "yellow" }, "(use --full-output for full output)"))));
161
+ })()))) : (React.createElement(Text, null, block.content))))),
162
+ status === 'done' && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
163
+ React.createElement(Text, { color: "green" }, "Done"),
164
+ request?.metadata?.thread_id && (React.createElement(Text, { dimColor: true },
165
+ "Thread: ",
166
+ request.metadata.thread_id)),
167
+ extractModelFromEvents(events) && (React.createElement(Text, { dimColor: true },
168
+ "Model: ",
169
+ extractModelFromEvents(events))),
170
+ request?.metadata?.thread_id && (React.createElement(Text, { dimColor: true },
171
+ "Continue: ruska chat \"message\" -t",
172
+ ' ',
173
+ request.metadata.thread_id))))));
139
174
  }
140
175
  /**
141
176
  * JSON mode chat command - direct streaming without React hooks
142
177
  * Outputs NDJSON for downstream consumption
143
178
  */
144
- async function runJsonMode(message, assistantId, threadId) {
179
+ async function runJsonMode(message, assistantId, threadId, tools) {
145
180
  const config = await loadConfig();
146
181
  if (!config) {
147
182
  const formatter = new OutputFormatter();
@@ -157,6 +192,7 @@ async function runJsonMode(message, assistantId, threadId) {
157
192
  /* eslint-disable @typescript-eslint/naming-convention */
158
193
  const request = {
159
194
  input: { messages: [{ role: 'user', content: message }] },
195
+ tools,
160
196
  metadata: {
161
197
  ...(assistantId && { assistant_id: assistantId }),
162
198
  ...(threadId && { thread_id: threadId }),
@@ -211,22 +247,22 @@ async function runJsonMode(message, assistantId, threadId) {
211
247
  /**
212
248
  * Main chat command component - handles JSON mode branching
213
249
  */
214
- function ChatCommand({ message, isJsonMode, assistantId, threadId, }) {
250
+ function ChatCommand({ message, isJsonMode, assistantId, threadId, tools, truncateOptions, }) {
215
251
  const { exit } = useApp();
216
252
  useEffect(() => {
217
253
  if (isJsonMode) {
218
254
  // JSON mode runs outside React, just exit immediately
219
- void runJsonMode(message, assistantId, threadId).finally(() => {
255
+ void runJsonMode(message, assistantId, threadId, tools).finally(() => {
220
256
  exit();
221
257
  });
222
258
  }
223
- }, [message, isJsonMode, assistantId, threadId, exit]);
259
+ }, [message, isJsonMode, assistantId, threadId, tools, exit]);
224
260
  // JSON mode: no UI (handled in useEffect)
225
261
  if (isJsonMode) {
226
262
  return null;
227
263
  }
228
264
  // TUI mode
229
- return (React.createElement(ChatCommandTui, { message: message, assistantId: assistantId, threadId: threadId }));
265
+ return (React.createElement(ChatCommandTui, { message: message, assistantId: assistantId, threadId: threadId, tools: tools, truncateOptions: truncateOptions }));
230
266
  }
231
267
  /**
232
268
  * Run the chat command
@@ -234,6 +270,8 @@ function ChatCommand({ message, isJsonMode, assistantId, threadId, }) {
234
270
  export async function runChatCommand(message, options = {}) {
235
271
  // Auto-detect: use JSON mode if not TTY (piped) or explicitly requested
236
272
  const isJsonMode = options.json ?? !checkIsTty();
237
- const { waitUntilExit } = render(React.createElement(ChatCommand, { message: message, isJsonMode: isJsonMode, assistantId: options.assistantId, threadId: options.threadId }));
273
+ // Parse tools flag (undefined = defaults, 'disabled' = [], 'a,b' = ['a', 'b'])
274
+ const parsedTools = parseToolsFlag(options.tools);
275
+ const { waitUntilExit } = render(React.createElement(ChatCommand, { message: message, isJsonMode: isJsonMode, assistantId: options.assistantId, threadId: options.threadId, tools: parsedTools, truncateOptions: options.truncateOptions }));
238
276
  await waitUntilExit();
239
277
  }
@@ -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
+ }
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
- type ModelSelectProps = {
2
+ type ModelSelectProperties = {
3
3
  readonly models: string[];
4
4
  readonly value: string;
5
5
  readonly onChange: (value: string) => void;
6
6
  readonly onSubmit: (value: string) => void;
7
7
  readonly onEscape?: () => void;
8
8
  };
9
- export declare function ModelSelect({ models, value, onChange, onSubmit, onEscape, }: ModelSelectProps): React.JSX.Element;
9
+ export declare function ModelSelect({ models, value, onChange, onSubmit, onEscape, }: ModelSelectProperties): React.JSX.Element;
10
10
  export {};
@@ -61,8 +61,8 @@ export function ModelSelect({ models, value, onChange, onSubmit, onEscape, }) {
61
61
  " \u2191 ",
62
62
  startIndex,
63
63
  " more"),
64
- visibleModels.map((model, idx) => {
65
- const actualIndex = startIndex + idx;
64
+ visibleModels.map((model, index) => {
65
+ const actualIndex = startIndex + index;
66
66
  const isSelected = actualIndex === selectedIndex;
67
67
  return (React.createElement(Box, { key: model },
68
68
  React.createElement(Text, { color: isSelected ? 'cyan' : undefined }, isSelected ? '❯ ' : ' '),
@@ -15,9 +15,9 @@ export function useStream(config, request) {
15
15
  const [finalResponse, setFinalResponse] = useState();
16
16
  const [error, setError] = useState();
17
17
  const [errorCode, setErrorCode] = useState();
18
- const handleRef = useRef();
18
+ const handleReference = useRef();
19
19
  const abort = useCallback(() => {
20
- handleRef.current?.abort();
20
+ handleReference.current?.abort();
21
21
  }, []);
22
22
  useEffect(() => {
23
23
  if (!config || !request)
@@ -33,7 +33,7 @@ export function useStream(config, request) {
33
33
  setErrorCode(undefined);
34
34
  try {
35
35
  const handle = await service.connect(request);
36
- handleRef.current = handle;
36
+ handleReference.current = handle;
37
37
  if (cancelled) {
38
38
  handle.abort();
39
39
  return;
@@ -90,7 +90,7 @@ export function useStream(config, request) {
90
90
  void run();
91
91
  return () => {
92
92
  cancelled = true;
93
- handleRef.current?.abort();
93
+ handleReference.current?.abort();
94
94
  };
95
95
  }, [config, request]);
96
96
  return {
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
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Default tools and parsing for chat command
3
+ * @see frontend/src/components/menus/BaseToolMenu.tsx for frontend equivalent
4
+ */
5
+ /**
6
+ * Default tools enabled for chat command
7
+ * Mirrors frontend DEFAULT_AGENT_TOOLS in BaseToolMenu.tsx
8
+ */
9
+ export declare const defaultAgentTools: readonly ["web_search", "web_scrape", "math_calculator", "think_tool", "python_sandbox"];
10
+ export type DefaultAgentTool = (typeof defaultAgentTools)[number];
11
+ /**
12
+ * Parse --tools flag value into array of tool names
13
+ *
14
+ * @param value - Raw flag value (undefined, 'disabled', or comma-separated)
15
+ * @returns Array of tool names to send to API
16
+ *
17
+ * @example
18
+ * parseToolsFlag(undefined) // returns DEFAULT_AGENT_TOOLS
19
+ * parseToolsFlag('') // returns DEFAULT_AGENT_TOOLS
20
+ * parseToolsFlag('disabled') // returns []
21
+ * parseToolsFlag('web_search,think_tool') // returns ['web_search', 'think_tool']
22
+ */
23
+ export declare function parseToolsFlag(value: string | undefined): string[];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Default tools and parsing for chat command
3
+ * @see frontend/src/components/menus/BaseToolMenu.tsx for frontend equivalent
4
+ */
5
+ /**
6
+ * Default tools enabled for chat command
7
+ * Mirrors frontend DEFAULT_AGENT_TOOLS in BaseToolMenu.tsx
8
+ */
9
+ export const defaultAgentTools = [
10
+ 'web_search',
11
+ 'web_scrape',
12
+ 'math_calculator',
13
+ 'think_tool',
14
+ 'python_sandbox',
15
+ ];
16
+ /**
17
+ * Parse --tools flag value into array of tool names
18
+ *
19
+ * @param value - Raw flag value (undefined, 'disabled', or comma-separated)
20
+ * @returns Array of tool names to send to API
21
+ *
22
+ * @example
23
+ * parseToolsFlag(undefined) // returns DEFAULT_AGENT_TOOLS
24
+ * parseToolsFlag('') // returns DEFAULT_AGENT_TOOLS
25
+ * parseToolsFlag('disabled') // returns []
26
+ * parseToolsFlag('web_search,think_tool') // returns ['web_search', 'think_tool']
27
+ */
28
+ export function parseToolsFlag(value) {
29
+ // No flag or empty string: use defaults
30
+ if (value === undefined || value.trim() === '') {
31
+ return [...defaultAgentTools];
32
+ }
33
+ // Explicit disable
34
+ if (value.toLowerCase() === 'disabled') {
35
+ return [];
36
+ }
37
+ // Parse comma-separated list
38
+ return value
39
+ .split(',')
40
+ .map(t => t.trim())
41
+ .filter(Boolean);
42
+ }
@@ -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
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruska/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "CLI for Orchestra - AI Agent Orchestration Platform",
5
5
  "license": "Apache-2.0",
6
6
  "author": "RUSKA <reggleston@ruska.ai>",
@@ -47,7 +47,7 @@
47
47
  "dev": "tsc --watch",
48
48
  "build": "tsc",
49
49
  "build:clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && npm run build",
50
- "format": "prettier --write \"**/*.{ts,tsx,json,md}\" && xo --fix",
50
+ "format": "xo --fix && prettier --write \"**/*.{ts,tsx,json,md}\"",
51
51
  "format:check": "prettier --check .",
52
52
  "lint": "xo",
53
53
  "test": "npm run lint && npm run build && ava",
@@ -87,7 +87,7 @@
87
87
  "ts-node": "^10.9.1",
88
88
  "tsx": "^4.21.0",
89
89
  "typescript": "^5.0.3",
90
- "xo": "^0.53.1"
90
+ "xo": "^0.60.0"
91
91
  },
92
92
  "ava": {
93
93
  "files": [
@@ -103,7 +103,16 @@
103
103
  "rules": {
104
104
  "react/prop-types": "off",
105
105
  "unicorn/expiring-todo-comments": "off",
106
- "ava/no-ignored-test-files": "off"
106
+ "ava/no-ignored-test-files": "off",
107
+ "unicorn/prevent-abbreviations": "off",
108
+ "@typescript-eslint/switch-exhaustiveness-check": "off",
109
+ "@typescript-eslint/no-unsafe-assignment": "off",
110
+ "@typescript-eslint/no-unsafe-call": "off",
111
+ "@typescript-eslint/restrict-plus-operands": "off",
112
+ "promise/prefer-await-to-then": "off",
113
+ "complexity": "off",
114
+ "unicorn/prefer-at": "off",
115
+ "prettier/prettier": "off"
107
116
  }
108
117
  },
109
118
  "prettier": "@vdemedes/prettier-config"