@ruska/cli 0.1.3 → 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 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,15 @@ $ 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
+ | `--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 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)
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" -a <assistant-id> # New conversation with assistant
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();
@@ -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>;
@@ -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
- 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")))));
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
+ }
@@ -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.4",
4
4
  "description": "CLI for Orchestra - AI Agent Orchestration Platform",
5
5
  "license": "Apache-2.0",
6
6
  "author": "RUSKA <reggleston@ruska.ai>",