swiftroutercli 4.0.3 → 4.0.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/dist/index.js CHANGED
@@ -71,7 +71,7 @@ async function resolveModel(config) {
71
71
  program
72
72
  .name("swiftroutercli")
73
73
  .description("CLI for SwiftRouter AI Gateway")
74
- .version("4.0.3");
74
+ .version("4.0.4");
75
75
  program
76
76
  .command("config")
77
77
  .description("Manually configure the CLI with your SwiftRouter API Key and Base URL")
package/dist/ui/Chat.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { render, Box, Text, useInput, Static } from "ink";
3
+ import TextInput from "ink-text-input";
3
4
  import { streamChatCompletion, fetchModels } from "../api/client.js";
4
5
  import chalk from "chalk";
5
6
  import { loadHistory, saveHistory } from "../config.js";
@@ -12,11 +13,10 @@ marked.setOptions({
12
13
  renderer: new TerminalRenderer()
13
14
  });
14
15
  const Chat = ({ config, model, initialPrompt, approvalMode }) => {
15
- const [messages, setMessages] = useState([
16
- { id: 0, role: "user", content: initialPrompt },
17
- ]);
16
+ const hasInitialPrompt = initialPrompt.trim().length > 0;
17
+ const [messages, setMessages] = useState(hasInitialPrompt ? [{ id: 0, role: "user", content: initialPrompt }] : []);
18
18
  const [streamingContent, setStreamingContent] = useState("");
19
- const [isStreaming, setIsStreaming] = useState(true);
19
+ const [isStreaming, setIsStreaming] = useState(hasInitialPrompt);
20
20
  const [error, setError] = useState(null);
21
21
  const [input, setInput] = useState("");
22
22
  const [history, setHistory] = useState([]);
@@ -56,7 +56,6 @@ const Chat = ({ config, model, initialPrompt, approvalMode }) => {
56
56
  { id: prev.length, role: "system", content: `Sandbox Output:\n${output || "Done"}` }
57
57
  ]);
58
58
  setIsExecuting(false);
59
- // Recursive automation could go here in V5
60
59
  });
61
60
  }
62
61
  else {
@@ -70,6 +69,7 @@ const Chat = ({ config, model, initialPrompt, approvalMode }) => {
70
69
  setIsStreaming(false);
71
70
  });
72
71
  }, [messages, isStreaming, config, activeModel]);
72
+ // Handle command confirmation and history navigation
73
73
  useInput((char, key) => {
74
74
  if (isSelectingModel)
75
75
  return;
@@ -93,44 +93,8 @@ const Chat = ({ config, model, initialPrompt, approvalMode }) => {
93
93
  }
94
94
  return;
95
95
  }
96
- if (key.return) {
97
- const trimmed = input.trim();
98
- if (trimmed === "/exit" || trimmed === "/quit") {
99
- process.exit(0);
100
- }
101
- if (trimmed === "/clear") {
102
- setMessages([]);
103
- setInput("");
104
- setHistoryIndex(-1);
105
- return;
106
- }
107
- if (trimmed === "/models") {
108
- setIsSelectingModel(true);
109
- setIsLoadingModels(true);
110
- setInput("");
111
- fetchModels(config).then(models => {
112
- const options = models.map(m => ({ label: m.id || m.name, value: m.id || m.name }));
113
- setModelOptions(options.length ? options : [{ label: "mock-model", value: "mock-model" }]);
114
- setIsLoadingModels(false);
115
- }).catch(err => {
116
- setError(err);
117
- setIsSelectingModel(false);
118
- setIsLoadingModels(false);
119
- });
120
- return;
121
- }
122
- if (trimmed.length > 0) {
123
- const newPrompt = trimmed;
124
- setMessages((prev) => [...prev, { id: prev.length, role: "user", content: newPrompt }]);
125
- const newHistory = [...history, newPrompt];
126
- setHistory(newHistory);
127
- saveHistory(newHistory);
128
- setInput("");
129
- setHistoryIndex(-1);
130
- setIsStreaming(true);
131
- }
132
- }
133
- else if (key.upArrow) {
96
+ // History navigation with Up/Down arrows
97
+ if (key.upArrow) {
134
98
  if (history.length > 0 && historyIndex < history.length - 1) {
135
99
  const newIdx = historyIndex + 1;
136
100
  setHistoryIndex(newIdx);
@@ -148,13 +112,43 @@ const Chat = ({ config, model, initialPrompt, approvalMode }) => {
148
112
  setInput("");
149
113
  }
150
114
  }
151
- else if (key.backspace || key.delete) {
152
- setInput((prev) => prev.slice(0, -1));
115
+ });
116
+ const handleSubmit = (value) => {
117
+ const trimmed = value.trim();
118
+ if (!trimmed)
119
+ return;
120
+ if (trimmed === "/exit" || trimmed === "/quit") {
121
+ process.exit(0);
153
122
  }
154
- else if (char) {
155
- setInput((prev) => prev + char);
123
+ if (trimmed === "/clear") {
124
+ setMessages([]);
125
+ setInput("");
126
+ setHistoryIndex(-1);
127
+ return;
156
128
  }
157
- });
129
+ if (trimmed === "/models") {
130
+ setIsSelectingModel(true);
131
+ setIsLoadingModels(true);
132
+ setInput("");
133
+ fetchModels(config).then(models => {
134
+ const options = models.map(m => ({ label: m.id || m.name, value: m.id || m.name }));
135
+ setModelOptions(options.length ? options : [{ label: "mock-model", value: "mock-model" }]);
136
+ setIsLoadingModels(false);
137
+ }).catch(err => {
138
+ setError(err);
139
+ setIsSelectingModel(false);
140
+ setIsLoadingModels(false);
141
+ });
142
+ return;
143
+ }
144
+ setMessages((prev) => [...prev, { id: prev.length, role: "user", content: trimmed }]);
145
+ const newHistory = [...history, trimmed];
146
+ setHistory(newHistory);
147
+ saveHistory(newHistory);
148
+ setInput("");
149
+ setHistoryIndex(-1);
150
+ setIsStreaming(true);
151
+ };
158
152
  return (React.createElement(React.Fragment, null,
159
153
  React.createElement(Static, { items: messages }, msg => (React.createElement(Box, { key: msg.id, flexDirection: "column", marginBottom: 1 },
160
154
  React.createElement(Text, { bold: true, color: msg.role === "user" ? "blue" : "green" },
@@ -175,10 +169,9 @@ const Chat = ({ config, model, initialPrompt, approvalMode }) => {
175
169
  setActiveModel(item.value);
176
170
  setIsSelectingModel(false);
177
171
  } })))) : (React.createElement(React.Fragment, null,
178
- !isStreaming && !pendingCommand && !isExecuting && (React.createElement(Box, { flexDirection: "row" },
179
- React.createElement(Text, { bold: true, color: "blue" }, "You: "),
180
- React.createElement(Text, null, input),
181
- React.createElement(Text, { dimColor: true }, " \u2588"))),
172
+ !isStreaming && !pendingCommand && !isExecuting && (React.createElement(Box, { borderStyle: "round", borderColor: "blue", paddingX: 1 },
173
+ React.createElement(Text, { bold: true, color: "blue" }, "\u276F "),
174
+ React.createElement(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, placeholder: "Type a message... (/models, /clear, /exit)" }))),
182
175
  pendingCommand && !isExecuting && (React.createElement(Box, { flexDirection: "row", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1 },
183
176
  React.createElement(Text, { color: "yellow" }, "Execute suggested command? "),
184
177
  React.createElement(Text, { bold: true }, pendingCommand),
@@ -215,8 +208,6 @@ export function startChat(config, model, prompt, approvalMode = "suggest", quiet
215
208
  fullResponse += text;
216
209
  }, () => {
217
210
  console.log("\n");
218
- // If full-auto is requested in quiet mode, we would parse bash blocks and execute them here.
219
- // For now, headless simply streams the raw text response for CI pipelines.
220
211
  if (approvalMode === "full-auto") {
221
212
  const bashMatch = fullResponse.match(/```(?:bash|sh)\n([\s\S]*?)\n```/);
222
213
  if (bashMatch) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swiftroutercli",
3
- "version": "4.0.3",
3
+ "version": "4.0.4",
4
4
  "description": "The official SwiftRouter Command Line Interface using React Ink Components",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -24,6 +24,7 @@
24
24
  "eventsource-parser": "^3.0.6",
25
25
  "ink": "^6.8.0",
26
26
  "ink-select-input": "^6.2.0",
27
+ "ink-text-input": "^6.0.0",
27
28
  "marked": "^15.0.12",
28
29
  "marked-terminal": "^7.3.0",
29
30
  "ora": "^5.4.1",
package/src/index.ts CHANGED
@@ -84,7 +84,7 @@ async function resolveModel(config: Config): Promise<string> {
84
84
  program
85
85
  .name("swiftroutercli")
86
86
  .description("CLI for SwiftRouter AI Gateway")
87
- .version("4.0.3");
87
+ .version("4.0.4");
88
88
 
89
89
  program
90
90
  .command("config")
package/src/ui/Chat.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { render, Box, Text, useInput, Static } from "ink";
3
+ import TextInput from "ink-text-input";
3
4
  import { streamChatCompletion, fetchModels } from "../api/client.js";
4
5
  import chalk from "chalk";
5
6
  import { Config, loadHistory, saveHistory } from "../config.js";
@@ -26,11 +27,13 @@ interface ChatProps {
26
27
  }
27
28
 
28
29
  const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt, approvalMode }) => {
29
- const [messages, setMessages] = useState<{ id: number; role: string; content: string }[]>([
30
- { id: 0, role: "user", content: initialPrompt },
31
- ]);
30
+ const hasInitialPrompt = initialPrompt.trim().length > 0;
31
+
32
+ const [messages, setMessages] = useState<{ id: number; role: string; content: string }[]>(
33
+ hasInitialPrompt ? [{ id: 0, role: "user", content: initialPrompt }] : []
34
+ );
32
35
  const [streamingContent, setStreamingContent] = useState("");
33
- const [isStreaming, setIsStreaming] = useState(true);
36
+ const [isStreaming, setIsStreaming] = useState(hasInitialPrompt);
34
37
  const [error, setError] = useState<Error | null>(null);
35
38
  const [input, setInput] = useState("");
36
39
  const [history, setHistory] = useState<string[]>([]);
@@ -82,7 +85,6 @@ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt, approvalMode
82
85
  { id: prev.length, role: "system", content: `Sandbox Output:\n${output || "Done"}` }
83
86
  ]);
84
87
  setIsExecuting(false);
85
- // Recursive automation could go here in V5
86
88
  });
87
89
  } else {
88
90
  setPendingCommand(command);
@@ -99,6 +101,7 @@ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt, approvalMode
99
101
  );
100
102
  }, [messages, isStreaming, config, activeModel]);
101
103
 
104
+ // Handle command confirmation and history navigation
102
105
  useInput((char: string, key: any) => {
103
106
  if (isSelectingModel) return;
104
107
  if (isStreaming) return;
@@ -121,46 +124,8 @@ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt, approvalMode
121
124
  return;
122
125
  }
123
126
 
124
- if (key.return) {
125
- const trimmed = input.trim();
126
- if (trimmed === "/exit" || trimmed === "/quit") {
127
- process.exit(0);
128
- }
129
- if (trimmed === "/clear") {
130
- setMessages([]);
131
- setInput("");
132
- setHistoryIndex(-1);
133
- return;
134
- }
135
- if (trimmed === "/models") {
136
- setIsSelectingModel(true);
137
- setIsLoadingModels(true);
138
- setInput("");
139
- fetchModels(config).then(models => {
140
- const options = models.map(m => ({ label: m.id || m.name, value: m.id || m.name }));
141
- setModelOptions(options.length ? options : [{ label: "mock-model", value: "mock-model" }]);
142
- setIsLoadingModels(false);
143
- }).catch(err => {
144
- setError(err);
145
- setIsSelectingModel(false);
146
- setIsLoadingModels(false);
147
- });
148
- return;
149
- }
150
-
151
- if (trimmed.length > 0) {
152
- const newPrompt = trimmed;
153
- setMessages((prev) => [...prev, { id: prev.length, role: "user", content: newPrompt }]);
154
-
155
- const newHistory = [...history, newPrompt];
156
- setHistory(newHistory);
157
- saveHistory(newHistory);
158
-
159
- setInput("");
160
- setHistoryIndex(-1);
161
- setIsStreaming(true);
162
- }
163
- } else if (key.upArrow) {
127
+ // History navigation with Up/Down arrows
128
+ if (key.upArrow) {
164
129
  if (history.length > 0 && historyIndex < history.length - 1) {
165
130
  const newIdx = historyIndex + 1;
166
131
  setHistoryIndex(newIdx);
@@ -175,13 +140,49 @@ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt, approvalMode
175
140
  setHistoryIndex(-1);
176
141
  setInput("");
177
142
  }
178
- } else if (key.backspace || key.delete) {
179
- setInput((prev) => prev.slice(0, -1));
180
- } else if (char) {
181
- setInput((prev) => prev + char);
182
143
  }
183
144
  });
184
145
 
146
+ const handleSubmit = (value: string) => {
147
+ const trimmed = value.trim();
148
+ if (!trimmed) return;
149
+
150
+ if (trimmed === "/exit" || trimmed === "/quit") {
151
+ process.exit(0);
152
+ }
153
+ if (trimmed === "/clear") {
154
+ setMessages([]);
155
+ setInput("");
156
+ setHistoryIndex(-1);
157
+ return;
158
+ }
159
+ if (trimmed === "/models") {
160
+ setIsSelectingModel(true);
161
+ setIsLoadingModels(true);
162
+ setInput("");
163
+ fetchModels(config).then(models => {
164
+ const options = models.map(m => ({ label: m.id || m.name, value: m.id || m.name }));
165
+ setModelOptions(options.length ? options : [{ label: "mock-model", value: "mock-model" }]);
166
+ setIsLoadingModels(false);
167
+ }).catch(err => {
168
+ setError(err);
169
+ setIsSelectingModel(false);
170
+ setIsLoadingModels(false);
171
+ });
172
+ return;
173
+ }
174
+
175
+ setMessages((prev) => [...prev, { id: prev.length, role: "user", content: trimmed }]);
176
+
177
+ const newHistory = [...history, trimmed];
178
+ setHistory(newHistory);
179
+ saveHistory(newHistory);
180
+
181
+ setInput("");
182
+ setHistoryIndex(-1);
183
+ setIsStreaming(true);
184
+ };
185
+
185
186
  return (
186
187
  <>
187
188
  <Static items={messages}>
@@ -224,10 +225,15 @@ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt, approvalMode
224
225
  ) : (
225
226
  <>
226
227
  {!isStreaming && !pendingCommand && !isExecuting && (
227
- <Box flexDirection="row">
228
- <Text bold color="blue">You: </Text>
229
- <Text>{input}</Text>
230
- <Text dimColor> █</Text>
228
+ <Box borderStyle="round" borderColor="blue" paddingX={1}>
229
+ <Text bold color="blue">❯ </Text>
230
+ {/* @ts-ignore */}
231
+ <TextInput
232
+ value={input}
233
+ onChange={setInput}
234
+ onSubmit={handleSubmit}
235
+ placeholder="Type a message... (/models, /clear, /exit)"
236
+ />
231
237
  </Box>
232
238
  )}
233
239
 
@@ -293,8 +299,6 @@ export function startChat(config: Config, model: string, prompt: string, approva
293
299
  () => {
294
300
  console.log("\n");
295
301
 
296
- // If full-auto is requested in quiet mode, we would parse bash blocks and execute them here.
297
- // For now, headless simply streams the raw text response for CI pipelines.
298
302
  if (approvalMode === "full-auto") {
299
303
  const bashMatch = fullResponse.match(/```(?:bash|sh)\n([\s\S]*?)\n```/);
300
304
  if (bashMatch) {