swiftroutercli 1.0.0

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.
@@ -0,0 +1,63 @@
1
+ import { createParser } from "eventsource-parser";
2
+ export async function fetchModels(config) {
3
+ const url = `${config.baseUrl.replace(/\/$/, "")}/v1/models`;
4
+ const response = await fetch(url, {
5
+ headers: {
6
+ Authorization: `Bearer ${config.apiKey}`,
7
+ },
8
+ });
9
+ if (!response.ok) {
10
+ throw new Error(`Failed to fetch models: ${response.statusText}`);
11
+ }
12
+ const data = await response.json();
13
+ return data.data || [];
14
+ }
15
+ export async function streamChatCompletion(config, model, prompt, onData, onComplete, onError) {
16
+ const url = `${config.baseUrl.replace(/\/$/, "")}/v1/chat/completions`;
17
+ try {
18
+ const response = await fetch(url, {
19
+ method: "POST",
20
+ headers: {
21
+ "Content-Type": "application/json",
22
+ Authorization: `Bearer ${config.apiKey}`,
23
+ },
24
+ body: JSON.stringify({
25
+ model,
26
+ messages: [{ role: "user", content: prompt }],
27
+ stream: true,
28
+ }),
29
+ });
30
+ if (!response.ok) {
31
+ const text = await response.text();
32
+ return onError(new Error(`Chat failed: ${response.status} ${text}`));
33
+ }
34
+ const parser = createParser({
35
+ onEvent: (event) => {
36
+ if (event.type === "event") {
37
+ if (event.data === "[DONE]") {
38
+ return;
39
+ }
40
+ try {
41
+ const parsed = JSON.parse(event.data);
42
+ const content = parsed.choices[0]?.delta?.content;
43
+ if (content) {
44
+ onData(content);
45
+ }
46
+ }
47
+ catch (e) {
48
+ // ignore parsing error
49
+ }
50
+ }
51
+ }
52
+ });
53
+ if (response.body) {
54
+ for await (const chunk of response.body) {
55
+ parser.feed(chunk.toString());
56
+ }
57
+ }
58
+ onComplete();
59
+ }
60
+ catch (err) {
61
+ onError(err);
62
+ }
63
+ }
package/dist/config.js ADDED
@@ -0,0 +1,23 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ const CONFIG_FILE = path.join(os.homedir(), ".swiftrouter-cli", "config.json");
5
+ export function loadConfig() {
6
+ try {
7
+ if (!fs.existsSync(CONFIG_FILE)) {
8
+ return null;
9
+ }
10
+ const data = fs.readFileSync(CONFIG_FILE, "utf-8");
11
+ return JSON.parse(data);
12
+ }
13
+ catch (error) {
14
+ return null;
15
+ }
16
+ }
17
+ export function saveConfig(config) {
18
+ const dir = path.dirname(CONFIG_FILE);
19
+ if (!fs.existsSync(dir)) {
20
+ fs.mkdirSync(dir, { recursive: true });
21
+ }
22
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
23
+ }
package/dist/index.js ADDED
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import * as readline from "readline/promises";
4
+ import { loadConfig, saveConfig } from "./config.js";
5
+ import { fetchModels } from "./api/client.js";
6
+ import { startChat } from "./ui/Chat.js";
7
+ const program = new Command();
8
+ async function ensureConfig() {
9
+ const existingConfig = loadConfig();
10
+ if (existingConfig && existingConfig.apiKey && existingConfig.baseUrl) {
11
+ return existingConfig;
12
+ }
13
+ console.log("🌊 Welcome to SwiftRouterCLI!");
14
+ console.log("It looks like this is your first time. Let's get you set up.\n");
15
+ const rl = readline.createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout,
18
+ });
19
+ const apiKey = await rl.question("Please enter your SwiftRouter API Key: ");
20
+ const baseUrlInput = await rl.question("Please enter your SwiftRouter Base URL (default: http://localhost:8080): ");
21
+ rl.close();
22
+ const config = {
23
+ apiKey: apiKey.trim(),
24
+ baseUrl: baseUrlInput.trim() || "http://localhost:8080",
25
+ };
26
+ saveConfig(config);
27
+ console.log("\nāœ… Configuration saved securely! Starting your session...\n");
28
+ return config;
29
+ }
30
+ program
31
+ .name("swiftrouter")
32
+ .description("CLI for SwiftRouter AI Gateway")
33
+ .version("1.0.0");
34
+ program
35
+ .command("config")
36
+ .description("Manually configure the CLI with your SwiftRouter API Key and Base URL")
37
+ .option("-k, --set-api-key <key>", "Set your SwiftRouter API key")
38
+ .option("-u, --set-base-url <url>", "Set the SwiftRouter API Base URL")
39
+ .action((options) => {
40
+ let config = loadConfig() || { apiKey: "", baseUrl: "" };
41
+ if (options.setApiKey)
42
+ config.apiKey = options.setApiKey;
43
+ if (options.setBaseUrl)
44
+ config.baseUrl = options.setBaseUrl;
45
+ if (!config.apiKey || !config.baseUrl) {
46
+ console.error("Please provide both an API Key and a Base URL.");
47
+ console.log("Usage: swiftrouter config --set-api-key <KEY> --set-base-url <URL>");
48
+ process.exit(1);
49
+ }
50
+ saveConfig(config);
51
+ console.log("āœ… Configuration saved successfully!");
52
+ });
53
+ program
54
+ .command("models")
55
+ .description("List available models from SwiftRouter")
56
+ .action(async () => {
57
+ const config = await ensureConfig();
58
+ try {
59
+ console.log("Fetching models...");
60
+ const models = await fetchModels(config);
61
+ console.log(`\nAvailable Models (${models.length}):`);
62
+ models.forEach((m) => console.log(` - ${m.id}`));
63
+ }
64
+ catch (e) {
65
+ console.error("Failed to fetch models:", e.message);
66
+ }
67
+ });
68
+ program
69
+ .command("chat")
70
+ .description("Start an interactive chat session")
71
+ .argument("[prompt]", "Initial prompt to start the chat")
72
+ .option("-m, --model <model>", "Model to use", "gpt-4o-mini")
73
+ .action(async (prompt, options) => {
74
+ const config = await ensureConfig();
75
+ if (!prompt) {
76
+ console.log("Starting empty chat session...");
77
+ startChat(config, options.model, "");
78
+ }
79
+ else {
80
+ startChat(config, options.model, prompt);
81
+ }
82
+ });
83
+ program.parse();
@@ -0,0 +1,75 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { render, Box, Text, useInput } from "ink";
3
+ import { streamChatCompletion } from "../api/client.js";
4
+ const Chat = ({ config, model, initialPrompt }) => {
5
+ const [messages, setMessages] = useState([
6
+ { role: "user", content: initialPrompt },
7
+ ]);
8
+ const [streamingContent, setStreamingContent] = useState("");
9
+ const [isStreaming, setIsStreaming] = useState(true);
10
+ const [error, setError] = useState(null);
11
+ const [input, setInput] = useState("");
12
+ useEffect(() => {
13
+ if (!isStreaming)
14
+ return;
15
+ const lastUserMessage = messages[messages.length - 1];
16
+ if (lastUserMessage.role !== "user")
17
+ return;
18
+ streamChatCompletion(config, model, lastUserMessage.content, (text) => setStreamingContent((prev) => prev + text), () => {
19
+ setMessages((prev) => [
20
+ ...prev,
21
+ { role: "assistant", content: streamingContent },
22
+ ]);
23
+ setStreamingContent("");
24
+ setIsStreaming(false);
25
+ }, (err) => {
26
+ setError(err);
27
+ setIsStreaming(false);
28
+ });
29
+ }, [messages, isStreaming, config, model]);
30
+ useInput((char, key) => {
31
+ if (isStreaming)
32
+ return;
33
+ if (key.return) {
34
+ if (input.trim().length > 0) {
35
+ setMessages((prev) => [...prev, { role: "user", content: input }]);
36
+ setInput("");
37
+ setIsStreaming(true);
38
+ }
39
+ }
40
+ else if (key.backspace || key.delete) {
41
+ setInput((prev) => prev.slice(0, -1));
42
+ }
43
+ else {
44
+ setInput((prev) => prev + char);
45
+ }
46
+ });
47
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
48
+ React.createElement(Box, { marginBottom: 1 },
49
+ React.createElement(Text, { bold: true, color: "cyan" }, "SwiftRouter CLI"),
50
+ React.createElement(Text, { dimColor: true },
51
+ " - Chatting with ",
52
+ model)),
53
+ messages.map((msg, idx) => (React.createElement(Box, { key: idx, flexDirection: "column", marginBottom: 1 },
54
+ React.createElement(Text, { bold: true, color: msg.role === "user" ? "blue" : "green" },
55
+ msg.role === "user" ? "You" : "Assistant",
56
+ ":"),
57
+ React.createElement(Text, null, msg.content)))),
58
+ isStreaming && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
59
+ React.createElement(Text, { bold: true, color: "green" }, "Assistant:"),
60
+ React.createElement(Text, null, streamingContent),
61
+ React.createElement(Text, { dimColor: true }, "..."))),
62
+ error && (React.createElement(Box, null,
63
+ React.createElement(Text, { color: "red" },
64
+ "Error: ",
65
+ error.message))),
66
+ !isStreaming && (React.createElement(Box, null,
67
+ React.createElement(Text, { bold: true, color: "blue" },
68
+ "You:",
69
+ " "),
70
+ React.createElement(Text, null, input),
71
+ React.createElement(Text, { dimColor: true }, " \u2588")))));
72
+ };
73
+ export function startChat(config, model, prompt) {
74
+ render(React.createElement(Chat, { config: config, model: model, initialPrompt: prompt }));
75
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "swiftroutercli",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "swiftrouter": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "test": "echo \"Error: no test specified\" && exit 1"
14
+ },
15
+ "keywords": [],
16
+ "author": "",
17
+ "license": "ISC",
18
+ "dependencies": {
19
+ "commander": "^14.0.3",
20
+ "dotenv": "^17.3.1",
21
+ "eventsource-parser": "^3.0.6",
22
+ "ink": "^6.8.0",
23
+ "react": "^19.2.4",
24
+ "undici": "^7.22.0",
25
+ "zod": "^4.3.6"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^25.3.0",
29
+ "@types/node-fetch": "^2.6.13",
30
+ "@types/react": "^19.2.14",
31
+ "ts-node": "^10.9.2",
32
+ "typescript": "^5.9.3"
33
+ }
34
+ }
@@ -0,0 +1,77 @@
1
+ import { Config, loadConfig } from "../config.js";
2
+ import { createParser } from "eventsource-parser";
3
+
4
+ export async function fetchModels(config: Config): Promise<any[]> {
5
+ const url = `${config.baseUrl.replace(/\/$/, "")}/v1/models`;
6
+ const response = await fetch(url, {
7
+ headers: {
8
+ Authorization: `Bearer ${config.apiKey}`,
9
+ },
10
+ });
11
+
12
+ if (!response.ok) {
13
+ throw new Error(`Failed to fetch models: ${response.statusText}`);
14
+ }
15
+
16
+ const data = await response.json();
17
+ return (data as any).data || [];
18
+ }
19
+
20
+ export async function streamChatCompletion(
21
+ config: Config,
22
+ model: string,
23
+ prompt: string,
24
+ onData: (text: string) => void,
25
+ onComplete: () => void,
26
+ onError: (error: Error) => void
27
+ ) {
28
+ const url = `${config.baseUrl.replace(/\/$/, "")}/v1/chat/completions`;
29
+
30
+ try {
31
+ const response = await fetch(url, {
32
+ method: "POST",
33
+ headers: {
34
+ "Content-Type": "application/json",
35
+ Authorization: `Bearer ${config.apiKey}`,
36
+ },
37
+ body: JSON.stringify({
38
+ model,
39
+ messages: [{ role: "user", content: prompt }],
40
+ stream: true,
41
+ }),
42
+ });
43
+
44
+ if (!response.ok) {
45
+ const text = await response.text();
46
+ return onError(new Error(`Chat failed: ${response.status} ${text}`));
47
+ }
48
+
49
+ const parser = createParser({
50
+ onEvent: (event: any) => {
51
+ if (event.type === "event") {
52
+ if (event.data === "[DONE]") {
53
+ return;
54
+ }
55
+ try {
56
+ const parsed = JSON.parse(event.data);
57
+ const content = parsed.choices[0]?.delta?.content;
58
+ if (content) {
59
+ onData(content);
60
+ }
61
+ } catch (e) {
62
+ // ignore parsing error
63
+ }
64
+ }
65
+ }
66
+ });
67
+
68
+ if (response.body) {
69
+ for await (const chunk of response.body) {
70
+ parser.feed(chunk.toString());
71
+ }
72
+ }
73
+ onComplete();
74
+ } catch (err: any) {
75
+ onError(err);
76
+ }
77
+ }
package/src/config.ts ADDED
@@ -0,0 +1,30 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+
5
+ export interface Config {
6
+ apiKey: string;
7
+ baseUrl: string;
8
+ }
9
+
10
+ const CONFIG_FILE = path.join(os.homedir(), ".swiftrouter-cli", "config.json");
11
+
12
+ export function loadConfig(): Config | null {
13
+ try {
14
+ if (!fs.existsSync(CONFIG_FILE)) {
15
+ return null;
16
+ }
17
+ const data = fs.readFileSync(CONFIG_FILE, "utf-8");
18
+ return JSON.parse(data) as Config;
19
+ } catch (error) {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ export function saveConfig(config: Config) {
25
+ const dir = path.dirname(CONFIG_FILE);
26
+ if (!fs.existsSync(dir)) {
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ }
29
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import * as readline from "readline/promises";
4
+ import { loadConfig, saveConfig, Config } from "./config.js";
5
+ import { fetchModels } from "./api/client.js";
6
+ import { startChat } from "./ui/Chat.js";
7
+
8
+ const program = new Command();
9
+
10
+ async function ensureConfig(): Promise<Config> {
11
+ const existingConfig = loadConfig();
12
+ if (existingConfig && existingConfig.apiKey && existingConfig.baseUrl) {
13
+ return existingConfig;
14
+ }
15
+
16
+ console.log("🌊 Welcome to SwiftRouterCLI!");
17
+ console.log("It looks like this is your first time. Let's get you set up.\n");
18
+
19
+ const rl = readline.createInterface({
20
+ input: process.stdin,
21
+ output: process.stdout,
22
+ });
23
+
24
+ const apiKey = await rl.question("Please enter your SwiftRouter API Key: ");
25
+ const baseUrlInput = await rl.question(
26
+ "Please enter your SwiftRouter Base URL (default: http://localhost:8080): "
27
+ );
28
+
29
+ rl.close();
30
+
31
+ const config: Config = {
32
+ apiKey: apiKey.trim(),
33
+ baseUrl: baseUrlInput.trim() || "http://localhost:8080",
34
+ };
35
+
36
+ saveConfig(config);
37
+ console.log("\nāœ… Configuration saved securely! Starting your session...\n");
38
+
39
+ return config;
40
+ }
41
+
42
+ program
43
+ .name("swiftrouter")
44
+ .description("CLI for SwiftRouter AI Gateway")
45
+ .version("1.0.0");
46
+
47
+ program
48
+ .command("config")
49
+ .description("Manually configure the CLI with your SwiftRouter API Key and Base URL")
50
+ .option("-k, --set-api-key <key>", "Set your SwiftRouter API key")
51
+ .option("-u, --set-base-url <url>", "Set the SwiftRouter API Base URL")
52
+ .action((options) => {
53
+ let config = loadConfig() || { apiKey: "", baseUrl: "" };
54
+
55
+ if (options.setApiKey) config.apiKey = options.setApiKey;
56
+ if (options.setBaseUrl) config.baseUrl = options.setBaseUrl;
57
+
58
+ if (!config.apiKey || !config.baseUrl) {
59
+ console.error("Please provide both an API Key and a Base URL.");
60
+ console.log("Usage: swiftrouter config --set-api-key <KEY> --set-base-url <URL>");
61
+ process.exit(1);
62
+ }
63
+
64
+ saveConfig(config);
65
+ console.log("āœ… Configuration saved successfully!");
66
+ });
67
+
68
+ program
69
+ .command("models")
70
+ .description("List available models from SwiftRouter")
71
+ .action(async () => {
72
+ const config = await ensureConfig();
73
+
74
+ try {
75
+ console.log("Fetching models...");
76
+ const models = await fetchModels(config);
77
+ console.log(`\nAvailable Models (${models.length}):`);
78
+ models.forEach((m: any) => console.log(` - ${m.id}`));
79
+ } catch (e: any) {
80
+ console.error("Failed to fetch models:", e.message);
81
+ }
82
+ });
83
+
84
+ program
85
+ .command("chat")
86
+ .description("Start an interactive chat session")
87
+ .argument("[prompt]", "Initial prompt to start the chat")
88
+ .option("-m, --model <model>", "Model to use", "gpt-4o-mini")
89
+ .action(async (prompt, options) => {
90
+ const config = await ensureConfig();
91
+
92
+ if (!prompt) {
93
+ console.log("Starting empty chat session...");
94
+ startChat(config, options.model, "");
95
+ } else {
96
+ startChat(config, options.model, prompt);
97
+ }
98
+ });
99
+
100
+ program.parse();
@@ -0,0 +1,112 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import { render, Box, Text, useInput } from "ink";
3
+ import { streamChatCompletion } from "../api/client.js";
4
+ import { Config } from "../config.js";
5
+
6
+ interface ChatProps {
7
+ config: Config;
8
+ model: string;
9
+ initialPrompt: string;
10
+ }
11
+
12
+ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt }) => {
13
+ const [messages, setMessages] = useState<{ role: string; content: string }[]>([
14
+ { role: "user", content: initialPrompt },
15
+ ]);
16
+ const [streamingContent, setStreamingContent] = useState("");
17
+ const [isStreaming, setIsStreaming] = useState(true);
18
+ const [error, setError] = useState<Error | null>(null);
19
+ const [input, setInput] = useState("");
20
+
21
+ useEffect(() => {
22
+ if (!isStreaming) return;
23
+
24
+ const lastUserMessage = messages[messages.length - 1];
25
+ if (lastUserMessage.role !== "user") return;
26
+
27
+ streamChatCompletion(
28
+ config,
29
+ model,
30
+ lastUserMessage.content,
31
+ (text) => setStreamingContent((prev) => prev + text),
32
+ () => {
33
+ setMessages((prev) => [
34
+ ...prev,
35
+ { role: "assistant", content: streamingContent },
36
+ ]);
37
+ setStreamingContent("");
38
+ setIsStreaming(false);
39
+ },
40
+ (err) => {
41
+ setError(err);
42
+ setIsStreaming(false);
43
+ }
44
+ );
45
+ }, [messages, isStreaming, config, model]);
46
+
47
+ useInput((char: string, key: any) => {
48
+ if (isStreaming) return;
49
+
50
+ if (key.return) {
51
+ if (input.trim().length > 0) {
52
+ setMessages((prev) => [...prev, { role: "user", content: input }]);
53
+ setInput("");
54
+ setIsStreaming(true);
55
+ }
56
+ } else if (key.backspace || key.delete) {
57
+ setInput((prev) => prev.slice(0, -1));
58
+ } else {
59
+ setInput((prev) => prev + char);
60
+ }
61
+ });
62
+
63
+ return (
64
+ <Box flexDirection="column" padding={1}>
65
+ <Box marginBottom={1}>
66
+ <Text bold color="cyan">
67
+ SwiftRouter CLI
68
+ </Text>
69
+ <Text dimColor> - Chatting with {model}</Text>
70
+ </Box>
71
+
72
+ {messages.map((msg, idx) => (
73
+ <Box key={idx} flexDirection="column" marginBottom={1}>
74
+ <Text bold color={msg.role === "user" ? "blue" : "green"}>
75
+ {msg.role === "user" ? "You" : "Assistant"}:
76
+ </Text>
77
+ <Text>{msg.content}</Text>
78
+ </Box>
79
+ ))}
80
+
81
+ {isStreaming && (
82
+ <Box flexDirection="column" marginBottom={1}>
83
+ <Text bold color="green">
84
+ Assistant:
85
+ </Text>
86
+ <Text>{streamingContent}</Text>
87
+ <Text dimColor>...</Text>
88
+ </Box>
89
+ )}
90
+
91
+ {error && (
92
+ <Box>
93
+ <Text color="red">Error: {error.message}</Text>
94
+ </Box>
95
+ )}
96
+
97
+ {!isStreaming && (
98
+ <Box>
99
+ <Text bold color="blue">
100
+ You:{" "}
101
+ </Text>
102
+ <Text>{input}</Text>
103
+ <Text dimColor> ā–ˆ</Text>
104
+ </Box>
105
+ )}
106
+ </Box>
107
+ );
108
+ };
109
+
110
+ export function startChat(config: Config, model: string, prompt: string) {
111
+ render(<Chat config={config} model={model} initialPrompt={prompt} />);
112
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "resolveJsonModule": true,
13
+ "jsx": "react"
14
+ },
15
+ "include": [
16
+ "src/**/*"
17
+ ],
18
+ "exclude": [
19
+ "node_modules",
20
+ "dist"
21
+ ]
22
+ }