@ruska/cli 0.1.4 → 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
@@ -176,11 +176,34 @@ $ ruska chat "Hello" -a <assistant-id> | jq '.type'
176
176
  | `-a, --assistant` | Assistant ID (optional, uses default chat if omitted) |
177
177
  | `-t, --thread` | Thread ID to continue an existing conversation |
178
178
  | `-m, --message` | Message to send (alternative to positional arg) |
179
+ | `--tools` | Tools for the chat session (see below for modes) |
179
180
  | `--json` | Output as newline-delimited JSON (NDJSON) |
180
181
  | `--truncate <n>` | Max characters for tool output (default: 500) |
181
182
  | `--truncate-lines` | Max lines for tool output (default: 10) |
182
183
  | `--full-output` | Disable truncation (show full output) |
183
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
+ ```
206
+
184
207
  **Exit codes:**
185
208
 
186
209
  | Code | Meaning |
package/dist/cli.js CHANGED
@@ -33,6 +33,9 @@ const cli = meow(`
33
33
  -a, --assistant Assistant ID (optional, uses default chat if omitted)
34
34
  -t, --thread Thread ID to continue a conversation
35
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
36
39
  --json Output as newline-delimited JSON (auto-enabled when piped)
37
40
  --truncate <n> Max characters for tool output (default: 500)
38
41
  --truncate-lines Max lines for tool output (default: 10)
@@ -50,7 +53,9 @@ const cli = meow(`
50
53
  $ ruska auth # Configure API key and host
51
54
  $ ruska assistants # List your assistants
52
55
  $ ruska assistant abc-123 # Get assistant details
53
- $ ruska chat "Hello" # Direct chat with default LLM
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
54
59
  $ ruska chat "Hello" -a <assistant-id> # Chat with specific assistant
55
60
  $ ruska chat "Follow up" -t <thread-id> # Continue existing thread
56
61
  $ ruska chat "Hello" -a <id> --json # Output as NDJSON
@@ -116,7 +121,7 @@ const cli = meow(`
116
121
  },
117
122
  },
118
123
  });
119
- const [command, ...args] = cli.input;
124
+ const [command, ...arguments_] = cli.input;
120
125
  // Route to appropriate command
121
126
  async function main() {
122
127
  if (cli.flags.ui) {
@@ -134,7 +139,7 @@ async function main() {
134
139
  break;
135
140
  }
136
141
  case 'assistant': {
137
- const assistantId = args[0];
142
+ const assistantId = arguments_[0];
138
143
  if (!assistantId) {
139
144
  console.error('Usage: ruska assistant <id>');
140
145
  console.log('Run `ruska assistants` to list available assistants');
@@ -153,7 +158,7 @@ async function main() {
153
158
  cli.flags['a']?.toString();
154
159
  const threadId = cli.flags.thread ??
155
160
  cli.flags['t']?.toString();
156
- const message = args.join(' ') || cli.flags.message;
161
+ const message = arguments_.join(' ') || cli.flags.message;
157
162
  if (!message) {
158
163
  console.error('Error: Message is required');
159
164
  console.error('Usage: ruska chat "<message>" [-a <assistant-id>]');
@@ -163,6 +168,7 @@ async function main() {
163
168
  json: cli.flags.json,
164
169
  assistantId,
165
170
  threadId,
171
+ tools: cli.flags.tools,
166
172
  truncateOptions: cli.flags.fullOutput
167
173
  ? undefined
168
174
  : {
@@ -10,5 +10,6 @@ export declare function runChatCommand(message: string, options?: {
10
10
  json?: boolean;
11
11
  assistantId?: string;
12
12
  threadId?: string;
13
+ tools?: string;
13
14
  truncateOptions?: TruncateOptions;
14
15
  }): Promise<void>;
@@ -14,6 +14,7 @@ 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
16
  import { truncate } from '../lib/output/truncate.js';
17
+ import { parseToolsFlag } from '../lib/tools.js';
17
18
  /**
18
19
  * Group messages into blocks by type + name boundaries
19
20
  */
@@ -83,7 +84,7 @@ function StatusIndicator({ status }) {
83
84
  /**
84
85
  * TUI mode chat command using React hook
85
86
  */
86
- function ChatCommandTui({ message, assistantId, threadId, truncateOptions, }) {
87
+ function ChatCommandTui({ message, assistantId, threadId, tools, truncateOptions, }) {
87
88
  const { exit } = useApp();
88
89
  const [config, setConfig] = useState();
89
90
  const [authError, setAuthError] = useState(false);
@@ -106,12 +107,13 @@ function ChatCommandTui({ message, assistantId, threadId, truncateOptions, }) {
106
107
  const request = useMemo(() => config
107
108
  ? {
108
109
  input: { messages: [{ role: 'user', content: message }] },
110
+ tools,
109
111
  metadata: {
110
112
  ...(assistantId && { assistant_id: assistantId }),
111
113
  ...(threadId && { thread_id: threadId }),
112
114
  },
113
115
  }
114
- : undefined, [config, assistantId, message, threadId]);
116
+ : undefined, [config, assistantId, message, threadId, tools]);
115
117
  /* eslint-enable @typescript-eslint/naming-convention */
116
118
  // Stream
117
119
  const { status, messages, events, error } = useStream(config, request);
@@ -174,7 +176,7 @@ function ChatCommandTui({ message, assistantId, threadId, truncateOptions, }) {
174
176
  * JSON mode chat command - direct streaming without React hooks
175
177
  * Outputs NDJSON for downstream consumption
176
178
  */
177
- async function runJsonMode(message, assistantId, threadId) {
179
+ async function runJsonMode(message, assistantId, threadId, tools) {
178
180
  const config = await loadConfig();
179
181
  if (!config) {
180
182
  const formatter = new OutputFormatter();
@@ -190,6 +192,7 @@ async function runJsonMode(message, assistantId, threadId) {
190
192
  /* eslint-disable @typescript-eslint/naming-convention */
191
193
  const request = {
192
194
  input: { messages: [{ role: 'user', content: message }] },
195
+ tools,
193
196
  metadata: {
194
197
  ...(assistantId && { assistant_id: assistantId }),
195
198
  ...(threadId && { thread_id: threadId }),
@@ -244,22 +247,22 @@ async function runJsonMode(message, assistantId, threadId) {
244
247
  /**
245
248
  * Main chat command component - handles JSON mode branching
246
249
  */
247
- function ChatCommand({ message, isJsonMode, assistantId, threadId, truncateOptions, }) {
250
+ function ChatCommand({ message, isJsonMode, assistantId, threadId, tools, truncateOptions, }) {
248
251
  const { exit } = useApp();
249
252
  useEffect(() => {
250
253
  if (isJsonMode) {
251
254
  // JSON mode runs outside React, just exit immediately
252
- void runJsonMode(message, assistantId, threadId).finally(() => {
255
+ void runJsonMode(message, assistantId, threadId, tools).finally(() => {
253
256
  exit();
254
257
  });
255
258
  }
256
- }, [message, isJsonMode, assistantId, threadId, exit]);
259
+ }, [message, isJsonMode, assistantId, threadId, tools, exit]);
257
260
  // JSON mode: no UI (handled in useEffect)
258
261
  if (isJsonMode) {
259
262
  return null;
260
263
  }
261
264
  // TUI mode
262
- return (React.createElement(ChatCommandTui, { message: message, assistantId: assistantId, threadId: threadId, truncateOptions: truncateOptions }));
265
+ return (React.createElement(ChatCommandTui, { message: message, assistantId: assistantId, threadId: threadId, tools: tools, truncateOptions: truncateOptions }));
263
266
  }
264
267
  /**
265
268
  * Run the chat command
@@ -267,6 +270,8 @@ function ChatCommand({ message, isJsonMode, assistantId, threadId, truncateOptio
267
270
  export async function runChatCommand(message, options = {}) {
268
271
  // Auto-detect: use JSON mode if not TTY (piped) or explicitly requested
269
272
  const isJsonMode = options.json ?? !checkIsTty();
270
- const { waitUntilExit } = render(React.createElement(ChatCommand, { message: message, isJsonMode: isJsonMode, assistantId: options.assistantId, threadId: options.threadId, truncateOptions: options.truncateOptions }));
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 }));
271
276
  await waitUntilExit();
272
277
  }
@@ -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 {
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ruska/cli",
3
- "version": "0.1.4",
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"