swiftroutercli 3.0.0 → 4.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.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Upon first launch, the CLI asks for configuration parameters. You can also confi
23
23
  ## Usage
24
24
  Simply run the CLI anywhere on your system:
25
25
  ```bash
26
- swiftrouter chat
26
+ swiftroutercli chat
27
27
  ```
28
28
 
29
29
  - Type `/models` to select models interactively.
@@ -31,10 +31,10 @@ swiftrouter chat
31
31
  - Type `/exit` to quit.
32
32
 
33
33
  ### Additional Commands
34
- - `swiftrouter config --set-api-key <KEY> --set-base-url <URL>`: Manually configure CLI
35
- - `swiftrouter models`: List available models natively
36
- - `swiftrouter status`: Check authentication and connection status
37
- - `swiftrouter logout`: Clear local configuration securely
34
+ - `swiftroutercli config --set-api-key <KEY> --set-base-url <URL>`: Manually configure CLI
35
+ - `swiftroutercli models`: List available models natively
36
+ - `swiftroutercli status`: Check authentication and connection status
37
+ - `swiftroutercli logout`: Clear local configuration securely
38
38
 
39
39
  ## Built With
40
40
  - `ink`
@@ -1,10 +1,44 @@
1
1
  import { createParser } from "eventsource-parser";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
+ import os from "os";
5
+ function getHierarchicalAgentsContext() {
6
+ const cwd = process.cwd();
7
+ const pathsToRead = [];
8
+ // 1. Global
9
+ pathsToRead.push(path.join(os.homedir(), ".swiftrouter-cli", "AGENTS.md"));
10
+ // 2. Repo Root
11
+ let currentDir = cwd;
12
+ let repoRoot = null;
13
+ while (currentDir !== path.parse(currentDir).root) {
14
+ if (fs.existsSync(path.join(currentDir, ".git"))) {
15
+ repoRoot = currentDir;
16
+ break;
17
+ }
18
+ currentDir = path.dirname(currentDir);
19
+ }
20
+ if (repoRoot) {
21
+ pathsToRead.push(path.join(repoRoot, "AGENTS.md"));
22
+ }
23
+ // 3. Local CWD
24
+ pathsToRead.push(path.join(cwd, "AGENTS.md"));
25
+ // Deduplicate
26
+ const uniquePaths = [...new Set(pathsToRead)];
27
+ let agentsContent = "";
28
+ for (const p of uniquePaths) {
29
+ if (fs.existsSync(p)) {
30
+ try {
31
+ agentsContent += `\n--- Context from ${p} ---\n` + fs.readFileSync(p, "utf-8") + "\n";
32
+ }
33
+ catch (e) { }
34
+ }
35
+ }
36
+ return agentsContent;
37
+ }
4
38
  function getWorkspaceContext() {
5
39
  const cwd = process.cwd();
6
40
  let context = `Current Workspace: ${cwd}\n`;
7
- const filesToRead = ["README.md", "AGENTS.md", "package.json"];
41
+ const filesToRead = ["README.md", "package.json"];
8
42
  for (const file of filesToRead) {
9
43
  const filePath = path.join(cwd, file);
10
44
  if (fs.existsSync(filePath)) {
@@ -15,6 +49,8 @@ function getWorkspaceContext() {
15
49
  catch (e) { }
16
50
  }
17
51
  }
52
+ // Append Hierarchical AGENTS.md
53
+ context += getHierarchicalAgentsContext();
18
54
  return context;
19
55
  }
20
56
  export async function fetchModels(config) {
package/dist/index.js CHANGED
@@ -46,9 +46,9 @@ async function ensureConfig() {
46
46
  return config;
47
47
  }
48
48
  program
49
- .name("swiftrouter")
49
+ .name("swiftroutercli")
50
50
  .description("CLI for SwiftRouter AI Gateway")
51
- .version("1.0.1");
51
+ .version("4.0.0");
52
52
  program
53
53
  .command("config")
54
54
  .description("Manually configure the CLI with your SwiftRouter API Key and Base URL")
@@ -96,13 +96,15 @@ program
96
96
  .description("Start an interactive chat session")
97
97
  .argument("[prompt]", "Initial prompt to start the chat")
98
98
  .option("-m, --model <model>", "Model to use", DEFAULT_MODEL)
99
+ .option("-a, --approval-mode <mode>", "AI assistant's permission mode (suggest, auto-edit, full-auto)", "suggest")
100
+ .option("-q, --quiet", "Run in headless CI/CD mode without interactive TUI rendering", false)
99
101
  .action(async (prompt, options) => {
100
102
  const config = await ensureConfig();
101
103
  if (!prompt) {
102
- startChat(config, options.model, "");
104
+ startChat(config, options.model, "", options.approvalMode, options.quiet);
103
105
  }
104
106
  else {
105
- startChat(config, options.model, prompt);
107
+ startChat(config, options.model, prompt, options.approvalMode, options.quiet);
106
108
  }
107
109
  });
108
110
  program
@@ -140,4 +142,14 @@ program
140
142
  console.log(chalk.gray("You were not logged in."));
141
143
  }
142
144
  });
145
+ // Default action: if no subcommand is given, launch chat (just like `codex`)
146
+ program
147
+ .argument("[prompt]", "Initial prompt to start chat directly")
148
+ .option("-m, --model <model>", "Model to use", DEFAULT_MODEL)
149
+ .option("-a, --approval-mode <mode>", "AI assistant's permission mode (suggest, auto-edit, full-auto)", "suggest")
150
+ .option("-q, --quiet", "Run in headless CI/CD mode without interactive TUI rendering", false)
151
+ .action(async (prompt, options) => {
152
+ const config = await ensureConfig();
153
+ startChat(config, options.model, prompt || "", options.approvalMode, options.quiet);
154
+ });
143
155
  program.parse();
package/dist/ui/Chat.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { render, Box, Text, useInput, Static } from "ink";
3
3
  import { streamChatCompletion, fetchModels } from "../api/client.js";
4
+ import chalk from "chalk";
4
5
  import { loadHistory, saveHistory } from "../config.js";
5
6
  import { marked } from "marked";
6
7
  import TerminalRenderer from "marked-terminal";
@@ -10,7 +11,7 @@ marked.setOptions({
10
11
  // @ts-ignore
11
12
  renderer: new TerminalRenderer()
12
13
  });
13
- const Chat = ({ config, model, initialPrompt }) => {
14
+ const Chat = ({ config, model, initialPrompt, approvalMode }) => {
14
15
  const [messages, setMessages] = useState([
15
16
  { id: 0, role: "user", content: initialPrompt },
16
17
  ]);
@@ -40,7 +41,27 @@ const Chat = ({ config, model, initialPrompt }) => {
40
41
  ]);
41
42
  const bashMatch = streamingContent.match(/```(?:bash|sh)\n([\s\S]*?)\n```/);
42
43
  if (bashMatch) {
43
- setPendingCommand(bashMatch[1].trim());
44
+ const command = bashMatch[1].trim();
45
+ if (approvalMode === "full-auto") {
46
+ setIsExecuting(true);
47
+ const dockerCmd = `docker run --rm -v "${process.cwd()}":/workspace -w /workspace node:20 bash -c "${command.replace(/"/g, '\\"')}"`;
48
+ setMessages((prev) => [
49
+ ...prev,
50
+ { id: prev.length, role: "system", content: `[Full-Auto Sandboxed] Executing: ${command}` }
51
+ ]);
52
+ exec(dockerCmd, (err, stdout, stderr) => {
53
+ const output = err ? stderr : stdout;
54
+ setMessages((prev) => [
55
+ ...prev,
56
+ { id: prev.length, role: "system", content: `Sandbox Output:\n${output || "Done"}` }
57
+ ]);
58
+ setIsExecuting(false);
59
+ // Recursive automation could go here in V5
60
+ });
61
+ }
62
+ else {
63
+ setPendingCommand(command);
64
+ }
44
65
  }
45
66
  setStreamingContent("");
46
67
  setIsStreaming(false);
@@ -182,6 +203,44 @@ const Chat = ({ config, model, initialPrompt }) => {
182
203
  history.length,
183
204
  " Prompts Saved"))))));
184
205
  };
185
- export function startChat(config, model, prompt) {
186
- render(React.createElement(Chat, { config: config, model: model, initialPrompt: prompt }));
206
+ export function startChat(config, model, prompt, approvalMode = "suggest", quiet = false) {
207
+ if (quiet) {
208
+ if (!prompt) {
209
+ console.error(chalk.red("Error: --quiet requires an initial prompt to be passed (e.g. swiftroutercli chat -q \"hello\")"));
210
+ process.exit(1);
211
+ }
212
+ let fullResponse = "";
213
+ streamChatCompletion(config, model, [{ role: "user", content: prompt }], (text) => {
214
+ process.stdout.write(text);
215
+ fullResponse += text;
216
+ }, () => {
217
+ 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
+ if (approvalMode === "full-auto") {
221
+ const bashMatch = fullResponse.match(/```(?:bash|sh)\n([\s\S]*?)\n```/);
222
+ if (bashMatch) {
223
+ const command = bashMatch[1].trim();
224
+ console.log(chalk.yellow(`\n[Full-Auto] Executing headless command: ${command}`));
225
+ exec(command, { cwd: process.cwd() }, (err, stdout, stderr) => {
226
+ if (err) {
227
+ console.error(chalk.red(`Error: ${stderr}`));
228
+ process.exit(1);
229
+ }
230
+ else {
231
+ console.log(stdout);
232
+ process.exit(0);
233
+ }
234
+ });
235
+ return;
236
+ }
237
+ }
238
+ process.exit(0);
239
+ }, (err) => {
240
+ console.error(chalk.red(`\nError: ${err.message}`));
241
+ process.exit(1);
242
+ });
243
+ return;
244
+ }
245
+ render(React.createElement(Chat, { config: config, model: model, initialPrompt: prompt, approvalMode: approvalMode }));
187
246
  }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "swiftroutercli",
3
- "version": "3.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "The official SwiftRouter Command Line Interface using React Ink Components",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
- "swiftrouter": "./dist/index.js"
8
+ "swiftrouter": "./dist/index.js",
9
+ "swiftroutercli": "./dist/index.js"
9
10
  },
10
11
  "scripts": {
11
12
  "build": "tsc",
package/src/api/client.ts CHANGED
@@ -2,11 +2,50 @@ import { Config, loadConfig } from "../config.js";
2
2
  import { createParser } from "eventsource-parser";
3
3
  import fs from "fs";
4
4
  import path from "path";
5
+ import os from "os";
6
+
7
+ function getHierarchicalAgentsContext(): string {
8
+ const cwd = process.cwd();
9
+ const pathsToRead: string[] = [];
10
+
11
+ // 1. Global
12
+ pathsToRead.push(path.join(os.homedir(), ".swiftrouter-cli", "AGENTS.md"));
13
+
14
+ // 2. Repo Root
15
+ let currentDir = cwd;
16
+ let repoRoot: string | null = null;
17
+ while (currentDir !== path.parse(currentDir).root) {
18
+ if (fs.existsSync(path.join(currentDir, ".git"))) {
19
+ repoRoot = currentDir;
20
+ break;
21
+ }
22
+ currentDir = path.dirname(currentDir);
23
+ }
24
+ if (repoRoot) {
25
+ pathsToRead.push(path.join(repoRoot, "AGENTS.md"));
26
+ }
27
+
28
+ // 3. Local CWD
29
+ pathsToRead.push(path.join(cwd, "AGENTS.md"));
30
+
31
+ // Deduplicate
32
+ const uniquePaths = [...new Set(pathsToRead)];
33
+
34
+ let agentsContent = "";
35
+ for (const p of uniquePaths) {
36
+ if (fs.existsSync(p)) {
37
+ try {
38
+ agentsContent += `\n--- Context from ${p} ---\n` + fs.readFileSync(p, "utf-8") + "\n";
39
+ } catch (e) { }
40
+ }
41
+ }
42
+ return agentsContent;
43
+ }
5
44
 
6
45
  function getWorkspaceContext(): string {
7
46
  const cwd = process.cwd();
8
47
  let context = `Current Workspace: ${cwd}\n`;
9
- const filesToRead = ["README.md", "AGENTS.md", "package.json"];
48
+ const filesToRead = ["README.md", "package.json"];
10
49
  for (const file of filesToRead) {
11
50
  const filePath = path.join(cwd, file);
12
51
  if (fs.existsSync(filePath)) {
@@ -16,6 +55,10 @@ function getWorkspaceContext(): string {
16
55
  } catch (e) { }
17
56
  }
18
57
  }
58
+
59
+ // Append Hierarchical AGENTS.md
60
+ context += getHierarchicalAgentsContext();
61
+
19
62
  return context;
20
63
  }
21
64
 
package/src/index.ts CHANGED
@@ -59,9 +59,9 @@ async function ensureConfig(): Promise<Config> {
59
59
  }
60
60
 
61
61
  program
62
- .name("swiftrouter")
62
+ .name("swiftroutercli")
63
63
  .description("CLI for SwiftRouter AI Gateway")
64
- .version("1.0.1");
64
+ .version("4.0.0");
65
65
 
66
66
  program
67
67
  .command("config")
@@ -112,13 +112,15 @@ program
112
112
  .description("Start an interactive chat session")
113
113
  .argument("[prompt]", "Initial prompt to start the chat")
114
114
  .option("-m, --model <model>", "Model to use", DEFAULT_MODEL)
115
+ .option("-a, --approval-mode <mode>", "AI assistant's permission mode (suggest, auto-edit, full-auto)", "suggest")
116
+ .option("-q, --quiet", "Run in headless CI/CD mode without interactive TUI rendering", false)
115
117
  .action(async (prompt, options) => {
116
118
  const config = await ensureConfig();
117
119
 
118
120
  if (!prompt) {
119
- startChat(config, options.model, "");
121
+ startChat(config, options.model, "", options.approvalMode, options.quiet);
120
122
  } else {
121
- startChat(config, options.model, prompt);
123
+ startChat(config, options.model, prompt, options.approvalMode, options.quiet);
122
124
  }
123
125
  });
124
126
 
@@ -159,4 +161,15 @@ program
159
161
  }
160
162
  });
161
163
 
164
+ // Default action: if no subcommand is given, launch chat (just like `codex`)
165
+ program
166
+ .argument("[prompt]", "Initial prompt to start chat directly")
167
+ .option("-m, --model <model>", "Model to use", DEFAULT_MODEL)
168
+ .option("-a, --approval-mode <mode>", "AI assistant's permission mode (suggest, auto-edit, full-auto)", "suggest")
169
+ .option("-q, --quiet", "Run in headless CI/CD mode without interactive TUI rendering", false)
170
+ .action(async (prompt, options) => {
171
+ const config = await ensureConfig();
172
+ startChat(config, options.model, prompt || "", options.approvalMode, options.quiet);
173
+ });
174
+
162
175
  program.parse();
package/src/ui/Chat.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import React, { useState, useEffect } from "react";
2
2
  import { render, Box, Text, useInput, Static } from "ink";
3
3
  import { streamChatCompletion, fetchModels } from "../api/client.js";
4
+ import chalk from "chalk";
4
5
  import { Config, loadHistory, saveHistory } from "../config.js";
5
6
  import { marked } from "marked";
6
7
  import TerminalRenderer from "marked-terminal";
@@ -21,9 +22,10 @@ interface ChatProps {
21
22
  config: Config;
22
23
  model: string;
23
24
  initialPrompt: string;
25
+ approvalMode: string;
24
26
  }
25
27
 
26
- const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt }) => {
28
+ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt, approvalMode }) => {
27
29
  const [messages, setMessages] = useState<{ id: number; role: string; content: string }[]>([
28
30
  { id: 0, role: "user", content: initialPrompt },
29
31
  ]);
@@ -63,7 +65,28 @@ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt }) => {
63
65
 
64
66
  const bashMatch = streamingContent.match(/```(?:bash|sh)\n([\s\S]*?)\n```/);
65
67
  if (bashMatch) {
66
- setPendingCommand(bashMatch[1].trim());
68
+ const command = bashMatch[1].trim();
69
+ if (approvalMode === "full-auto") {
70
+ setIsExecuting(true);
71
+ const dockerCmd = `docker run --rm -v "${process.cwd()}":/workspace -w /workspace node:20 bash -c "${command.replace(/"/g, '\\"')}"`;
72
+
73
+ setMessages((prev) => [
74
+ ...prev,
75
+ { id: prev.length, role: "system", content: `[Full-Auto Sandboxed] Executing: ${command}` }
76
+ ]);
77
+
78
+ exec(dockerCmd, (err, stdout, stderr) => {
79
+ const output = err ? stderr : stdout;
80
+ setMessages((prev) => [
81
+ ...prev,
82
+ { id: prev.length, role: "system", content: `Sandbox Output:\n${output || "Done"}` }
83
+ ]);
84
+ setIsExecuting(false);
85
+ // Recursive automation could go here in V5
86
+ });
87
+ } else {
88
+ setPendingCommand(command);
89
+ }
67
90
  }
68
91
 
69
92
  setStreamingContent("");
@@ -251,6 +274,53 @@ const Chat: React.FC<ChatProps> = ({ config, model, initialPrompt }) => {
251
274
  );
252
275
  };
253
276
 
254
- export function startChat(config: Config, model: string, prompt: string) {
255
- render(<Chat config={config} model={model} initialPrompt={prompt} />);
277
+ export function startChat(config: Config, model: string, prompt: string, approvalMode: string = "suggest", quiet: boolean = false) {
278
+ if (quiet) {
279
+ if (!prompt) {
280
+ console.error(chalk.red("Error: --quiet requires an initial prompt to be passed (e.g. swiftroutercli chat -q \"hello\")"));
281
+ process.exit(1);
282
+ }
283
+
284
+ let fullResponse = "";
285
+ streamChatCompletion(
286
+ config,
287
+ model,
288
+ [{ role: "user", content: prompt }],
289
+ (text) => {
290
+ process.stdout.write(text);
291
+ fullResponse += text;
292
+ },
293
+ () => {
294
+ console.log("\n");
295
+
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
+ if (approvalMode === "full-auto") {
299
+ const bashMatch = fullResponse.match(/```(?:bash|sh)\n([\s\S]*?)\n```/);
300
+ if (bashMatch) {
301
+ const command = bashMatch[1].trim();
302
+ console.log(chalk.yellow(`\n[Full-Auto] Executing headless command: ${command}`));
303
+ exec(command, { cwd: process.cwd() }, (err, stdout, stderr) => {
304
+ if (err) {
305
+ console.error(chalk.red(`Error: ${stderr}`));
306
+ process.exit(1);
307
+ } else {
308
+ console.log(stdout);
309
+ process.exit(0);
310
+ }
311
+ });
312
+ return;
313
+ }
314
+ }
315
+ process.exit(0);
316
+ },
317
+ (err) => {
318
+ console.error(chalk.red(`\nError: ${err.message}`));
319
+ process.exit(1);
320
+ }
321
+ );
322
+ return;
323
+ }
324
+
325
+ render(<Chat config={config} model={model} initialPrompt={prompt} approvalMode={approvalMode} />);
256
326
  }