swiftroutercli 3.0.1 → 4.0.1

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.
@@ -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
@@ -19,16 +19,23 @@ async function ensureConfig() {
19
19
  if (existingConfig && existingConfig.apiKey && existingConfig.baseUrl) {
20
20
  return existingConfig;
21
21
  }
22
+ let logoRendered = false;
22
23
  try {
23
24
  const logoPath = path.join(__dirname, "..", "assets", "logo.png");
24
25
  if (fs.existsSync(logoPath)) {
25
- console.log(await terminalImage.file(logoPath, { height: "25%" }));
26
+ const smallLogo = await terminalImage.file(logoPath, { height: 3 });
27
+ // Print logo inline with welcome text
28
+ process.stdout.write(smallLogo.trimEnd() + " ");
29
+ console.log(chalk.cyan.bold("Welcome to SwiftRouterCLI!"));
30
+ logoRendered = true;
26
31
  }
27
32
  }
28
33
  catch (e) {
29
34
  // Ignore if logo cannot be rendered
30
35
  }
31
- console.log(chalk.cyan.bold("\n🌊 Welcome to SwiftRouterCLI!"));
36
+ if (!logoRendered) {
37
+ console.log(chalk.cyan.bold("\n🌊 Welcome to SwiftRouterCLI!"));
38
+ }
32
39
  console.log(chalk.gray("It looks like this is your first time. Let's get you set up.\n"));
33
40
  const rl = readline.createInterface({
34
41
  input: process.stdin,
@@ -48,7 +55,7 @@ async function ensureConfig() {
48
55
  program
49
56
  .name("swiftroutercli")
50
57
  .description("CLI for SwiftRouter AI Gateway")
51
- .version("3.0.1");
58
+ .version("4.0.1");
52
59
  program
53
60
  .command("config")
54
61
  .description("Manually configure the CLI with your SwiftRouter API Key and Base URL")
@@ -96,13 +103,15 @@ program
96
103
  .description("Start an interactive chat session")
97
104
  .argument("[prompt]", "Initial prompt to start the chat")
98
105
  .option("-m, --model <model>", "Model to use", DEFAULT_MODEL)
106
+ .option("-a, --approval-mode <mode>", "AI assistant's permission mode (suggest, auto-edit, full-auto)", "suggest")
107
+ .option("-q, --quiet", "Run in headless CI/CD mode without interactive TUI rendering", false)
99
108
  .action(async (prompt, options) => {
100
109
  const config = await ensureConfig();
101
110
  if (!prompt) {
102
- startChat(config, options.model, "");
111
+ startChat(config, options.model, "", options.approvalMode, options.quiet);
103
112
  }
104
113
  else {
105
- startChat(config, options.model, prompt);
114
+ startChat(config, options.model, prompt, options.approvalMode, options.quiet);
106
115
  }
107
116
  });
108
117
  program
@@ -140,4 +149,14 @@ program
140
149
  console.log(chalk.gray("You were not logged in."));
141
150
  }
142
151
  });
152
+ // Default action: if no subcommand is given, launch chat (just like `codex`)
153
+ program
154
+ .argument("[prompt]", "Initial prompt to start chat directly")
155
+ .option("-m, --model <model>", "Model to use", DEFAULT_MODEL)
156
+ .option("-a, --approval-mode <mode>", "AI assistant's permission mode (suggest, auto-edit, full-auto)", "suggest")
157
+ .option("-q, --quiet", "Run in headless CI/CD mode without interactive TUI rendering", false)
158
+ .action(async (prompt, options) => {
159
+ const config = await ensureConfig();
160
+ startChat(config, options.model, prompt || "", options.approvalMode, options.quiet);
161
+ });
143
162
  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,6 +1,6 @@
1
1
  {
2
2
  "name": "swiftroutercli",
3
- "version": "3.0.1",
3
+ "version": "4.0.1",
4
4
  "description": "The official SwiftRouter Command Line Interface using React Ink Components",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
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
@@ -23,16 +23,23 @@ async function ensureConfig(): Promise<Config> {
23
23
  return existingConfig;
24
24
  }
25
25
 
26
+ let logoRendered = false;
26
27
  try {
27
28
  const logoPath = path.join(__dirname, "..", "assets", "logo.png");
28
29
  if (fs.existsSync(logoPath)) {
29
- console.log(await terminalImage.file(logoPath, { height: "25%" }));
30
+ const smallLogo = await terminalImage.file(logoPath, { height: 3 });
31
+ // Print logo inline with welcome text
32
+ process.stdout.write(smallLogo.trimEnd() + " ");
33
+ console.log(chalk.cyan.bold("Welcome to SwiftRouterCLI!"));
34
+ logoRendered = true;
30
35
  }
31
36
  } catch (e) {
32
37
  // Ignore if logo cannot be rendered
33
38
  }
34
39
 
35
- console.log(chalk.cyan.bold("\n🌊 Welcome to SwiftRouterCLI!"));
40
+ if (!logoRendered) {
41
+ console.log(chalk.cyan.bold("\n🌊 Welcome to SwiftRouterCLI!"));
42
+ }
36
43
  console.log(chalk.gray("It looks like this is your first time. Let's get you set up.\n"));
37
44
 
38
45
  const rl = readline.createInterface({
@@ -61,7 +68,7 @@ async function ensureConfig(): Promise<Config> {
61
68
  program
62
69
  .name("swiftroutercli")
63
70
  .description("CLI for SwiftRouter AI Gateway")
64
- .version("3.0.1");
71
+ .version("4.0.1");
65
72
 
66
73
  program
67
74
  .command("config")
@@ -112,13 +119,15 @@ program
112
119
  .description("Start an interactive chat session")
113
120
  .argument("[prompt]", "Initial prompt to start the chat")
114
121
  .option("-m, --model <model>", "Model to use", DEFAULT_MODEL)
122
+ .option("-a, --approval-mode <mode>", "AI assistant's permission mode (suggest, auto-edit, full-auto)", "suggest")
123
+ .option("-q, --quiet", "Run in headless CI/CD mode without interactive TUI rendering", false)
115
124
  .action(async (prompt, options) => {
116
125
  const config = await ensureConfig();
117
126
 
118
127
  if (!prompt) {
119
- startChat(config, options.model, "");
128
+ startChat(config, options.model, "", options.approvalMode, options.quiet);
120
129
  } else {
121
- startChat(config, options.model, prompt);
130
+ startChat(config, options.model, prompt, options.approvalMode, options.quiet);
122
131
  }
123
132
  });
124
133
 
@@ -159,4 +168,15 @@ program
159
168
  }
160
169
  });
161
170
 
171
+ // Default action: if no subcommand is given, launch chat (just like `codex`)
172
+ program
173
+ .argument("[prompt]", "Initial prompt to start chat directly")
174
+ .option("-m, --model <model>", "Model to use", DEFAULT_MODEL)
175
+ .option("-a, --approval-mode <mode>", "AI assistant's permission mode (suggest, auto-edit, full-auto)", "suggest")
176
+ .option("-q, --quiet", "Run in headless CI/CD mode without interactive TUI rendering", false)
177
+ .action(async (prompt, options) => {
178
+ const config = await ensureConfig();
179
+ startChat(config, options.model, prompt || "", options.approvalMode, options.quiet);
180
+ });
181
+
162
182
  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
  }